React 的代碼庫如今已經比較龐大了,加上 v16 的 Fiber 重構,初學者很容易陷入細節的汪洋大海,搞懂了會讓人以爲本身很牛逼,搞不懂很容易讓人失去信心, 懷疑本身是否應該繼續搞前端。那麼嘗試在本文這裏找回一點自信吧(高手繞路).html
Preact 是 React 的縮略版, 體積很是小, 但五臟俱全. 若是你想了解 React 的基本原理, 能夠去學習學習 Preact 的源碼, 這也正是本文的目的。前端
關於 React 原理的優秀的文章已經很是多, 本文就是老酒裝新瓶, 算是本身的一點總結,也爲後面的文章做一下鋪墊吧.node
文章篇幅較長,閱讀時間約 20min,主要被代碼佔據,另外也畫了流程圖配合理解代碼。react
注意:代碼有所簡化,忽略掉 svg、replaceNode、context 等特性 本文代碼基於 Preact v10 版本git
Virtual-DOM 其實就是一顆對象樹,沒有什麼特別的,這個對象樹最終要映射到圖形對象. Virtual-DOM 比較核心的是它的diff算法
.github
你能夠想象這裏有一個DOM映射器
,見名知義,這個’DOM 映射器‘的工做就是將 Virtual-DOM 對象樹映射瀏覽器頁面的 DOM,只不過爲了提升 DOM 的'操做性能'. 它不是每一次都全量渲染整個 Virtual-DOM 樹,而是支持接收兩顆 Virtual-DOM 對象樹(一個更新前,一個更新後), 經過 diff 算法計算出兩顆 Virtual-DOM 樹差別的地方,而後只應用這些差別的地方到實際的 DOM 樹, 從而減小 DOM 變動的成本.web
Virtual-DOM 是比較有爭議性,推薦閱讀《網上都說操做真實 DOM 慢,但測試結果卻比 React 更快,爲何?》 。切記永遠都不要離開場景去評判一個技術的好壞。當初網上把 React 吹得多麼牛逼, 一些小白就會以爲 Virtual-DOM 很吊,JQuery 弱爆了。算法
我以爲兩個可比性不大,從性能上看, 框架再怎麼牛逼它也是須要操做原生 DOM 的,並且它未必有你使用 JQuery 手動操做 DOM 來得'精細'. 框架不合理使用也可能出現修改一個小狀態,致使渲染雪崩(大範圍從新渲染)的狀況; 同理 JQuery 雖然能夠精細化操做 DOM, 可是不合理的 DOM 更新策略可能也會成爲應用的性能瓶頸. 因此關鍵還得看你怎麼用.shell
那爲何須要 Virtual-DOM?數組
我我的的理解就是爲了解放生產力。現現在硬件的性能愈來愈好,web 應用也愈來愈複雜,生產力也是要跟上的. 儘管手動操做 DOM 可能能夠達到更高的性能和靈活性,可是這樣對大部分開發者來講過低效了,咱們是能夠接受犧牲一點性能換取更高的開發效率的.
因此說 Virtual-DOM 更大的意義在於開發方式的改變: 聲明式、 數據驅動, 讓開發者不須要關心 DOM 的操做細節(屬性操做、事件綁定、DOM 節點變動),也就是說應用的開發方式變成了view=f(state)
, 這對生產力的解放是有很大推進做用的.
固然 Virtual-DOM 不是惟一,也不是第一個的這樣解決方案. 好比 AngularJS, Vue1.x 這些基於模板的實現方式, 也能夠說實現這種開發方式轉變的. 那相對於他們 Virtual-DOM 的買點可能就是更高的性能了, 另外 Virtual-DOM 在渲染層上面的抽象更加完全, 再也不耦合於 DOM 自己,好比能夠渲染爲 ReactNative,PDF,終端 UI 等等。
不少小白將 JSX
等價爲 Virtual-DOM,其實這二者並無直接的關係, 咱們知道 JSX 不過是一個語法糖.
例如<a href="/"><span>Home</span></a>
最終會轉換爲h('a', { href:'/' }, h('span', null, 'Home'))
這種形式, h
是 JSX Element 工廠方法.
h
在 React 下約定是React.createElement
, 而大部分 Virtual-DOM 框架則使用h
. h
是 createElement
的別名, Vue 生態系統也是使用這個慣例, 具體爲何沒做考究(比較簡短?)。
可使用@jsx
註解或 babel 配置項來配置 JSX 工廠:
/** * @jsx h */
render(<div>hello jsx</div>, el);
複製代碼
本文不是 React 或 Preact 的入門文章,因此點到爲止,更多內容能夠查看官方教程.
如今來看看createElement
, createElement 不過就是構造一個對象(VNode):
// ⚛️type 節點的類型,有DOM元素(string)和自定義組件,以及Fragment, 爲null時表示文本節點
export function createElement(type, props, children) {
props.children = children;
// ⚛️應用defaultProps
if (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;
// ...
// ⚛️構建VNode對象
return createVNode(type, props, key, ref);
}
export function createVNode(type, props, key, ref) {
return { type, props, key, ref, /* ... 忽略部份內置字段 */ constructor: undefined };
}
複製代碼
經過 JSX 和組件, 能夠構造複雜的對象樹:
render(
<div className="container"> <SideBar /> <Body /> </div>,
root,
);
複製代碼
對於一個視圖框架來講,組件就是它的靈魂, 就像函數之於函數式語言,類之於面嚮對象語言, 沒有組件則沒法組成複雜的應用.
組件化的思惟推薦將一個應用分而治之, 拆分和組合不一樣級別的組件,這樣能夠簡化應用的開發和維護,讓程序更好理解. 從技術上看組件是一個自定義的元素類型,能夠聲明組件的輸入(props)、有本身的生命週期和狀態以及方法、最終輸出 Virtual-DOM 對象樹, 做爲應用 Virtual-DOM 樹的一個分支存在.
Preact 的自定義組件是基於 Component 類實現的. 對組件來講最基本的就是狀態的維護, 這個經過 setState 來實現:
function Component(props, context) {}
// ⚛️setState實現
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) { // 已掛載
// 推入渲染回調隊列, 在渲染完成後批量調用
if (callback) this._renderCallbacks.push(callback);
// 放入異步調度隊列
enqueueRender(this);
}
};
複製代碼
enqueueRender
將組件放進一個異步的批執行隊列中,這樣能夠歸併頻繁的 setState 調用,實現也很是簡單:
let q = [];
// 異步調度器,用於異步執行一個回調
const defer = typeof Promise == 'function'
? Promise.prototype.then.bind(Promise.resolve()) // micro task
: setTimeout; // 回調到setTimeout
function enqueueRender(c) {
// 不須要重複推入已經在隊列的Component
if (!c._dirty && (c._dirty = true) && q.push(c) === 1)
defer(process); // 當隊列從空變爲非空時,開始調度
}
// 批量清空隊列, 調用Component的forceUpdate
function process() {
let p;
// 排序隊列,從低層的組件優先更新?
q.sort((a, b) => b._depth - a._depth);
while ((p = q.pop()))
if (p._dirty) p.forceUpdate(false); // false表示不要強制更新,即不要忽略shouldComponentUpdate
}
複製代碼
Ok, 上面的代碼能夠看出 setState
本質上是調用 forceUpdate
進行組件從新渲染的,來往下挖一挖 forceUpdate 的實現.
這裏暫且忽略 diff, 將 diff 視做一個黑盒,他就是一個 DOM 映射器, 像上面說的 diff 接收兩棵 VNode 樹, 以及一個 DOM 掛載點, 在比對的過程當中它能夠會建立、移除或更新組件和 DOM 元素,觸發對應的生命週期方法.
Component.prototype.forceUpdate = function(callback) { // callback放置渲染完成後的回調
let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom;
if (parentDom) { // 已掛載過
const force = callback !== false;
let mounts = [];
// 調用diff對當前組件進行從新渲染和Virtual-DOM比對
// ⚛️暫且忽略這些參數, 將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
方法, 實現跟 forceUpdate 差很少, 都是調用 diff 算法來執行 DOM 更新,只不過由外部指定一個 DOM 容器:
// 簡化版
export function render(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);
}
複製代碼
梳理一下上面的流程:
到目前爲止沒有看到組件的其餘功能,如初始化、生命週期函數。這些特性在 diff 函數中定義,也就是說在組件掛載或更新的過程當中被調用。下一節就會介紹 diff
千呼萬喚始出來,經過上文能夠看出,createElement
和 Component
邏輯都很薄, 主要的邏輯仍是集中在 diff 函數中. React 將這個過程稱爲 Reconciliation
, 在 Preact 中稱爲 Differantiate
.
爲了簡化程序 Preact 的實現將 diff 和 DOM 雜糅在一塊兒, 但邏輯仍是很清晰,看下目錄結構就知道了:
src/diff
├── children.js # 比對children數組
├── index.js # 比對兩個節點
└── props.js # 比對兩個DOM節點的props
複製代碼
在深刻 diff 程序以前,先看一下基本的對象結構, 方便後面理解程序流程. 先來看下 VNode 的外形:
type ComponentFactory<P> = preact.ComponentClass<P> | FunctionalComponent<P>;
interface VNode<P = {}> {
// 節點類型, 內置DOM元素爲string類型,而自定義組件則是Component類型,Preact中函數組件只是特殊的Component類型
type: string | ComponentFactory<P> | null;
props: P & { children: ComponentChildren } | string | number | null;
key: Key
ref: Ref<any> | null;
/** * 內部緩存信息 */
// VNode子節點
_children: Array<VNode> | null;
// 關聯的DOM節點, 對於Fragment來講第一個子節點
_dom: PreactElement | Text | null;
// Fragment, 或者組件返回Fragment的最後一個DOM子節點,
_lastDomChild: PreactElement | Text | null;
// Component實例
_component: Component | null;
}
複製代碼
先從最簡單的開始, 上面已經猜出 diffChildren 用於比對兩個 VNode 列表.
如上圖, 首先這裏須要維護一個表示當前插入位置的變量 oldDOM, 它一開始指向 DOM childrenNode 的第一個元素, 後面每次插入更新或插入 newDOM,都會指向 newDOM 的下一個兄弟元素.
在遍歷 newChildren 列表過程當中, 會嘗試找出相同 key 的舊 VNode,和它進行 diff. 若是新 VNode 和舊 VNode 位置不同,這就須要移動它們;對於新增的 DOM,若是插入位置(oldDOM)已經到告終尾,則直接追加到父節點, 不然插入到 oldDOM 以前。
最後卸載舊 VNode 列表中未使用的 VNode.
來詳細看看源碼:
export function diffChildren( parentDom, // children的父DOM元素 newParentVNode, // children的新父VNode oldParentVNode, // children的舊父VNode,diffChildren主要比對這兩個Vnode的children mounts, // 保存在此次比對過程當中被掛載的組件實例,在比對後,會觸發這些組件的componentDidMount生命週期函數 ancestorComponent, // children的直接父'組件', 即渲染(render)VNode的組件實例 oldDom, // 當前掛載的DOM,對於diffChildren來講,oldDom一開始指向第一個子節點 ) {
let newChildren = newParentVNode._children || toChildArray(newParentVNode.props.children, (newParentVNode._children = []), coerceToVNode, true,);
let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
// ...
// ⚛️遍歷新children
for (i = 0; i < newChildren.length; i++) {
childVNode = newChildren[i] = coerceToVNode(newChildren[i]); // 規範化VNode
if (childVNode == null) continue
// ⚛️查找oldChildren中是否有對應的元素,若是找到則經過設置爲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) {
// ⚛️當前VNode是Fragment類型
// 只有Fragment或組件返回Fragment的Vnode會有非null的_lastDomChild, 從Fragment的結尾的DOM樹開始比對:
// <A> <A>
// <> <> 👈 Fragment類型,diff會遞歸比對它的children,因此最後咱們只須要將newDom指向比對後的最後一個子節點便可
// <a>a</a> <- diff -> <b>b</b>
// <b>b</b> <a>a</a> ----+
// </> </> \
// <div>x</div> 👈oldDom會指向這裏
// </A> </A>
newDom = childVNode._lastDomChild;
} else if (oldVNode == null || newDom != oldDom || newDom.parentNode == null) {
// ⚛️ newDom和當前oldDom不匹配,嘗試新增或修改位置
outer: if (oldDom == null || oldDom.parentNode !== parentDom) {
// ⚛️oldDom指向告終尾, 即後面沒有更多元素了,直接插入便可; 首次渲染通常會調用到這裏
parentDom.appendChild(newDom);
} else {
// 這裏是一個優化措施,去掉也不會影響正常程序. 爲了便於理解能夠忽略這段代碼
// 嘗試向後查找oldChildLength/2個元素,若是找到則不須要調用insertBefore. 這段代碼能夠減小insertBefore的調用頻率
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節點
oldDom = newDom.nextSibling;
}
}
// ⚛️ 卸載掉沒有被置爲undefined的元素
for (i = oldChildrenLength; i--; )
if (oldChildren[i] != null) unmount(oldChildren[i], ancestorComponent);
}
複製代碼
配圖理解一下 diffChilrend 的調用過程:
總結一下流程圖
diff 用於比對兩個 VNode 節點. diff 函數比較冗長, 可是這裏面並無特別複雜邏輯,主要是一些自定義組件生命週期的處理。因此先上流程圖,代碼不感興趣能夠跳過.
源代碼解析:
export function diff( parentDom, // 父DOM節點 newVNode, // 新VNode oldVNode, // 舊VNode mounts, // 存放已掛載的組件, 將在diff結束後批量處理 ancestorComponent, // 直接父組件 force, // 是否強制更新, 爲true將忽略掉shouldComponentUpdate oldDom, // 當前掛載的DOM節點 ) {
//...
try {
outer: if (oldVNode.type === Fragment || newType === Fragment) {
// ⚛️ Fragment類型,使用diffChildren進行比對
diffChildren(parentDom, newVNode, oldVNode, mounts, ancestorComponent, oldDom);
// ⚛️記錄Fragment的起始DOM和結束DOM
let 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;
}
}
} else if (typeof newType === 'function') {
// ⚛️自定義組件類型
if (oldVNode._component) {
// ⚛️ ️已經存在組件實例
c = newVNode._component = oldVNode._component;
newVNode._dom = oldVNode._dom;
} else {
// ⚛️初始化組件實例
if (newType.prototype && newType.prototype.render) {
// ⚛️類組件
newVNode._component = c = new newType(newVNode.props, cctx); // eslint-disable-line new-cap
} else {
// ⚛️函數組件
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) {
// ⚛️ 調用掛載前的一些生命週期方法
// ⚛️ componentWillMount
if (newType.getDerivedStateFromProps == null && c.componentWillMount != null) c.componentWillMount();
// ⚛️ componentDidMount
// 將組件推入mounts數組,在整個組件樹diff完成後批量調用, 他們在commitRoot方法中被調用
// 按照先進後出(棧)的順序調用, 即子組件的componentDidMount會先調用
if (c.componentDidMount != null) mounts.push(c);
} else {
// ⚛️ 調用從新渲染相關的一些生命週期方法
// ⚛️ componentWillReceiveProps
if (newType.getDerivedStateFromProps == null && force == null && c.componentWillReceiveProps != null)
c.componentWillReceiveProps(newVNode.props, cctx);
// ⚛️ shouldComponentUpdate
if (!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;
}
// ⚛️ componentWillUpdate
if (c.componentWillUpdate != null) c.componentWillUpdate(newVNode.props, c._nextState, cctx);
}
// ⚛️至此props和state已經肯定下來,緩存和更新props和state準備渲染
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)));
// ⚛️getSnapshotBeforeUpdate
if (!isNew && c.getSnapshotBeforeUpdate != null) snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
// ⚛️組件層級,會影響更新的優先級
c._depth = ancestorComponent ? (ancestorComponent._depth || 0) + 1 : 0;
// ⚛️遞歸diff渲染結果
c.base = newVNode._dom = diff(parentDom, vnode, prev, mounts, c, null, oldDom);
if (vnode != null) {
newVNode._lastDomChild = vnode._lastDomChild;
}
c._parentDom = parentDom;
// ⚛️應用ref
if ((tmp = newVNode.ref)) applyRef(tmp, c, ancestorComponent);
// ⚛️調用renderCallbacks,即setState的回調
while ((tmp = c._renderCallbacks.pop())) tmp.call(c);
// ⚛️componentDidUpdate
if (!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;
}
複製代碼
比對兩個 DOM 元素, 流程很是簡單:
function diffElementNodes(dom, newVNode, oldVNode, mounts, ancestorComponent) {
// ...
// ⚛️建立DOM節點
if (dom == null) {
if (newVNode.type === null) {
// ⚛️文本節點, 沒有屬性和子級,直接返回
return document.createTextNode(newProps);
}
dom = document.createElement(newVNode.type);
}
if (newVNode.type === null) {
// ⚛️文本節點更新
if (oldProps !== newProps) dom.data = newProps;
} else {
if (newVNode !== oldVNode) {
// newVNode !== oldVNode 這說明是一個靜態節點
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 用於更新 DOM 元素的屬性
export function diffProps(dom, newProps, oldProps, isSvg) {
let i;
const keys = Object.keys(newProps).sort();
// ⚛️比較並設置屬性
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);
}
複製代碼
diffProps 實現比較簡單,就是遍歷一下屬性有沒有變更,有變更則經過 setProperty 設置屬性。對於失效的 props 也會經過 setProperty 置空。這裏面稍微有點複雜的是 setProperty. 這裏涉及到事件的處理, 命名的轉換等等:
function setProperty(dom, name, value, oldValue, isSvg) {
if (name === 'style') {
// ⚛️樣式設置
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]
: '', // 清空
);
}
} else if (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統一將全部事件處理器收集在dom._listeners對象中,統一進行分發
// 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);
}
// 保存事件隊列
(dom._listeners || (dom._listeners = {}))[name] = value;
} else if (name !== 'list' && name !== 'tagName' && name in dom) {
// ⚛️DOM對象屬性
dom[name] = value == null ? '' : value;
} else if (
typeof value !== 'function' &&
name !== 'dangerouslySetInnerHTML'
) {
// ⚛️DOM元素屬性
if (value == null || value === false) {
dom.removeAttribute(name);
} else {
dom.setAttribute(name, value);
}
}
}
複製代碼
OK 至此 Diff 算法介紹完畢,其實這裏面的邏輯並非特別複雜, 固然 Preact 只是一個極度精簡的框架,React 複雜度要高得多,尤爲 React Fiber 重構以後。你也能夠把 Preact 當作 React 的歷史回顧,有興趣再深刻了解 React 的最新架構。
React16.8 正式引入的 hooks,這玩意帶來了全新的 React 組件開發方式,讓代碼變得更加簡潔。 React hooks: not magic, just arrays這篇文章已經揭示了 hooks 的基本實現原理, 它不過是基於數組實現的。preact 也實現了 hooks 機制,實現代碼也就百來行,讓咱們來體會體會.
hooks 功能自己是沒有集成在 Preact 代碼庫內部的,而是經過preact/hooks
導入
import { h } from 'preact';
import { useEffect } from 'preact/hooks';
function Foo() {
useEffect(() => {
console.log('mounted');
}, []);
return <div>hello hooks</div>;
}
複製代碼
那 Preact 是如何擴展 diff 算法來實現 hooks 的呢? 實際上 Preact 提供了options
對象來對 Preact diff 進行擴展,options 相似於 Preact 生命週期鉤子,在 diff 過程當中被調用(爲了行文簡潔,上面的代碼我忽略掉了)。例如:
export function diff(/*...*/) {
// ...
// ⚛️開始diff
if ((tmp = options.diff)) tmp(newVNode);
try {
outer: if (oldVNode.type === Fragment || newType === Fragment) {
// Fragment diff
} else if (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結束
if ((tmp = options.diffed)) tmp(newVNode);
} catch (e) {
catchErrorInComponent(e, ancestorComponent);
}
return newVNode._dom;
}
// ...
複製代碼
先從最經常使用的 useState 開始:
export function useState(initialState) {
// ⚛️OK只是數組,沒有Magic,每一個hooks調用都會遞增currenIndex, 從當前組件中取出狀態
const hookState = getHookState(currentIndex++);
// ⚛️ 初始化
if (!hookState._component) {
hookState._component = currentComponent; // 當前組件實例
hookState._value = [
// ⚛️state, 初始化state
typeof initialState === 'function' ? initialState() : initialState,
// ⚛️dispatch
value => {
const nextValue = typeof value === 'function' ? value(hookState._value[0]) : value;
if (hookState._value[0] !== nextValue) {
// ⚛️ 保存狀態並調用setState強制更新
hookState._value[0] = nextValue;
hookState._component.setState({});
}
},
];
}
return hookState._value; // [state, dispatch]
}
複製代碼
從代碼能夠看到,關鍵在於getHookState
的實現
import { options } from 'preact';
let currentIndex; // 保存當前hook的索引
let currentComponent;
// ⚛️render 鉤子, 在組件開始渲染以前調用
// 由於Preact是同步遞歸向下渲染的,並且Javascript是單線程的,因此能夠安全地引用當前正在渲染的組件實例
options.render = vnode => {
currentComponent = vnode._component; // 保存當前正在渲染的組件
currentIndex = 0; // 開始渲染時index重置爲0
// 暫時忽略,下面講到useEffect就能理解
// 清空上次渲染未處理的Effect(useEffect),只有在快速從新渲染時纔會出現這種狀況,通常在異步隊列中被處理
if (currentComponent.__hooks) {
currentComponent.__hooks._pendingEffects = handleEffects(
currentComponent.__hooks._pendingEffects,
);
}
};
// ⚛️no magic!, 只是一個數組, 狀態保存在組件實例的_list數組中
function getHookState(index) {
// 獲取或初始化列表
const hooks = currentComponent.__hooks ||
(currentComponent.__hooks = {
_list: [], // 放置狀態
_pendingEffects: [], // 放置待處理的effect,由useEffect保存
_pendingLayoutEffects: [], // 放置待處理的layoutEffect,有useLayoutEffect保存
});
// 新建狀態
if (index >= hooks._list.length) {
hooks._list.push({});
}
return hooks._list[index];
}
複製代碼
大概的流程以下:
再看看 useEffect 和 useLayoutEffect. useEffect 和 useLayouteEffect 差很少, 只是觸發 effect 的時機不同,useEffect 在完成渲染後繪製觸發,而 useLayoutEffect 在 diff 完成後觸發:
export function useEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
// ⚛️狀態變化
state._value = callback;
state._args = args;
currentComponent.__hooks._pendingEffects.push(state); // ⚛️推動_pendingEffects隊列
afterPaint(currentComponent);
}
}
export function useLayoutEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
// ⚛️狀態變化
state._value = callback;
state._args = args;
currentComponent.__hooks._pendingLayoutEffects.push(state); // ⚛️推動_pendingLayoutEffects隊列
}
}
複製代碼
看看如何觸發 effect. useEffect 和上面看到的enqueueRender
差很少,放進一個異步隊列中,由requestAnimationFrame
進行調度,批量處理:
// 這是一個相似於上面提到的異步隊列
afterPaint = component => {
if (!component._afterPaintQueued && // 避免組件重複推入
(component._afterPaintQueued = true) &&
afterPaintEffects.push(component) === 1 // 開始調度
)
requestAnimationFrame(scheduleFlushAfterPaint); // 由requestAnimationFrame調度
};
function scheduleFlushAfterPaint() {
setTimeout(flushAfterPaintEffects);
}
function flushAfterPaintEffects() {
afterPaintEffects.some(component => {
component._afterPaintQueued = false;
if (component._parentDom)
// 清空_pendingEffects隊列
component.__hooks._pendingEffects = handleEffects(component.__hooks._pendingEffects);
});
afterPaintEffects = [];
}
function handleEffects(effects) {
// 先清除後調用effect
effects.forEach(invokeCleanup); // 請調用清理
effects.forEach(invokeEffect); // 再調用effect
return [];
}
function invokeCleanup(hook) {
if (hook._cleanup) hook._cleanup();
}
function invokeEffect(hook) {
const result = hook._value();
if (typeof result === 'function') hook._cleanup = result;
}
複製代碼
再看看如何觸發 LayoutEffect, 很簡單,在 diff 完成後觸發, 這個過程是同步的.
options.diffed = vnode => {
const c = vnode._component;
if (!c) return;
const hooks = c.__hooks;
if (hooks) {
hooks._pendingLayoutEffects = handleEffects(hooks._pendingLayoutEffects);
}
};
複製代碼
👌,hooks 基本原理基本瞭解完畢, 最後仍是用一張圖來總結一下吧。
文章篇幅很長,主要是太多代碼了, 我本身也不喜歡看這種文章,因此沒指望讀者會看到這裏. 後面文章再想辦法改善改善. 謝謝你閱讀到這裏。
本期的主角自己是一個小而美的視圖框架,沒有其餘技術棧. 這裏就安利一下 Preact 做者developit的另一些小而美的庫吧.