React Hooks核心原理深入分析講解
React Hooks已經(jīng)推出一段時間,大家應(yīng)該比較熟悉,或者多多少少在項目中用過。寫這篇文章簡單分析一下Hooks的原理,并帶大家實現(xiàn)一個簡易版的Hooks。
這篇寫的比較細(xì),相關(guān)的知識點都會解釋,給大家刷新一下記憶。
Hooks
Hooks是React 16.8推出的新功能。以這種更簡單的方式進(jìn)行邏輯復(fù)用。之前函數(shù)組件被認(rèn)為是無狀態(tài)的。但是通過Hooks,函數(shù)組件也可以有狀態(tài),以及類組件的生命周期方法。
useState用法示例:
import React, { useState } from 'react'; function Example() { // count是組件的狀態(tài) const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
閉包
開始之前,我們來簡單回顧一下閉包的概念,因為Hooks的實現(xiàn)是高度依賴閉包的。
閉包(Closure),Kyle Simpson在《你不知道的Javascript》中總結(jié)閉包是:
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
閉包就是,函數(shù)可以訪問到它所在的詞法作用域,即使是在定義以外的位置調(diào)用。
閉包的一個重要應(yīng)用就是,實現(xiàn)內(nèi)部變量/私有數(shù)據(jù)。
var counter = 0; // 給計數(shù)器加1 function add() { counter += 1; } // 調(diào)用 add() 3次 add(); // 1 add(); // 2 counter = 1000; add(); // 1003
這里因為counter不是內(nèi)部變量,所以誰都能修改它的值。我們不想讓人隨意修改counter怎么辦?這時候就可以用閉包:
function getAdd() { var counter = 0; return function add() {counter += 1;} } var add = getAdd(); add(); // 1 add(); // 2 add(); // 3 counter = 1000 // error! 當(dāng)前位置無法訪問counter
我們還可以把函數(shù)的定義挪到調(diào)用的位置,用一個立即執(zhí)行函數(shù)表達(dá)式IIFE(Immediately Invoked Function Expression):
var add = (function getAdd() { var counter = 0; return function add() {counter += 1;} })(); add(); // 1 add(); // 2 add(); // 3
這種通過IIFE創(chuàng)建閉包的方式也叫做模塊模式(Module Pattern),它創(chuàng)建了一個封閉的作用域,只有通過返回的對象/方法來操縱作用域中的值。這個模式由來已久了,之前很多Javascript的庫,比如jQuery,就是用它來導(dǎo)出自己的實例的。
開始動手實現(xiàn)
理清閉包的概念后可以著手寫了。從簡單的入手,先來實現(xiàn)setState。
function useState(initialValue) { var _val = initialValue; // _val是useState的變量 function state() { // state是一個內(nèi)部函數(shù),是閉包 return _val; } function setState(newVal) { _val = newVal; } return [state, setState]; } var [foo, setFoo] = useState(0); console.log(foo()); // 0 setFoo(1); console.log(foo()) // 1
根據(jù)useState的定義來實現(xiàn)。比較簡單不需要多解釋。
將useState應(yīng)用到組件中
現(xiàn)在我們將這個簡易版的useState應(yīng)用到一個Counter組件中:
function Counter() { const [count, setCount] = useState(0); return { click: () => setCount(count() + 1), render: () => console.log('render:', { count: count() }) } } const C = Counter(); C.render(); // render: { count: 0 } C.click(); C.render(); // render: { count: 1 }
這里簡單起見,就不render真實DOM了,因為我們只關(guān)心組件的狀態(tài),所以每次render的時候打印count的值。
這里點擊click之后,counter的值加一,useState的基本功能實現(xiàn)了。但現(xiàn)在state是一個函數(shù)而不是一個變量,這和React的API不一致,接下來我們就來改正這一點。
過期閉包
function useState(initialValue) { var _val = initialValue // 去掉了state()函數(shù) function setState(newVal) { _val = newVal } return [_val, setState] //直接返回_val } var [foo, setFoo] = useState(0) console.log(foo) // 0 setFoo(1) // 更新_val console.log(foo) // 0 - BUG!
如果我們直接把state從函數(shù)改成變量,問題就出現(xiàn)了,state不更新了。無論點擊幾次,Counter的值始終不變。這個是過期閉包問題(Stale Closure Problem)。因為在useState返回的時候,state就指向了初始值,所以后面即使counter的值改變了,打印出來的仍然就舊值。我們想要的是,返回一個變量的同時,還能讓這個變量和真實狀態(tài)同步。那如何來實現(xiàn)呢?
模塊模式
解決辦法就是將閉包放在另一個閉包中。
const MyReact = (function() { let _val //將_val提升到外層閉包 return { render(Component) { const Comp = Component() Comp.render() return Comp }, useState(initialValue) { _val = _val || initialValue //每次刷新 function setState(newVal) { _val = newVal } return [_val, setState] } } })()
我們運用之前提到的模塊模式,創(chuàng)建一個MyReact模塊(第一層閉包),返回的對象中包含useState方法(第二層閉包)。useState返回值中的state,指向的是useState閉包中的_val,而每次調(diào)用useState,_val都會重新綁定到上層的_val上,保證返回的state的值是最新的。解決了過期閉包的問題。
MyReact還提供了另外一個方法render,方法中調(diào)用組件的render方法來“渲染”組件,也是為了不渲染DOM的情況下進(jìn)行測試。
function Counter() { const [count, setCount] = MyReact.useState(0) return { click: () => setCount(count + 1), render: () => console.log('render:', { count }) } } let App App = MyReact.render(Counter) // render: { count: 0 } App.click() App = MyReact.render(Counter) // render: { count: 1 }
這里每次調(diào)用MyReact.render(Counter),都會生成新的Counter實例,調(diào)用實例的render方法。render方法中調(diào)用了MyReact.useState()。MyReact.useState()在多次執(zhí)行之間,外層閉包中的_val值保持不變,所以count會綁定到當(dāng)前的_val上,這樣就可以打印出正確的count值了。
實現(xiàn)useEffect
實現(xiàn)了useState之后,接下來實現(xiàn)useEffect。
const MyReact = (function() { let _val, _deps // 將狀態(tài)和依賴數(shù)組保存到外層的閉包中 return { render(Component) { const Comp = Component() Comp.render() return Comp }, useEffect(callback, depArray) { const hasNoDeps = !depArray const hasChangedDeps = _deps ? !depArray.every((el, i) => el === _deps[i]) : true if (hasNoDeps || hasChangedDeps) { callback() _deps = depArray } }, useState(initialValue) { _val = _val || initialValue function setState(newVal) { _val = newVal } return [_val, setState] } } })() // usage function Counter() { const [count, setCount] = MyReact.useState(0) MyReact.useEffect(() => { console.log('effect', count) }, [count]) return { click: () => setCount(count + 1), noop: () => setCount(count), render: () => console.log('render', { count }) } } let App App = MyReact.render(Counter) // effect 0 // render {count: 0} App.click() App = MyReact.render(Counter) // effect 1 // render {count: 1} App.noop() App = MyReact.render(Counter) // // 沒有執(zhí)行effect // render {count: 1} App.click() App = MyReact.render(Counter) // effect 2 // render {count: 2}
在MyReact.useEffect中,我們將依賴數(shù)組保存到_deps,每次調(diào)用,都和前一次的依賴數(shù)組進(jìn)行比對。發(fā)生變化才觸發(fā)回調(diào)。
注意這里在比較依賴時用的是Object.is, React在比較state變化時也是用它。注意Object.is在比較時不會做類型轉(zhuǎn)換(和==不同)。另外NaN === NaN返回false,但是Object.is(NaN, NaN)會返回true。
(簡單起見,我們實現(xiàn)的useEffect,回調(diào)函數(shù)是同步執(zhí)行的,所以打印出來的log是effect先執(zhí)行,然后才是render。實際React中useEffect的回調(diào)函數(shù)應(yīng)該是異步執(zhí)行的)
支持多個Hooks
到此為止我們已經(jīng)簡單實現(xiàn)了useState和useEffect。但還有一個問題,就是useState和useEffect每個組件中只能用一次。
那么怎么才能支持使用多次hooks呢,我們可以將hooks保存到一個數(shù)組中。
const MyReact = (function() { let hooks = [], currentHook = 0 // 存儲hooks的數(shù)組,和數(shù)組指針 return { render(Component) { const Comp = Component() // 執(zhí)行effect Comp.render() currentHook = 0 // 每次render后,hooks的指針清零 return Comp }, useEffect(callback, depArray) { const hasNoDeps = !depArray const deps = hooks[currentHook] const hasChangedDeps = deps ? !depArray.some((el, i) => !Object.is(el, deps[i])) : true if (hasNoDeps || hasChangedDeps) { callback() hooks[currentHook] = depArray } currentHook++ // 每調(diào)用一次指針加一 }, useState(initialValue) { hooks[currentHook] = hooks[currentHook] || initialValue const setStateHookIndex = currentHook // 注意??這句不是沒用。是避免過期閉包問題。 const setState = newState => (hooks[setStateHookIndex] = newState) return [hooks[currentHook++], setState] } } })()
注意這里用了一個新的變量setStateHookIndex來保存currentHook的值。這是為了避免useState閉包包住舊的currentHook的值。
將改動應(yīng)用到組件中:
function Counter() { const [count, setCount] = MyReact.useState(0) const [text, setText] = MyReact.useState('foo') // 第二次用了useState MyReact.useEffect(() => { console.log('effect', count, text) }, [count, text]) return { click: () => setCount(count + 1), type: txt => setText(txt), noop: () => setCount(count), render: () => console.log('render', { count, text }) } } let App App = MyReact.render(Counter) // effect 0 foo // render {count: 0, text: 'foo'} App.click() App = MyReact.render(Counter) // effect 1 foo // render {count: 1, text: 'foo'} App.type('bar') App = MyReact.render(Counter) // effect 1 bar // render {count: 1, text: 'bar'} App.noop() App = MyReact.render(Counter) // // 不運行effect // render {count: 1, text: 'bar'} App.click() App = MyReact.render(Counter) // effect 2 bar // render {count: 2, text: 'bar'}
實現(xiàn)多個hooks支持的基本思路,就是用一個數(shù)組存放hooks。每次使用hooks時,將hooks指針加1。每次render以后,將指針清零。
Custom Hooks
接下來,可以借助已經(jīng)實現(xiàn)的hooks繼續(xù)實現(xiàn)custom hooks:
function Component() { const [text, setText] = useSplitURL('www.google.com') return { type: txt => setText(txt), render: () => console.log({ text }) } } function useSplitURL(str) { const [text, setText] = MyReact.useState(str) const masked = text.split('.') return [masked, setText] } let App App = MyReact.render(Component) // { text: [ 'www', 'google', 'com' ] } App.type('www.reactjs.org') App = MyReact.render(Component) // { text: [ 'www', 'reactjs', 'org' ] }}
重新理解Hooks規(guī)則
了解Hooks的實現(xiàn)可以幫助我們理解Hooks的使用規(guī)則。還記得使用Hooks的原則嗎?hooks只能用到組件最外層的代碼中,不能包裹在if或者循環(huán)里,原因是在React內(nèi)部,通過數(shù)組來存儲hooks。所以必須保證每次render,hooks的順序不變,數(shù)量不變,才能做deps的比對。
到此這篇關(guān)于React Hooks核心原理深入分析講解的文章就介紹到這了,更多相關(guān)React Hooks內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React語法中設(shè)置TS校驗規(guī)則的步驟詳解
這篇文章主要給大家介紹了React語法中如何設(shè)置TS校驗規(guī)則,文中有詳細(xì)的代碼示例供大家參考,對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-10-10React+Typescript創(chuàng)建項目的實現(xiàn)步驟
通過React組件庫和TypeScript的強類型特性,開發(fā)者可以創(chuàng)建出具有優(yōu)秀用戶體驗和穩(wěn)定性的Web應(yīng)用程序,本文主要介紹了React+Typescript創(chuàng)建項目的實現(xiàn)步驟,感興趣的可以了解一下2023-08-08使用?React?Hooks?重構(gòu)類組件的示例詳解
這篇文章主要介紹了如何使用?React?Hooks?重構(gòu)類組件,本文就來通過一些常見示例看看如何使用 React Hooks 來重構(gòu)類組件,需要的朋友可以參考下2022-07-07ahooks useVirtualList 封裝虛擬滾動列表
這篇文章主要為大家介紹了ahooks useVirtualList 封裝虛擬滾動列表詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09