新聞中心
與其看各種解釋,不如手寫一個(gè) fiber 版 React,當(dāng)你能實(shí)現(xiàn)的時(shí)候,一定是徹底理解了。

創(chuàng)新互聯(lián)建站專注于企業(yè)成都營銷網(wǎng)站建設(shè)、網(wǎng)站重做改版、息烽網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、H5響應(yīng)式網(wǎng)站、商城網(wǎng)站開發(fā)、集團(tuán)公司官網(wǎng)建設(shè)、成都外貿(mào)網(wǎng)站建設(shè)、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性價(jià)比高,為息烽等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。
vdom 和 fiber
首先,我們用 vdom 來描述界面結(jié)構(gòu),比如這樣:
{
"type": "ul",
"props": {
"className": "list",
"children": [
{
"type": "li",
"props": {
"className": "item",
"children": [
"aa"
]
}
},
{
"type": "li",
"props": {
"className": "item",
"children": [
"bb"
]
}
}
]
}
}
這很明顯就是一個(gè) ul、li 的結(jié)構(gòu)。但是我們不會(huì)直接手寫 vdom,而是會(huì)用 jsx:
const data = {
item1: 'bb',
item2: 'cc'
}
const jsx = - alert(2)}>aa
- {data.item1}xxx
- {data.item2}
jsx 使用 babel 編譯,我們配置一下 .babelrc:
module.exports = {
presets: [
[
'@babel/preset-react',
{
pragma: 'Dong.createElement'
}
]
]
}
然后用 babel 編譯它:
babel index.js -d ./dist
編譯結(jié)果是這樣的:
const data = {
item1: 'bb',
item2: 'cc'
};
const jsx = Dong.createElement("ul", {
className: "list"
}, Dong.createElement("li", {
className: "item",
style: {
background: 'blue',
color: 'pink'
},
onClick: () => alert(2)
}, "aa"), Dong.createElement("li", {
className: "item"
}, data.item1, Dong.createElement("i", null, "xxx")), Dong.createElement("li", {
className: "item"
}, data.item2));
這里的 createElement 就叫做 render function,它的執(zhí)行結(jié)果是 vdom。
為什么不直接把 jsx 編譯為 vdom 呢?
因?yàn)?render function 可以執(zhí)行動(dòng)態(tài)邏輯呀。我們可以加入 state、props,也可以包裝一下實(shí)現(xiàn)組件。
這樣,我們只要實(shí)現(xiàn) Dong.createElement 就能拿到 vdom 了:
createElement 就是返回 type、props、children 的對(duì)象。
我們把 children 也放在 props 里,并且文本節(jié)點(diǎn)單獨(dú)創(chuàng)建:
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
}
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
const Dong = {
createElement
}
這樣執(zhí)行以后渲染出來的就是 vdom:
我打印了一下:
接下來遞歸渲染這棵 vdom 不就是渲染么,也就是通過 document.createElement 創(chuàng)建元素、設(shè)置屬性、樣式、事件監(jiān)聽器等。
等等,如果這樣做,那就是 React 16 之前的架構(gòu)了。這個(gè)我們實(shí)現(xiàn)過:
手寫簡(jiǎn)易前端框架:vdom 渲染和 jsx 編譯
手寫簡(jiǎn)易前端框架:function 和 class 組件
手寫簡(jiǎn)易前端框架:vdom 渲染和 jsx 編譯
React 16 之后引入了 fiber 架構(gòu),就是在這里做了改變,它不是直接渲染 vdom 了,而是先轉(zhuǎn)成 fiber:
本來 vdom 里通過 children 關(guān)聯(lián)父子節(jié)點(diǎn),而 fiber 里面則是通過 child 關(guān)聯(lián)第一個(gè)子節(jié)點(diǎn),然后通過 sibling 串聯(lián)起下一個(gè),所有的節(jié)點(diǎn)可以 return 到父節(jié)點(diǎn)。
這樣不就把一顆 vdom 樹,變成了 fiber 鏈表么?
然后渲染 fiber 就可以了,和渲染 vdom 的時(shí)候一樣。
為什么費(fèi)這么多是轉(zhuǎn)成另一種結(jié)構(gòu)再渲染呢?這不是多次一舉么?
那肯定不是,fiber 架構(gòu)的意義在這:
之前我們是遞歸渲染 vdom 的,然后 diff 下來做 patch 的渲染:
這個(gè)渲染和 diff 是遞歸進(jìn)行的。
現(xiàn)在變成了這樣:
先把 vdom 轉(zhuǎn) fiber,也就是 reconcile 的過程,因?yàn)?fiber 是鏈表,就可以打斷,用 schedule 來空閑時(shí)調(diào)度(requestIdleCallback)就行,最后全部轉(zhuǎn)完之后,再一次性 render,這個(gè)過程叫做 commit。
這樣,之前只有 vdom 的 render 和 patch,現(xiàn)在卻變成了 vdom 轉(zhuǎn) fiber 的 reconcile,空閑調(diào)度 reconcile 的 scdule,最后把 fiber 渲染的 commit 三個(gè)階段。
意義就在于這個(gè)可打斷上。因?yàn)檫f歸渲染 vdom 可能耗時(shí)很多,JS 計(jì)算量大了會(huì)阻塞渲染,而 fiber 是可打斷的,就不會(huì)阻塞渲染,而且還會(huì)在這個(gè)過程中把需要用到的 dom 創(chuàng)建好,做好 diff 來確定是增是刪還是改。
dom 有了,增刪改也知道了咋做了,一次性 commit 不就很快了么。
這就是 fiber 架構(gòu)的意義!
接下來我們實(shí)現(xiàn)下。
實(shí)現(xiàn) fiber 版 react
我們從上到下來做吧,也就是分別實(shí)現(xiàn) schedule、reconcile、commit
scheduleschdule
就是空閑調(diào)度,也就是這樣的:
function workLoop(deadline) {
// do xxx
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
它就是一個(gè)不斷的循環(huán),就像 event loop 一樣,可以叫做 reconcile loop。
然后它做的事情就是 vdom 轉(zhuǎn) fiber,也就是 reconcile:
我們用兩個(gè)全局變量來記錄當(dāng)前處理到的 fiber 節(jié)點(diǎn)、根 fiber 節(jié)點(diǎn):
let nextFiberReconcileWork = null;
let wipRoot = null;
它做的事情就是循環(huán)處理完所有的 reconcile:
let shouldYield = false;
while (nextFiberReconcileWork && !shouldYield) {
nextFiberReconcileWork = performNextWork(
nextFiberReconcileWork
);
shouldYield = deadline.timeRemaining() < 1;
}
如果有下一個(gè) fiber,并且還有空閑時(shí)間,那就執(zhí)行下一個(gè) vdom 轉(zhuǎn) fiber 的 renconcile
如果全部都轉(zhuǎn)完了,那就 commit:
if (!nextFiberReconcileWork) {
commitRoot();
}
所以,schedule 的代碼就是這樣的:
let nextFiberReconcileWork = null;
let wipRoot = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextFiberReconcileWork && !shouldYield) {
nextFiberReconcileWork = performNextWork(
nextFiberReconcileWork
);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextFiberReconcileWork) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
每次執(zhí)行的 performNextWork 就是 reconcile:
function performNextWork(fiber) {
reconcile(fiber);
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.return;
}
}
reconcile 當(dāng)前 fiber 節(jié)點(diǎn),然后再按照順序繼續(xù)處理 child、sibling,處理完之后回到 return 的 fiber 節(jié)點(diǎn)。
這樣不斷的調(diào)度 reconcile。
這就是 schedule 做的事情:schedule 就是通過空閑調(diào)度每個(gè) fiber 節(jié)點(diǎn)的 reconcile(vdom 轉(zhuǎn) fiber),全部 reconcile 完了就執(zhí)行 commit。
接下來實(shí)現(xiàn) reconcile:
reconcile
schdule 的 loop 已經(jīng)在不斷進(jìn)行了,那么只要提交一個(gè) nextFiberReconcileWork,下次 loop 就能處理到。
所以,這就是 render 的實(shí)現(xiàn):
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
}
}
nextFiberReconcileWork = wipRoot
}
創(chuàng)建根 fiber 節(jié)點(diǎn),賦值給 wipRoot,也就是 working in progress 的 fiber root 的意思。并且下一個(gè)處理的 fiber 節(jié)點(diǎn)指向它,那么下次 schedule 就會(huì)調(diào)度這個(gè) fiber 節(jié)點(diǎn),開始 reconcile。
reconcile 是 vdom 轉(zhuǎn) fiber,但還會(huì)做兩件事:一個(gè)是提前創(chuàng)建對(duì)應(yīng)的 dom 節(jié)點(diǎn),一個(gè)是做 diff,確定是增、刪還是改。
reconcile 的實(shí)現(xiàn)是這樣的:
function reconcile(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
fiber.props.children 就是 vdom 的子節(jié)點(diǎn),這里的 reconcileChildren 就是把之前的 vdom 轉(zhuǎn)成 child、sibling、return 這樣串聯(lián)起來的 fiber 鏈表:
循環(huán)處理每一個(gè) vdom 的 elements,如果 index 是 0,那就是 child 串聯(lián),否則是 sibling 串聯(lián)。創(chuàng)建出的節(jié)點(diǎn)都要用 return 指向父節(jié)點(diǎn):
function reconcileChildren(wipFiber, elements) {
let index = 0
let prevSibling = null
while (
index < elements.length
) {
const element = elements[index]
let newFiber = {
type: element.type,
props: element.props,
dom: null,
return: wipFiber,
effectTag: "PLACEMENT",
}
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
因?yàn)槲覀冎粚?shí)現(xiàn)渲染,暫時(shí)不做 diff 和刪除修改,所以這里的 effectTag 都是 placement,也就是新增元素。
通過 schdule 空閑調(diào)度這樣處理每一個(gè) vdom 轉(zhuǎn) fiber,就能生成整個(gè) fiber 鏈表。
所以,這就是 reconcile 做的事情:reconcile 負(fù)責(zé) vdom 轉(zhuǎn) fiber,并且還會(huì)準(zhǔn)備好要用的 dom 節(jié)點(diǎn)、確定好是增、刪、還是改,通過 schdule 的調(diào)度,最終把整個(gè) vdom 樹轉(zhuǎn)成了 fiber 鏈表。
當(dāng) fiber 轉(zhuǎn)完了,那么 schdule 調(diào)度就進(jìn)入到了這里:
if (!nextFiberReconcileWork) {
commitRoot();
}
開始執(zhí)行 commit:
commit
commit 就是對(duì) dom 的增刪改,而且比之前 vdom 架構(gòu)時(shí)的渲染還要快,因?yàn)?dom 都提前創(chuàng)建了、也知道是增是刪還是改了,那剩下的不就很簡(jiǎn)單了么?
我們從根 fiber 開始 commit,并且把 wipRoot 設(shè)置為空,因?yàn)椴辉傩枰{(diào)度它了:
function commitRoot() {
commitWork(wipRoot.child);
wipRoot = null
}每個(gè) fiber 節(jié)點(diǎn)的渲染就是按照 child、sibling 的順序以此插入到 dom 中:
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.return
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.return
}
const domParent = domParentFiber.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}這里每個(gè) fiber 節(jié)點(diǎn)都要往上找它的父節(jié)點(diǎn),因?yàn)槲覀冎皇切略?,那么只需?appendChild 就行。
dom 已經(jīng)在 reconcile 節(jié)點(diǎn)就創(chuàng)建好了,當(dāng)時(shí)我們沒細(xì)講,現(xiàn)在來看下 dom 創(chuàng)建邏輯:
function createDom(fiber) {
const dom = fiber.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(fiber.type);
for (const prop in fiber.props) {
setAttribute(dom, prop, fiber.props[prop]);
}
return dom;
}就是根據(jù)類型創(chuàng)建元素,然后設(shè)置屬性:
屬性要分別處理 style、文本節(jié)點(diǎn)的 value、事件監(jiān)聽器:
function isEventListenerAttr(key, value) {
return typeof value == 'function' && key.startsWith('on');
}
function isStyleAttr(key, value) {
return key == 'style' && typeof value == 'object';
}
function isPlainAttr(key, value) {
return typeof value != 'object' && typeof value != 'function';
}
const setAttribute = (dom, key, value) => {
if (key === 'children') {
return;
}
if (key === 'nodeValue') {
dom.textContent = value;
} else if (isEventListenerAttr(key, value)) {
const eventType = key.slice(2).toLowerCase();
dom.addEventListener(eventType, value);
} else if (isStyleAttr(key, value)) {
Object.assign(dom.style, value);
} else if (isPlainAttr(key, value)) {
dom.setAttribute(key, value);
}
};這在 reconcile 時(shí)就做好了,commit 自然很快。
這就是 commit 做的事情:把 reconcile 產(chǎn)生的 fiber 鏈表一次性添加到 dom 中,因?yàn)?fiber 對(duì)應(yīng)的節(jié)點(diǎn)提前創(chuàng)建好了、是增是刪還是改也都知道了,所以,這一個(gè)階段很快。
這樣,我們就實(shí)現(xiàn)了簡(jiǎn)易版 React,當(dāng)然,目前只實(shí)現(xiàn)了渲染,我們來試下效果:
這樣一段 jsx:
const data = {
item1: 'bb',
item2: 'cc'
}
const jsx = - alert(2)}>aa
- {data.item1}xxx
- {data.item2}
console.log(JSON.stringify(jsx, null, 4));
Dong.render(jsx, document.getElementById("root"));
渲染以后是這樣的:
代碼上傳到了 github:https://github.com/QuarkGluonPlasma/frontend-framework-exercize
總結(jié)
fiber 是 React16 引入的架構(gòu)變動(dòng),為了徹底理解它,我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)易版的 fiber 架構(gòu)的 React。
界面通過 vdom 描述,但是不是直接手寫 vdom,而是 jsx 編譯產(chǎn)生的 render function 之后以后生成的。這樣就可以加上 state、props 和一些動(dòng)態(tài)邏輯,動(dòng)態(tài)產(chǎn)生 vdom。
vdom 生成之后不再是直接渲染,而是先轉(zhuǎn)成 fiber,這個(gè) vdom 轉(zhuǎn) fiber 的過程叫做 reconcile。
fiber 是一個(gè)鏈表結(jié)構(gòu),可以打斷,這樣就可以通過 requestIdleCallback 來空閑調(diào)度 reconcile,這樣不斷的循環(huán),直到處理完所有的 vdom 轉(zhuǎn) fiber 的 reconcile,就開始 commit,也就是更新到 dom。
reconcile 的過程會(huì)提前創(chuàng)建好 dom,還會(huì)標(biāo)記出增刪改,那么 commit 階段就很快了。
從之前遞歸渲染時(shí)做 diff 來確定增刪改以及創(chuàng)建 dom,提前到了可打斷的 reconcile 階段,讓 commit 變得非???,這就是 fiber 架構(gòu)的目的和意義。
當(dāng)然,我們還沒實(shí)現(xiàn) hooks 以及更新刪除,后續(xù)會(huì)陸續(xù)實(shí)現(xiàn)。
如果想徹底搞懂 fiber 架構(gòu),不妨按照文章所寫來實(shí)現(xiàn)一遍 reconcile 的過程,一定會(huì)讓你對(duì)它有更深的認(rèn)識(shí)。
當(dāng)前文章:手寫簡(jiǎn)易版React來徹底搞懂Fiber架構(gòu)
本文網(wǎng)址:http://m.fisionsoft.com.cn/article/dpsgcsp.html


咨詢
建站咨詢
