教你如何從 html 實(shí)現(xiàn)一個(gè) react
什么是 React
React是一個(gè)簡單的javascript UI庫,用于構(gòu)建高效、快速的用戶界面。它是一個(gè)輕量級庫,因此很受歡迎。它遵循組件設(shè)計(jì)模式、聲明式編程范式和函數(shù)式編程概念,以使前端應(yīng)用程序更高效。它使用虛擬DOM來有效地操作DOM。它遵循從高階組件到低階組件的單向數(shù)據(jù)流。
前言 📝
👉 我們認(rèn)為,React 是用 JavaScript 構(gòu)建快速響應(yīng)的大型 Web 應(yīng)用程序的首選方式。它在 Facebook 和 Instagram 上表現(xiàn)優(yōu)秀。官網(wǎng)地址
react 的理念是在于對大型項(xiàng)目的快速響應(yīng)
,對于新版的 react 16.8 而言更是帶來的全新的理念fiber
去解決網(wǎng)頁快速響應(yīng)時(shí)所伴隨的問題,即 CPU 的瓶頸,傳統(tǒng)網(wǎng)頁瀏覽受制于瀏覽器刷新率、js 執(zhí)行時(shí)間過長等因素會造成頁面掉幀,甚至卡頓
react 由于自身的底層設(shè)計(jì)從而規(guī)避這一問題的發(fā)生,所以 react16.8 的面世對于前端領(lǐng)域只辦三件事:快速響應(yīng)、快速響應(yīng)、還是 Tmd 快速響應(yīng) !,這篇文章將會從一個(gè) html 出發(fā),跟隨 react 的 fiber 理念,仿一個(gè)非?;A(chǔ)的 react
一開始的準(zhǔn)備工作 🤖
html
我們需要一個(gè) html 去撐起來整個(gè)頁面,支撐 react 運(yùn)行,頁面中添加<div></div>
,之后添加一個(gè) script 標(biāo)簽,因?yàn)樾枰褂?code>import進(jìn)行模塊化構(gòu)建,所以需要為 script 添加 type 為module
的屬性
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="root"></div> <script type="module" src="./index.js" ></script> </body> </html>
推薦安裝一個(gè) Live Server
插件,有助于我們對代碼進(jìn)行調(diào)試,接下來的操作也會用到
JavaScript
我們會仿寫一個(gè)如下的 react,實(shí)現(xiàn)一個(gè)基礎(chǔ)的操作,在 <input/>
綁定事件,將輸入的值插入在 <h2/>
標(biāo)簽內(nèi):
... function App() { return ( <div> <input onInput={updateValue} value={value} /> <h2>Hello {value}</h2> <hr /> </div> ); } ...
在 react 進(jìn)行 babel 編譯的時(shí)候,會將 JSX
語法轉(zhuǎn)化為 React.createElement()
的形式,如上被 retuen 的代碼就會被轉(zhuǎn)換成
... React.createElement( "div", null, React.createElement("input", { onInput: updateValue, value: value, }), React.createElement("h2", null, "Hello ", value), React.createElement("hr", null) ); ...
從轉(zhuǎn)換后的代碼我們可以看出 React.createElement 支持多個(gè)參數(shù):
- type,節(jié)點(diǎn)類型
- config, 節(jié)點(diǎn)上的屬性,比如 id 和 href
- children, 子元素了,子元素可以有多個(gè),類型可以是簡單的文本,也可以還是 React.createElement,如果是 React.createElement,其實(shí)就是子節(jié)點(diǎn)了,子節(jié)點(diǎn)下面還可以有子節(jié)點(diǎn)。這樣就用 React.createElement 的嵌套關(guān)系實(shí)現(xiàn)了 HTML 節(jié)點(diǎn)的樹形結(jié)構(gòu)。
我們可以按照 React.createElement
的形式仿寫一個(gè)可以實(shí)現(xiàn)同樣功能的 createElement
將 jsx 通過一種簡單的數(shù)據(jù)結(jié)構(gòu)展示出來即 虛擬DOM
這樣在更新時(shí),新舊節(jié)點(diǎn)的對比也可以轉(zhuǎn)化為虛擬 DOM 的對比
{ type:'節(jié)點(diǎn)標(biāo)簽', props:{ props:'節(jié)點(diǎn)上的屬性,包括事件、類...', children:'節(jié)點(diǎn)的子節(jié)點(diǎn)' } }
這里我們可以寫一個(gè)函數(shù)實(shí)現(xiàn)下列需求
- 原則是將所有的參數(shù)返回到一個(gè)對象上
- children 也要放到 props 里面去,這樣我們在組件里面就能通過 props.children 拿到子元素
- 當(dāng)子組件是文本節(jié)點(diǎn)時(shí),通過構(gòu)造一種 type 為
TEXT_ELEMENT
的節(jié)點(diǎn)類型表示
/** * 創(chuàng)建虛擬 DOM 結(jié)構(gòu) * @param {type} 標(biāo)簽名 * @param {props} 屬性對象 * @param {children} 子節(jié)點(diǎn) * @return {element} 虛擬 DOM */ const createElement = (type, props, ...children) => ({ type, props: { ...props, children: children.map(child => typeof child === "object" ? child : { type: "TEXT_ELEMENT", props: { nodeValue: child, children: [], }, } ), }, });
react 中 createElement 源碼實(shí)現(xiàn)
實(shí)現(xiàn) createElement
之后我們可以拿到虛擬 DOM,但是還需要 render
將代碼渲染到頁面,此時(shí)我們需要對 index.js
進(jìn)行處理,添加輸入事件,將 createElement
和 render
通過 import 進(jìn)行引入,render 時(shí)傳入被編譯后的虛擬 DOM 和頁面的根元素 root
, 最后再進(jìn)行executeRender
調(diào)用,頁面被渲染,在頁面更新的時(shí)候再次調(diào)用executeRender
進(jìn)行更新渲染
import {createElement,render} from "./mini/index.js"; const updateValue = e => executeRender(e.target.value); const executeRender = (value = "World") => { const element = createElement( "div", null, createElement("input", { onInput: updateValue, value: value, }), createElement("h2", null, "Hello ", value), createElement("hr", null) ); render(element, document.getElementById("root")); }; executeRender();
render 的時(shí)候做了什么 🥔
before 版本
render
函數(shù)幫助我們將 element 添加至真實(shí)節(jié)點(diǎn)中,首先它接受兩個(gè)參數(shù):
根組件,其實(shí)是一個(gè) JSX 組件,也就是一個(gè) createElement 返回的虛擬 DOM
父節(jié)點(diǎn),也就是我們要將這個(gè)虛擬 DOM 渲染的位置
在 react 16.8 之前,渲染的方法是通過一下幾步進(jìn)行的
- 創(chuàng)建 element.type 類型的 dom 節(jié)點(diǎn),并添加到 root 元素下(文本節(jié)點(diǎn)特殊處理)
- 將 element 的 props 添加到對應(yīng)的 DOM 上,事件進(jìn)行特殊處理,掛載到 document 上(react17 調(diào)整為掛在到 container 上)
- 將 element.children 循環(huán)添加至 dom 節(jié)點(diǎn)中;
拿到虛擬 dom 進(jìn)行如上三步的遞歸調(diào)用,渲染出頁面 類似于如下流程
/** * 將虛擬 DOM 添加至真實(shí) DOM * @param {element} 虛擬 DOM * @param {container} 真實(shí) DOM */ const render = (element, container) => { let dom; /* 處理節(jié)點(diǎn)(包括文本節(jié)點(diǎn)) */ if (typeof element !== "object") { dom = document.createTextNode(element); } else { dom = document.createElement(element.type); } /* 處理屬性(包括事件屬性) */ if (element.props) { Object.keys(element.props) .filter((key) => key != "children") .forEach((item) => { dom[item] = element.props[item]; }); Object.keys(element.props) .filter((key) => key.startsWith("on")) .forEach((name) => { const eventType = name.toLowerCase().substring(2); dom.addEventListener(eventType, nextProps[name]); }); } if ( element.props && element.props.children && element.props.children.length ) { /* 循環(huán)添加到dom */ element.props.children.forEach((child) => render(child, dom)); } container.appendChild(dom); };
after 版本(fiber)
當(dāng)我們寫完如上的代碼,會發(fā)現(xiàn)這個(gè)遞歸調(diào)用是有問題的
如上這部分工作被 React 官方稱為 renderer,renderer 是第三方可以自己實(shí)現(xiàn)的一個(gè)模塊,還有個(gè)核心模塊叫做 reconsiler,reconsiler 的一大功能就是 diff 算法,他會計(jì)算出應(yīng)該更新哪些頁面節(jié)點(diǎn),然后將需要更新的節(jié)點(diǎn)虛擬 DOM 傳遞給 renderer,renderer 負(fù)責(zé)將這些節(jié)點(diǎn)渲染到頁面上,但是但是他卻是同步的,一旦開始渲染,就會將所有節(jié)點(diǎn)及其子節(jié)點(diǎn)全部渲染完成這個(gè)進(jìn)程才會結(jié)束。
React 的官方演講中有個(gè)例子,可以很明顯的看到這種同步計(jì)算造成的卡頓:
當(dāng) dom tree 很大的情況下,JS 線程的運(yùn)行時(shí)間可能會比較長,在這段時(shí)間瀏覽器是不會響應(yīng)其他事件的,因?yàn)?JS 線程和 GUI 線程是互斥的,JS 運(yùn)行時(shí)頁面就不會響應(yīng),這個(gè)時(shí)間太長了,用戶就可能看到卡頓,
此時(shí)我們可以分為兩步解決這個(gè)問題
- 允許中斷渲染工作,如果有優(yōu)先級更高的工作插入,則暫時(shí)中斷瀏覽器渲染,待完成該工作后,恢復(fù)瀏覽器渲染;
- 將渲染工作進(jìn)行分解,分解成一個(gè)個(gè)小單元;
solution I 引入一個(gè)新的 Api
requestIdleCallback 接收一個(gè)回調(diào),這個(gè)回調(diào)會在瀏覽器空閑時(shí)調(diào)用,每次調(diào)用會傳入一個(gè) IdleDeadline,可以拿到當(dāng)前還空余多久, options 可以傳入?yún)?shù)最多等多久,等到了時(shí)間瀏覽器還不空就強(qiáng)制執(zhí)行了。
window.requestIdleCallback 將在瀏覽器的空閑時(shí)段內(nèi)調(diào)用的函數(shù)排隊(duì)。這使開發(fā)者能夠在主事件循環(huán)上執(zhí)行后臺和低優(yōu)先級工作,而不會影響延遲關(guān)鍵事件
但是這個(gè) API 還在實(shí)驗(yàn)中,兼容性不好,所以 React 官方自己實(shí)現(xiàn)了一套。本文會繼續(xù)使用 requestIdleCallback 來進(jìn)行任務(wù)調(diào)度
// 下一個(gè)工作單元 let nextUnitOfWork = null /** * workLoop 工作循環(huán)函數(shù) * @param {deadline} 截止時(shí)間 */ function workLoop(deadline) { // 是否應(yīng)該停止工作循環(huán)函數(shù) let shouldYield = false // 如果存在下一個(gè)工作單元,且沒有優(yōu)先級更高的其他工作時(shí),循環(huán)執(zhí)行 while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) // 如果截止時(shí)間快到了,停止工作循環(huán)函數(shù) shouldYield = deadline.timeRemaining() < 1 } // 通知瀏覽器,空閑時(shí)間應(yīng)該執(zhí)行 workLoop requestIdleCallback(workLoop) } // 通知瀏覽器,空閑時(shí)間應(yīng)該執(zhí)行 workLoop requestIdleCallback(workLoop) // 執(zhí)行單元事件,并返回下一個(gè)單元事件 function performUnitOfWork(nextUnitOfWork) { // TODO }
solution II 創(chuàng)建 fiber 的數(shù)據(jù)結(jié)構(gòu)
Fiber 之前的數(shù)據(jù)結(jié)構(gòu)是一棵樹,父節(jié)點(diǎn)的 children 指向了子節(jié)點(diǎn),但是只有這一個(gè)指針是不能實(shí)現(xiàn)中斷繼續(xù)的。比如我現(xiàn)在有一個(gè)父節(jié)點(diǎn) A,A 有三個(gè)子節(jié)點(diǎn) B,C,D,當(dāng)我遍歷到 C 的時(shí)候中斷了,重新開始的時(shí)候,其實(shí)我是不知道 C 下面該執(zhí)行哪個(gè)的,因?yàn)橹恢?C,并沒有指針指向他的父節(jié)點(diǎn),也沒有指針指向他的兄弟。
Fiber 就是改造了這樣一個(gè)結(jié)構(gòu),加上了指向父節(jié)點(diǎn)和兄弟節(jié)點(diǎn)的指針:
- child 指向子組件
- sibling 指向兄弟組件
- return 指向父組件
每個(gè) fiber 都有一個(gè)鏈接指向它的第一個(gè)子節(jié)點(diǎn)、下一個(gè)兄弟節(jié)點(diǎn)和它的父節(jié)點(diǎn)。這種數(shù)據(jù)結(jié)構(gòu)可以讓我們更方便的查找下一個(gè)工作單元,假定 A
是掛在 root 上的節(jié)點(diǎn) fiber 的渲染順序也如下步驟
- 從 root 開始,找到第一個(gè)子節(jié)點(diǎn) A;
- 找到 A 的第一個(gè)子節(jié)點(diǎn) B
- 找到 B 的第一個(gè)子節(jié)點(diǎn) E
- 找 E 的第一個(gè)子節(jié)點(diǎn),如無子節(jié)點(diǎn),則找下一個(gè)兄弟節(jié)點(diǎn),找到 E 的兄弟節(jié)點(diǎn) F
- 找 F 的第一個(gè)子節(jié)點(diǎn),如無子節(jié)點(diǎn),也無兄弟節(jié)點(diǎn),則找它的父節(jié)點(diǎn)的下一個(gè)兄弟節(jié)點(diǎn),找到 F 的 父節(jié)點(diǎn)的兄弟節(jié)點(diǎn) C;
- 找 C 的第一個(gè)子節(jié)點(diǎn),找不到,找兄弟節(jié)點(diǎn),D
- 找 D 的第一個(gè)子節(jié)點(diǎn),G
- 找 G 的第一個(gè)子節(jié)點(diǎn),找不到,找兄弟節(jié)點(diǎn),找不到,找父節(jié)點(diǎn) D 的兄弟節(jié)點(diǎn),也找不到,繼續(xù)找 D 的父節(jié)點(diǎn)的兄弟節(jié)點(diǎn),找到 root;
- 上一步已經(jīng)找到了 root 節(jié)點(diǎn),渲染已全部完成。
我們通過這個(gè)數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)一個(gè) fiber
//創(chuàng)建最初的根fiber wipRoot = { dom: container, props: { children: [element] }, }; performUnitOfWork(wipRoot);
隨后調(diào)用performUnitOfWork
自上而下構(gòu)造整個(gè) fiber 樹
/** * performUnitOfWork用來執(zhí)行任務(wù) * @param {fiber} 我們的當(dāng)前fiber任務(wù) * @return {fiber} 下一個(gè)任務(wù)fiber任務(wù) */ const performUnitOfWork = fiber => { if (!fiber.dom) fiber.dom = createDom(fiber); // 創(chuàng)建一個(gè)DOM掛載上去 const elements = fiber.props.children; //當(dāng)前元素下的所有同級節(jié)點(diǎn) // 如果有父節(jié)點(diǎn),將當(dāng)前節(jié)點(diǎn)掛載到父節(jié)點(diǎn)上 if (fiber.return) { fiber.return.dom.appendChild(fiber.dom); } let prevSibling = null; /* 之后代碼中我們將把此處的邏輯進(jìn)行抽離 */ if (elements && elements.length) { elements.forEach((element, index) => { const newFiber = { type: element.type, props: element.props, return: fiber, dom: null, }; // 父級的child指向第一個(gè)子元素 if (index === 0) { fiber.child = newFiber; } else { // 每個(gè)子元素?fù)碛兄赶蛳乱粋€(gè)子元素的指針 prevSibling.sibling = newFiber; } prevSibling = fiber; }); } // 先找子元素,沒有子元素了就找兄弟元素 // 兄弟元素也沒有了就返回父元素 // 最后到根節(jié)點(diǎn)結(jié)束 // 這個(gè)遍歷的順序是從上到下,從左到右 if (fiber.child) { return fiber.child; } else { let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling; } nextFiber = nextFiber.return; } } }
after 版本(reconcile)
currentRoot
reconcile 其實(shí)就是虛擬 DOM 樹的 diff 操作,將更新前的 fiber tree 和更新后的 fiber tree 進(jìn)行比較,得到比較結(jié)果后,僅對有變化的 fiber 對應(yīng)的 dom 節(jié)點(diǎn)進(jìn)行更新。
- 刪除不需要的節(jié)點(diǎn)
- 更新修改過的節(jié)點(diǎn)
- 添加新的節(jié)點(diǎn)
新增 currentRoot 變量,保存根節(jié)點(diǎn)更新前的 fiber tree,為 fiber 新增 alternate 屬性,保存 fiber 更新前的 fiber tree
let currentRoot = null function render (element, container) { wipRoot = { // 省略 alternate: currentRoot } } function commitRoot () { commitWork(wipRoot.child) /* 更改fiber樹的指向,將緩存中的fiber樹替換到頁面中的fiber tree */ currentRoot = wipRoot wipRoot = null }
- 如果新老節(jié)點(diǎn)類型一樣,復(fù)用老節(jié)點(diǎn) DOM,更新 props
- 如果類型不一樣,而且新的節(jié)點(diǎn)存在,創(chuàng)建新節(jié)點(diǎn)替換老節(jié)點(diǎn)
- 如果類型不一樣,沒有新節(jié)點(diǎn),有老節(jié)點(diǎn),刪除老節(jié)點(diǎn)
reconcileChildren
- 將 performUnitOfWork 中關(guān)于新建 fiber 的邏輯,抽離到 reconcileChildren 函數(shù)
- 在 reconcileChildren 中對比新舊 fiber;
在對比 fiber tree 時(shí)
- 當(dāng)新舊 fiber 類型相同時(shí) 保留 dom,
僅更新 props,設(shè)置 effectTag 為 UPDATE
; - 當(dāng)新舊 fiber 類型不同,且有新元素時(shí)
創(chuàng)建一個(gè)新的 dom 節(jié)點(diǎn),設(shè)置 effectTag 為 PLACEMENT
; - 當(dāng)新舊 fiber 類型不同,且有舊 fiber 時(shí)
刪除舊 fiber,設(shè)置 effectTag 為 DELETION
/** * 協(xié)調(diào)子節(jié)點(diǎn) * @param {fiber} fiber * @param {elements} fiber 的 子節(jié)點(diǎn) */ function reconcileChildren(wipFiber, elements) { let index = 0;// 用于統(tǒng)計(jì)子節(jié)點(diǎn)的索引值 let oldFiber = wipFiber.alternate && wipFiber.alternate.child; //更新時(shí)才會產(chǎn)生 let prevSibling;// 上一個(gè)兄弟節(jié)點(diǎn) while (index < elements.length || oldFiber) { /** * 遍歷子節(jié)點(diǎn) * oldFiber判斷是更新觸發(fā)還是首次觸發(fā),更新觸發(fā)時(shí)為元素下所有節(jié)點(diǎn) */ let newFiber; const element = elements[index]; const sameType = oldFiber && element && element.type == oldFiber.type; // fiber 類型是否相同點(diǎn) /** * 更新時(shí) * 同標(biāo)簽不同屬性,更新屬性 */ if (sameType) { newFiber = { type: oldFiber.type, props: element.props, //只更新屬性 dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE", }; } /** * 不同標(biāo)簽,即替換了標(biāo)簽 or 創(chuàng)建新標(biāo)簽 */ if (element && !sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT", }; } /** * 節(jié)點(diǎn)被刪除了 */ if (oldFiber && !sameType) { oldFiber.effectTag = "DELETION"; deletions.push(oldFiber); } if (oldFiber) oldFiber = oldFiber.sibling; // 父級的child指向第一個(gè)子元素 if (index === 0) { // fiber的第一個(gè)子節(jié)點(diǎn)是它的子節(jié)點(diǎn) wipFiber.child = newFiber; } else { // fiber 的其他子節(jié)點(diǎn),是它第一個(gè)子節(jié)點(diǎn)的兄弟節(jié)點(diǎn) prevSibling.sibling = newFiber; } // 把新建的 newFiber 賦值給 prevSibling,這樣就方便為 newFiber 添加兄弟節(jié)點(diǎn)了 prevSibling = newFiber; // 索引值 + 1 index++; } }
在 commit 時(shí),根據(jù) fiber 節(jié)點(diǎn)上effectTag
的屬性執(zhí)行不同的渲染操作
after 版本(commit)
在 commitWork 中對 fiber 的 effectTag 進(jìn)行判斷,處理真正的 DOM 操作。
- 當(dāng) fiber 的 effectTag 為 PLACEMENT 時(shí),表示是新增 fiber,將該節(jié)點(diǎn)新增至父節(jié)點(diǎn)中。
- 當(dāng) fiber 的 effectTag 為 DELETION 時(shí),表示是刪除 fiber,將父節(jié)點(diǎn)的該節(jié)點(diǎn)刪除。
- 當(dāng) fiber 的 effectTag 為 UPDATE 時(shí),表示是更新 fiber,更新 props 屬性。
/** * @param {fiber} fiber 結(jié)構(gòu)的虛擬dom */ function commitWork(fiber) { if (!fiber) return; const domParent = fiber.parent.dom; if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) { domParent.appendChild(fiber.dom); } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) { updateDom(fiber.dom, fiber.alternate.props, fiber.props); } else if (fiber.effectTag === "DELETION") { domParent.removeChild(fiber.dom); } // 遞歸操作子元素和兄弟元素 commitWork(fiber.child); commitWork(fiber.sibling); }
此時(shí)我們著重來看updateDom
發(fā)生了什么,我們拿到 dom 上被改變的新舊屬性,進(jìn)行操作
/* isEvent :拿到事件屬性 isProperty :拿到非節(jié)點(diǎn)、非事件屬性 isNew :拿到前后改變的屬性 */ const isEvent = key => key.startsWith("on"); const isProperty = key => key !== "children" && !isEvent(key); const isNew = (prev, next) => key => prev[key] !== next[key]; /** * 更新dom屬性 * @param {dom} fiber dom * @param {prevProps} fiber dom上舊的屬性 * @param {nextProps} fiber dom上新的屬性 */ function updateDom(dom, prevProps, nextProps) { /** * 便利舊屬性 * 1、拿到on開頭的事件屬性 * 2、拿到被刪除的事件 * 3、已刪除的事件取消監(jiān)聽 */ Object.keys(prevProps) .filter(isEvent) .filter(key => !(key in nextProps)) .forEach(name => { const eventType = name.toLowerCase().substring(2); dom.removeEventListener(eventType, prevProps[name]); }); /** * 便利舊屬性 * 1、拿到非事件屬性和非子節(jié)點(diǎn)的屬性 * 2、拿到被刪除的屬性 * 3、刪除屬性 */ Object.keys(prevProps) .filter(isProperty) .filter(key => !(key in nextProps)) .forEach(key => delete dom[key]); /** * 便利新屬性 * 1、拿到非事件屬性和非子節(jié)點(diǎn)的屬性 * 2、拿到前后改變的屬性 * 3、添加屬性 */ Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name]; }); /** * 便利新屬性 * 1、拿到on開頭的事件屬性 * 2、拿到前后改變的事件屬性 * 3、為新增的事件屬性添加監(jiān)聽 */ Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name.toLowerCase().substring(2); dom.addEventListener(eventType, nextProps[name]); }); }
完成了一系列對 dom 的操作,我們將新改變的 dom 渲染到頁面,當(dāng) input 事件執(zhí)行時(shí),頁面又會進(jìn)行渲染,但此時(shí)會進(jìn)入更新 fiber 樹的邏輯,
alternate 指向之前的 fiber 節(jié)點(diǎn)進(jìn)行復(fù)用,更快的執(zhí)行 Update 操作,如圖:
大功告成!
完整代碼可以看我github。
結(jié)論與總結(jié) 💢
結(jié)論
- 我們寫的 JSX 代碼被 babel 轉(zhuǎn)化成了 React.createElement。
- React.createElement 返回的其實(shí)就是虛擬 DOM 結(jié)構(gòu)。
- 虛擬 DOM 的調(diào)和和渲染可以簡單粗暴的遞歸,但是這個(gè)過程是同步的,如果需要處理的節(jié)點(diǎn)過多,可能會阻塞用戶輸入和動畫播放,造成卡頓。
- Fiber 是 16.x 引入的新特性,用處是將同步的調(diào)和變成異步的。
- Fiber 改造了虛擬 DOM 的結(jié)構(gòu),具有 父->第一個(gè)子, 子->兄, 子->父這幾個(gè)指針,有了這幾個(gè)指針,可以從任意一個(gè) Fiber 節(jié)點(diǎn)找到其他節(jié)點(diǎn)。
- Fiber 將整棵樹的同步任務(wù)拆分成了每個(gè)節(jié)點(diǎn)可以單獨(dú)執(zhí)行的異步執(zhí)行結(jié)構(gòu)。
- Fiber 可以從任意一個(gè)節(jié)點(diǎn)開始遍歷,遍歷是深度優(yōu)先遍歷,順序是 父->子->兄->父,也就是從上往下,從左往右。
- Fiber 的調(diào)和階段可以是異步的小任務(wù),但是提交階段( commit)必須是同步的。因?yàn)楫惒降?commit 可能讓用戶看到節(jié)點(diǎn)一個(gè)一個(gè)接連出現(xiàn),體驗(yàn)不好。
總結(jié)
- react hook 實(shí)現(xiàn) ✖
- react 合成事件 ✖
- 還有很多沒有實(shí)現(xiàn) 😤...
至此,謝謝各位在百忙之中點(diǎn)開這篇文章,希望對你們能有所幫助,如有問題歡迎各位大佬指正。工作原因這篇文章大概斷斷續(xù)續(xù)寫了有一個(gè)月,工作上在忙一個(gè)基于 騰訊云TRTC
+websocket
的小程序電話功能,有時(shí)間也會寫成文章分享一下,當(dāng)然 react 的實(shí)現(xiàn)文章也會繼續(xù)
👋:跳轉(zhuǎn) github 歡迎給個(gè) star,謝謝大家了
參考文獻(xiàn)
🍑:手寫系列-實(shí)現(xiàn)一個(gè)鉑金段位的 React
🍑:build-your-own-react(強(qiáng)烈推薦)
🍑:手寫 React 的 Fiber 架構(gòu),深入理解其原理
🍑:妙味課堂大圣老師 手寫 react 的 fiber 和 hooks 架構(gòu)
到此這篇關(guān)于教你如何從 html 實(shí)現(xiàn)一個(gè) react的文章就介紹到這了,更多相關(guān) html 實(shí)現(xiàn)react內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React?TypeScript?應(yīng)用中便捷使用Redux?Toolkit方法詳解
這篇文章主要為大家介紹了React?TypeScript?應(yīng)用中便捷使用Redux?Toolkit方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11詳解webpack + react + react-router 如何實(shí)現(xiàn)懶加載
這篇文章主要介紹了詳解webpack + react + react-router 如何實(shí)現(xiàn)懶加載,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-11-11