/* * @Descripttion: React hook 以及 React Fiber 原理 * @version: * @Author: lhl * @Date: 2021-03-10 15:42:23 * @LastEditors: lhl * @LastEditTime: 2021-03-23 15:59:18 */
// hook使用規則 // 只在最頂層使用 Hook // 不要在循環,條件或嵌套函數中調用 Hook, 確保總是在你的 React 函數的最頂層調用他們 // 只在 React 函數中調用 Hook 不要在普通的 JavaScript 函數中調用 Hoo // 在 React 的函數組件中調用 Hook 在自定義 Hook 中調用其他 Hook // npm install eslint-plugin-react-hooks --save-dev ESLint 插件來強制執行這兩條規則 // ESLint 配置 /* { "plugins": [ // ... "react-hooks" ], "rules": { // ... "react-hooks/rules-of-hooks": "error", // 檢查 Hook 的規則 "react-hooks/exhaustive-deps": "warn" // 檢查 effect 的依賴 } } */
// 為什么選擇使用 Hook /** * 1.在組件之間復用狀態邏輯很難 * 2.復雜組件變得難以理解 * 3.用更少的代碼,實現同樣的效果 */
// React Hooks的幾個常用鈎子 // useCallback 的功能完全可以由 useMemo 所取代 // useCallback(fn, inputs) === useMemo(() => fn, inputs) /** 1.useState() // 狀態鈎子 2.useContext() // 共享狀態鈎子 3.useReducer() // action 鈎子 4.useEffect() // 副作用鈎子 useEffect在瀏覽器渲染完成后執行 5.useCallback() // 記憶函數 6.useMemo() // 記憶組件 7.useRef() // 保存引用值 8.useImperativeHandle() // 穿透 Ref 9.useLayoutEffect() // 同步執行副作用 不常用 useLayoutEffect在瀏覽器渲染前執行 10.自定義 hook // 以useXX開頭的 封裝重復使用的代碼提高復用性 * */ import React, { useState, useEffect, useRef, useReducer, useCallback, useMemo, forwardRef, useImperativeHandle, Component } from 'react'; const HookComp = () => { const size = useChangeSize(); const [count, setCount] = useState(0); const [title, setTitle] = useState('標題') let timer = useRef(); const ref = useRef(); // useEffect 就是一個 Effect Hook,給函數組件增加了操作副作用的能力。
// 它跟 class 組件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不過被合並成了一個 API
// 相當於 componentDidMount 和 componentDidUpdate:
useEffect(() => { timer.current = setInterval(() => { console.log(1) },1000) document.title = `You clicked ${count} times`; return () => { clearInterval(timer.current) //相當於 componentWillUnmount
} },[count]); useEffect(() => { ref.current.open() },[]) return ( <div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}> Click me </button>
<input type="text" value={title} onChange={e => setTitle(e.target.value)} />
<p>{title}</p>
<div>頁面大小{size.width}*****{size.height}</div>
<TimeTest/>
<TestRef ref={ref}></TestRef>
<TestRef1 ref={ref}></TestRef1>
<BookkList/>
</div>
) } // useReducer 用法
function BookkList() { const inputRef = useRef(); const [items, dispatch] = useReducer((state,action)=> { console.log(state,action) switch(action.type){ case 'add': return [ ...state, { id: state.length, name: action.name } ] case 'del': return state.filter((_,index) => index != action.index) } },[]) function handleAdd(event){ event.preventDefault(); dispatch({ type:'add', name:inputRef.current.value }); inputRef.current.value = ''; } return ( <>
<form>
<input ref={inputRef}/>
<button onClick={ handleAdd }>點擊添加</button>
</form>
<ul> { items.map((item, index) => ( <li key={item.id}> {item.name} <button onClick={ () => dispatch({type:'del',index})}>點擊刪除</button>
</li>
)) } </ul>
</>
) } /* useRef是一個方法,且useRef返回一個可變的ref對象 修改 ref 的值是不會引發組件的重新 render useRef非常常用的一個操作,訪問DOM節點,對DOM進行操作,監聽事件等等 ref 在所有 render 都保持着唯一的引用,因此所有的對 ref 的賦值或者取值拿到的都是一個 最終 的狀態,而不會存在隔離 使用 useRef 來跨越渲染周期存儲數據,而且對它修改也不會引起組件渲染 createRef 與 useRef 的區別: createRef 每次渲染都會返回一個新的引用,而 useRef 每次都會返回相同的引用。 forwardRef是用來解決HOC組件傳遞ref的問題的 */ const TestRef = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ open() { alert("ref") } })) return ( <div>useImperativeHandle 穿透 ref的使用</div>
) }) // 等同於 => // useImperativeHandle(ref,createHandle,[deps])可以自定義暴露給父組件的實例值。如果不使用, // 父組件的 ref(chidlRef) 訪問不到任何值(childRef.current==null) // useImperativeHandle應該與forwradRef搭配使用 // React.forwardRef會創建一個React組件,這個組件能夠將其接受的ref屬性轉發到其組件樹下的另一個組件中。 // React.forward接受渲染函數作為參數,React將使用prop和ref作為參數來調用此函數
const ChildComponent = (props, ref) => { useImperativeHandle(ref, () => ({ open() { alert("ref") } })); return <h3>useImperativeHandle 穿透 ref的使用</h3>;
}; const TestRef1 = forwardRef(ChildComponent); function TimeTest(){ const [count, setCount] = useState(0); const preCount = usePreVal(count) // 使用自定義hook
const doubleCount = useMemo(() => { return 2 * count; }, [count]); const timerID = useRef(); useEffect(() => { timerID.current = setInterval(()=>{ setCount(count => count + 1); }, 1000); }, []); useEffect(()=>{ if(count > 10){ console.log('大於10定時器不再走了') clearInterval(timerID.current); } }); return ( <>
<button ref={timerID} onClick={() => {setCount(count + 1)}}> Count: {count} === double: {doubleCount} === preCount:{preCount} </button>
</>
); } // 使用 const preState = usePreVal(state) 獲取上一個值 // useRef 不僅僅是用來管理 DOM ref 的,它還相當於 this , 可以存放任何變量
function usePreVal(state){ const ref = useRef(); useEffect(() => { ref.current = state }) return ref.current } // 自定義hooks,用use開頭命名,封裝重復使用的代碼,在多個場景下使用,提高代碼的復用性 // react hook監聽窗口大小
function useChangeSize(){ const win = { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight } const [size, setSize] = useState(win) // useCallback緩存方法 [] 只執行一次
const onResize = useCallback(()=>{ console.log('111') setSize(win) },[]) useEffect(() => { window.addEventListener('resize',onResize) // 銷毀時
return () => { window.removeEventListener('resize',onResize) } },[onResize]) return size } export default HookComp /** * 復用一個有狀態的組件引發的思考: * HOC【高階組件: 一個函數接受一個組件作為參數,經過一系列加工后,最后返回一個新的組件】使用的問題: 嵌套地獄,每一次HOC調用都會產生一個組件實例 可以使用類裝飾器緩解組件嵌套帶來的可維護性問題,但裝飾器本質上還是HOC 包裹太多層級之后,可能會帶來props屬性的覆蓋問題 Render Props【渲染屬性】: 數據流向更直觀了,子孫組件可以很明確地看到數據來源 但本質上Render Props是基於閉包實現的,大量地用於組件的復用將不可避免地引入了callback hell問題 丟失了組件的上下文,因此沒有this.props屬性,不能像HOC那樣訪問this.props.children * 涉及優化對比 函數組件 hook 和 class 【類】組件、函數組件 useCallback 和 useMemo 區別 useCallback(fn, inputs) is equivalent to useMemo(() => fn, inputs) useCallback和useMemo的參數跟useEffect一致 useMemo和useCallback都會在組件第一次渲染的時候執行,之后會在其依賴的變量發生改變時再次執行;並且這兩個hooks都返回緩存的值, useMemo返回緩存的變量,useCallback返回緩存的函數。 * 唯一的區別是:useCallback 不會執行第一個參數函數,而是將它返回給你,而 useMemo 會執行第一個函數並且將函數執行結果返回給你 demo: useMemo 的用法 export default function WithMemo() { const [count, setCount] = useState(1); const [val, setValue] = useState(''); const countSum = useMemo(() => { console.log('compute'); let sum = 0; for (let i = 0; i < count * 100; i++) { sum += i; } return sum; }, [count]); return <div> <h4>{count}-{countSum}</h4> {val} <div> <button onClick={() => setCount(count + 1)}>+c1</button> <input value={val} onChange={event => setValue(event.target.value)}/> </div> </div>; } demo: useCallback 的用法 function Parent() { const [count, setCount] = useState(0); const [val, setVal] = useState(''); const callback = useCallback(() => { return count; }, [count]); return <div> <p>{count}</p> <Child callback={callback}/> <div> <input value={val} onChange={event => setVal(event.target.value)}/> <button onClick={() => setCount(count + 1)}>add</button> </div> </div>; } function Child({ callback }) { const [count, setCount] = useState(() => callback()); useEffect(() => { setCount(callback()); }, [callback]); return <div> {count} </div> } 使用場景是:有一個父組件,其中包含子組件,子組件接收一個函數作為props;通常而言, 如果父組件更新了,子組件也會執行更新;但是大多數場景下,更新是沒有必要的,我們可以借助useCallback來返回函數, 然后把這個函數作為props傳遞給子組件;這樣,子組件就能避免不必要的更新 所有依賴本地狀態或props來創建函數,需要使用到緩存函數的地方,都是useCallback的應用場景 * * 和 函數組件 class【類】 組件對比 * import React, { PureComponent } from 'react' * import React, { Component } from 'react' * React.memo(comp) 是React v16.6引進來的新屬性 * React.memo會返回一個純化(purified)的組件MemoFuncComponent * 當組件的參數props和狀態state發生改變時,React將會檢查前一個狀態和參數是否和下一個狀態和參數是否相同,如果相同,組件將不會被渲染,如果不同,組件將會被重新渲染 * 它的作用和React.PureComponent類似,是用來控制函數組件的重新渲染的。React.memo(...) 其實就是函數組件的 React.PureComponent * * demo: * class: Component 需要判斷何時渲染 何時不會進行組件的重新渲染 * shouldComponentUpdate(nextProps, nextState) { if (this.state.count === nextState.count) { return false } return true } * const Funcomponent = ()=> { return ( <div> Hiya!! I am a Funtional component </div> ) } const MemodFuncComponent = React.memo(FunComponent) React.PureComponent是給ES6的類組件使用的 React.memo(comp)是給函數組件使用的 React.PureComponent減少ES6的類組件的無用渲染 React.memo(comp)減少函數組件的無用渲染 繼承PureComponent時,不能再重寫shouldComponentUpdate useEffect、useMemo、useCallback都是自帶閉包的。每一次組件的渲染,其都會捕獲當前組件函數上下文中的狀態(state, props), 所以每一次這三種hooks的執行,反映的也都是當前的狀態,你無法使用它們來捕獲上一次的狀態。 對於這種情況,應該使用ref來訪問。 依賴為 [] 時: re-render 不會重新執行 effect 函數 沒有依賴:re-render 會重新執行 effect 函數 useLayoutEffect 和 componentDidMount 和 componentDidUpdate 觸發時機一致(都在在DOM修改后且瀏覽器渲染之前); useLayoutEffect 要比 useEffect 更早的觸發執行; useLayoutEffect 會阻塞瀏覽器渲染,切記執行同步的耗時操作 關於class 組件的 createRef class RefTest extends React.Component{ constructor(props){ super(props); this.myRef = React.createRef(); } componentDidMount(){ console.log(this.myRef.current); } render(){ return <input ref={this.myRef}/> } } * */
// React 組件種類 // 1 Es5原生方式React.createClass定義的組件 // const Eg1 = React.createClass({ // getInitialState:function(){ // return { // name:'react' // }; // }, // render:function(){ // return <div onClick={this._ClickEvent}>{ this.state.name }</div> // }, // _ClickEvent:function(){ // console.log(`事件`) // } // })
// 2 無狀態組件 【功能組件】
function Eg2(props){ return <div>{ props.name }</div>
} // 3 類組件 【有狀態組件 or 容器組件】
class Eg3 extends Component { constructor(props){ super(props) this.state = { } } render() { return ( <div>
</div>
) } } // 4 渲染組件 與 無狀態組件類似
const Eg4 = (props) => { return ( <div>{ props.name }</div>
) } // 5 高階組件 HOC 【不要在 render 方法中使用 HOC】 // 一個高階組件是一個函數,它輸入一個組件,然后返回一個新的組件 const EnhancedComponent = higherOrderComponent(WrappedComponent);
function Eg5(WrappedComponent) { // HOC設置顯示名稱
Eg5.displayName = `Eg5(${getDisplayName(WrappedComponent)})`; return class extends React.Component { componentDidUpdate(prevProps) { console.log('Current props: ', this.props); console.log('Previous props: ', prevProps); } render() { return <WrappedComponent {...this.props} />;
} } } // HOC設置顯示名稱
function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || "Component"; } // 6 function + hook 組件
/** * React Fiber 【Fiber 架構就是用 異步的方式解決舊版本 同步遞歸導致的性能問題】 舊版 React 通過遞歸的方式進行渲染,使用的是 【JS引擎自身的函數調用棧】,它會一直執行到棧空為止。 Fiber實現了自己的【組件調用棧】,它以鏈表的形式遍歷組件樹,可以靈活的暫停、繼續和丟棄執行的任務。 實現方式是使用了瀏覽器的 requestIdleCallback 這一 API: 將 計算任務 分給成一個個小任務,分批完成,在完成一個小任務后,將控制權還給瀏覽器,讓瀏覽器利用間隙進行 UI 渲染 React Fiber 出現的背景: 當頁面元素很多,且需要頻繁刷新的場景下,瀏覽器頁面會出現卡頓現象,原因是因為 計算任務 持續占據着主線程,從而阻塞了 UI 渲染 React 框架內部的運作可以分為 3 層 Virtual DOM 層 --> 描述頁面長什么樣 Reconciler 層 --> 負責調用組件生命周期方法,進行 Diff 運算等。 Renderer 層 --> 根據不同的平台,渲染出相應的頁面,比較常見的是 ReactDOM 和 ReactNative * * Fiber 其實指的是一種數據結構,它可以用一個純 JS 對象來表示 * * const fiber = { stateNode, // 節點實例 child, // 子節點 sibling, // 兄弟節點 return, // 父節點 } Fiber Reconciler 的 2 個階段: 階段一,生成 Fiber 樹【本質來說是一個鏈表】,得出需要更新的節點信息。這一步是一個漸進的過程,可以被打斷。 【讓優先級更高的任務先執行,從框架層面大大降低了頁面掉幀的概率】 階段二,將需要更新的節點一次過批量更新,這個過程不能被打斷 階段一有兩顆樹,Virtual DOM 樹和 Fiber 樹,Fiber 樹是在 Virtual DOM 樹的基礎上通過額外信息生成的。 它每生成一個新節點,就會將控制權還給瀏覽器,如果瀏覽器沒有更高級別的任務要執行,則繼續構建;反之則會丟棄 正在生成的 Fiber 樹,等空閑的時候再重新執行一遍 【V16版本之前】棧調和(Stack reconciler) --> 【V16版本之后】 Fiber reconciler React diff 將傳統 diff 算法的復雜度 O(n^3) 復雜度的問題轉換成 O(n) 復雜度的問題 React Diff三大策略: 【將 Virtual DOM 樹轉換成 actual DOM 樹的最少操作的過程稱為 協調(Reconciliaton)】 1.tree diff: 【Web UI中DOM節點跨層級的移動操作特別少,可以忽略不計】 React對 Virtual DOM 樹進行層級控制,只會對相同層級的DOM節點進行比較,即同一個父元素下的所有子節點, 當發現節點已經不存在了,則會刪除掉該節點下所有的子節點,不會再進行比較。 這樣只需要對DOM樹進行一次遍歷,就可以完成整個樹的比較。復雜度變為O(n); 2.component diff: 【擁有相同類的兩個組件 生成相似的樹形結構,擁有不同類的兩個組件 生成不同的樹形結構】 3.element diff: 【對於同一層級的一組子節點,通過唯一id區分】 當節點屬於同一層級:插入、移動、刪除 【設置唯一 key的策略】 */
未經允許,請勿隨意轉載!!謝謝合作!!!