從Preact中了解React組件和hooks基本原理
關(guān)注并將「趣談前端」設(shè)為星標(biāo)
每早08:30按時推送技術(shù)干貨/優(yōu)秀開源/技術(shù)思維
作者:荒山
https://juejin.im/post/5cfa29e151882539c33e4f5e
React 的代碼庫現(xiàn)在已經(jīng)比較龐大了,加上 v16 的 Fiber 重構(gòu),初學(xué)者很容易陷入細(xì)節(jié)的汪洋大海,搞懂了會讓人覺得自己很牛逼,搞不懂很容易讓人失去信心, 懷疑自己是否應(yīng)該繼續(xù)搞前端。那么嘗試在本文這里找回一點(diǎn)自信吧(高手繞路).
Preact 是 React 的縮略版, 體積非常小, 但五臟俱全. 如果你想了解 React 的基本原理, 可以去學(xué)習(xí)學(xué)習(xí) Preact 的源碼, 這也正是本文的目的。
關(guān)于 React 原理的優(yōu)秀的文章已經(jīng)非常多, 本文就是老酒裝新瓶, 算是自己的一點(diǎn)總結(jié),也為后面的文章作一下鋪墊吧.
文章篇幅較長,閱讀時間約 20min,主要被代碼占據(jù),另外也畫了流程圖配合理解代碼。
注意:代碼有所簡化,忽略掉 svg、replaceNode、context 等特性 本文代碼基于 Preact v10 版本
Virtual-DOM
從 createElement 開始
Component 的實(shí)現(xiàn)
diff 算法
- diffChildren
- diff
- diffElementNodes
- diffProps
Hooks 的實(shí)現(xiàn)
- useState
- useEffect
技術(shù)地圖
擴(kuò)展
Virtual-DOM

Virtual-DOM 其實(shí)就是一顆對象樹,沒有什么特別的,這個對象樹最終要映射到圖形對象. Virtual-DOM 比較核心的是它的diff算法.
你可以想象這里有一個DOM映射器,見名知義,這個’DOM 映射器‘的工作就是將 Virtual-DOM 對象樹映射瀏覽器頁面的 DOM,只不過為了提高 DOM 的'操作性能'. 它不是每一次都全量渲染整個 Virtual-DOM 樹,而是支持接收兩顆 Virtual-DOM 對象樹(一個更新前,一個更新后), 通過 diff 算法計(jì)算出兩顆 Virtual-DOM 樹差異的地方,然后只應(yīng)用這些差異的地方到實(shí)際的 DOM 樹, 從而減少 DOM 變更的成本.
Virtual-DOM 是比較有爭議性,推薦閱讀《網(wǎng)上都說操作真實(shí) DOM 慢,但測試結(jié)果卻比 React 更快,為什么?》 。切記永遠(yuǎn)都不要離開場景去評判一個技術(shù)的好壞。當(dāng)初網(wǎng)上把 React 吹得多么牛逼, 一些小白就會覺得 Virtual-DOM 很吊,JQuery 弱爆了。
我覺得兩個可比性不大,從性能上看, 框架再怎么牛逼它也是需要操作原生 DOM 的,而且它未必有你使用 JQuery 手動操作 DOM 來得'精細(xì)'. 框架不合理使用也可能出現(xiàn)修改一個小狀態(tài),導(dǎo)致渲染雪崩(大范圍重新渲染)的情況; 同理 JQuery 雖然可以精細(xì)化操作 DOM, 但是不合理的 DOM 更新策略可能也會成為應(yīng)用的性能瓶頸. 所以關(guān)鍵還得看你怎么用.
那為什么需要 Virtual-DOM?
我個人的理解就是為了解放生產(chǎn)力?,F(xiàn)如今硬件的性能越來越好,web 應(yīng)用也越來越復(fù)雜,生產(chǎn)力也是要跟上的. 盡管手動操作 DOM 可能可以達(dá)到更高的性能和靈活性,但是這樣對大部分開發(fā)者來說太低效了,我們是可以接受犧牲一點(diǎn)性能換取更高的開發(fā)效率的.
所以說 Virtual-DOM 更大的意義在于開發(fā)方式的改變: 聲明式、 數(shù)據(jù)驅(qū)動, 讓開發(fā)者不需要關(guān)心 DOM 的操作細(xì)節(jié)(屬性操作、事件綁定、DOM 節(jié)點(diǎn)變更),也就是說應(yīng)用的開發(fā)方式變成了view=f(state), 這對生產(chǎn)力的解放是有很大推動作用的.
當(dāng)然 Virtual-DOM 不是唯一,也不是第一個的這樣解決方案. 比如 AngularJS, Vue1.x 這些基于模板的實(shí)現(xiàn)方式, 也可以說實(shí)現(xiàn)這種開發(fā)方式轉(zhuǎn)變的. 那相對于他們 Virtual-DOM 的買點(diǎn)可能就是更高的性能了, 另外 Virtual-DOM 在渲染層上面的抽象更加徹底, 不再耦合于 DOM 本身,比如可以渲染為 ReactNative,PDF,終端 UI 等等。
從 createElement 開始
很多小白將 JSX 等價為 Virtual-DOM,其實(shí)這兩者并沒有直接的關(guān)系, 我們知道 JSX 不過是一個語法糖.
例如<a href="/"><span>Home</span></a>最終會轉(zhuǎn)換為h('a', { href:'/' }, h('span', null, 'Home'))這種形式, h是 JSX Element 工廠方法.
h 在 React 下約定是React.createElement, 而大部分 Virtual-DOM 框架則使用h. h 是 createElement 的別名, Vue 生態(tài)系統(tǒng)也是使用這個慣例, 具體為什么沒作考究(比較簡短?)。
可以使用@jsx注解或 babel 配置項(xiàng)來配置 JSX 工廠:
/**
* @jsx h
*/
render(<div>hello jsx</div>, el);
本文不是 React 或 Preact 的入門文章,所以點(diǎn)到為止,更多內(nèi)容可以查看官方教程.
現(xiàn)在來看看createElement, createElement 不過就是構(gòu)造一個對象(VNode):
// ??type 節(jié)點(diǎn)的類型,有DOM元素(string)和自定義組件,以及Fragment, 為null時表示文本節(jié)點(diǎn)exportfunctioncreateElement(type, props, children) {
props.children = children;
// ??應(yīng)用defaultPropsif (type != null && type.defaultProps != null)
for (let i in type.defaultProps)
if (props[i] === undefined) props[i] = type.defaultProps[i];
let ref = props.ref;
let key = props.key;
// ...// ??構(gòu)建VNode對象return createVNode(type, props, key, ref);
}
exportfunctioncreateVNode(type, props, key, ref) {
return { type, props, key, ref, /* ... 忽略部分內(nèi)置字段 */constructor: undefined };
}
通過 JSX 和組件, 可以構(gòu)造復(fù)雜的對象樹:
render(
<divclassName="container"><SideBar /><Body /></div>,
root,
);
Component 的實(shí)現(xiàn)
對于一個視圖框架來說,組件就是它的靈魂, 就像函數(shù)之于函數(shù)式語言,類之于面向?qū)ο笳Z言, 沒有組件則無法組成復(fù)雜的應(yīng)用.
組件化的思維推薦將一個應(yīng)用分而治之, 拆分和組合不同級別的組件,這樣可以簡化應(yīng)用的開發(fā)和維護(hù),讓程序更好理解. 從技術(shù)上看組件是一個自定義的元素類型,可以聲明組件的輸入(props)、有自己的生命周期和狀態(tài)以及方法、最終輸出 Virtual-DOM 對象樹, 作為應(yīng)用 Virtual-DOM 樹的一個分支存在.
Preact 的自定義組件是基于 Component 類實(shí)現(xiàn)的. 對組件來說最基本的就是狀態(tài)的維護(hù), 這個通過 setState 來實(shí)現(xiàn):
functionComponent(props, context) {}
// ??setState實(shí)現(xiàn)
Component.prototype.setState = function(update, callback) {
// 克隆下一次渲染的State, _nextState會在一些生命周期方式中用到(例如shouldComponentUpdate)let s = (this._nextState !== this.state && this._nextState) ||
(this._nextState = assign({}, this.state));
// state更新if (typeof update !== 'function' || (update = update(s, this.props)))
assign(s, update);
if (this._vnode) { // 已掛載// 推入渲染回調(diào)隊(duì)列, 在渲染完成后批量調(diào)用if (callback) this._renderCallbacks.push(callback);
// 放入異步調(diào)度隊(duì)列
enqueueRender(this);
}
};
enqueueRender 將組件放進(jìn)一個異步的批執(zhí)行隊(duì)列中,這樣可以歸并頻繁的 setState 調(diào)用,實(shí)現(xiàn)也非常簡單:
let q = [];
// 異步調(diào)度器,用于異步執(zhí)行一個回調(diào)const defer = typeofPromise == 'function'
? Promise.prototype.then.bind(Promise.resolve()) // micro task
: setTimeout; // 回調(diào)到setTimeoutfunctionenqueueRender(c) {
// 不需要重復(fù)推入已經(jīng)在隊(duì)列的Componentif (!c._dirty && (c._dirty = true) && q.push(c) === 1)
defer(process); // 當(dāng)隊(duì)列從空變?yōu)榉强諘r,開始調(diào)度
}
// 批量清空隊(duì)列, 調(diào)用Component的forceUpdatefunctionprocess() {
let p;
// 排序隊(duì)列,從低層的組件優(yōu)先更新?
q.sort((a, b) => b._depth - a._depth);
while ((p = q.pop()))
if (p._dirty) p.forceUpdate(false); // false表示不要強(qiáng)制更新,即不要忽略shouldComponentUpdate
}
Ok, 上面的代碼可以看出 setState 本質(zhì)上是調(diào)用 forceUpdate 進(jìn)行組件重新渲染的,來往下挖一挖 forceUpdate 的實(shí)現(xiàn).
這里暫且忽略 diff, 將 diff 視作一個黑盒,他就是一個 DOM 映射器, 像上面說的 diff 接收兩棵 VNode 樹, 以及一個 DOM 掛載點(diǎn), 在比對的過程中它可以會創(chuàng)建、移除或更新組件和 DOM 元素,觸發(fā)對應(yīng)的生命周期方法.
Component.prototype.forceUpdate = function(callback) { // callback放置渲染完成后的回調(diào)let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom;
if (parentDom) { // 已掛載過const force = callback !== false;
let mounts = [];
// 調(diào)用diff對當(dāng)前組件進(jìn)行重新渲染和Virtual-DOM比對// ??暫且忽略這些參數(shù), 將diff視作一個黑盒,他就是一個DOM映射器,
dom = diff(parentDom, vnode, vnode, mounts, this._ancestorComponent, force, dom);
if (dom != null && dom.parentNode !== parentDom)
parentDom.appendChild(dom);
commitRoot(mounts, vnode);
}
if (callback) callback();
};
在看看 render 方法, 實(shí)現(xiàn)跟 forceUpdate 差不多, 都是調(diào)用 diff 算法來執(zhí)行 DOM 更新,只不過由外部指定一個 DOM 容器:
// 簡化版exportfunctionrender(vnode, parentDom) {
vnode = createElement(Fragment, null, [vnode]);
parentDom.childNodes.forEach(i => i.remove())
let mounts = [];
diffChildren(parentDom, null oldVNode, mounts, vnode, EMPTY_OBJ);
commitRoot(mounts, vnode);
}
梳理一下上面的流程:
到目前為止沒有看到組件的其他功能,如初始化、生命周期函數(shù)。這些特性在 diff 函數(shù)中定義,也就是說在組件掛載或更新的過程中被調(diào)用。下一節(jié)就會介紹 diff
diff 算法
千呼萬喚始出來,通過上文可以看出,createElement 和 Component 邏輯都很薄, 主要的邏輯還是集中在 diff 函數(shù)中. React 將這個過程稱為 Reconciliation, 在 Preact 中稱為 Differantiate.
為了簡化程序 Preact 的實(shí)現(xiàn)將 diff 和 DOM 雜糅在一起, 但邏輯還是很清晰,看下目錄結(jié)構(gòu)就知道了:
src/diff
├── children.js # 比對children數(shù)組
├── index.js # 比對兩個節(jié)點(diǎn)
└── props.js # 比對兩個DOM節(jié)點(diǎn)的props
```js

在深入 diff 程序之前,先看一下基本的對象結(jié)構(gòu), 方便后面理解程序流程. 先來看下 VNode 的外形:
```js
type ComponentFactory<P> = preact.ComponentClass<P> | FunctionalComponent<P>;
interface VNode<P = {}> {
// 節(jié)點(diǎn)類型, 內(nèi)置DOM元素為string類型,而自定義組件則是Component類型,Preact中函數(shù)組件只是特殊的Component類型
type: string | ComponentFactory<P> | null;
props: P & { children: ComponentChildren } | string | number | null;
key: Key
ref: Ref<any> | null;
/**
* 內(nèi)部緩存信息
*/// VNode子節(jié)點(diǎn)
_children: Array<VNode> | null;
// 關(guān)聯(lián)的DOM節(jié)點(diǎn), 對于Fragment來說第一個子節(jié)點(diǎn)
_dom: PreactElement | Text | null;
// Fragment, 或者組件返回Fragment的最后一個DOM子節(jié)點(diǎn),
_lastDomChild: PreactElement | Text | null;
// Component實(shí)例
_component: Component | null;
}
diffChildren
先從最簡單的開始, 上面已經(jīng)猜出 diffChildren 用于比對兩個 VNode 列表.
如上圖, 首先這里需要維護(hù)一個表示當(dāng)前插入位置的變量 oldDOM, 它一開始指向 DOM childrenNode 的第一個元素, 后面每次插入更新或插入 newDOM,都會指向 newDOM 的下一個兄弟元素.
在遍歷 newChildren 列表過程中, 會嘗試找出相同 key 的舊 VNode,和它進(jìn)行 diff. 如果新 VNode 和舊 VNode 位置不一樣,這就需要移動它們;對于新增的 DOM,如果插入位置(oldDOM)已經(jīng)到了結(jié)尾,則直接追加到父節(jié)點(diǎn), 否則插入到 oldDOM 之前。
最后卸載舊 VNode 列表中未使用的 VNode.
來詳細(xì)看看源碼:
exportfunctiondiffChildren(
parentDom, // children的父DOM元素
newParentVNode, // children的新父VNode
oldParentVNode, // children的舊父VNode,diffChildren主要比對這兩個Vnode的children
mounts, // 保存在這次比對過程中被掛載的組件實(shí)例,在比對后,會觸發(fā)這些組件的componentDidMount生命周期函數(shù)
ancestorComponent, // children的直接父'組件', 即渲染(render)VNode的組件實(shí)例
oldDom, // 當(dāng)前掛載的DOM,對于diffChildren來說,oldDom一開始指向第一個子節(jié)點(diǎn)
) {
let newChildren = newParentVNode._children || toChildArray(newParentVNode.props.children, (newParentVNode._children = []), coerceToVNode, true,);
let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
// ...// ??遍歷新childrenfor (i = 0; i < newChildren.length; i++) {
childVNode = newChildren[i] = coerceToVNode(newChildren[i]); // 規(guī)范化VNodeif (childVNode == null) continue// ??查找oldChildren中是否有對應(yīng)的元素,如果找到則通過設(shè)置為undefined,從oldChildren中移除// 如果沒有找到則保持為null
oldVNode = oldChildren[i];
for (j = 0; j < oldChildrenLength; j++) {
oldVNode = oldChildren[j];
if (oldVNode && childVNode.key == oldVNode.key && childVNode.type === oldVNode.type) {
oldChildren[j] = undefined;
break;
}
oldVNode = null; // 沒有找到任何舊node,表示是一個新的
}
// ?? 遞歸比對VNode
newDom = diff(parentDom, childVNode, oldVNode, mounts, ancestorComponent, null, oldDom);
// vnode沒有被diff卸載掉if (newDom != null) {
if (childVNode._lastDomChild != null) {
// ??當(dāng)前VNode是Fragment類型// 只有Fragment或組件返回Fragment的Vnode會有非null的_lastDomChild, 從Fragment的結(jié)尾的DOM樹開始比對:// <A> <A>// <> <> ?? Fragment類型,diff會遞歸比對它的children,所以最后我們只需要將newDom指向比對后的最后一個子節(jié)點(diǎn)即可// <a>a</a> <- diff -> <b>b</b>// <b>b</b> <a>a</a> ----+// </> </> \// <div>x</div> ??oldDom會指向這里// </A> </A>
newDom = childVNode._lastDomChild;
} elseif (oldVNode == null || newDom != oldDom || newDom.parentNode == null) {
// ?? newDom和當(dāng)前oldDom不匹配,嘗試新增或修改位置
outer: if (oldDom == null || oldDom.parentNode !== parentDom) {
// ??oldDom指向了結(jié)尾, 即后面沒有更多元素了,直接插入即可; 首次渲染一般會調(diào)用到這里
parentDom.appendChild(newDom);
} else {
// 這里是一個優(yōu)化措施,去掉也不會影響正常程序. 為了便于理解可以忽略這段代碼// 嘗試向后查找oldChildLength/2個元素,如果找到則不需要調(diào)用insertBefore. 這段代碼可以減少insertBefore的調(diào)用頻率for (sibDom = oldDom, j = 0; (sibDom = sibDom.nextSibling) && j < oldChildrenLength; j += 2) {
if (sibDom == newDom)
break outer;
}
// ??insertBefore() 將newDom移動到oldDom之前
parentDom.insertBefore(newDom, oldDom);
}
}
// ??其他情況,newDom === oldDOM不需要處理// ?? oldDom指向下一個DOM節(jié)點(diǎn)
oldDom = newDom.nextSibling;
}
}
// ?? 卸載掉沒有被置為undefined的元素for (i = oldChildrenLength; i--; )
if (oldChildren[i] != null) unmount(oldChildren[i], ancestorComponent);
}
配圖理解一下 diffChilrend 的調(diào)用過程:
總結(jié)一下流程圖
diff
diff 用于比對兩個 VNode 節(jié)點(diǎn). diff 函數(shù)比較冗長, 但是這里面并沒有特別復(fù)雜邏輯,主要是一些自定義組件生命周期的處理。所以先上流程圖,代碼不感興趣可以跳過.
源代碼解析:
exportfunctiondiff(
parentDom, // 父DOM節(jié)點(diǎn)
newVNode, // 新VNode
oldVNode, // 舊VNode
mounts, // 存放已掛載的組件, 將在diff結(jié)束后批量處理
ancestorComponent, // 直接父組件
force, // 是否強(qiáng)制更新, 為true將忽略掉shouldComponentUpdate
oldDom, // 當(dāng)前掛載的DOM節(jié)點(diǎn)
) {
//...try {
outer: if (oldVNode.type === Fragment || newType === Fragment) {
// ?? Fragment類型,使用diffChildren進(jìn)行比對
diffChildren(parentDom, newVNode, oldVNode, mounts, ancestorComponent, oldDom);
// ??記錄Fragment的起始DOM和結(jié)束DOMlet i = newVNode._children.length;
if (i && (tmp = newVNode._children[0]) != null) {
newVNode._dom = tmp._dom;
while (i--) {
tmp = newVNode._children[i];
if (newVNode._lastDomChild = tmp && (tmp._lastDomChild || tmp._dom))
break;
}
}
} elseif (typeof newType === 'function') {
// ??自定義組件類型if (oldVNode._component) {
// ?? ?已經(jīng)存在組件實(shí)例
c = newVNode._component = oldVNode._component;
newVNode._dom = oldVNode._dom;
} else {
// ??初始化組件實(shí)例if (newType.prototype && newType.prototype.render) {
// ??類組件
newVNode._component = c = new newType(newVNode.props, cctx); // eslint-disable-line new-cap
} else {
// ??函數(shù)組件
newVNode._component = c = new Component(newVNode.props, cctx);
c.constructor = newType;
c.render = doRender;
}
c._ancestorComponent = ancestorComponent;
c.props = newVNode.props;
if (!c.state) c.state = {};
isNew = c._dirty = true;
c._renderCallbacks = [];
}
c._vnode = newVNode;
if (c._nextState == null) c._nextState = c.state;
// ??getDerivedStateFromProps 生命周期方法if (newType.getDerivedStateFromProps != null)
assign(c._nextState == c.state
? (c._nextState = assign({}, c._nextState)) // 惰性拷貝
: c._nextState,
newType.getDerivedStateFromProps(newVNode.props, c._nextState),
);
if (isNew) {
// ?? 調(diào)用掛載前的一些生命周期方法// ?? componentWillMountif (newType.getDerivedStateFromProps == null && c.componentWillMount != null) c.componentWillMount();
// ?? componentDidMount// 將組件推入mounts數(shù)組,在整個組件樹diff完成后批量調(diào)用, 他們在commitRoot方法中被調(diào)用// 按照先進(jìn)后出(棧)的順序調(diào)用, 即子組件的componentDidMount會先調(diào)用if (c.componentDidMount != null) mounts.push(c);
} else {
// ?? 調(diào)用重新渲染相關(guān)的一些生命周期方法// ?? componentWillReceivePropsif (newType.getDerivedStateFromProps == null && force == null && c.componentWillReceiveProps != null)
c.componentWillReceiveProps(newVNode.props, cctx);
// ?? shouldComponentUpdateif (!force && c.shouldComponentUpdate != null && c.shouldComponentUpdate(newVNode.props, c._nextState, cctx) === false) {
// shouldComponentUpdate返回false,取消渲染更新
c.props = newVNode.props;
c.state = c._nextState;
c._dirty = false;
newVNode._lastDomChild = oldVNode._lastDomChild;
break outer;
}
// ?? componentWillUpdateif (c.componentWillUpdate != null) c.componentWillUpdate(newVNode.props, c._nextState, cctx);
}
// ??至此props和state已經(jīng)確定下來,緩存和更新props和state準(zhǔn)備渲染
oldProps = c.props;
oldState = c.state;
c.props = newVNode.props;
c.state = c._nextState;
let prev = c._prevVNode || null;
c._dirty = false;
// ??渲染let vnode = (c._prevVNode = coerceToVNode(c.render(c.props, c.state)));
// ??getSnapshotBeforeUpdateif (!isNew && c.getSnapshotBeforeUpdate != null) snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
// ??組件層級,會影響更新的優(yōu)先級
c._depth = ancestorComponent ? (ancestorComponent._depth || 0) + 1 : 0;
// ??遞歸diff渲染結(jié)果
c.base = newVNode._dom = diff(parentDom, vnode, prev, mounts, c, null, oldDom);
if (vnode != null) {
newVNode._lastDomChild = vnode._lastDomChild;
}
c._parentDom = parentDom;
// ??應(yīng)用refif ((tmp = newVNode.ref)) applyRef(tmp, c, ancestorComponent);
// ??調(diào)用renderCallbacks,即setState的回調(diào)while ((tmp = c._renderCallbacks.pop())) tmp.call(c);
// ??componentDidUpdateif (!isNew && oldProps != null && c.componentDidUpdate != null) c.componentDidUpdate(oldProps, oldState, snapshot);
} else {
// ??比對兩個DOM元素
newVNode._dom = diffElementNodes(oldVNode._dom, newVNode, oldVNode, mounts, ancestorComponent);
if ((tmp = newVNode.ref) && oldVNode.ref !== tmp) applyRef(tmp, newVNode._dom, ancestorComponent);
}
} catch (e) {
// ??捕獲渲染錯誤,傳遞給上級組件的didCatch生命周期方法
catchErrorInComponent(e, ancestorComponent);
}
return newVNode._dom;
}
diffElementNodes
比對兩個 DOM 元素, 流程非常簡單:
functiondiffElementNodes(dom, newVNode, oldVNode, mounts, ancestorComponent) {
// ...// ??創(chuàng)建DOM節(jié)點(diǎn)if (dom == null) {
if (newVNode.type === null) {
// ??文本節(jié)點(diǎn), 沒有屬性和子級,直接返回returndocument.createTextNode(newProps);
}
dom = document.createElement(newVNode.type);
}
if (newVNode.type === null) {
// ??文本節(jié)點(diǎn)更新if (oldProps !== newProps) dom.data = newProps;
} else {
if (newVNode !== oldVNode) {
// newVNode !== oldVNode 這說明是一個靜態(tài)節(jié)點(diǎn)let oldProps = oldVNode.props || EMPTY_OBJ;
let newProps = newVNode.props;
// ?? dangerouslySetInnerHTML處理let oldHtml = oldProps.dangerouslySetInnerHTML;
let newHtml = newProps.dangerouslySetInnerHTML;
if (newHtml || oldHtml)
if (!newHtml || !oldHtml || newHtml.__html != oldHtml.__html)
dom.innerHTML = (newHtml && newHtml.__html) || '';
// ??遞歸比對子元素
diffChildren(dom, newVNode, oldVNode, context, mounts, ancestorComponent, EMPTY_OBJ);
// ??遞歸比對DOM屬性
diffProps(dom, newProps, oldProps, isSvg);
}
}
return dom;
}
diffProps
diffProps 用于更新 DOM 元素的屬性
exportfunctiondiffProps(dom, newProps, oldProps, isSvg) {
let i;
const keys = Object.keys(newProps).sort();
// ??比較并設(shè)置屬性for (i = 0; i < keys.length; i++) {
const k = keys[i];
if (k !== 'children' && k !== 'key' &&
(!oldProps || (k === 'value' || k === 'checked' ? dom : oldProps)[k] !== newProps[k]))
setProperty(dom, k, newProps[k], oldProps[k], isSvg);
}
// ??清空屬性for (i in oldProps)
if (i !== 'children' && i !== 'key' && !(i in newProps))
setProperty(dom, i, null, oldProps[i], isSvg);
}
```js
`diffProps` 實(shí)現(xiàn)比較簡單,就是遍歷一下屬性有沒有變動,有變動則通過`setProperty` 設(shè)置屬性。對于失效的 `props` 也會通過 `setProperty` 置空。這里面稍微有點(diǎn)復(fù)雜的是 `setProperty`. 這里涉及到事件的處理, 命名的轉(zhuǎn)換等等:
```js
functionsetProperty(dom, name, value, oldValue, isSvg) {
if (name === 'style') {
// ??樣式設(shè)置const set = assign(assign({}, oldValue), value);
for (let i in set) {
// 樣式屬性沒有變動if ((value || EMPTY_OBJ)[i] === (oldValue || EMPTY_OBJ)[i]) continue;
dom.style.setProperty(
i[0] === '-' && i[1] === '-' ? i : i.replace(CAMEL_REG, '-$&'),
value && i in value
? typeof set[i] === 'number' && IS_NON_DIMENSIONAL.test(i) === false
? set[i] + 'px'
: set[i]
: '', // 清空
);
}
} elseif (name[0] === 'o' && name[1] === 'n') {
// ??事件綁定let useCapture = name !== (name = name.replace(/Capture$/, ''));
let nameLower = name.toLowerCase();
name = (nameLower in dom ? nameLower : name).slice(2);
if (value) {
// ??首次添加事件, 注意這里是eventProxy為事件處理器// preact統(tǒng)一將所有事件處理器收集在dom._listeners對象中,統(tǒng)一進(jìn)行分發(fā)// function eventProxy(e) {// return this._listeners[e.type](options.event ? options.event(e) : e);// }if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);
} else {
// 移除事件
dom.removeEventListener(name, eventProxy, useCapture);
}
// 保存事件隊(duì)列
(dom._listeners || (dom._listeners = {}))[name] = value;
} elseif (name !== 'list' && name !== 'tagName' && name in dom) {
// ??DOM對象屬性
dom[name] = value == null ? '' : value;
} elseif (
typeof value !== 'function' &&
name !== 'dangerouslySetInnerHTML'
) {
// ??DOM元素屬性if (value == null || value === false) {
dom.removeAttribute(name);
} else {
dom.setAttribute(name, value);
}
}
}
OK 至此 Diff 算法介紹完畢,其實(shí)這里面的邏輯并不是特別復(fù)雜, 當(dāng)然 Preact 只是一個極度精簡的框架,React 復(fù)雜度要高得多,尤其 React Fiber 重構(gòu)之后。你也可以把 Preact 當(dāng)做 React 的歷史回顧,有興趣再深入了解 React 的最新架構(gòu)。
Hooks 的實(shí)現(xiàn)
React16.8 正式引入的 hooks,這玩意帶來了全新的 React 組件開發(fā)方式,讓代碼變得更加簡潔。React hooks: not magic, just arrays這篇文章已經(jīng)揭示了 hooks 的基本實(shí)現(xiàn)原理, 它不過是基于數(shù)組實(shí)現(xiàn)的。preact 也實(shí)現(xiàn)了 hooks 機(jī)制,實(shí)現(xiàn)代碼也就百來行,讓我們來體會體會.
hooks 功能本身是沒有集成在 Preact 代碼庫內(nèi)部的,而是通過preact/hooks導(dǎo)入
import { h } from'preact';
import { useEffect } from'preact/hooks';
functionFoo() {
useEffect(() => {
console.log('mounted');
}, []);
return<div>hello hooks</div>;
}
那 Preact 是如何擴(kuò)展 diff 算法來實(shí)現(xiàn) hooks 的呢?實(shí)際上 Preact 提供了options對象來對 Preact diff 進(jìn)行擴(kuò)展,options 類似于 Preact 生命周期鉤子,在 diff 過程中被調(diào)用(為了行文簡潔,上面的代碼我忽略掉了)。例如:
exportfunctiondiff(/*...*/) {
// ...// ??開始diffif ((tmp = options.diff)) tmp(newVNode);
try {
outer: if (oldVNode.type === Fragment || newType === Fragment) {
// Fragment diff
} elseif (typeof newType === 'function') {
// 自定義組件diff// ??開始渲染if ((tmp = options.render)) tmp(newVNode);
try {
// ..
c.render(c.props, c.state, c.context),
} catch (e) {
// ??捕獲異常if ((tmp = options.catchRender) && tmp(e, c)) return;
throw e;
}
} else {
// DOM element diff
}
// ??diff結(jié)束if ((tmp = options.diffed)) tmp(newVNode);
} catch (e) {
catchErrorInComponent(e, ancestorComponent);
}
return newVNode._dom;
}
// ...
useState
先從最常用的 useState 開始:
exportfunctionuseState(initialState) {
// ??OK只是數(shù)組,沒有Magic,每個hooks調(diào)用都會遞增currenIndex, 從當(dāng)前組件中取出狀態(tài)const hookState = getHookState(currentIndex++);
// ?? 初始化if (!hookState._component) {
hookState._component = currentComponent; // 當(dāng)前組件實(shí)例
hookState._value = [
// ??state, 初始化statetypeof initialState === 'function' ? initialState() : initialState,
// ??dispatch
value => {
const nextValue = typeof value === 'function' ? value(hookState._value[0]) : value;
if (hookState._value[0] !== nextValue) {
// ?? 保存狀態(tài)并調(diào)用setState強(qiáng)制更新
hookState._value[0] = nextValue;
hookState._component.setState({});
}
},
];
}
return hookState._value; // [state, dispatch]
}
```js
從代碼可以看到,關(guān)鍵在于`getHookState`的實(shí)現(xiàn)
```js
import { options } from'preact';
let currentIndex; // 保存當(dāng)前hook的索引let currentComponent;
// ??render 鉤子, 在組件開始渲染之前調(diào)用// 因?yàn)镻react是同步遞歸向下渲染的,而且Javascript是單線程的,所以可以安全地引用當(dāng)前正在渲染的組件實(shí)例
options.render = vnode => {
currentComponent = vnode._component; // 保存當(dāng)前正在渲染的組件
currentIndex = 0; // 開始渲染時index重置為0// 暫時忽略,下面講到useEffect就能理解// 清空上次渲染未處理的Effect(useEffect),只有在快速重新渲染時才會出現(xiàn)這種情況,一般在異步隊(duì)列中被處理if (currentComponent.__hooks) {
currentComponent.__hooks._pendingEffects = handleEffects(
currentComponent.__hooks._pendingEffects,
);
}
};
// ??no magic!, 只是一個數(shù)組, 狀態(tài)保存在組件實(shí)例的_list數(shù)組中functiongetHookState(index) {
// 獲取或初始化列表const hooks = currentComponent.__hooks ||
(currentComponent.__hooks = {
_list: [], // 放置狀態(tài)
_pendingEffects: [], // 放置待處理的effect,由useEffect保存
_pendingLayoutEffects: [], // 放置待處理的layoutEffect,有useLayoutEffect保存
});
// 新建狀態(tài)if (index >= hooks._list.length) {
hooks._list.push({});
}
return hooks._list[index];
}
大概的流程如下:
useEffect
再看看 useEffect 和 useLayoutEffect. useEffect 和 useLayouteEffect 差不多, 只是觸發(fā) effect 的時機(jī)不一樣,useEffect 在完成渲染后繪制觸發(fā),而 useLayoutEffect 在 diff 完成后觸發(fā):
exportfunctionuseEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
// ??狀態(tài)變化
state._value = callback;
state._args = args;
currentComponent.__hooks._pendingEffects.push(state); // ??推進(jìn)_pendingEffects隊(duì)列
afterPaint(currentComponent);
}
}
exportfunctionuseLayoutEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
// ??狀態(tài)變化
state._value = callback;
state._args = args;
currentComponent.__hooks._pendingLayoutEffects.push(state); // ??推進(jìn)_pendingLayoutEffects隊(duì)列
}
}
看看如何觸發(fā) effect. useEffect 和上面看到的enqueueRender差不多,放進(jìn)一個異步隊(duì)列中,由requestAnimationFrame進(jìn)行調(diào)度,批量處理:
// 這是一個類似于上面提到的異步隊(duì)列
afterPaint = component => {
if (!component._afterPaintQueued && // 避免組件重復(fù)推入
(component._afterPaintQueued = true) &&
afterPaintEffects.push(component) === 1// 開始調(diào)度
)
requestAnimationFrame(scheduleFlushAfterPaint); // 由requestAnimationFrame調(diào)度
};
functionscheduleFlushAfterPaint() {
setTimeout(flushAfterPaintEffects);
}
functionflushAfterPaintEffects() {
afterPaintEffects.some(component => {
component._afterPaintQueued = false;
if (component._parentDom)
// 清空_pendingEffects隊(duì)列
component.__hooks._pendingEffects = handleEffects(component.__hooks._pendingEffects);
});
afterPaintEffects = [];
}
functionhandleEffects(effects) {
// 先清除后調(diào)用effect
effects.forEach(invokeCleanup); // 請調(diào)用清理
effects.forEach(invokeEffect); // 再調(diào)用effectreturn [];
}
functioninvokeCleanup(hook) {
if (hook._cleanup) hook._cleanup();
}
functioninvokeEffect(hook) {
const result = hook._value();
if (typeof result === 'function') hook._cleanup = result;
}
再看看如何觸發(fā) LayoutEffect, 很簡單,在 diff 完成后觸發(fā), 這個過程是同步的.
options.diffed = vnode => {
const c = vnode._component;
if (!c) return;
const hooks = c.__hooks;
if (hooks) {
hooks._pendingLayoutEffects = handleEffects(hooks._pendingLayoutEffects);
}
};
??,hooks 基本原理基本了解完畢, 最后還是用一張圖來總結(jié)一下吧。
技術(shù)地圖
文章篇幅很長,主要是太多代碼了, 我自己也不喜歡看這種文章,所以沒期望讀者會看到這里. 后面文章再想辦法改善改善. 謝謝你閱讀到這里。
本期的主角本身是一個小而美的視圖框架,沒有其他技術(shù)棧. 這里就安利一下 Preact 作者developit的另外一些小而美的庫吧.
- Workerize 優(yōu)雅地在 webWorker 中執(zhí)行和調(diào)用程序
- microbundle 零配置的庫打包工具
- greenlet 和 workerize 差不多,這個將單個異步函數(shù)放到 webworker 中執(zhí)行,而 workerize 是將一個模塊
- mitt 200byte 的 EventEmitter
- dlv 安全地訪問深嵌套的對象屬性,類似于 lodash 的 get 方法
- snarkdown 1kb 的 markdown parser
- unistore 簡潔類 Redux 狀態(tài)容器,支持 React 和 Preact
- stockroom 在 webWorker 支持狀態(tài)管理器
擴(kuò)展
- Preact:Into the void 0(譯)
- React Virtual DOM vs Incremental DOM vs Ember’s Glimmer: Fight
完
?? 看完三件事
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點(diǎn)個【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容
關(guān)注公眾號【趣談前端】,不定期分享?前端工程化?/ 可視化 / 低代碼?等技術(shù)文章。

2個小時, 從學(xué)到做, 我用Dooring制作了3個電商H5


點(diǎn)個在看你最好看
