一、 React 的使用
React
的基本知識點,如下所示:
JSX
基本使用,變量、表達式、class style
、子元素和組件等等,代碼如下所示:
import React from 'react'
import './style.css'
import List from '../List'
class JSXBaseDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
name: '張三',
imgUrl: 'https://img1.mukewang.com/5a9fc8070001a82402060220-140-140.jpg',
flag: true
}
}
render() {
// // 獲取變量 插值
// const pElem = <p>{this.state.name}</p>
// return pElem
// // 表達式
// const exprElem = <p>{this.state.flag ? 'yes' : 'no'}</p>
// return exprElem
// // 子元素
// const imgElem = <div>
// <p>我的頭像</p>
// <img src="xxxx.png"/>
// <img src={this.state.imgUrl}/>
// </div>
// return imgElem
// // class
// const classElem = <p className="title">設置 css class</p>
// return classElem
// // style
// const styleData = { fontSize: '30px', color: 'blue' }
// const styleElem = <p style={styleData}>設置 style</p>
// // 內聯寫法,注意 {{ 和 }}
// // const styleElem = <p style={{ fontSize: '30px', color: 'blue' }}>設置 style</p>
// return styleElem
// 原生 html
const rawHtml = '<span>富文本內容<i>斜體</i><b>加粗</b></span>'
const rawHtmlData = {
__html: rawHtml // 注意,必須是這種格式
}
const rawHtmlElem = <div>
<p dangerouslySetInnerHTML={rawHtmlData}></p>
<p>{rawHtml}</p>
</div>
return rawHtmlElem
// // 加載組件
// const componentElem = <div>
// <p>JSX 中加載一個組件</p>
// <hr/>
// <List/>
// </div>
// return componentElem
}
}
export default JSXBaseDemo
- 條件判斷,
if else
、三元表達式、邏輯運算符&& ||
,代碼如下所示:
import React from 'react'
import './style.css'
class ConditionDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
theme: 'black'
}
}
render() {
const blackBtn = <button className="btn-black">black btn</button>
const whiteBtn = <button className="btn-white">white btn</button>
// // if else
// if (this.state.theme === 'black') {
// return blackBtn
// } else {
// return whiteBtn
// }
// // 三元運算符
// return <div>
// { this.state.theme === 'black' ? blackBtn : whiteBtn }
// </div>
// &&
return <div>
{ this.state.theme === 'black' && blackBtn }
</div>
}
}
export default ConditionDemo
- 渲染列表,
map
和key
,代碼如下所示:
import React from 'react'
class ListDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
list: [
{
id: 'id-1',
title: '標題1'
},
{
id: 'id-2',
title: '標題2'
},
{
id: 'id-3',
title: '標題3'
}
]
}
}
render() {
return <ul>
{ /* vue v-for */
this.state.list.map(
(item, index) => {
// 這裏的 key 和 Vue 的 key 類似,必填,不能是 index 或 random
return <li key={item.id}>
index {index}; id {item.id}; title {item.title}
</li>
}
)
}
</ul>
}
}
export default ListDemo
- 事件,
bind this
,關於event
參數,傳遞自定義參數,代碼如下所示:
import React from 'react'
class EventDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'zhangsan',
list: [
{
id: 'id-1',
title: '標題1'
},
{
id: 'id-2',
title: '標題2'
},
{
id: 'id-3',
title: '標題3'
}
]
}
// 修改方法的 this 指向
this.clickHandler1 = this.clickHandler1.bind(this)
}
render() {
// // this - 使用 bind
// return <p onClick={this.clickHandler1}>
// {this.state.name}
// </p>
// // this - 使用靜態方法
// return <p onClick={this.clickHandler2}>
// clickHandler2 {this.state.name}
// </p>
// // event
// return <a href="https://imooc.com/" onClick={this.clickHandler3}>
// click me
// </a>
// 傳遞參數 - 用 bind(this, a, b)
return <ul>{this.state.list.map((item, index) => {
return <li key={item.id} onClick={this.clickHandler4.bind(this, item.id, item.title)}>
index {index}; title {item.title}
</li>
})}</ul>
}
clickHandler1() {
// console.log('this....', this) // this 默認是 undefined
this.setState({
name: 'lisi'
})
}
// 靜態方法,this 指向當前實例
clickHandler2 = () => {
this.setState({
name: 'lisi'
})
}
// 獲取 event
clickHandler3 = (event) => {
event.preventDefault() // 阻止默認行爲
event.stopPropagation() // 阻止冒泡
console.log('target', event.target) // 指向當前元素,即當前元素觸發
console.log('current target', event.currentTarget) // 指向當前元素,假象!!!
// 注意,event 其實是 React 封裝的。可以看 __proto__.constructor 是 SyntheticEvent 組合事件
console.log('event', event) // 不是原生的 Event ,原生的 MouseEvent
console.log('event.__proto__.constructor', event.__proto__.constructor)
// 原生 event 如下。其 __proto__.constructor 是 MouseEvent
console.log('nativeEvent', event.nativeEvent)
console.log('nativeEvent target', event.nativeEvent.target) // 指向當前元素,即當前元素觸發
console.log('nativeEvent current target', event.nativeEvent.currentTarget) // 指向 document !!!
// 1. event 是 SyntheticEvent ,模擬出來 DOM 事件所有能力
// 2. event.nativeEvent 是原生事件對象
// 3. 所有的事件,都被掛載到 document 上
// 4. 和 DOM 事件不一樣,和 Vue 事件也不一樣
}
// 傳遞參數
clickHandler4(id, title, event) {
console.log(id, title)
console.log('event', event) // 最後追加一個參數,即可接收 event
}
}
export default EventDemo
- 表單,受控組件,
input、textarea、 select
用value
,checkbox、 radio
用checked
,代碼如下所示:
import React from 'react'
class FormDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
name: '張三',
info: '個人信息',
city: 'shanghai',
flag: true,
gender: 'male'
}
}
render() {
// // 受控組件(非受控組件,後面再講)
// return <div>
// <p>{this.state.name}</p>
// <label htmlFor="inputName">姓名:</label> {/* 用 htmlFor 代替 for */}
// <input id="inputName" value={this.state.name} onChange={this.onInputChange}/>
// </div>
// textarea - 使用 value
return <div>
<textarea value={this.state.info} onChange={this.onTextareaChange}/>
<p>{this.state.info}</p>
</div>
// // select - 使用 value
// return <div>
// <select value={this.state.city} onChange={this.onSelectChange}>
// <option value="beijing">北京</option>
// <option value="shanghai">上海</option>
// <option value="shenzhen">深圳</option>
// </select>
// <p>{this.state.city}</p>
// </div>
// // checkbox
// return <div>
// <input type="checkbox" checked={this.state.flag} onChange={this.onCheckboxChange}/>
// <p>{this.state.flag.toString()}</p>
// </div>
// // radio
// return <div>
// male <input type="radio" name="gender" value="male" checked={this.state.gender === 'male'} onChange={this.onRadioChange}/>
// female <input type="radio" name="gender" value="female" checked={this.state.gender === 'female'} onChange={this.onRadioChange}/>
// <p>{this.state.gender}</p>
// </div>
// 非受控組件
}
onInputChange = (e) => {
this.setState({
name: e.target.value
})
}
onTextareaChange = (e) => {
this.setState({
info: e.target.value
})
}
onSelectChange = (e) => {
this.setState({
city: e.target.value
})
}
onCheckboxChange = () => {
this.setState({
flag: !this.state.flag
})
}
onRadioChange = (e) => {
this.setState({
gender: e.target.value
})
}
}
export default FormDemo
- 對於組件的使用,父子組件通訊,
props
傳遞數據,props
傳遞函數,props
類型檢查,代碼如下所示:
import React from 'react'
import PropTypes from 'prop-types'
class Input extends React.Component {
constructor(props) {
super(props)
this.state = {
title: ''
}
}
render() {
return <div>
<input value={this.state.title} onChange={this.onTitleChange}/>
<button onClick={this.onSubmit}>提交</button>
</div>
}
onTitleChange = (e) => {
this.setState({
title: e.target.value
})
}
onSubmit = () => {
const { submitTitle } = this.props
submitTitle(this.state.title) // 'abc'
this.setState({
title: ''
})
}
}
// props 類型檢查
Input.propTypes = {
submitTitle: PropTypes.func.isRequired
}
class List extends React.Component {
constructor(props) {
super(props)
}
render() {
const { list } = this.props
return <ul>{list.map((item, index) => {
return <li key={item.id}>
<span>{item.title}</span>
</li>
})}</ul>
}
}
// props 類型檢查
List.propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired
}
class Footer extends React.Component {
constructor(props) {
super(props)
}
render() {
return <p>
{this.props.text}
{this.props.length}
</p>
}
componentDidUpdate() {
console.log('footer did update')
}
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.text !== this.props.text
|| nextProps.length !== this.props.length) {
return true // 可以渲染
}
return false // 不重複渲染
}
// React 默認:父組件有更新,子組件則無條件也更新!!!
// 性能優化對於 React 更加重要!
// SCU 一定要每次都用嗎?—— 需要的時候才優化
}
class TodoListDemo extends React.Component {
constructor(props) {
super(props)
// 狀態(數據)提升
this.state = {
list: [
{
id: 'id-1',
title: '標題1'
},
{
id: 'id-2',
title: '標題2'
},
{
id: 'id-3',
title: '標題3'
}
],
footerInfo: '底部文字'
}
}
render() {
return <div>
<Input submitTitle={this.onSubmitTitle}/>
<List list={this.state.list}/>
<Footer text={this.state.footerInfo} length={this.state.list.length}/>
</div>
}
onSubmitTitle = (title) => {
this.setState({
list: this.state.list.concat({
id: `id-${Date.now()}`,
title
})
})
}
}
export default TodoListDemo
setState
是不可變值,可能是異步更新,可能會被合併,代碼如下所示:
import React from 'react'
// 函數組件(後面會講),默認沒有 state
class StateDemo extends React.Component {
constructor(props) {
super(props)
// 第一,state 要在構造函數中定義
this.state = {
count: 0
}
}
render() {
return <div>
<p>{this.state.count}</p>
<button onClick={this.increase}>累加</button>
</div>
}
increase = () => {
// // 第二,不要直接修改 state ,使用不可變值 ----------------------------
// // this.state.count++ // 錯誤
// this.setState({
// count: this.state.count + 1 // SCU
// })
// 操作數組、對象的的常用形式
// 第三,setState 可能是異步更新(有可能是同步更新) ----------------------------
// this.setState({
// count: this.state.count + 1
// }, () => {
// // 聯想 Vue $nextTick - DOM
// console.log('count by callback', this.state.count) // 回調函數中可以拿到最新的 state
// })
// console.log('count', this.state.count) // 異步的,拿不到最新值
// // setTimeout 中 setState 是同步的
// setTimeout(() => {
// this.setState({
// count: this.state.count + 1
// })
// console.log('count in setTimeout', this.state.count)
// }, 0)
// 自己定義的 DOM 事件,setState 是同步的。再 componentDidMount 中
// 第四,state 異步更新的話,更新前會被合併 ----------------------------
// // 傳入對象,會被合併(類似 Object.assign )。執行結果只一次 +1
// this.setState({
// count: this.state.count + 1
// })
// this.setState({
// count: this.state.count + 1
// })
// this.setState({
// count: this.state.count + 1
// })
// 傳入函數,不會被合併。執行結果是 +3
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
}
// bodyClickHandler = () => {
// this.setState({
// count: this.state.count + 1
// })
// console.log('count in body event', this.state.count)
// }
// componentDidMount() {
// // 自己定義的 DOM 事件,setState 是同步的
// document.body.addEventListener('click', this.bodyClickHandler)
// }
// componentWillUnmount() {
// // 及時銷燬自定義 DOM 事件
// document.body.removeEventListener('click', this.bodyClickHandler)
// // clearTimeout
// }
}
export default StateDemo
// // 不可變值(函數式編程,純函數) - 數組
// const list5Copy = this.state.list5.slice()
// list5Copy.splice(2, 0, 'a') // 中間插入/刪除
// this.setState({
// list1: this.state.list1.concat(100), // 追加
// list2: [...this.state.list2, 100], // 追加
// list3: this.state.list3.slice(0, 3), // 截取
// list4: this.state.list4.filter(item => item > 100), // 篩選
// list5: list5Copy // 其他操作
// })
// // 注意,不能直接對 this.state.list 進行 push pop splice 等,這樣違反不可變值
// // 不可變值 - 對象
// this.setState({
// obj1: Object.assign({}, this.state.obj1, {a: 100}),
// obj2: {...this.state.obj2, a: 100}
// })
// // 注意,不能直接對 this.state.obj 進行屬性設置,這樣違反不可變值
setState
有的是同步的,有的是異步的,代碼如下所示:
import React from 'react'
class ListDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
render() {
return <p>{this.state.count}</p>
}
componentDidMount() {
// count 初始值爲 0
this.setState({ count: this.state.count + 1 })
console.log('1', this.state.count) // 0
this.setState({ count: this.state.count + 1 })
console.log('2', this.state.count) // 0
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log('3', this.state.count) // 2
})
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log('4', this.state.count) // 3
})
}
}
export default ListDemo
-
React
的組件生命週期,單組件生命週期,父子組件生命週期,和Vue
的一樣。 -
對於
React
的高級特性,分爲函數組件、非受控組件、Portals
、context
、異步組件、性能優化、高階組件HOC
和Render Props
,如下所示:
- 函數組件,純函數,輸入
props
,輸出JSX
。沒有實例,沒有生命週期,沒有state
,不能擴展其它辦法 - 非受控組件,
ref
、defaultValue
和defaultChecked
,手動操作DOM
元素。對於非受控組件的使用場景,必須手動操作DOM
元素,setState
實現不了。文件上傳<input type=file>
, 某些富文本編輯器,需要傳入DOM
元素。對於受控組件和非受控組件的比較,優先使用受控組件,符合React
的設計原則。必須操作DOM
時,再使用非受控組件,代碼如下所示:
import React from 'react'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
name: '張三',
flag: true,
}
this.nameInputRef = React.createRef() // 創建 ref
this.fileInputRef = React.createRef()
}
render() {
// // input defaultValue
// return <div>
// {/* 使用 defaultValue 而不是 value ,使用 ref */}
// <input defaultValue={this.state.name} ref={this.nameInputRef}/>
// {/* state 並不會隨着改變 */}
// <span>state.name: {this.state.name}</span>
// <br/>
// <button onClick={this.alertName}>alert name</button>
// </div>
// // checkbox defaultChecked
// return <div>
// <input
// type="checkbox"
// defaultChecked={this.state.flag}
// />
// </div>
// file
return <div>
<input type="file" ref={this.fileInputRef}/>
<button onClick={this.alertFile}>alert file</button>
</div>
}
alertName = () => {
const elem = this.nameInputRef.current // 通過 ref 獲取 DOM 節點
alert(elem.value) // 不是 this.state.name
}
alertFile = () => {
const elem = this.fileInputRef.current // 通過 ref 獲取 DOM 節點
alert(elem.files[0].name)
}
}
export default App
React Portals
,組件默認會按照既定層次嵌套渲染,讓組件渲染到父組件之外。對於Portals
的使用場景,overflow: hidden
,父組件z-index
值太小,flexd
需要放在body
第一層級,代碼如下所示:
import React from 'react'
import ReactDOM from 'react-dom'
import './style.css'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
}
}
render() {
// // 正常渲染
// return <div className="modal">
// {this.props.children} {/* vue slot */}
// </div>
// 使用 Portals 渲染到 body 上。
// fixed 元素要放在 body 上,有更好的瀏覽器兼容性。
return ReactDOM.createPortal(
<div className="modal">{this.props.children}</div>,
document.body // DOM 節點
)
}
}
export default App
context
,上下文環境,公共信息傳遞給每個組件,用props
太繁瑣,用redux
小題大做,代碼如下所示:
import React from 'react'
// 創建 Context 填入默認值(任何一個 js 變量)
const ThemeContext = React.createContext('light')
// 底層組件 - 函數是組件
function ThemeLink (props) {
// const theme = this.context // 會報錯。函數式組件沒有實例,即沒有 this
// 函數式組件可以使用 Consumer
return <ThemeContext.Consumer>
{ value => <p>link's theme is {value}</p> }
</ThemeContext.Consumer>
}
// 底層組件 - class 組件
class ThemedButton extends React.Component {
// 指定 contextType 讀取當前的 theme context。
// static contextType = ThemeContext // 也可以用 ThemedButton.contextType = ThemeContext
render() {
const theme = this.context // React 會往上找到最近的 theme Provider,然後使用它的值。
return <div>
<p>button's theme is {theme}</p>
</div>
}
}
ThemedButton.contextType = ThemeContext // 指定 contextType 讀取當前的 theme context。
// 中間的組件再也不必指明往下傳遞 theme 了。
function Toolbar(props) {
return (
<div>
<ThemedButton />
<ThemeLink />
</div>
)
}
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
theme: 'light'
}
}
render() {
return <ThemeContext.Provider value={this.state.theme}>
<Toolbar />
<hr/>
<button onClick={this.changeTheme}>change theme</button>
</ThemeContext.Provider>
}
changeTheme = () => {
this.setState({
theme: this.state.theme === 'light' ? 'dark' : 'light'
})
}
}
export default App
- 異步組件,
import()、React.lazy、React.Suspense
,代碼如下所示:
import React from 'react'
const ContextDemo = React.lazy(() => import('./ContextDemo'))
class App extends React.Component {
constructor(props) {
super(props)
}
render() {
return <div>
<p>引入一個動態組件</p>
<hr />
<React.Suspense fallback={<div>Loading...</div>}>
<ContextDemo/>
</React.Suspense>
</div>
// 1. 強制刷新,可看到 loading (看不到就限制一下 chrome 網速)
// 2. 看 network 的 js 加載
}
}
export default App
- 對於
React
的性能優化,shouldComponentUpdate
,簡稱SCU
,PureComponent
和React.memo
,不可變值immutable.js
,如下所示:
SCU
,SCU
默認返回true
,即React
默認重新渲染所有子組件,必須配合 不可變值 一起使用。可先不用SCU
,有性能問題時再考慮使用,代碼如下所示:
import React from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
class Input extends React.Component {
constructor(props) {
super(props)
this.state = {
title: ''
}
}
render() {
return <div>
<input value={this.state.title} onChange={this.onTitleChange}/>
<button onClick={this.onSubmit}>提交</button>
</div>
}
onTitleChange = (e) => {
this.setState({
title: e.target.value
})
}
onSubmit = () => {
const { submitTitle } = this.props
submitTitle(this.state.title)
this.setState({
title: ''
})
}
}
// props 類型檢查
Input.propTypes = {
submitTitle: PropTypes.func.isRequired
}
class List extends React.Component {
constructor(props) {
super(props)
}
render() {
const { list } = this.props
return <ul>{list.map((item, index) => {
return <li key={item.id}>
<span>{item.title}</span>
</li>
})}</ul>
}
// 增加 shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState) {
// _.isEqual 做對象或者數組的深度比較(一次性遞歸到底)
if (_.isEqual(nextProps.list, this.props.list)) {
// 相等,則不重複渲染
return false
}
return true // 不相等,則渲染
}
}
// props 類型檢查
List.propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired
}
class TodoListDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
list: [
{
id: 'id-1',
title: '標題1'
},
{
id: 'id-2',
title: '標題2'
},
{
id: 'id-3',
title: '標題3'
}
]
}
}
render() {
return <div>
<Input submitTitle={this.onSubmitTitle}/>
<List list={this.state.list}/>
</div>
}
onSubmitTitle = (title) => {
// 正確的用法
this.setState({
list: this.state.list.concat({
id: `id-${Date.now()}`,
title
})
})
// // 爲了演示 SCU ,故意寫的錯誤用法
// this.state.list.push({
// id: `id-${Date.now()}`,
// title
// })
// this.setState({
// list: this.state.list
// })
}
}
export default TodoListDemo
-
PureComponent
和memo
,PureComponent
,SCU
中實現了淺比較。memo
,函數組件中的PureComponent
,淺比較已使用大部分情況,儘量不要做深度比較 -
immutable.js
,徹底擁抱不可變質,基於共享數據,不是深拷貝,速度好,按需使用
- 關於組件公共邏輯的抽離,
mixin
,已被React
棄用,高階組件HOC,Render Props
。HOC
是模式簡單,但會增加組件層級,Render Props
是代碼簡潔,學習成本較高,按需使用,代碼如下所示:
- HOC.js
import React from 'react' // 高階組件 const withMouse = (Component) => { class withMouseComponent extends React.Component { constructor(props) { super(props) this.state = { x: 0, y: 0 } } handleMouseMove = (event) => { this.setState({ x: event.clientX, y: event.clientY }) } render() { return ( <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}> {/* 1. 透傳所有 props 2. 增加 mouse 屬性 */} <Component {...this.props} mouse={this.state}/> </div> ) } } return withMouseComponent } const App = (props) => { const a = props.a const { x, y } = props.mouse // 接收 mouse 屬性 return ( <div style={{ height: '500px' }}> <h1>The mouse position is ({x}, {y})</h1> <p>{a}</p> </div> ) } export default withMouse(App) // 返回高階函數
- RenderProp.js
import React from 'react' import PropTypes from 'prop-types' class Mouse extends React.Component { constructor(props) { super(props) this.state = { x: 0, y: 0 } } handleMouseMove = (event) => { this.setState({ x: event.clientX, y: event.clientY }) } render() { return ( <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}> {/* 將當前 state 作爲 props ,傳遞給 render (render 是一個函數組件) */} {this.props.render(this.state)} </div> ) } } Mouse.propTypes = { render: PropTypes.func.isRequired // 必須接收一個 render 屬性,而且是函數 } const App = (props) => ( <div style={{ height: '500px' }}> <p>{props.a}</p> <Mouse render={ /* render 是一個函數組件 */ ({ x, y }) => <h1>The mouse position is ({x}, {y})</h1> }/> </div> ) /** * 即,定義了 Mouse 組件,只有獲取 x y 的能力。 * 至於 Mouse 組件如何渲染,App 說了算,通過 render prop 的方式告訴 Mouse 。 */ export default App
Redux
的使用,分爲基本概念、單項數據流、react-redux
、異步action
和中間件,如下所示:
redux
的單向數據流,store、state、action 和 reducer
。應用中所有的state
都以一個對象樹的形式儲存在一個單一的store
中。 惟一改變state
的辦法是觸發action
,一個描述發生什麼的對象。 爲了描述action
如何改變state
樹,你需要編寫reducers
。這是一個reducer
,形式爲(state, action) => state
的純函數,描述了action
如何把state
轉變成下一個state
。state
的形式取決於你,可以是基本類型、數組、對象、甚至是Immutable.js
生成的數據結構。惟一的要點是當state
變化時需要返回全新的對象,而不是修改傳入的參數。改變內部state
惟一方法是dispatch
一個action
。action
可以被序列化,用日記記錄和儲存下來,後期還可以以回放的方式執行。對於單向數據流,dispatch(action)、reducer -> newState、subscribe
觸發通知- 對於
react-redux,<Provider> content、connect、mapStateToProps 和 mapDispatchToProps
- 對於異步
action
,有redux-thunk、redux-promise 和 redux-saga
- 對於
redux
中間件,在單向數據流的時候,button
進行callback
指向dispatch
,dispatch
通過action
指向reducer
,reducer
通過state
指向view
。在有中間件以後,button
通過callback
指向new dispatch
。在new dispatch
中,mid1 -> mid2 -> action -> dispatch
。最後new dispatch
通過action
指向reducer
。
- 對於
redux
的使用,代碼如下所示:
-
action
文件夾- index.js
let nextTodoId = 0 // 創建一個 todo export const addTodo = text => { return { type: 'ADD_TODO', id: nextTodoId++, text } } // 設置完成狀態 export const setVisibilityFilter = filter => { return { type: 'SET_VISIBILITY_FILTER', filter } } // 切換 todo 完成狀態 export const toggleTodo = id => { return { type: 'TOGGLE_TODO', id } }
- index.js
-
components
文件夾-
App.js
import React from 'react' import Footer from './Footer' import AddTodo from '../containers/AddTodo' import VisibleTodoList from '../containers/VisibleTodoList' const App = () => ( <div> <AddTodo /> <VisibleTodoList /> <Footer /> </div> ) export default App
-
Footer.js
import React from 'react' import FilterLink from '../containers/FilterLink' const Footer = () => ( <p> Show: {' '} <FilterLink filter="SHOW_ALL"> All </FilterLink> {', '} <FilterLink filter="SHOW_ACTIVE"> Active </FilterLink> {', '} <FilterLink filter="SHOW_COMPLETED"> Completed </FilterLink> </p> ) export default Footer ```
-
Link.js
import React from 'react' import PropTypes from 'prop-types' const Link = ({ active, children, onClick }) => { if (active) { return <span>{children}</span> } return ( <a href="" onClick={e => { e.preventDefault() onClick() // 設置過濾狀態 }} > {children} </a> ) } Link.propTypes = { active: PropTypes.bool.isRequired, children: PropTypes.node.isRequired, onClick: PropTypes.func.isRequired } export default Link
-
Todo.js
import React from 'react' import PropTypes from 'prop-types' // 函數組件 { onClick, completed, text } = props const Todo = ({ onClick, completed, text }) => ( <li onClick={onClick} style={ { textDecoration: completed ? 'line-through' : 'none' }} > {text} </li> ) Todo.propTypes = { onClick: PropTypes.func.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired } export default Todo
-
TodoList.js
import React from 'react' import PropTypes from 'prop-types' import Todo from './Todo' // 函數組件 { todos, onTodoClick } = props const TodoList = ({ todos, onTodoClick }) => ( <ul> {todos.map(todo => ( <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} /> // 點擊切換完成狀態 ))} </ul> ) TodoList.propTypes = { todos: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }).isRequired ).isRequired, onTodoClick: PropTypes.func.isRequired } export default TodoList
-
-
containers
文件夾- AddTodo.js
import React from 'react' import { connect } from 'react-redux' import { addTodo } from '../actions' // 函數組件,接收 props 參數 let AddTodo = ({ dispatch }) => { // dispatch 即 props.dispatch let input return ( <div> <form onSubmit={e => { e.preventDefault() if (!input.value.trim()) { return } // 創建一個 todo dispatch(addTodo(input.value)) input.value = '' }} > <input ref={node => { input = node }} /> <button type="submit"> Add Todo </button> </form> </div> ) } // connect 高階組件 ,將 dispatch 作爲 props 注入到 AddTodo 組件中 AddTodo = connect()(AddTodo) export default AddTodo
- FilterLink.js
import { connect } from 'react-redux' import { setVisibilityFilter } from '../actions' import Link from '../components/Link' const mapStateToProps = (state, ownProps) => { // ownProps 即父組件傳來的 `filter="SHOW_ALL"` return { active: ownProps.filter === state.visibilityFilter } } const mapDispatchToProps = (dispatch, ownProps) => { // ownProps 即父組件傳來的 `filter="SHOW_ALL"` return { onClick: () => { // 設置過濾狀態 dispatch(setVisibilityFilter(ownProps.filter)) } } } const FilterLink = connect( mapStateToProps, mapDispatchToProps )(Link) export default FilterLink
- VisibleTodoList.js
import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' // 不同類型的 todo 列表 const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) } } const mapStateToProps = state => { // state 即 vuex 的總狀態,在 reducer/index.js 中定義 return { // 根據完成狀態,篩選數據 todos: getVisibleTodos(state.todos, state.visibilityFilter) } } const mapDispatchToProps = dispatch => { return { // 切換完成狀態 onTodoClick: id => { dispatch(toggleTodo(id)) } } } // connect 高階組件,將 state 和 dispatch 注入到組件 props 中 const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList) export default VisibleTodoList
- AddTodo.js
-
reducers
文件夾- index.js
import { combineReducers } from 'redux' import todos from './todos' import visibilityFilter from './visibilityFilter' const todoApp = combineReducers({ todos, visibilityFilter }) export default todoApp
- todos.js
const todos = (state = [], action) => { switch (action.type) { // 創建一個 todo case 'ADD_TODO': // 注意,返回不可變數據 return [ ...state, { id: action.id, text: action.text, // title completed: false } ] // 切換一個 todo 的完成狀態 case 'TOGGLE_TODO': // 注意,返回不可變數據 return state.map(todo => (todo.id === action.id) ? {...todo, completed: !todo.completed} // 切換完成狀態 : todo ) default: return state } } export default todos
- visibilityFilter.js
const visibilityFilter = (state = 'SHOW_ALL', action) => { switch (action.type) { // 設置顯示類型(所有、完成、未完成) case 'SET_VISIBILITY_FILTER': return action.filter // 默認是 SHOW_ALL default: return state } } export default visibilityFilter
- index.js
-
index.js
import React from 'react' import { Provider } from 'react-redux' import { createStore } from 'redux' import todoApp from './reducers' import App from './components/App' let store = createStore(todoApp) export default function () { return <Provider store={store}> <App /> </Provider> }
React-router
,路由模式hash
和H5 history
,路由配置 動態路由和懶加載。hash
模式是默認的,如http://abc.com/#/user/10。H5 history
模式,如http://abc.com/user/20
,後者需要server
端支持,因此無特殊需求可選擇前者。
二、React 的原理
-
對於
React
的原理,分爲函數式編程、vdom
和diff
、JSX
本質、合成事件、setState
和batchUpdate
、組件渲染過程。 -
對於函數式編程,一種編程範式,純函數,不可變值。不可變值數組,不能直接對
this.state.list
進行push pop splice
等,這樣違反不可變值。不可變值對象,不能直接對this.state.obj
進行屬性設置,這樣違反不可變值。state
要在構造函數中定義
;不要直接修改state
,使用不可變值;setState
可能是異步更新(有可能是同步更新);state
異步更新的話,更新前會被合併。 -
對於
vdom
和diff
,h
函數、vdom
數據結構、patch
函數。只比較同一層級,不跨級比較。tag
不相同,則直接刪掉重建,不再深度比較。tag
和key
,兩者都相同,則認爲是相同節點,不再深度比較。 -
對於
JSX
的本質,JSX
等同於Vue
模版,Vue
模版不是html
,JSX
也不是JS
。如第一個參數是List
組件,找到List
組件jsx
結構,繼續拆分。 -
對於合成事件,所有事件掛載到
document
上,event
不是原生的,是SyntheticEvent
合成事件對象。它和Vue
事件不同,和DOM
事件也不同。需要合成事件機制的原因,如下所示:
- 更好的兼容性和跨平臺
- 掛載到
document
,減少內存消耗,避免頻繁解綁 - 方便事件的統一管理,如事務機制
setState
和batchUpdate
,如下所示:
- 有時異步,普通使用的時候;有時同步,
setTimeout
、DOM
事件的時候 - 有時合併,對象形式;有時不合並,函數形式;
- 後者相對於好理解,像
Object.assign
,前者相對於更重要 - 對於核心要點,
setState
主流程、batchUpdate
機制、transition
事務機制 - 對於
setState
主流程,this.setState(newState)
到newState
存入pending
隊列,然後判斷是否處於batch update
。如果是的話,保存組件於dirtyComponents
中。如果不是的話,遍歷所有的dirtyComponents
,調用updateComponent
,更新pending state or props
- 對於
setState
是同步的還是異步的,其實setState
無所謂異步還是同步,看是否能夠命中batchUpdate
機制,判斷isBatchingUpdates
- 對於哪些能夠命中
batchUpdate
機制,如下所示:- 生命週期和它調用的函數
React
中註冊的事件和它調用的函數React
可以管理的入口
- 對應哪些不能夠命中
batchUpdate
機制,如下所示:setTimeout、setInterval
等和它調用的函數- 自定義的
DOM
事件和它調用的函數 React
管不到的入口
- 對於
transition
事務機制,開始處於batchUpdate
,isBatchUpdates = true
,其它任何操作,結束,isBatchUpdates = false
- 對於組件渲染和更新過程,
JSX
如何渲染爲頁面,setState
之後如何更新頁面,以及對於全流程的理解,如下所示:
JSX
即createElement
函數,執行生成vnode
,patch(elem, vnode)
和patch(vnode, newVnode)
- 對於組件的渲染過程,
props state,render()
生成vnode
,patch(elem, vnode)
- 對於組件的更新過程,
setState(newState) -> dirtyComponents
,可能有子組件。render()
生成newVnode
,patch(vnode, newVnode)
- 對於
patch
更新的兩個階段,reconciliation
階段執行diff
算法,純JS
計算。在commit
階段,將diff
結果渲染DOM
- 對於可能出現的性能問題,
JS
是單線程的,且和DOM
渲染共用一個線程。當組件足夠複雜的時候,組件更新時計算和渲染都壓力大,同時再有DOM
操作需求,如動畫、鼠標拖拽等,將卡頓 - 解決方案就是
fiber
,將reconciliation
階段進行任務拆分,但是commit
無法拆分。DOM
需要渲染時暫停,空閒時修復,window.requestIdleCallback
三、React 的面試真題
- 組件之間如何通訊,答案如下所示:
- 父子組件
props
- 自定義事件
Redux
和Context
JSX
的本質是什麼,答案如下所示:
createElement
- 執行返回
vnode
Context
是什麼,如何應用,答案如下所示:
- 父組件,向其下所有子孫組件傳遞信息
- 一些簡單的公共信息,主題色、顏色等等
- 複雜的公共信息,用
redux
shouldComponentUpdate
的用途,答案如下所示:
- 性能優化
- 配合不可變值一起使用,否則會出錯
- 什麼是純函數,答案如下所示:
- 返回一個新值,沒有副作用,不會修改其它值
- 重點就是不可變值
- 如
arr1 = arr.slice()
- 對於
React
的組件生命週期,答案如下所示:
- 單組件生命週期
- 父子組件生命週期
- 注意
SCU
React
發起ajax
應該在哪個生命週期,答案如下所示:
componentDidMount
這個生命週期函數
- 渲染列表,爲何使用
key
,答案如下所示:
- 必須用
key
,且不能是index
和random
diff
算法中通過tag
和key
來判斷,是否是sameNode
- 減少渲染次數,提升渲染性能
- 函數組件和
class
組件的區別,答案如下所示:
- 純函數,輸入
props
,輸出JSX
- 沒有實例,沒有生命週期,沒有
state
- 不能擴展其它辦法
- 什麼是受控組件,答案如下所示:
- 表單的值,受
state
控制 - 需要自行監聽
onChange
,更新state
- 對比非受控組件
- 何時使用異步組件,答案如下所示:
- 加載大組件
- 路由懶加載
- 多個組件有公共邏輯,如何抽離,答案如下所示:
- 高階組件
Render Props
mixin
已被React
廢棄
redux
如何進行異步請求,答案如下所示:
- 使用異步
action
- 如
redux-thunk
PureComponent
有何區別,答案如下所示:
- 實現了淺比較的
shouldComponentUpdate
- 優化性能
- 需要結合不可變值使用
React
事件和DOM
事件的區別,答案如下所示:
- 所有事件掛載到
document
上 event
不是原生的,是SyntheticEvent
合成事件對象dispatchEvent
- 對於
React
的性能優化,答案如下所示:
- 渲染列表時添加
key
- 自定義事件、
DOM
事件及時銷燬 - 合理使用異步組件
- 減少函數
bind this
的次數 - 合理使用
SCU、PureComponent 和 memo
- 合理使用
Immutable.js
webpack
層面的優化- 前端通用的性能優化,如圖片懶加載
- 使用
SSR
React
和Vue
的區別,答案如下所示:
- 都支持組件化
- 都是數據驅動視圖
- 都使用
vdom
操作DOM
React
使用JSX
擁抱JS
,Vue
使用模版擁抱html
React
函數式編程,Vue
聲明式編程React
更多的需要是自力更生,Vue
是把想要的都給你