當我們由淺入深地認知一樣新事物的時候,,往往需要遵循 Why > What > How 這樣一個認知過程,。它們是相輔相成,、缺一不可的。而了解了具體的 What 和 How 之后,,往往能夠更加具象地回答理論層面的 Why,,因此,在進入 Why 的探索之前,,我們先整體感知一下 What 和 How 兩個過程,。 What 打開一眼便能看到官方給出的回答。 React 是用于構(gòu)建用戶界面的 JavaScript 庫,。 不知道你有沒有想過,,構(gòu)建用戶界面的方式有千百種,為什么 React 會突出,?站長交易同樣,,我們可以從 里得到回應(yīng)。 我們認為,, React 是用 JavaScript 構(gòu)建快速響應(yīng)的大型 Web 應(yīng)用程序的首選方式,。它在 Facebook 和 Instagram 上表現(xiàn)優(yōu)秀。 可見,,關(guān)鍵是實現(xiàn)了 快速響應(yīng) ,,那么制約 快速響應(yīng) 的因素有哪些呢?React 是如何解決的呢,? How 讓我們帶著上面的兩個問題,,在遵循真實的React代碼架構(gòu)的前提下,并舍棄部分優(yōu)化代碼和非必要的功能,,將其命名為 HuaMu,。 注意:為了和源碼有點區(qū)分,函數(shù)名首字母大寫,,源碼是小寫,。 CreateElement 函數(shù) 在開始之前,我們先簡單的了解一下JSX,,如果你感興趣,,可以關(guān)注下一篇《JSX背后的故事》。 JSX會被工具鏈Babel編譯為React.createElement(),接著React.createElement()返回一個叫作React.Element的JS對象,。 這么說有些抽象,,通過下面demo看下轉(zhuǎn)換前后的代碼: // JSX 轉(zhuǎn)換前const el = <h1 title="el_title">HuaMu<h1>; // 轉(zhuǎn)換后的 JS 對象const el = { type:"h1", props:{ title:"el_title", children:"HuaMu", } } 可見,,元素是具有 type 和 props 屬性的對象,,而 CreateElement 函數(shù)的主要任務(wù)就是創(chuàng)建該對象。 /** * @param {string} type HTML標簽類型 * @param {object} props 具有JSX屬性中的所有鍵和值 * @param {string | array} children 元素樹 */function CreateElement(type, props, ...children) { return { type, props:{ ...props, children, } } } 說明:我們將剩余參數(shù)賦予children,,擴展運算符用于構(gòu)造字面量對象props,對象表達式將按照 key-value 的方式展開,,從而保證 props.children 始終是一個數(shù)組,。接下來,我們一起看下 demo: CreateElement("h1", {title:"el_title"}, 'hello', 'HuaMu') // 返回的 JS 對象 { "type": "h1", "props": { "title": "el_title" // key-value "children": ["hello", "HuaMu"] // 數(shù)組類型 } } 注意:當 ...children 為空或為原始值時,,React 不會創(chuàng)建 props.children,,但為了簡化代碼,,暫不考慮性能,,我們?yōu)樵贾祫?chuàng)建特殊的類型TEXT_EL。 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_EL", props: { nodeValue: text, children: [] } } } Render 函數(shù) CreateElement 函數(shù)將標簽轉(zhuǎn)化為對象輸出,,接著 React 進行一系列處理,,Render 函數(shù)將處理好的節(jié)點根據(jù)標記進行添加、更新或刪除內(nèi)容,,最后附加到容器中,。下面簡單的實現(xiàn) Render 函數(shù)是如何實現(xiàn)添加內(nèi)容的: 首先創(chuàng)建對應(yīng)的DOM節(jié)點,然后將新節(jié)點附加到容器中,,并遞歸每個孩子節(jié)點做同樣的操作,。 將元素的 props 屬性分配給節(jié)點。 function Render(el,container) { // 創(chuàng)建節(jié)點 const dom = el.type === 'TEXT_EL'? document.createTextNode("") : document.createElement(el.type); el.props.children.forEach(child => Render(child, dom)) // 為節(jié)點分配 props 屬性 const isProperty = key => key !== 'children'; const setProperty = name => dom[name] = el.props[name]; Object.keys(el.props).filter(isProperty).forEach(setProperty) container.appendChild(dom); } 注意:文本節(jié)點使用textNode而不是innerText,,是為了保證以相同的方式對待所有的元素 ,。 到目前為止,我們已經(jīng)實現(xiàn)了一個簡易的用于構(gòu)建用戶界面的 JavaScript 庫?,F(xiàn)在,,讓 Babel 使用自定義的 HuaMu 代替 React,將 /** @jsx HuaMu.CreateElement */ 添加到代碼中 并發(fā)模式 在繼續(xù)向下探索之前,,我們先思考一下上面的代碼中,,有哪些代碼制約 快速響應(yīng) 了呢? 是的,,在Render函數(shù)中遞歸每個孩子節(jié)點,,即這句代碼el.props.children.forEach(child => Render(child, dom))存在問題。一旦開始渲染,,便不會停止,,直到渲染了整棵元素樹,我們知道,,GUI渲染線程與JS線程是互斥的,,JS腳本執(zhí)行和瀏覽器布局、繪制不能同時執(zhí)行,。如果元素樹很大,,JS腳本執(zhí)行時間過長,可能會阻塞主線程,,導致頁面掉幀,,造成卡頓,,且妨礙瀏覽器執(zhí)行高優(yōu)作業(yè)。 那如何解決呢,? 通過時間切片的方式,,即將任務(wù)分解為多個工作單元,每完成一個工作單元,,判斷是否有高優(yōu)作業(yè),,若有,則讓瀏覽器中斷渲染,。下面通過requestIdleCallback模擬實現(xiàn): 簡單說明一下: window.requestIdleCallback(cb[, options]) :瀏覽器將在主線程空閑時運行回調(diào),。函數(shù)會接收到一個IdleDeadline的參數(shù),這個參數(shù)可以獲取當前空閑時間(timeRemaining)以及回調(diào)是否在超時前已經(jīng)執(zhí)行的狀態(tài)(didTimeout),。 React 已不再使用requestIdleCallback,,目前使用 但在概念上是相同的。 依據(jù)上面的分析,,代碼結(jié)構(gòu)如下: // 當瀏覽器準備就緒時,,它將調(diào)用 WorkLoop requestIdleCallback(WorkLoop) let nextUnitOfWork = null; function PerformUnitOfWork(nextUnitOfWork) { // TODO } function WorkLoop(deadline) { // 當前線程的閑置時間是否可以在結(jié)束前執(zhí)行更多的任務(wù) let shouldYield = false; while(nextUnitOfWork && !shouldYield) { nextUnitOfWork = PerformUnitOfWork(nextUnitOfWork) // 賦值下一個工作單元 shouldYield = deadline.timeRemaining() < 1; // 如果 idle period 已經(jīng)結(jié)束,則它的值是 0 } requestIdleCallback(WorkLoop) } 我們在 PerformUnitOfWork 函數(shù)里實現(xiàn)當前工作的執(zhí)行并返回下一個執(zhí)行的工作單元,,可下一個工作單元如何快速查找呢,?讓我們初步了解 Fibers 吧。 Fibers 為了組織工作單元,,即方便查找下一個工作單元,,需引入fiber tree的數(shù)據(jù)結(jié)構(gòu)。即每個元素都有一個fiber,,鏈接到其第一個子節(jié)點,,下一個兄弟姐妹節(jié)點和父節(jié)點,且每個fiber都將成為一個工作單元,。 // 假設(shè)我們要渲染的元素樹如下const el = ( <div> <h1> <p /> <a /> </h1> <h2 /> </div> ) function UseState(initial) { const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex] const hook = { state: oldHook ? oldHook.state : initial, } wipFiber.hooks.push(hook) hookIndex++ return [hook.state] } UseState還需返回一個可更新狀態(tài)的函數(shù),,因此,需要定義一個接收action的setState函數(shù),。 將action添加到隊列中,,再將隊列添加到fiber。 在下一次渲染時,,獲取old hook的action隊列,,并代入new state逐一執(zhí)行,以保證返回的狀態(tài)是已更新的,。 在setState函數(shù)中,,執(zhí)行跟Render函數(shù)類似的操作,將currentRoot設(shè)置為下一個工作單元,以便開始新的渲染,。 function UseState(initial) { ... const hook = { state: oldHook ? oldHook.state : initial, queue: [], } const actions = oldHook ? oldHook.queue : [] actions.forEach(action => { hook.state = action(hook.state) }) const setState = action => { hook.queue.push(action) wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot, } nextUnitOfWork = wipRoot deletions = [] } wipFiber.hooks.push(hook) hookIndex++ return [hook.state, setState] } 現(xiàn)在,,我們已經(jīng)實現(xiàn)一個包含時間切片、fiber,、Hooks 的簡易 React,。 結(jié)語 到目前為止,我們從 What > How 梳理了大概的 React 知識鏈路,,后面的章節(jié)我們對文中所提及的知識點進行 Why 的探索,,相信會反哺到 What 的理解和 How 的實踐。 |
|