- 資訊首頁(yè) > 開(kāi)發(fā)技術(shù) > web開(kāi)發(fā) > JavaScript >
- 教你如何從 html 實(shí)現一個(gè) react
React是一個(gè)簡(jiǎn)單的javascript UI庫,用于構建高效、快速的用戶(hù)界面。它是一個(gè)輕量級庫,因此很受歡迎。它遵循組件設計模式、聲明式編程范式和函數式編程概念,以使前端應用程序更高效。它使用虛擬DOM來(lái)有效地操作DOM。它遵循從高階組件到低階組件的單向數據流。
👉 我們認為,React 是用 JavaScript 構建快速響應的大型 Web 應用程序的首選方式。它在 Facebook 和 Instagram 上表現優(yōu)秀。
react 的理念是在于對大型項目的快速響應
,對于新版的 react 16.8 而言更是帶來(lái)的全新的理念fiber
去解決網(wǎng)頁(yè)快速響應時(shí)所伴隨的問(wèn)題,即 CPU 的瓶頸,傳統網(wǎng)頁(yè)瀏覽受制于瀏覽器刷新率、js 執行時(shí)間過(guò)長(cháng)等因素會(huì )造成頁(yè)面掉幀,甚至卡頓
react 由于自身的底層設計從而規避這一問(wèn)題的發(fā)生,所以 react16.8 的面世對于前端領(lǐng)域只辦三件事:快速響應、快速響應、還是 Tmd 快速響應 !,這篇文章將會(huì )從一個(gè) html 出發(fā),跟隨 react 的 fiber 理念,仿一個(gè)非?;A的 react
html
我們需要一個(gè) html 去撐起來(lái)整個(gè)頁(yè)面,支撐 react 運行,頁(yè)面中添加<div></div>
,之后添加一個(gè) script 標簽,因為需要使用import
進(jìn)行模塊化構建,所以需要為 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)行調試,接下來(lái)的操作也會(huì )用到
JavaScript
我們會(huì )仿寫(xiě)一個(gè)如下的 react,實(shí)現一個(gè)基礎的操作,在 <input/>
綁定事件,將輸入的值插入在 <h2/>
標簽內:
... function App() { return ( <div> <input onInput={updateValue} value={value} /> <h2>Hello {value}</h2> <hr /> </div> ); } ...
在 react 進(jìn)行 babel 編譯的時(shí)候,會(huì )將 JSX
語(yǔ)法轉化為 React.createElement()
的形式,如上被 retuen 的代碼就會(huì )被轉換成
... React.createElement( "div", null, React.createElement("input", { onInput: updateValue, value: value, }), React.createElement("h2", null, "Hello ", value), React.createElement("hr", null) ); ...
從轉換后的代碼我們可以看出 React.createElement 支持多個(gè)參數:
我們可以按照 React.createElement
的形式仿寫(xiě)一個(gè)可以實(shí)現同樣功能的 createElement
將 jsx 通過(guò)一種簡(jiǎn)單的數據結構展示出來(lái)即 虛擬DOM
這樣在更新時(shí),新舊節點(diǎn)的對比也可以轉化為虛擬 DOM 的對比
{ type:'節點(diǎn)標簽', props:{ props:'節點(diǎn)上的屬性,包括事件、類(lèi)...', children:'節點(diǎn)的子節點(diǎn)' } }
這里我們可以寫(xiě)一個(gè)函數實(shí)現下列需求
TEXT_ELEMENT
的節點(diǎn)類(lèi)型表示/** * 創(chuàng )建虛擬 DOM 結構 * @param {type} 標簽名 * @param {props} 屬性對象 * @param {children} 子節點(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: [], }, } ), }, });
實(shí)現 createElement
之后我們可以拿到虛擬 DOM,但是還需要 render
將代碼渲染到頁(yè)面,此時(shí)我們需要對 index.js
進(jìn)行處理,添加輸入事件,將 createElement
和 render
通過(guò) import 進(jìn)行引入,render 時(shí)傳入被編譯后的虛擬 DOM 和頁(yè)面的根元素 root
, 最后再進(jìn)行executeRender
調用,頁(yè)面被渲染,在頁(yè)面更新的時(shí)候再次調用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();
before 版本
render
函數幫助我們將 element 添加至真實(shí)節點(diǎn)中,首先它接受兩個(gè)參數:
根組件,其實(shí)是一個(gè) JSX 組件,也就是一個(gè) createElement 返回的虛擬 DOM
父節點(diǎn),也就是我們要將這個(gè)虛擬 DOM 渲染的位置
在 react 16.8 之前,渲染的方法是通過(guò)一下幾步進(jìn)行的
拿到虛擬 dom 進(jìn)行如上三步的遞歸調用,渲染出頁(yè)面 類(lèi)似于如下流程
/** * 將虛擬 DOM 添加至真實(shí) DOM * @param {element} 虛擬 DOM * @param {container} 真實(shí) DOM */ const render = (element, container) => { let dom; /* 處理節點(diǎn)(包括文本節點(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)
當我們寫(xiě)完如上的代碼,會(huì )發(fā)現這個(gè)遞歸調用是有問(wèn)題的
如上這部分工作被 React 官方稱(chēng)為 renderer,renderer 是第三方可以自己實(shí)現的一個(gè)模塊,還有個(gè)核心模塊叫做 reconsiler,reconsiler 的一大功能就是 diff 算法,他會(huì )計算出應該更新哪些頁(yè)面節點(diǎn),然后將需要更新的節點(diǎn)虛擬 DOM 傳遞給 renderer,renderer 負責將這些節點(diǎn)渲染到頁(yè)面上,但是但是他卻是同步的,一旦開(kāi)始渲染,就會(huì )將所有節點(diǎn)及其子節點(diǎn)全部渲染完成這個(gè)進(jìn)程才會(huì )結束。
React 的官方演講中有個(gè)例子,可以很明顯的看到這種同步計算造成的卡頓:
當 dom tree 很大的情況下,JS 線(xiàn)程的運行時(shí)間可能會(huì )比較長(cháng),在這段時(shí)間瀏覽器是不會(huì )響應其他事件的,因為 JS 線(xiàn)程和 GUI 線(xiàn)程是互斥的,JS 運行時(shí)頁(yè)面就不會(huì )響應,這個(gè)時(shí)間太長(cháng)了,用戶(hù)就可能看到卡頓,
此時(shí)我們可以分為兩步解決這個(gè)問(wèn)題
solution I 引入一個(gè)新的 Api
requestIdleCallback 接收一個(gè)回調,這個(gè)回調會(huì )在瀏覽器空閑時(shí)調用,每次調用會(huì )傳入一個(gè) IdleDeadline,可以拿到當前還空余多久, options 可以傳入參數最多等多久,等到了時(shí)間瀏覽器還不空就強制執行了。
將在瀏覽器的空閑時(shí)段內調用的函數排隊。這使開(kāi)發(fā)者能夠在主事件循環(huán)上執行后臺和低優(yōu)先級工作,而不會(huì )影響延遲關(guān)鍵事件
但是這個(gè) API 還在實(shí)驗中,兼容性不好,所以 React 官方自己實(shí)現了一套。本文會(huì )繼續使用 requestIdleCallback 來(lái)進(jìn)行任務(wù)調度
// 下一個(gè)工作單元 let nextUnitOfWork = null /** * workLoop 工作循環(huán)函數 * @param {deadline} 截止時(shí)間 */ function workLoop(deadline) { // 是否應該停止工作循環(huán)函數 let shouldYield = false // 如果存在下一個(gè)工作單元,且沒(méi)有優(yōu)先級更高的其他工作時(shí),循環(huán)執行 while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) // 如果截止時(shí)間快到了,停止工作循環(huán)函數 shouldYield = deadline.timeRemaining() < 1 } // 通知瀏覽器,空閑時(shí)間應該執行 workLoop requestIdleCallback(workLoop) } // 通知瀏覽器,空閑時(shí)間應該執行 workLoop requestIdleCallback(workLoop) // 執行單元事件,并返回下一個(gè)單元事件 function performUnitOfWork(nextUnitOfWork) { // TODO }
solution II 創(chuàng )建 fiber 的數據結構
Fiber 之前的數據結構是一棵樹(shù),父節點(diǎn)的 children 指向了子節點(diǎn),但是只有這一個(gè)指針是不能實(shí)現中斷繼續的。比如我現在有一個(gè)父節點(diǎn) A,A 有三個(gè)子節點(diǎn) B,C,D,當我遍歷到 C 的時(shí)候中斷了,重新開(kāi)始的時(shí)候,其實(shí)我是不知道 C 下面該執行哪個(gè)的,因為只知道 C,并沒(méi)有指針指向他的父節點(diǎn),也沒(méi)有指針指向他的兄弟。
Fiber 就是改造了這樣一個(gè)結構,加上了指向父節點(diǎn)和兄弟節點(diǎn)的指針:
每個(gè) fiber 都有一個(gè)鏈接指向它的第一個(gè)子節點(diǎn)、下一個(gè)兄弟節點(diǎn)和它的父節點(diǎn)。這種數據結構可以讓我們更方便的查找下一個(gè)工作單元,假定 A
是掛在 root 上的節點(diǎn) fiber 的渲染順序也如下步驟
我們通過(guò)這個(gè)數據結構實(shí)現一個(gè) fiber
//創(chuàng )建最初的根fiber wipRoot = { dom: container, props: { children: [element] }, }; performUnitOfWork(wipRoot);
隨后調用performUnitOfWork
自上而下構造整個(gè) fiber 樹(shù)
/** * performUnitOfWork用來(lái)執行任務(wù) * @param {fiber} 我們的當前fiber任務(wù) * @return {fiber} 下一個(gè)任務(wù)f(shuō)iber任務(wù) */ const performUnitOfWork = fiber => { if (!fiber.dom) fiber.dom = createDom(fiber); // 創(chuàng )建一個(gè)DOM掛載上去 const elements = fiber.props.children; //當前元素下的所有同級節點(diǎn) // 如果有父節點(diǎn),將當前節點(diǎn)掛載到父節點(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è)子元素擁有指向下一個(gè)子元素的指針 prevSibling.sibling = newFiber; } prevSibling = fiber; }); } // 先找子元素,沒(méi)有子元素了就找兄弟元素 // 兄弟元素也沒(méi)有了就返回父元素 // 最后到根節點(diǎn)結束 // 這個(gè)遍歷的順序是從上到下,從左到右 if (fiber.child) { return fiber.child; } else { let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling; } nextFiber = nextFiber.return; } } }
currentRoot
reconcile 其實(shí)就是虛擬 DOM 樹(shù)的 diff 操作,將更新前的 fiber tree 和更新后的 fiber tree 進(jìn)行比較,得到比較結果后,僅對有變化的 fiber 對應的 dom 節點(diǎn)進(jìn)行更新。
新增 currentRoot 變量,保存根節點(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樹(shù)的指向,將緩存中的fiber樹(shù)替換到頁(yè)面中的fiber tree */ currentRoot = wipRoot wipRoot = null }
reconcileChildren
在對比 fiber tree 時(shí)
僅更新 props,設置 effectTag 為 UPDATE
;創(chuàng )建一個(gè)新的 dom 節點(diǎn),設置 effectTag 為 PLACEMENT
;刪除舊 fiber,設置 effectTag 為 DELETION
/** * 協(xié)調子節點(diǎn) * @param {fiber} fiber * @param {elements} fiber 的 子節點(diǎn) */ function reconcileChildren(wipFiber, elements) { let index = 0;// 用于統計子節點(diǎn)的索引值 let oldFiber = wipFiber.alternate && wipFiber.alternate.child; //更新時(shí)才會(huì )產(chǎn)生 let prevSibling;// 上一個(gè)兄弟節點(diǎn) while (index < elements.length || oldFiber) { /** * 遍歷子節點(diǎn) * oldFiber判斷是更新觸發(fā)還是首次觸發(fā),更新觸發(fā)時(shí)為元素下所有節點(diǎn) */ let newFiber; const element = elements[index]; const sameType = oldFiber && element && element.type == oldFiber.type; // fiber 類(lèi)型是否相同點(diǎn) /** * 更新時(shí) * 同標簽不同屬性,更新屬性 */ if (sameType) { newFiber = { type: oldFiber.type, props: element.props, //只更新屬性 dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE", }; } /** * 不同標簽,即替換了標簽 or 創(chuàng )建新標簽 */ if (element && !sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT", }; } /** * 節點(diǎn)被刪除了 */ if (oldFiber && !sameType) { oldFiber.effectTag = "DELETION"; deletions.push(oldFiber); } if (oldFiber) oldFiber = oldFiber.sibling; // 父級的child指向第一個(gè)子元素 if (index === 0) { // fiber的第一個(gè)子節點(diǎn)是它的子節點(diǎn) wipFiber.child = newFiber; } else { // fiber 的其他子節點(diǎn),是它第一個(gè)子節點(diǎn)的兄弟節點(diǎn) prevSibling.sibling = newFiber; } // 把新建的 newFiber 賦值給 prevSibling,這樣就方便為 newFiber 添加兄弟節點(diǎn)了 prevSibling = newFiber; // 索引值 + 1 index++; } }
在 commit 時(shí),根據 fiber 節點(diǎn)上effectTag
的屬性執行不同的渲染操作
after 版本(commit)
在 commitWork 中對 fiber 的 effectTag 進(jìn)行判斷,處理真正的 DOM 操作。
/** * @param {fiber} fiber 結構的虛擬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í)我們著(zhù)重來(lái)看updateDom
發(fā)生了什么,我們拿到 dom 上被改變的新舊屬性,進(jìn)行操作
/* isEvent :拿到事件屬性 isProperty :拿到非節點(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開(kāi)頭的事件屬性 * 2、拿到被刪除的事件 * 3、已刪除的事件取消監聽(tīng) */ Object.keys(prevProps) .filter(isEvent) .filter(key => !(key in nextProps)) .forEach(name => { const eventType = name.toLowerCase().substring(2); dom.removeEventListener(eventType, prevProps[name]); }); /** * 便利舊屬性 * 1、拿到非事件屬性和非子節點(diǎn)的屬性 * 2、拿到被刪除的屬性 * 3、刪除屬性 */ Object.keys(prevProps) .filter(isProperty) .filter(key => !(key in nextProps)) .forEach(key => delete dom[key]); /** * 便利新屬性 * 1、拿到非事件屬性和非子節點(diǎn)的屬性 * 2、拿到前后改變的屬性 * 3、添加屬性 */ Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name]; }); /** * 便利新屬性 * 1、拿到on開(kāi)頭的事件屬性 * 2、拿到前后改變的事件屬性 * 3、為新增的事件屬性添加監聽(tīng) */ Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name.toLowerCase().substring(2); dom.addEventListener(eventType, nextProps[name]); }); }
完成了一系列對 dom 的操作,我們將新改變的 dom 渲染到頁(yè)面,當 input 事件執行時(shí),頁(yè)面又會(huì )進(jìn)行渲染,但此時(shí)會(huì )進(jìn)入更新 fiber 樹(shù)的邏輯,
alternate 指向之前的 fiber 節點(diǎn)進(jìn)行復用,更快的執行 Update 操作,如圖:
大功告成!
完整代碼可以看我。
結論與總結 💢
結論
至此,謝謝各位在百忙之中點(diǎn)開(kāi)這篇文章,希望對你們能有所幫助,如有問(wèn)題歡迎各位大佬指正。工作原因這篇文章大概斷斷續續寫(xiě)了有一個(gè)月,工作上在忙一個(gè)基于 騰訊云TRTC
+websocket
的小程序電話(huà)功能,有時(shí)間也會(huì )寫(xiě)成文章分享一下,當然 react 的實(shí)現文章也會(huì )繼續
👋: 歡迎給個(gè) star,謝謝大家了
參考文獻
🍑:
🍑:
🍑:
🍑:
🍑:
🍑:
🍑:
到此這篇關(guān)于教你如何從 html 實(shí)現一個(gè) react的文章就介紹到這了,更多相關(guān) html 實(shí)現react內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自本網(wǎng)站內容采集于網(wǎng)絡(luò )互聯(lián)網(wǎng)轉載等其它媒體和分享為主,內容觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如侵犯了原作者的版權,請告知一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容,聯(lián)系我們QQ:712375056,同時(shí)歡迎投稿傳遞力量。
Copyright ? 2009-2022 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 特網(wǎng)科技 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 百度云 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站