React 進(jìn)階必備:從函數(shù)式組件看 Hooks 設(shè)計(jì)原理(收藏?。?/h1>
從函數(shù)式說起
React 在現(xiàn)有的三大主流框架中是非?!昂瘮?shù)式”的語言,小到 setState,render 函數(shù)的設(shè)計(jì),大到函數(shù)組件,周邊組件 Redux 等,都蘊(yùn)含了一定的函數(shù)式風(fēng)格。因此要了解 React,我們就需要了解一定的函數(shù)式編程。
函數(shù)式編程作為聲明式編程的一種形式,與命令式編程相對,來源于范疇論,最早是為了解決數(shù)學(xué)問題而誕生。范疇論認(rèn)為,同一個(gè)范疇的所有成員,就是不同狀態(tài)的"變形",通過態(tài)射,一個(gè)成員可以變形成另一個(gè)成員。

我們可以把物件理解為集合,態(tài)射理解為函數(shù),通過函數(shù),來規(guī)定范疇中成員之間的關(guān)系,函數(shù)扮演管道的角色,一個(gè)值進(jìn)去,一個(gè)新值出來,沒有其他副作用,也就是所謂的 y = f(x)。
函數(shù)式風(fēng)格包含了多種特性,典型的如函數(shù)一等公民、純函數(shù)、副作用、柯里化、組合等,這里我們主要以基礎(chǔ)的純函數(shù)和副作用做進(jìn)一步解釋。
純函數(shù)
純函數(shù) ——輸入輸出數(shù)據(jù)流全是顯式的。
顯式的意思是,函數(shù)與外界交換數(shù)據(jù)只有一個(gè)唯一渠道——參數(shù)和返回值;函數(shù)從函數(shù)外部接受的所有輸入信息都通過參數(shù)傳遞到該函數(shù)內(nèi)部;函數(shù)輸出到函數(shù)外部的所有信息都通過返回值傳遞到該函數(shù)外部。
相同的輸入,永遠(yuǎn)得到相同的輸出,而且沒有任何副作用,與環(huán)境變量無關(guān),可以在任意地方調(diào)用。
//splice是不純的函數(shù)
let arr = [1,2,3,4,5];
arr.splice(0,3); //[1,2,3]
arr.splice(0,3); //[4,5]
//slice是純函數(shù)
arr = [1,2,3,4,5];
arr.slice(0,3); //[1,2,3]
arr.slice(0,3); //[1,2,3]
副作用
在計(jì)算機(jī)科學(xué)中,函數(shù)副作用指當(dāng)調(diào)用函數(shù)時(shí),除了返回函數(shù)值之外,還對主調(diào)用函數(shù)產(chǎn)生附加的影響。例如修改全局變量(函數(shù)外的變量),修改參數(shù)或改變外部存儲(chǔ)。
典型的副作用:
發(fā)送一個(gè)http請求
new Date() / Math.random();
console.log / IO
DOM查詢 (外部數(shù)據(jù))
然而,只有純函數(shù)而無副作用的程序,在程序運(yùn)行完畢后,不留一絲痕跡,僅僅只是空耗 CPU 而已,因此,副作用是必要的。在函數(shù)式語言中,為了保證函數(shù)的盡可能純粹性,副作用使用函子進(jìn)行統(tǒng)一管理。這里我們不去深究函子的原理,我們需要記住的是:盡可能保持函數(shù)的純粹性,將副作用收攏統(tǒng)一管理。
追溯歷史 - Hook 誕生背景
React 組件
React 組件根據(jù)書寫形式分為函數(shù)組件和類組件。
class ComponentA extends React.Component {
constructor(props) {
super(props);
this.state = { displayContent: 'Hello World' }
}
render() {
return <div>{this.state.displayContent}</div>
}
}
function ComponentFunctionA = (props) => <div>{props.displayContent}</div>
函數(shù)組件定位為展示組件,自身無狀態(tài),無生命周期。
類組件定位為容器組件,自身管理狀態(tài)與生命周期。
問題
函數(shù)組件想管理狀態(tài)、生命周期
隨著需求變更,在未作出良好設(shè)計(jì)的情況下。常會(huì)出現(xiàn)本是函數(shù)的組件處于功能內(nèi)聚性等因素想管理狀態(tài)的情況。
解決方案:
改為類組件
提升狀態(tài)至上層容器
然而,改為類組件需要一定的人力成本;而數(shù)據(jù)提升至上層容器,既有可能會(huì)增加 React 組件層級結(jié)構(gòu),又喪失了組件功能的內(nèi)聚性,都不能認(rèn)為是良好的解決方案。
類組件生命周期帶來邏輯分離
class DemoA extends React.PureComponent {
constructor(props) {
super(props);
this.listener = () => {/* do something */};
}
componentDidMount() {
document.addEventListener('click', this.listener);
}
componentWillUnmount() {
document.removeEventListener('click', this.listener);
}
}
這里只是一個(gè)簡單的例子,展示了由于生命周期的存在,需要我們將一個(gè)事件的監(jiān)聽和取消邏輯分別置于不同生命周期內(nèi)。一旦邏輯復(fù)雜,極易導(dǎo)致遺漏這類對事件,數(shù)據(jù)的清理工作,從而造成內(nèi)存泄漏甚至邏輯錯(cuò)誤。
邏輯抽象復(fù)用
方案 優(yōu)點(diǎn) 缺點(diǎn) Mixin 使用簡單方便 靈活 維護(hù)性差容易 mixin 覆蓋 HOC 解決了class不能使用 mixin 問題 擁有額外組件層級屬性來源不定,容易屬性覆蓋 Render Props 明確來源,解決了屬性覆蓋問題 額外組件層級可讀性差 Hook 消除額外組件層級可讀性高,適用邏輯抽象數(shù)據(jù)來源輸出明確 依賴管理閉包問題
class 本身的問題,比如不能很好的壓縮,在熱重載時(shí)會(huì)出現(xiàn)不穩(wěn)定的情況
解決問題 - Hook 設(shè)計(jì)
通過 Hook 方式,React 為函數(shù)組件引入了可管理副作用的 useState、useEffect、useMemo 等 Hook,以保證函數(shù)盡可能純粹的基礎(chǔ)上,有效解決了上述幾個(gè)之前組件開發(fā)的痛點(diǎn)。
這篇文章中以 useState 的數(shù)據(jù)存儲(chǔ)和 useEffect 的副作用管理為例子展開。
由于函數(shù)組件有著純函數(shù)的特點(diǎn),本身不負(fù)責(zé)數(shù)據(jù)存儲(chǔ)和副作用處理。因此我們首先要解決的問題就是數(shù)據(jù)的存儲(chǔ)和副作用管理。
useState
在 JavaScript 中,解決數(shù)據(jù)存儲(chǔ)主要有以下幾個(gè)方案。
class 成員變量
全局狀態(tài)
Dom
localStorage 等本地存儲(chǔ)方案
閉包
其中,類的成員變量是 class 采用的數(shù)據(jù)存儲(chǔ)方案;
考慮到盡可能規(guī)避副作用的影響,我們排除全局狀態(tài)、本地存儲(chǔ)和 DOM 的方案;相對而言,閉包可以滿足我們數(shù)據(jù)存儲(chǔ)和可靠性的要求。
DEMO 演化
參考 React.useState 的使用方案,應(yīng)該返回一個(gè) state 數(shù)據(jù)字段和一個(gè)更新 state 的 dispatch。
function Demo () {
const [count, setCount] = useState(0)
return <div onClick={() => { setCount(count++); }}>{count}<div>
}
根據(jù)閉包的定義和使用的返回值,我們可以很輕易的定義出以下方法:
var useState = (initState) => {
let data = initState;
const dispatch = (newData) => {
data = newData;
}
return [data, dispatch];
}
在初始化階段,我們可以驗(yàn)證上面的基礎(chǔ) useState 可以運(yùn)行。然而在每次渲染的過程中,函數(shù)都會(huì)被重新調(diào)用而重新初始化,這并不是我們期望的。因此我們需要一個(gè)數(shù)據(jù)結(jié)構(gòu)對每次執(zhí)行的 state 進(jìn)行存儲(chǔ),同時(shí)還需要區(qū)分初始化和更新狀態(tài)的不同執(zhí)行方式。
type Hook {
memorizedState: any;
}
var useState = (initState) => {
// 根據(jù)不同生命周期判斷
if (mounted) {
mountedState(initState);
}
if (updated) {
updatedState(initState);
}
}
var mountedState = (initState) => {
const hook = createNewHook();
// 初始化渲染
hook.memoizedState = initalState;
return [hook.memorizedState, dispatchAction]
}
var createNewHook = () => {
return {
memorizedState: null
}
}
function dispatchAction(action){
// 使用數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)所有的更新行為,以便在 rerender 流程中計(jì)算最新的狀態(tài)值
storeUpdateActions(action);
// 執(zhí)行 fiber 的渲染
scheduleWork();
}
// 第一次之后每一次執(zhí)行 useState 時(shí)實(shí)際調(diào)用的方法
function updateState(initialState){
// 獲取當(dāng)前正在工作中的 hook
const hook = updateWorkInProgressHook();
// 根據(jù) dispatchAction 中存儲(chǔ)的更新行為計(jì)算出新的狀態(tài)值,并返回給組件
updateMemorizedState();
return [hook.memoizedState, dispatchAction];
}
到這里我們有兩個(gè)問題
對于同一個(gè) state,在 mounted 和 updated 的不同狀態(tài)下,hook 是如何共享的
dispatchAction 中的 storeUpdateActions 和 updateState 中的 updateMemorizedState 是如何運(yùn)作的
針對第一個(gè)問題,對于一個(gè)組件而言,Hook 是相對于組件存在的。因此,React 組件存儲(chǔ)的 ReactNode 十分適合該場景,在當(dāng)前版本下,我們將其掛載于 FiberNode 節(jié)點(diǎn)下。
type FiberNode {
memorizedState: any;
}
而針對第二個(gè)問題, 需要我們考慮一些復(fù)雜場景問題。
在我們實(shí)際場景中,普遍存在著一個(gè)更新周期中多次調(diào)用的 re-render 行為
以一個(gè)例子描述:
function Demo () {
const [count, setCount] = useState(0);
return <div onClick={() => {
setCount(count++);
setCount(count++)
setCount(count++)
}}>{count}<div>
}
實(shí)際上組件不會(huì)渲染3次,而是根據(jù)最后的狀態(tài)渲染。這意味著在調(diào)用 dispatch 更新的時(shí)候,我們并不是直接進(jìn)行更新邏輯,而是將其存儲(chǔ)進(jìn)行update時(shí)統(tǒng)一的調(diào)度更新,根據(jù)執(zhí)行的有序性,我們采用隊(duì)列存儲(chǔ)一個(gè) hook 的多次調(diào)用。
type Queue {
last: Update,
dispatch: any,
lastRenderedState: any
}
type Update {
action: any,
next: Update
}
type Hook {
memorizedState: any,
queue: Queue;
}
function mountState(initState) {
const hook = mountWorkInProgressHook();
hook.memorizedState = initState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedState: null
});
// 閉包綁定 queue,實(shí)現(xiàn)共享
const dispatch = dispatchAction.bind(null, queue);
queue.dispatch = dispatch;
return [hook.memorizedState, dispatch]
}
function dispatchAction(queue, action) {
const update = {
action,
next: null,
};
// 處理隊(duì)列更新
let last = queue.last;
if (last === null) {
update.next = update;
} else {
// ... 更新循環(huán)鏈表
}
// 執(zhí)行 fiber 的渲染
scheduleWork();
}
function updateState(initialState){
// 獲取當(dāng)前正在工作中的 hook
const hook = updateWorkInProgressHook();
// 根據(jù) dispatchAction 中存儲(chǔ)的更新行為計(jì)算出新的狀態(tài)值,并返回給組件
(function doReducerWork(){
let newState = null;
do{
// 循環(huán)鏈表,執(zhí)行每一次更新
}while(...)
hook.memoizedState = newState;
})();
return [hook.memoizedState, hook.queue.dispatch];
}
此外,在真實(shí)的應(yīng)用場景中,我們會(huì)根據(jù)邏輯進(jìn)行狀態(tài)分割。需要在一個(gè)組件內(nèi)多次使用一個(gè) Hook,因此需要記錄所有使用的 Hook 信息。這方面,與之前存儲(chǔ)同一組更新相同的 Hook 多次調(diào)用相同,可采用鏈表形式進(jìn)行存儲(chǔ)。
type Hook = {
memoizedState: any, // 上一次完整更新之后的最終狀態(tài)值
queue: UpdateQueue<any, any> | null, // 更新隊(duì)列
next: any // 下一個(gè) hook
}
這里我們可以看一個(gè)例子:
const Demo = () => {
const [count, setCount] = useState(0);
const [time, setTime] = useState(Date.now());
return <div onClick={() => {
setCount(count++);
setTime(Date.now());
}}>{count}-{time}</div>
}
在 ReactNode 中存儲(chǔ)的節(jié)點(diǎn)形式:
const fiber = {
//...
memoizedState: {
memoizedState: 0,
queue: {
last: {
action: 1
},
dispatch: dispatch,
lastRenderedState: 0
},
next: {
memoizedState: 1603594106044,
queue: {
// ...
},
next: null
}
},
//...
}
整個(gè)鏈表在 mounted 的時(shí)候構(gòu)建,在 update 時(shí)按照順序執(zhí)行。因此不能在條件循環(huán)等場景下使用。
useEffect
有了 useState 設(shè)計(jì)經(jīng)驗(yàn),useEffect 可以同比借鑒。在mount時(shí)創(chuàng)建一個(gè) hook 對象,新建一個(gè) effectQueue,以單向鏈表的方式存儲(chǔ)每一個(gè) effect,將 effectQueue 綁定在 fiberNode 上,并在完成渲染之后依次執(zhí)行該隊(duì)列中存儲(chǔ)的 effect 函數(shù)。
type EffectQueue{
lastEffect: Effect
}
type FiberNode{
memoizedState: any, // 用來存放某個(gè)組件內(nèi)所有的 Hook 狀態(tài)
updateQueue: EffectQueue
}
type Effect {
create: any;
destory: any;
deps: Array;
next: any;
}
與 useState 不同的一點(diǎn)是,useEffect 擁有一個(gè) deps 依賴數(shù)組。當(dāng)依賴數(shù)組變更的時(shí)候,一個(gè)新的副作用函數(shù)會(huì)被追加至鏈尾。
function useEffect(fn, dependencies) {
if (mounted) {
mounteEffect(fn, dependencies)
}
if (updated) {
updateEffect(fn, dependencies)
}
}
function mountEffect(fn, deps) {
const hook = mountWorkInProgressHook();
hook.memorizedState = pushEffect(xxxTag, fn, deps)
}
function updateEffect(fn, deps) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 依賴改變則觸發(fā)銷毀重置
if(currentHook!==null){
const prevEffect = currentHook.memoizedState;
const destroy = prevEffect.destroy;
if (nextDeps!== null){
if(areHookInputsEqual(deps, prevEffect.deps)){
pushEffect(xxxTag, create, destroy, deps);
return;
}
}
}
hook.memoizedState = pushEffect(xxxTag, create, deps);
}
function pushEffect(tag, create, destroy, deps) {
const effect = {
create,
destory,
deps,
next: null
};
// 構(gòu)建 effect 隊(duì)列
const updateQueue = fiberNode.updateQueue = fiberNode.updateQueue || newUpdateQueue();
if (updateQueue.lastEffect) {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
updateQueue.lastEffect = effect;
} else {
updateQueue.lastEffect = effect.next = effect;
}
return effect;
}
我們可以看到,在 useEffect 階段,實(shí)際并沒有對 effect 進(jìn)行執(zhí)行,僅僅是構(gòu)建一條 effect 執(zhí)行存儲(chǔ)鏈表,而真正 create 和 destroy 的執(zhí)行,在于 commit 階段,該部分內(nèi)容不在本次分享范圍,感興趣的小伙伴可以自行了解
相關(guān)鏈接:ReactFiberCommitWork[1]
總結(jié)
通過查找定位問題 -> 得出需求 -> 實(shí)現(xiàn)設(shè)計(jì) -> 設(shè)計(jì)演進(jìn)的步驟,我們一步步了解了 Hook 設(shè)計(jì)的初衷和設(shè)計(jì)的一步步完善,在日常工作中,我們也應(yīng)該借鑒這種思維模式,完善自身對于業(yè)務(wù)痛點(diǎn)的認(rèn)識以真正采取相應(yīng)的解決措施
參考資料
[1]ReactFiberCommitWork: https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberCommitWork.new.js#L375
最后
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「 sherlocked_93 」拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
關(guān)注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

點(diǎn)個(gè)在看支持我吧,轉(zhuǎn)發(fā)就更好了
瀏覽
91
從函數(shù)式說起
React 在現(xiàn)有的三大主流框架中是非?!昂瘮?shù)式”的語言,小到 setState,render 函數(shù)的設(shè)計(jì),大到函數(shù)組件,周邊組件 Redux 等,都蘊(yùn)含了一定的函數(shù)式風(fēng)格。因此要了解 React,我們就需要了解一定的函數(shù)式編程。
函數(shù)式編程作為聲明式編程的一種形式,與命令式編程相對,來源于范疇論,最早是為了解決數(shù)學(xué)問題而誕生。范疇論認(rèn)為,同一個(gè)范疇的所有成員,就是不同狀態(tài)的"變形",通過態(tài)射,一個(gè)成員可以變形成另一個(gè)成員。

我們可以把物件理解為集合,態(tài)射理解為函數(shù),通過函數(shù),來規(guī)定范疇中成員之間的關(guān)系,函數(shù)扮演管道的角色,一個(gè)值進(jìn)去,一個(gè)新值出來,沒有其他副作用,也就是所謂的 y = f(x)。
函數(shù)式風(fēng)格包含了多種特性,典型的如函數(shù)一等公民、純函數(shù)、副作用、柯里化、組合等,這里我們主要以基礎(chǔ)的純函數(shù)和副作用做進(jìn)一步解釋。
純函數(shù)
純函數(shù) ——輸入輸出數(shù)據(jù)流全是顯式的。
顯式的意思是,函數(shù)與外界交換數(shù)據(jù)只有一個(gè)唯一渠道——參數(shù)和返回值;函數(shù)從函數(shù)外部接受的所有輸入信息都通過參數(shù)傳遞到該函數(shù)內(nèi)部;函數(shù)輸出到函數(shù)外部的所有信息都通過返回值傳遞到該函數(shù)外部。
相同的輸入,永遠(yuǎn)得到相同的輸出,而且沒有任何副作用,與環(huán)境變量無關(guān),可以在任意地方調(diào)用。
//splice是不純的函數(shù)
let arr = [1,2,3,4,5];
arr.splice(0,3); //[1,2,3]
arr.splice(0,3); //[4,5]
//slice是純函數(shù)
arr = [1,2,3,4,5];
arr.slice(0,3); //[1,2,3]
arr.slice(0,3); //[1,2,3]
副作用
在計(jì)算機(jī)科學(xué)中,函數(shù)副作用指當(dāng)調(diào)用函數(shù)時(shí),除了返回函數(shù)值之外,還對主調(diào)用函數(shù)產(chǎn)生附加的影響。例如修改全局變量(函數(shù)外的變量),修改參數(shù)或改變外部存儲(chǔ)。
典型的副作用:
發(fā)送一個(gè)http請求
new Date() / Math.random();
console.log / IO
DOM查詢 (外部數(shù)據(jù))
然而,只有純函數(shù)而無副作用的程序,在程序運(yùn)行完畢后,不留一絲痕跡,僅僅只是空耗 CPU 而已,因此,副作用是必要的。在函數(shù)式語言中,為了保證函數(shù)的盡可能純粹性,副作用使用函子進(jìn)行統(tǒng)一管理。這里我們不去深究函子的原理,我們需要記住的是:盡可能保持函數(shù)的純粹性,將副作用收攏統(tǒng)一管理。
追溯歷史 - Hook 誕生背景
React 組件
React 組件根據(jù)書寫形式分為函數(shù)組件和類組件。
class ComponentA extends React.Component {
constructor(props) {
super(props);
this.state = { displayContent: 'Hello World' }
}
render() {
return <div>{this.state.displayContent}</div>
}
}
function ComponentFunctionA = (props) => <div>{props.displayContent}</div>
函數(shù)組件定位為展示組件,自身無狀態(tài),無生命周期。
類組件定位為容器組件,自身管理狀態(tài)與生命周期。
問題
函數(shù)組件想管理狀態(tài)、生命周期
隨著需求變更,在未作出良好設(shè)計(jì)的情況下。常會(huì)出現(xiàn)本是函數(shù)的組件處于功能內(nèi)聚性等因素想管理狀態(tài)的情況。
解決方案:
改為類組件
提升狀態(tài)至上層容器
然而,改為類組件需要一定的人力成本;而數(shù)據(jù)提升至上層容器,既有可能會(huì)增加 React 組件層級結(jié)構(gòu),又喪失了組件功能的內(nèi)聚性,都不能認(rèn)為是良好的解決方案。
類組件生命周期帶來邏輯分離
class DemoA extends React.PureComponent {
constructor(props) {
super(props);
this.listener = () => {/* do something */};
}
componentDidMount() {
document.addEventListener('click', this.listener);
}
componentWillUnmount() {
document.removeEventListener('click', this.listener);
}
}
這里只是一個(gè)簡單的例子,展示了由于生命周期的存在,需要我們將一個(gè)事件的監(jiān)聽和取消邏輯分別置于不同生命周期內(nèi)。一旦邏輯復(fù)雜,極易導(dǎo)致遺漏這類對事件,數(shù)據(jù)的清理工作,從而造成內(nèi)存泄漏甚至邏輯錯(cuò)誤。
邏輯抽象復(fù)用
| 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|
| Mixin | 使用簡單方便 靈活 | 維護(hù)性差容易 mixin 覆蓋 |
| HOC | 解決了class不能使用 mixin 問題 | 擁有額外組件層級屬性來源不定,容易屬性覆蓋 |
| Render Props | 明確來源,解決了屬性覆蓋問題 | 額外組件層級可讀性差 |
| Hook | 消除額外組件層級可讀性高,適用邏輯抽象數(shù)據(jù)來源輸出明確 | 依賴管理閉包問題 |
class 本身的問題,比如不能很好的壓縮,在熱重載時(shí)會(huì)出現(xiàn)不穩(wěn)定的情況
解決問題 - Hook 設(shè)計(jì)
通過 Hook 方式,React 為函數(shù)組件引入了可管理副作用的 useState、useEffect、useMemo 等 Hook,以保證函數(shù)盡可能純粹的基礎(chǔ)上,有效解決了上述幾個(gè)之前組件開發(fā)的痛點(diǎn)。
這篇文章中以 useState 的數(shù)據(jù)存儲(chǔ)和 useEffect 的副作用管理為例子展開。
由于函數(shù)組件有著純函數(shù)的特點(diǎn),本身不負(fù)責(zé)數(shù)據(jù)存儲(chǔ)和副作用處理。因此我們首先要解決的問題就是數(shù)據(jù)的存儲(chǔ)和副作用管理。
useState
在 JavaScript 中,解決數(shù)據(jù)存儲(chǔ)主要有以下幾個(gè)方案。
class 成員變量
全局狀態(tài)
Dom
localStorage 等本地存儲(chǔ)方案
閉包
其中,類的成員變量是 class 采用的數(shù)據(jù)存儲(chǔ)方案;
考慮到盡可能規(guī)避副作用的影響,我們排除全局狀態(tài)、本地存儲(chǔ)和 DOM 的方案;相對而言,閉包可以滿足我們數(shù)據(jù)存儲(chǔ)和可靠性的要求。
DEMO 演化
參考 React.useState 的使用方案,應(yīng)該返回一個(gè) state 數(shù)據(jù)字段和一個(gè)更新 state 的 dispatch。
function Demo () {
const [count, setCount] = useState(0)
return <div onClick={() => { setCount(count++); }}>{count}<div>
}
根據(jù)閉包的定義和使用的返回值,我們可以很輕易的定義出以下方法:
var useState = (initState) => {
let data = initState;
const dispatch = (newData) => {
data = newData;
}
return [data, dispatch];
}
在初始化階段,我們可以驗(yàn)證上面的基礎(chǔ) useState 可以運(yùn)行。然而在每次渲染的過程中,函數(shù)都會(huì)被重新調(diào)用而重新初始化,這并不是我們期望的。因此我們需要一個(gè)數(shù)據(jù)結(jié)構(gòu)對每次執(zhí)行的 state 進(jìn)行存儲(chǔ),同時(shí)還需要區(qū)分初始化和更新狀態(tài)的不同執(zhí)行方式。
type Hook {
memorizedState: any;
}
var useState = (initState) => {
// 根據(jù)不同生命周期判斷
if (mounted) {
mountedState(initState);
}
if (updated) {
updatedState(initState);
}
}
var mountedState = (initState) => {
const hook = createNewHook();
// 初始化渲染
hook.memoizedState = initalState;
return [hook.memorizedState, dispatchAction]
}
var createNewHook = () => {
return {
memorizedState: null
}
}
function dispatchAction(action){
// 使用數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)所有的更新行為,以便在 rerender 流程中計(jì)算最新的狀態(tài)值
storeUpdateActions(action);
// 執(zhí)行 fiber 的渲染
scheduleWork();
}
// 第一次之后每一次執(zhí)行 useState 時(shí)實(shí)際調(diào)用的方法
function updateState(initialState){
// 獲取當(dāng)前正在工作中的 hook
const hook = updateWorkInProgressHook();
// 根據(jù) dispatchAction 中存儲(chǔ)的更新行為計(jì)算出新的狀態(tài)值,并返回給組件
updateMemorizedState();
return [hook.memoizedState, dispatchAction];
}
到這里我們有兩個(gè)問題
對于同一個(gè) state,在 mounted 和 updated 的不同狀態(tài)下,hook 是如何共享的
dispatchAction 中的 storeUpdateActions 和 updateState 中的 updateMemorizedState 是如何運(yùn)作的
針對第一個(gè)問題,對于一個(gè)組件而言,Hook 是相對于組件存在的。因此,React 組件存儲(chǔ)的 ReactNode 十分適合該場景,在當(dāng)前版本下,我們將其掛載于 FiberNode 節(jié)點(diǎn)下。
type FiberNode {
memorizedState: any;
}
而針對第二個(gè)問題, 需要我們考慮一些復(fù)雜場景問題。
在我們實(shí)際場景中,普遍存在著一個(gè)更新周期中多次調(diào)用的 re-render 行為
以一個(gè)例子描述:
function Demo () {
const [count, setCount] = useState(0);
return <div onClick={() => {
setCount(count++);
setCount(count++)
setCount(count++)
}}>{count}<div>
}
實(shí)際上組件不會(huì)渲染3次,而是根據(jù)最后的狀態(tài)渲染。這意味著在調(diào)用 dispatch 更新的時(shí)候,我們并不是直接進(jìn)行更新邏輯,而是將其存儲(chǔ)進(jìn)行update時(shí)統(tǒng)一的調(diào)度更新,根據(jù)執(zhí)行的有序性,我們采用隊(duì)列存儲(chǔ)一個(gè) hook 的多次調(diào)用。
type Queue {
last: Update,
dispatch: any,
lastRenderedState: any
}
type Update {
action: any,
next: Update
}
type Hook {
memorizedState: any,
queue: Queue;
}
function mountState(initState) {
const hook = mountWorkInProgressHook();
hook.memorizedState = initState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedState: null
});
// 閉包綁定 queue,實(shí)現(xiàn)共享
const dispatch = dispatchAction.bind(null, queue);
queue.dispatch = dispatch;
return [hook.memorizedState, dispatch]
}
function dispatchAction(queue, action) {
const update = {
action,
next: null,
};
// 處理隊(duì)列更新
let last = queue.last;
if (last === null) {
update.next = update;
} else {
// ... 更新循環(huán)鏈表
}
// 執(zhí)行 fiber 的渲染
scheduleWork();
}
function updateState(initialState){
// 獲取當(dāng)前正在工作中的 hook
const hook = updateWorkInProgressHook();
// 根據(jù) dispatchAction 中存儲(chǔ)的更新行為計(jì)算出新的狀態(tài)值,并返回給組件
(function doReducerWork(){
let newState = null;
do{
// 循環(huán)鏈表,執(zhí)行每一次更新
}while(...)
hook.memoizedState = newState;
})();
return [hook.memoizedState, hook.queue.dispatch];
}
此外,在真實(shí)的應(yīng)用場景中,我們會(huì)根據(jù)邏輯進(jìn)行狀態(tài)分割。需要在一個(gè)組件內(nèi)多次使用一個(gè) Hook,因此需要記錄所有使用的 Hook 信息。這方面,與之前存儲(chǔ)同一組更新相同的 Hook 多次調(diào)用相同,可采用鏈表形式進(jìn)行存儲(chǔ)。
type Hook = {
memoizedState: any, // 上一次完整更新之后的最終狀態(tài)值
queue: UpdateQueue<any, any> | null, // 更新隊(duì)列
next: any // 下一個(gè) hook
}
這里我們可以看一個(gè)例子:
const Demo = () => {
const [count, setCount] = useState(0);
const [time, setTime] = useState(Date.now());
return <div onClick={() => {
setCount(count++);
setTime(Date.now());
}}>{count}-{time}</div>
}
在 ReactNode 中存儲(chǔ)的節(jié)點(diǎn)形式:
const fiber = {
//...
memoizedState: {
memoizedState: 0,
queue: {
last: {
action: 1
},
dispatch: dispatch,
lastRenderedState: 0
},
next: {
memoizedState: 1603594106044,
queue: {
// ...
},
next: null
}
},
//...
}
整個(gè)鏈表在 mounted 的時(shí)候構(gòu)建,在 update 時(shí)按照順序執(zhí)行。因此不能在條件循環(huán)等場景下使用。
useEffect
有了 useState 設(shè)計(jì)經(jīng)驗(yàn),useEffect 可以同比借鑒。在mount時(shí)創(chuàng)建一個(gè) hook 對象,新建一個(gè) effectQueue,以單向鏈表的方式存儲(chǔ)每一個(gè) effect,將 effectQueue 綁定在 fiberNode 上,并在完成渲染之后依次執(zhí)行該隊(duì)列中存儲(chǔ)的 effect 函數(shù)。
type EffectQueue{
lastEffect: Effect
}
type FiberNode{
memoizedState: any, // 用來存放某個(gè)組件內(nèi)所有的 Hook 狀態(tài)
updateQueue: EffectQueue
}
type Effect {
create: any;
destory: any;
deps: Array;
next: any;
}
與 useState 不同的一點(diǎn)是,useEffect 擁有一個(gè) deps 依賴數(shù)組。當(dāng)依賴數(shù)組變更的時(shí)候,一個(gè)新的副作用函數(shù)會(huì)被追加至鏈尾。
function useEffect(fn, dependencies) {
if (mounted) {
mounteEffect(fn, dependencies)
}
if (updated) {
updateEffect(fn, dependencies)
}
}
function mountEffect(fn, deps) {
const hook = mountWorkInProgressHook();
hook.memorizedState = pushEffect(xxxTag, fn, deps)
}
function updateEffect(fn, deps) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 依賴改變則觸發(fā)銷毀重置
if(currentHook!==null){
const prevEffect = currentHook.memoizedState;
const destroy = prevEffect.destroy;
if (nextDeps!== null){
if(areHookInputsEqual(deps, prevEffect.deps)){
pushEffect(xxxTag, create, destroy, deps);
return;
}
}
}
hook.memoizedState = pushEffect(xxxTag, create, deps);
}
function pushEffect(tag, create, destroy, deps) {
const effect = {
create,
destory,
deps,
next: null
};
// 構(gòu)建 effect 隊(duì)列
const updateQueue = fiberNode.updateQueue = fiberNode.updateQueue || newUpdateQueue();
if (updateQueue.lastEffect) {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
updateQueue.lastEffect = effect;
} else {
updateQueue.lastEffect = effect.next = effect;
}
return effect;
}
我們可以看到,在 useEffect 階段,實(shí)際并沒有對 effect 進(jìn)行執(zhí)行,僅僅是構(gòu)建一條 effect 執(zhí)行存儲(chǔ)鏈表,而真正 create 和 destroy 的執(zhí)行,在于 commit 階段,該部分內(nèi)容不在本次分享范圍,感興趣的小伙伴可以自行了解
相關(guān)鏈接:ReactFiberCommitWork[1]
總結(jié)
通過查找定位問題 -> 得出需求 -> 實(shí)現(xiàn)設(shè)計(jì) -> 設(shè)計(jì)演進(jìn)的步驟,我們一步步了解了 Hook 設(shè)計(jì)的初衷和設(shè)計(jì)的一步步完善,在日常工作中,我們也應(yīng)該借鑒這種思維模式,完善自身對于業(yè)務(wù)痛點(diǎn)的認(rèn)識以真正采取相應(yīng)的解決措施
參考資料
ReactFiberCommitWork: https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberCommitWork.new.js#L375
最后
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「 sherlocked_93 」拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
關(guān)注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

