React系列
React簡單模擬語法(一)
Jsx, 合成事件與Refs(二)
virtualdom diff算法實現(xiàn)分析(三)
從Mixin到HOC再到HOOKS(四)
createElement, ReactElement與Component部分源碼解析(五)
Mixins(已廢棄)
這是React初期提供的一種組合方案,通過引入一個公用組件,然后可以應(yīng)用公用組件的一些生命周期操作或者定義方法,達到抽離公用代碼提供不同模塊使用的目的.
曾經(jīng)的官方文檔demo如下
var SetIntervalMixin = { componentWillMount: function() { this.intervals = []; }, setInterval: function() { this.intervals.push(setInterval.apply(null, arguments)); }, componentWillUnmount: function() { this.intervals.map(clearInterval); } }; var TickTock = React.createClass({ mixins: [SetIntervalMixin], // Use the mixin getInitialState: function() { return {seconds: 0}; }, componentDidMount: function() { this.setInterval(this.tick, 1000); // Call a method on the mixin }, tick: function() { this.setState({seconds: this.state.seconds + 1}); }, render: function() { return (React has been running for {this.state.seconds} seconds.
); } }); React.render(, document.getElementById('example') );
但是Mixins只能應(yīng)用在createClass
的創(chuàng)建方式,在后來的class寫法中已經(jīng)被廢棄了.原因在于:
- mixin引入了隱式依賴關(guān)系
- 不同mixins之間可能會有先后順序甚至代碼沖突覆蓋的問題
- mixin代碼會導(dǎo)致滾雪球式的復(fù)雜性
詳細介紹mixin危害性文章可直接查閱Mixins Considered Harmful
高階組件(Higher-order component)
HOC是一種React的進階使用方法,大概原理就是接收一個組件然后返回一個新的繼承組件,繼承方式分兩種
屬性代理(Props Proxy)
最基本的實現(xiàn)方式
function PropsProxyHOC(WrappedComponent) { return class NewComponent extends React.Component { render() { return} } }
從代碼可以看出屬性代理方式其實就是接受一個 WrappedComponent
組件作為參數(shù)傳入,并返回一個繼承了 React.Component
組件的類,且在該類的 render()
方法中返回被傳入的 WrappedComponent
組件
抽離state && 操作props
function PropsProxyHOC(WrappedComponent) { return class NewComponent extends React.Component { constructor(props) { super(props) this.state = { name: 'PropsProxyHOC' } } logName() { console.log(this.name) } render() { const newProps = { name: this.state.name, logName: this.logName } return} } } class Main extends Component { componentDidMount() { this.props.logName() } render() { return ( PropsProxyHOC ) } } export default PropsProxyHOC(Main);
demo代碼可以參考這里
有種常見的情況是用來做雙向綁定
function PropsProxyHOC(WrappedComponent) { return class NewComponent extends React.Component { constructor(props) { super(props) this.state = { fields: {} } } getField(fieldName) { const _s = this.state if (!_s.fields[fieldName]) { _s.fields[fieldName] = { value: '', onChange: event => { this.state.fields[fieldName].value = event.target.value // 強行觸發(fā)render this.forceUpdate() console.log(this.state) } } } return { value: _s.fields[fieldName].value, onChange: _s.fields[fieldName].onChange } } render() { const newProps = { fields: this.getField.bind(this), } return} } } // 被獲取ref實例組件 class Main extends Component { render() { return } } export default PropsProxyHOC(Main);
demo代碼可以參考這里
獲取被繼承refs實例
因為這是一個被HOC包裝過的新組件,所以想要在HOC里面獲取新組件的ref需要用些特殊方式,但是不管哪種,都需要在組件掛載之后才能獲取到.并且不能在無狀態(tài)組件(函數(shù)類型組件)上使用 ref 屬性,因為無狀態(tài)組件沒有實例。愛掏網(wǎng) - it200.com
通過父元素傳遞方法獲取
function PropsProxyHOC(WrappedComponent) { return class NewComponent extends React.Component { render() { const newProps = {} // 監(jiān)聽到有對應(yīng)方法才生成props實例 typeof this.props.getInstance === "function" && (newProps.ref = this.props.getInstance) return} } } // 被獲取ref實例組件 class Main extends Component { render() { return ( Main ) } } const HOCComponent = PropsProxyHOC(Main) class ParentComponent extends Component { componentWillMount() { console.log('componentWillMount: ', this.wrappedInstance) } componentDidMount() { console.log('componentDidMount: ', this.wrappedInstance) } // 提供給高階組件調(diào)用生成實例 getInstance(ref) { this.wrappedInstance = ref; } render() { return ( ) } } export default (ParentComponent);
demo代碼可以參考這里
通過高階組件當中間層
相比較上一方式,需要在高階組件提供設(shè)置賦值函數(shù),并且需要一個props屬性做標記
function PropsProxyHOC(WrappedComponent) { return class NewComponent extends React.Component { // 返回ref實例 getWrappedInstance = () => { if (this.props.withRef) { return this.wrappedInstance; } } //設(shè)置ref實例 setWrappedInstance = (ref) => { this.wrappedInstance = ref; } render() { const newProps = {} // 監(jiān)聽到有對應(yīng)方法才賦值props實例 this.props.withRef && (newProps.ref = this.setWrappedInstance) return} } } // 被獲取ref實例組件 class Main extends Component { render() { return ( Main ) } } const HOCComponent = PropsProxyHOC(Main) class ParentComponent extends Component { componentWillMount() { console.log('componentWillMount: ', this.refs.child) } componentDidMount() { console.log('componentDidMount: ', this.refs.child.getWrappedInstance()) } render() { return ( ) } } export default (ParentComponent);
demo代碼可以參考這里
forwardRef
React.forwardRef 會創(chuàng)建一個React組件,這個組件能夠?qū)⑵浣邮艿?ref 屬性轉(zhuǎn)發(fā)到其組件樹下的另一個組件中。愛掏網(wǎng) - it200.com這種技術(shù)并不常見,但在以下兩種場景中特別有用:
- 轉(zhuǎn)發(fā) refs 到 DOM 組件
- 在高階組件中轉(zhuǎn)發(fā) refs
const FancyButton = React.forwardRef((props, ref) => ( )); // You can now get a ref directly to the DOM button: const ref = React.createRef();Click me! ;
以下是對上述示例發(fā)生情況的逐步解釋:
- 我們通過調(diào)用 React.createRef 創(chuàng)建了一個 React ref 并將其賦值給 ref 變量。愛掏網(wǎng) - it200.com
- 我們通過指定 ref 為 JSX 屬性,將其向下傳遞給
。愛掏網(wǎng) - it200.com - React 傳遞 ref 給 fowardRef 內(nèi)函數(shù) (props, ref) => ...,作為其第二個參數(shù)。愛掏網(wǎng) - it200.com
- 我們向下轉(zhuǎn)發(fā)該 ref 參數(shù)到
,將其指定為 JSX 屬性。愛掏網(wǎng) - it200.com
- 當 ref 掛載完成,ref.current 將指向
DOM 節(jié)點。愛掏網(wǎng) - it200.com
劫持渲染
最簡單的例子莫過于loading組件了
function PropsProxyHOC(WrappedComponent) { return class NewComponent extends React.Component { render() { return this.props.isLoading ? Loading... :} } } // 被獲取ref實例組件 class Main extends Component { render() { return ( Main ) } } const HOCComponent = PropsProxyHOC(Main) class ParentComponent extends Component { constructor() { super() this.state = { isLoading: true } } render() { setTimeout(() => this.setState({ isLoading: false }), 2000) return ( ) } } export default (ParentComponent);
當然也能用于布局上嵌套在其他元素輸出
demo代碼可以參考這里
反向繼承(Inheritance Inversion)
最簡單的demo代碼
function InheritanceInversionHOC(WrappedComponent) { return class NewComponent extends WrappedComponent { render() { return super.render() } } }
在這里WrappedComponent
成了被繼承的那一方,從而可以在高階組件中獲取到傳遞組件的所有相關(guān)實例
獲取繼承組件實例
function InheritanceInversionHOC(WrappedComponent) { return class NewComponent extends WrappedComponent { componentDidMount() { console.log('componentDidMount: ', this) } render() { return super.render() } } } // 被獲取ref實例組件 class Main extends Component { constructor() { super() this.state = { name: 'WrappedComponent' } } render() { return ( Main ) } } export default (InheritanceInversionHOC(Main));
demo代碼可以參考這里
修改props和劫持渲染
再講解demo之前先科普React的一個方法
React.cloneElement( element, [props], [...children] )
以 element 元素為樣板克隆并返回新的 React 元素。愛掏網(wǎng) - it200.com返回元素的 props 是將新的 props 與原始元素的 props 淺層合并后的結(jié)果。愛掏網(wǎng) - it200.com新的子元素將取代現(xiàn)有的子元素,而來自原始元素的 key 和 ref 將被保留。愛掏網(wǎng) - it200.com
React.cloneElement() 幾乎等同于:
{children}
但是,這也保留了組件的 ref。愛掏網(wǎng) - it200.com這意味著當通過 ref 獲取子節(jié)點時,你將不會意外地從你祖先節(jié)點上竊取它。愛掏網(wǎng) - it200.com相同的 ref 將添加到克隆后的新元素中。愛掏網(wǎng) - it200.com
相比屬性繼承來說,反向繼承修改props會比較復(fù)雜一點
function InheritanceInversionHOC(WrappedComponent) { return class NewComponent extends WrappedComponent { constructor() { super() this.state = { 'a': 'b' } } componentDidMount() { console.log('componentDidMount: ', this) } render() { const wrapperTree = super.render() const newProps = { name: 'NewComponent' } const newTree = React.cloneElement(wrapperTree, newProps, wrapperTree.props.children) console.log('newTree: ', newTree) return newTree } } } // 被獲取ref實例組件 class Main extends Component { render() { return ( Main ) } } export default (InheritanceInversionHOC(Main));
demo代碼可以參考這里
為什么需要用到cloneElement
方法?
因為render函數(shù)內(nèi)實際上是調(diào)用React.creatElement產(chǎn)生的React元素,盡管我們可以拿到這個方法但是無法修改它.可以用getOwnPropertyDescriptors
查看它的配置項
所以用cloneElement創(chuàng)建新的元素替代
相比較屬性繼承來說,后者只能條件性選擇是否渲染WrappedComponent
,但是前者可以更加細粒度劫持渲染元素,可以獲取到 state,props,組件生命周期(component lifecycle)鉤子,以及渲染方法(render)
,但是依舊不能保證WrappedComponent里的子組件
是否渲染,也無法劫持.
注意
- 靜態(tài)屬性失效
因為高階組件返回的已經(jīng)不是原組件了,所以原組件的靜態(tài)屬性方法已經(jīng)無法獲取,除非你主動將它們拷貝到返回組件中 - 渲染機制
因為高階組件返回的是新組件,里面的唯一標志也會變化,所以不建議在render里面也調(diào)用高階組件,這會導(dǎo)致其每次都重新卸載再渲染,即使它可能長得一樣.
所以建議高階組件都是無副作用的純函數(shù),即相同輸入永遠都是相同輸出,不允許任何有可變因素. - 嵌套過深
在原組件中如果包裹層級過多會產(chǎn)生類似回調(diào)地獄的煩惱,難以調(diào)試,可閱讀性糟糕 - 遵守規(guī)則
如果沒有規(guī)范情況下,也可能造成代碼沖突覆蓋的局面
HOOKS
Hooks是React v16.7.0-alpha
中加入的新特性。愛掏網(wǎng) - it200.com它可以讓你在class以外使用state和其他React特性。愛掏網(wǎng) - it200.com
Hooks是可以讓你與React狀態(tài)以及函數(shù)式組件的生命周期特性“掛鉤”的函數(shù)。愛掏網(wǎng) - it200.com鉤子是為了讓你拋棄類使用React的,所以它不能在類中運行,但是可以用在純函數(shù)中,這就解決了一直以來可能因為需要用到生命周期或者react狀態(tài)的時候,你不得不將原本的純函數(shù)代碼整個替換成Class寫法的煩惱.
Hooks也分兩種
State Hook
能夠讓你在不使用Class的情況下使用state和其他的React功能
useState
function Example() { // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); return (You clicked {count} times
); }
等價于下面Class寫法
class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } render() { return (You clicked {this.state.count} times
); } }
demo代碼可以參考這里
從上面可以看出useState實際上就是在state里聲明一個變量并且初始化了一個值而且提供一個可以改變對應(yīng)state的函數(shù).因為在純函數(shù)中沒有this.state.count的這種用法,所以直接使用count替代
上面的count就是聲明的變量,setCount就是改變變量的方法.
需要注意的一點是useState和this.state有點不同,它只有在組件第一次render才會創(chuàng)建狀態(tài),之后每次都只會返回當前的值.
賦值初始值的時候如果需要經(jīng)過某些邏輯處理才能得到的話,可以通過函數(shù)傳遞,例如
const [count, setCount] = useState(() => doSomethings())
如果改變需要根據(jù)之前的數(shù)據(jù)變化,可以通過函數(shù)接收舊數(shù)據(jù),例如
setCount(prevCount => prevCount + 1)
如果是想聲明多個state的時候,就需要使用多次useState
function ExampleWithManyStates() { // Declare multiple state variables! const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]); }
或者通過組合對象一次合并多個數(shù)據(jù)
Effect Hook
執(zhí)行有副作用的函數(shù),你可以把 useEffect
Hooks 視作 componentDidMount
、componentDidUpdate
和 componentWillUnmount
的結(jié)合,useEffect 會在瀏覽器繪制后延遲執(zhí)行,但會保證在任何新的渲染前執(zhí)行。愛掏網(wǎng) - it200.comReact 將在組件更新前刷新上一輪渲染的 effect。愛掏網(wǎng) - it200.comReact 組件中的 side effects 大致可以分為兩種
不需要清理
有時我們想要在 React 更新過 DOM 之后執(zhí)行一些額外的操作。愛掏網(wǎng) - it200.com比如網(wǎng)絡(luò)請求、手動更新 DOM 、以及打印日志都是常見的不需要清理的 effects
class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { console.log(`componentDidMount: You clicked ${this.state.count} times`) } componentDidUpdate() { console.log(`componentDidUpdate: You clicked ${this.state.count} times`) } render() { return (You clicked {this.state.count} times
); } }
如上所示,如果放在render的話在掛載前也會觸發(fā),但是為了避免這個問題我們不得不在兩個生命周期寫同樣的代碼.但是如果我們換成HOOKS的寫法
import { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); return (You clicked {count} times
); }
demo代碼可以參考這里
useEffect 做了什么? 通過這個 Hook,React 知道你想要這個組件在每次 render 之后做些事情。愛掏網(wǎng) - it200.comReact 會記錄下你傳給 useEffect
的這個方法,然后在進行了 DOM 更新之后調(diào)用這個方法。愛掏網(wǎng) - it200.com但我們同樣也可以進行數(shù)據(jù)獲取或是調(diào)用其它必要的 API。愛掏網(wǎng) - it200.com
為什么 useEffect 在組件內(nèi)部調(diào)用? 將 useEffect
放在一個組件內(nèi)部,可以讓我們在 effect 中,即可獲得對 count
state(或其它 props)的訪問,而不是使用一個特殊的 API 去獲取它。愛掏網(wǎng) - it200.com
useEffect 是不是在每次 render 之后都會調(diào)用? 默認情況下,它會在第一次 render 和 之后的每次 update 后運行。愛掏網(wǎng) - it200.comReact 保證每次運行 effects 之前 DOM 已經(jīng)更新了。愛掏網(wǎng) - it200.com
使用上還有哪些區(qū)別"需要清理的-Effect">需要清理的 Effect
比較常見的就類似掛載的時候監(jiān)聽事件或者開啟定時器,卸載的時候就移除.
class Example extends React.Component { constructor(props) { super(props); } componentDidMount() { document.addEventListener('click', this.clickFunc, false) } componentWillUnmount() { document.removeEventListener('click', this.clickFunc) } clickFunc(e) { // doSomethings console.log(e) } render() { return ( ); } }
換成HOOKS寫法類似,只是會返回新的函數(shù)
function Example() { useEffect(() => { document.addEventListener('click', clickFunc, false) return () => { document.removeEventListener('click', clickFunc) } }); function clickFunc(e) { // doSomethings console.log(e) } return ( ); }
demo代碼可以參考這里
我們?yōu)槭裁丛?effect 中返回一個函數(shù) 這是一種可選的清理機制。愛掏網(wǎng) - it200.com每個 effect 都可以返回一個用來在晚些時候清理它的函數(shù)。愛掏網(wǎng) - it200.com這讓我們讓添加和移除訂閱的邏輯彼此靠近。愛掏網(wǎng) - it200.com它們是同一個 effect 的一部分!
React 究竟在什么時候清理 effect? React 在每次組件 unmount 的時候執(zhí)行清理。愛掏網(wǎng) - it200.com然而,正如我們之前了解的那樣,effect 會在每次 render 時運行,而不是僅僅運行一次。愛掏網(wǎng) - it200.com這也就是為什么 React 也 會在執(zhí)行下一個 effect 之前,上一個 effect 就已被清除。愛掏網(wǎng) - it200.com
我們可以修改一下代碼看看effect的運行機制
function Example() { const [count, setCount] = useState(0); useEffect(() => { console.log('addEventListener') document.addEventListener('click', clickFunc, false) return () => { console.log('removeEventListener') document.removeEventListener('click', clickFunc) } }); function clickFunc(e) { // doSomethings console.log(e) setCount(count+1) } return ( ); }
demo代碼可以參考這里
可以看到上面代碼在每次更新都是重新監(jiān)聽,想要避免這種情況可以往下繼續(xù)看.
進階使用
有時候我們可能有多套邏輯寫在不同的生命周期里,如果換成HOOKS寫法的話我們可以按功能劃分使用多個,React將會按照指定的順序應(yīng)用每個effect。愛掏網(wǎng) - it200.com
function Example() { const [count, setCount] = useState(0); useEffect(() => { console.log(`You clicked ${count} times`) }); useEffect(() => { document.addEventListener('click', clickFunc, false) return () => { document.removeEventListener('click', clickFunc) } }); function clickFunc(e) { // doSomethings console.log(e) } return (You clicked {count} times
); }
demo代碼可以參考這里
為什么Effects會在每次更新后執(zhí)行
如果你們以前使用class的話可能會有疑惑,為什么不是在卸載階段執(zhí)行一次.從官網(wǎng)解釋代碼看
componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); }
它在掛載階段監(jiān)聽,移除階段移除監(jiān)聽,每次觸發(fā)就根據(jù)this.props.friend.id
做出對應(yīng)處理.但是這里有個隱藏的bug就是當移除階段的時候獲取的this.props.friend.id
可能是舊的數(shù)據(jù),引起的問題就是卸載時候會使用錯誤的id而導(dǎo)致內(nèi)存泄漏或崩潰,所以在class的時候一般都會在componentDidUpdate
做處理
componentDidUpdate(prevProps) { // Unsubscribe from the previous friend.id ChatAPI.unsubscribeFromFriendStatus( prevProps.friend.id, this.handleStatusChange ); // Subscribe to the next friend.id ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); }
但是如果我們換成HOOKS的寫法就不會有這種bug
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
這是因為HOOKS會在應(yīng)用下一個effects之前清除前一個effects,此行為默認情況下確保一致性,并防止由于缺少更新邏輯而在類組件中常見的錯誤
通過跳過effects提升性能
就在上面我們知道每次render
都會觸發(fā)effects機制可能會有性能方面的問題,在class的寫法里我們可以通過componentDidUpdate
做選擇是否更新
componentDidUpdate(prevProps, prevState) { if (prevState.count !== this.state.count) { document.title = `You clicked ${this.state.count} times`; } }
而在useEffect里我們可以通過傳遞一組數(shù)據(jù)給它作為第二參數(shù),如果在下次執(zhí)行的時候該數(shù)據(jù)沒有發(fā)生變化的話React會跳過當次應(yīng)用
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // Only re-run the effect if count changes
所以上面提到的bug案例可以通過這個方式做解決
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }, [props.friend.id]); // Only re-subscribe if props.friend.id changes
注意
如果你想使用這種優(yōu)化方式,請確保數(shù)組中包含了所有外部作用域中會發(fā)生變化且在 effect 中使用的變量,否則你的代碼會一直引用上一次render的舊數(shù)據(jù).
如果你想要effects只在掛載和卸載時各清理一次的話,可以傳遞一個空數(shù)組
作為第二參數(shù).相當于告訴React你的effects不依賴于任何的props或者state,所以沒必要重復(fù)執(zhí)行.
useCallback
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
返回一個 memoized 回調(diào)函數(shù)。愛掏網(wǎng) - it200.com
把內(nèi)聯(lián)回調(diào)函數(shù)及依賴項數(shù)組作為參數(shù)傳入 useCallback,它將返回該回調(diào)函數(shù)的 memoized 版本,該回調(diào)函數(shù)僅在某個依賴項改變時才會更新。愛掏網(wǎng) - it200.com當你把回調(diào)函數(shù)傳遞給經(jīng)過優(yōu)化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時,它將非常有用。愛掏網(wǎng) - it200.com
useCallback(fn, deps) 相當于 useMemo(() => fn, deps)。愛掏網(wǎng) - it200.com
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState 的替代方案。愛掏網(wǎng) - it200.com它接收一個形如 (state, action) => newState
的 reducer,并返回當前的 state 以及與其配套的 dispatch 方法。愛掏網(wǎng) - it200.com
在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較復(fù)雜且包含多個子值,或者下一個 state 依賴于之前的 state 等。愛掏網(wǎng) - it200.com并且,使用 useReducer 還能給那些會觸發(fā)深更新的組件做性能優(yōu)化,因為你可以向子組件傳遞 dispatch 而不是回調(diào)函數(shù) 。愛掏網(wǎng) - it200.com
const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( Count: {state.count} ); }
demo代碼可以參考這里
從語法上你們會看到還有一個init
的入?yún)?是用來做惰性初始化,將 init 函數(shù)作為 useReducer 的第三個參數(shù)傳入,這樣初始 state 將被設(shè)置為 init(initialArg)
,這么做可以將用于計算 state 的邏輯提取到 reducer 外部,這也為將來對重置 state 的 action 做處理提供了便利
const initialState = 0; function init(initialCount) { return { count: initialCount }; } function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; case 'reset': return init(action.payload); default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState, init); return ( Count: {state.count} ); }
demo代碼可以參考這里
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一個 memoized 值。愛掏網(wǎng) - it200.com
把“創(chuàng)建”函數(shù)和依賴項數(shù)組作為參數(shù)傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。愛掏網(wǎng) - it200.com這種優(yōu)化有助于避免在每次渲染時都進行高開銷的計算。愛掏網(wǎng) - it200.com
記住,傳入 useMemo 的函數(shù)會在渲染期間執(zhí)行。愛掏網(wǎng) - it200.com請不要在這個函數(shù)內(nèi)部執(zhí)行與渲染無關(guān)的操作,諸如副作用這類的操作屬于 useEffect 的適用范疇,而不是 useMemo。愛掏網(wǎng) - it200.com
如果沒有提供依賴項數(shù)組,useMemo 在每次渲染時都會計算新的值。愛掏網(wǎng) - it200.com
useRef
const refContainer = useRef(initialValue);
useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)。愛掏網(wǎng) - it200.com返回的 ref 對象在組件的整個生命周期內(nèi)保持不變。愛掏網(wǎng) - it200.com
function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // `current` 指向已掛載到 DOM 上的文本輸入元素 inputEl.current.focus(); }; return ( ); }
demo代碼可以參考這里
本質(zhì)上,useRef 就像是可以在其 .current 屬性中保存一個可變值的“盒子”。愛掏網(wǎng) - it200.com
你應(yīng)該熟悉 ref 這一種訪問 DOM 的主要方式。愛掏網(wǎng) - it200.com如果你將 ref 對象以
然而,useRef() 比 ref 屬性更有用。愛掏網(wǎng) - it200.com它可以很方便地保存任何可變值,其類似于在 class 中使用實例字段的方式。愛掏網(wǎng) - it200.com
這是因為它創(chuàng)建的是一個普通 Javascript 對象。愛掏網(wǎng) - it200.com而 useRef() 和自建一個 {current: ...} 對象的唯一區(qū)別是,useRef 會在每次渲染時返回同一個 ref 對象。愛掏網(wǎng) - it200.com
請記住,當 ref 對象內(nèi)容發(fā)生變化時,useRef 并不會通知你。愛掏網(wǎng) - it200.com變更 .current 屬性不會引發(fā)組件重新渲染。愛掏網(wǎng) - it200.com如果想要在 React 綁定或解綁 DOM 節(jié)點的 ref 時運行某些代碼,則需要使用回調(diào) ref 來實現(xiàn)。愛掏網(wǎng) - it200.com
HOOKS規(guī)范
在頂層調(diào)用HOOKS
不要在循環(huán),條件,或者內(nèi)嵌函數(shù)中調(diào)用.這都是為了保證你的代碼在每次組件render的時候會按照相同的順序執(zhí)行HOOKS,而這也是能夠讓React在多個useState和useEffect執(zhí)行中正確保存數(shù)據(jù)的原因
只在React函數(shù)調(diào)用HOOKS
- React函數(shù)組件調(diào)用
- 從自定義HOOKS中調(diào)用
可以確保你源碼中組件的所有有狀態(tài)邏輯都是清晰可見的.
自定義HOOKS
我們可以將相關(guān)邏輯抽取出來
function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; }
我必須以“use”開頭為自定義鉤子命名嗎? 這項公約非常重要。愛掏網(wǎng) - it200.com如果沒有它,我們就不能自動檢查鉤子是否違反了規(guī)則,因為我們無法判斷某個函數(shù)是否包含對鉤子的調(diào)用。愛掏網(wǎng) - it200.com
使用相同鉤子的兩個組件是否共享狀態(tài)? 不。愛掏網(wǎng) - it200.com自定義鉤子是一種重用有狀態(tài)邏輯的機制(例如設(shè)置訂閱并記住當前值),但是每次使用自定義鉤子時,其中的所有狀態(tài)和效果都是完全隔離的。愛掏網(wǎng) - it200.com
自定義鉤子如何獲得隔離狀態(tài)? 對鉤子的每個調(diào)用都處于隔離狀態(tài)。愛掏網(wǎng) - it200.com從React的角度來看,我們的組件只調(diào)用useState
和useEffect
。愛掏網(wǎng) - it200.com
問題
Hook 會替代 render props 和高階組件嗎?
通常,render props 和高階組件只渲染一個子節(jié)點。愛掏網(wǎng) - it200.com我們認為讓 Hook 來服務(wù)這個使用場景更加簡單。愛掏網(wǎng) - it200.com這兩種模式仍有用武之地,(例如,一個虛擬滾動條組件或許會有一個 renderItem 屬性,或是一個可見的容器組件或許會有它自己的 DOM 結(jié)構(gòu))。愛掏網(wǎng) - it200.com但在大部分場景下,Hook 足夠了,并且能夠幫助減少嵌套。愛掏網(wǎng) - it200.com
生命周期方法要如何對應(yīng)到 Hook?
- constructor:函數(shù)組件不需要構(gòu)造函數(shù)。愛掏網(wǎng) - it200.com你可以通過調(diào)用
useState
來初始化 state。愛掏網(wǎng) - it200.com如果計算的代價比較昂貴,你可以傳一個函數(shù)給 useState。愛掏網(wǎng) - it200.com - getDerivedStateFromProps:改為在渲染時安排一次更新。愛掏網(wǎng) - it200.com
- shouldComponentUpdate:詳見
React.memo
. - render:這是函數(shù)組件體本身。愛掏網(wǎng) - it200.com
- componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 可以表達所有這些的組合。愛掏網(wǎng) - it200.com
- componentDidCatch and getDerivedStateFromError:目前還沒有這些方法的 Hook 等價寫法,但很快會加上。愛掏網(wǎng) - it200.com
我可以只在更新時運行 effect 嗎?
這是個比較罕見的使用場景。愛掏網(wǎng) - it200.com如果你需要的話,你可以 使用一個可變的 ref 手動存儲一個布爾值來表示是首次渲染還是后續(xù)渲染,然后在你的 effect 中檢查這個標識。愛掏網(wǎng) - it200.com
如何獲取上一輪的 props 或 state?
目前,你可以通過ref來手動實現(xiàn):
function Counter() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); returnNow: {count}, before: {prevCount}
; } function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; }
有類似 forceUpdate 的東西嗎?
如果前后兩次的值相同,useState 和 useReducer Hook 都會放棄更新。愛掏網(wǎng) - it200.com原地修改 state 并調(diào)用 setState 不會引起重新渲染。愛掏網(wǎng) - it200.com
通常,你不應(yīng)該在 React 中修改本地 state。愛掏網(wǎng) - it200.com然而,作為一條出路,你可以用一個增長的計數(shù)器來在 state 沒變的時候依然強制一次重新渲染:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0); function handleClick() { forceUpdate(); }
我該如何測量 DOM 節(jié)點?
要想測量一個 DOM 節(jié)點的位置或是尺寸,你可以使用 callback ref。愛掏網(wǎng) - it200.com每當 ref 被附加到另一個節(jié)點,React 就會調(diào)用 callback。愛掏網(wǎng) - it200.com
function MeasureExample() { const [rect, ref] = useClientRect(); return (Hello, world
{rect !== null && } ); } function useClientRect() { const [rect, setRect] = useState(null); const ref = useCallback(node => { if (node !== null) { setRect(node.getBoundingClientRect()); } }, []); return [rect, ref]; }
demo代碼可以參考這里
使用 callback ref 可以確保 即便子組件延遲顯示被測量的節(jié)點 (比如為了響應(yīng)一次點擊),我們依然能夠在父組件接收到相關(guān)的信息,以便更新測量結(jié)果。愛掏網(wǎng) - it200.com
注意到我們傳遞了 []
作為 useCallback 的依賴列表。愛掏網(wǎng) - it200.com這確保了 ref callback 不會在再次渲染時改變,因此 React 不會在非必要的時候調(diào)用它。愛掏網(wǎng) - it200.com
我該如何實現(xiàn) shouldComponentUpdate
?
你可以用 React.memo 包裹一個組件來對它的 props 進行淺比較:
const Button = React.memo((props) => { // 你的組件 });
React.memo
等效于 PureComponent
,但它只比較 props。愛掏網(wǎng) - it200.com(你也可以通過第二個參數(shù)指定一個自定義的比較函數(shù)來比較新舊 props。愛掏網(wǎng) - it200.com如果函數(shù)返回 true,就會跳過更新。愛掏網(wǎng) - it200.com)
React.memo 不比較 state,因為沒有單一的 state 對象可供比較。愛掏網(wǎng) - it200.com但你也可以讓子節(jié)點變?yōu)榧兘M件,或者 用useMemo
優(yōu)化每一個具體的子節(jié)點。愛掏網(wǎng) - it200.com
如何惰性創(chuàng)建昂貴的對象?
第一個常見的使用場景是當創(chuàng)建初始 state 很昂貴時,為避免重新創(chuàng)建被忽略的初始 state,我們可以傳一個函數(shù)給 useState
,React 只會在首次渲染時調(diào)用這個函數(shù)
function Table(props) { // createRows() 只會被調(diào)用一次 const [rows, setRows] = useState(() => createRows(props.count)); // ... }
你或許也會偶爾想要避免重新創(chuàng)建 useRef()
的初始值。愛掏網(wǎng) - it200.comuseRef 不會像 useState
那樣接受一個特殊的函數(shù)重載。愛掏網(wǎng) - it200.com相反,你可以編寫你自己的函數(shù)來創(chuàng)建并將其設(shè)為惰性的:
function Image(props) { const ref = useRef(null); // IntersectionObserver 只會被惰性創(chuàng)建一次 function getObserver() { let observer = ref.current; if (observer !== null) { return observer; } let newObserver = new IntersectionObserver(onIntersect); ref.current = newObserver; return newObserver; } // 當你需要時,調(diào)用 getObserver() // ... }
Hook 會因為在渲染時創(chuàng)建函數(shù)而變慢嗎?
不會。愛掏網(wǎng) - it200.com在現(xiàn)代瀏覽器中,閉包和類的原始性能只有在極端場景下才會有明顯的差別。愛掏網(wǎng) - it200.com
除此之外,可以認為 Hook 的設(shè)計在某些方面更加高效:
- Hook 避免了 class 需要的額外開支,像是創(chuàng)建類實例和在構(gòu)造函數(shù)中綁定事件處理器的成本。愛掏網(wǎng) - it200.com
- 符合語言習(xí)慣的代碼在使用 Hook 時不需要很深的組件樹嵌套。愛掏網(wǎng) - it200.com這個現(xiàn)象在使用高階組件、render props、和 context 的代碼庫中非常普遍。愛掏網(wǎng) - it200.com組件樹小了,React 的工作量也隨之減少。愛掏網(wǎng) - it200.com
傳統(tǒng)上認為,在 React 中使用內(nèi)聯(lián)函數(shù)對性能的影響,與每次渲染都傳遞新的回調(diào)會如何破壞子組件的 shouldComponentUpdate
優(yōu)化有關(guān)。愛掏網(wǎng) - it200.comHook 從三個方面解決了這個問題。愛掏網(wǎng) - it200.com
- useCallback Hook 允許你在重新渲染之間保持對相同的回調(diào)引用以使得 shouldComponentUpdate 繼續(xù)工作:
- useMemo Hook 使控制具體子節(jié)點何時更新變得更容易,減少了對純組件的需要。愛掏網(wǎng) - it200.com
- 最后,useReducer Hook 減少了對深層傳遞回調(diào)的需要,就如下面解釋的那樣。愛掏網(wǎng) - it200.com
如何避免向下傳遞回調(diào)?
在大型的組件樹中,我們推薦的替代方案是通過 context
用 useReducer
往下傳一個 dispatch
函數(shù):
const TodosDispatch = React.createContext(null); function TodosApp() { // 提示:`dispatch` 不會在重新渲染之間變化 const [todos, dispatch] = useReducer(todosReducer); return (); }
TodosApp 內(nèi)部組件樹里的任何子節(jié)點都可以使用 dispatch 函數(shù)來向上傳遞 actions
function DeepChild(props) { // 如果我們想要執(zhí)行一個 action,我們可以從 context 中獲取 dispatch。愛掏網(wǎng) - it200.com const dispatch = useContext(TodosDispatch); function handleClick() { dispatch({ type: 'add', text: 'hello' }); } return ( ); }
總而言之,從維護的角度來這樣看更加方便(不用不斷轉(zhuǎn)發(fā)回調(diào)),同時也避免了回調(diào)的問題。愛掏網(wǎng) - it200.com像這樣向下傳遞 dispatch 是處理深度更新的推薦模式。愛掏網(wǎng) - it200.com
React 是如何把對 Hook 的調(diào)用和組件聯(lián)系起來的?
React 保持對當先渲染中的組件的追蹤。愛掏網(wǎng) - it200.com多虧了 Hook 規(guī)范,我們得知 Hook 只會在 React 組件中被調(diào)用(或自定義 Hook —— 同樣只會在 React 組件中被調(diào)用)。愛掏網(wǎng) - it200.com
每個組件內(nèi)部都有一個「記憶單元格」列表。愛掏網(wǎng) - it200.com它們只不過是我們用來存儲一些數(shù)據(jù)的 JavaScript 對象。愛掏網(wǎng) - it200.com當你用 useState() 調(diào)用一個 Hook 的時候,它會讀取當前的單元格(或在首次渲染時將其初始化),然后把指針移動到下一個。愛掏網(wǎng) - it200.com這就是多個 useState() 調(diào)用會得到各自獨立的本地 state 的原因。愛掏網(wǎng) - it200.com