您好,登錄后才能下訂單哦!
這篇文章主要講解了“從零開始學習React”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“從零開始學習React”吧!
0: 從一次最簡單的 React 渲染說起
const element = <h2 title="hello">Hello World!</h2>; const container = document.getElementById("root"); ReactDOM.render(element, container);
上面這三行代碼是一個再簡單不過的 React 應用:在 root 根結點上渲染一個 Hello World! h2 節點。
第一步的目標是用原生 DOM 方式替換 React 代碼。
JSX
熟悉 React 的讀者都知道,我們直接在組件渲染的時候返回一段類似 html 模版的結構,這個就是所謂的 JSX。JSX 本質上還是 JS,是語法糖而不是 html 模版(相比 html 模版要學習千奇百怪的語法比如:{{#if value}},JSX 可以直接使用 JS 原生的 && || map reduce 等語法更易學表達能力也更強)。一般需要 babel 配合@babel/plugin-transform-react-jsx 插件(babel 轉換過程不是本文重點,感興趣可以閱讀插件源碼)轉換成調用 React.createElement,函數入參如下:
React.createElement( type, [props], [...children] )
例如上面的例子中的 <h2 title="hello">Hello World!</h2>,換成 createElement 調用就是:
const element = React.createElement( 'h2', { title: 'hello' }, 'Hello World!' );
React.createElement 返回一個包含元素(element)信息的對象,即:
const element = { type: "h2", props: { title: "hello", // createElement 第三個及之后參數移到 props.children children: "Hello World!", }, };
react 官方實現還包括了很多額外屬性,簡單起見本文未涉及,參看官方定義。
這個對象描述了 React 創建一個節點(node)所需要的信息,type 就是 DOM 節點的名字,比如這里是 h2,也可以是函數組件,后面會講到。props 包含所有元素的屬性(比如 title)和特殊屬性 children,children 可以包含其他元素,從根到葉也就能構成一顆完整的樹,也就是描述了整個 UI 界面。
為了避免含義不清,“元素”特指 “React elements”,“節點”特指 “DOM elements”。
ReactDOM.render
下面替換掉 ReactDOM.render 調用,這里 React 會把元素更新到 DOM。
const element = { type: "h2", props: { title: "hello", children: ["Hello World!"], }, }; const container = document.getElementById("root"); const node = document.createElement(element.type); node["title"] = element.props.title; const text = document.createTextNode(""); text["nodeValue"] = element.props.children; node.appendChild(text); container.appendChild(node);
對比元素對象,首先用 element.type 創建節點,再把非 children 屬性(這里是 title)賦值給節點。
然后創建 children 節點,由于 children 是字符串,故創建 textNode 節點,并把字符串賦值給 nodeValue,這里之所以用 createTextNode 而不是 innerText,是為了方便之后統一處理。
再把 children 節點 text 插到元素節點的子節點上,最后把元素節點插到根結點即完成了這次 React 的替換。
像上面代碼 element 這樣 JSX 轉成的描述 UI 界面的對象就是所謂的 虛擬 DOM,相對的 node 即 真實 DOM。render/渲染 過程就是把虛擬 DOM 轉換成真實 DOM 的過程。
I: 實現 createElement 函數
第一步首先實現 createElement 函數,把 JSX 轉換成 JS。以下面這個新的渲染為例,createElement 就是把 JSX 結構轉成元素描述對象。
const element = ( <div id="foo"> <a>bar</a> <b /> </div> ); // 等價轉換 ? const element = React.createElement( "div", { id: "foo" }, React.createElement("a", null, "bar"), React.createElement("b") ); const container = document.getElementById("root"); ReactDOM.render(element, container);
就像之前示例那樣,createElement 返回一個包含 type 和 props 的元素對象,描述節點信息。
// 這里用了最新 ECMAScript 剩余參數和展開語法(Rest parameter/Spread syntax), // 參考 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax // 注意:這里 children 始終是數組 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: [], }, } }
children 可能包含字符串或者數字這類基礎類型值,給這里值包裹成 TEXT_ELEMENT 特殊類型,方便后面統一處理。
注意:React 并不會包裹字符串這類值,如果沒有 children 也不會創建空數組,這里簡單起見,統一這樣處理可以簡化我們的代碼。
我們把本文的框架叫做 redact,以區別 react。示例 app 如下。
const element = Redact.createElement( "div", { id: "foo" }, Redact.createElement("a", null, "bar"), Redact.createElement("b") ); const container = document.getElementById("root"); ReactDOM.render(element, container);
但是我們還是習慣用 JSX 來寫組件,這里還能用嗎?答案是能的,只需要加一行注釋即可。
/** @jsx Redact.createElement */ const element = ( <div id="foo"> <a>bar</a> <b /> </div> ); const container = document.getElementById("root"); ReactDOM.render(element, container);
注意第一行注釋 @jsx 告訴 babel 用 Redact.createElement 替換默認的 React.createElement。或者直接修改 .babelrc 配置文件的 pragma 項,就不用每次都添加注釋了。
{ "presets": [ [ "@babel/preset-react", { "pragma": "Redact.createElement", } ] ] }
II: 實現 render 函數
實現我們的 render 函數,目前只需要添加節點到 DOM,刪除和更新操作后面再加。
function render(element, container) { // 創建節點 const dom = element.type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(element.type); // 賦值屬性(props) const isProperty = key => key !== "children"; Object.keys(element.props) .filter(isProperty) .forEach(name => { dom[name] = element.props[name] }); // 遞歸遍歷子節點 element.props.children.forEach(child => render(child, dom) ); // 插入父節點 container.appendChild(dom); }
上面的代碼放在了 CodeSandbox(在線開發環境),項目基于 Create React App 模版,試一試改下面的代碼驗證下。
redact-1
III: 并發模式 / Concurrent Mode
在我們深入其他 React 功能之前,先對代碼重構,引入 React 最新的并發模式(截止本文發表該功能還未正式發布)。
可能讀者會疑惑我們目前連最基本的組件狀態更新都還沒實現就先實現并發模式,其實目前代碼邏輯還十分簡單,現在重構,比之后實現所有功能再回頭要容易很多,所謂積重難返就是這個道理。
有經驗的開發者很容易發現上面的 render 代碼有一個問題,渲染子節點時遞歸遍歷了整棵樹,當我們頁面非常復雜時很容易阻塞主線程(和 stack over flow, 堆棧溢出),我們都知道每個頁面是單線程的(不考慮 worker 線程),主線程阻塞會導致頁面不能及時響應高優先級操作,如用戶點擊或者渲染動畫,頁面給用戶 “很卡,難用” 的負面印象,這肯定不是我們想要的。
因此,理想情況下,我們應該把 render 拆成更細分的單元,每完成一個單元的工作,允許瀏覽器打斷渲染響應更高優先級的的工作,這個過程即 “并發模式”。
這里我們用 requestIdleCallback 這個瀏覽器 API 來實現。這個 API 有點類似 setTimeout,不過不是我們告訴瀏覽器什么時候執行回調函數,而是瀏覽器在線程空閑(idle)的時侯主動執行回調函數。
React 目前已經不用這個 API 了,而是用 調度器/scheduler 這個包,自己實現調度算法。但它們核心思路是類似的,簡化起見用 requestIdleCallback 足矣。
let nextUnitOfWork = null function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) // 回調函數入參 deadline 可以告訴我們在這個渲染周期還剩多少時間可用 // 剩余時間小于1毫秒就退出回調,等待瀏覽器再次空閑 shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) // 注意,這個函數執行完本次單元任務之后要返回下一個單元任務 function performUnitOfWork(nextUnitOfWork) { // TODO }
IV: Fibers 數據結構
為了方便描述渲染樹和單元任務,React 設計了一種數據結構 “fiber 樹”。每個元素都是一個 fiber,每個 fiber 就是一個單元任務。
假如我們渲染如下這樣一棵樹:
Redact.render( <div> <h2> <p /> <a /> </h2> <h3 /> </div>, container )
用 Fiber 樹來描述就是:
在 render 函數我們創建根 fiber,再把它設為 nextUnitOfWork。在 workLoop 函數把 nextUnitOfWork 給 performUnitOfWork 執行,主要包含以下三步:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
把元素添加到 DOM
為元素的后代創建 fiber 節點
選擇下一個單元任務,并返回
為了完成這些目標需要設計的數據結構方便找到下一個任務單元。所以每個 fiber 直接鏈接它的第一個子節點(child),子節點鏈接它的兄弟節點(sibling),兄弟節點鏈接到父節點(parent)。 示意圖如下(注意不同節點之間的高亮箭頭):
當我們完成了一個 fiber 的單元任務,如果他有一個 子節點/child 則這個節點作為 nextUnitOfWork。如下圖所示,當完成 div 單元任務之后,下一個單元任務就是 h2。
如果一個 fiber 沒有 child,我們用 兄弟節點/sibling 作為下一個任務單元。如下圖所示,p 節點沒有 child 而有 sibling,所以下一個任務單元是 a 節點。
如果一個 fiber 既沒有 child 也沒有 sibling,則找到父節點的兄弟節點,。如下圖所示的 a 和 h3。
如果父節點沒有兄弟節點,則繼續往上找,直到找到一個兄弟節點或者到達 fiber 根結點。到達根結點即意味本次 render 任務全部完成。
把這個思路用代碼表達如下:
// 之前 render 的邏輯挪到這個函數 function createDom(fiber) { const dom = fiber.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(fiber.type); const isProperty = key => key !== "children"; Object.keys(fiber.props) .filter(isProperty) .forEach(name => { dom[name] = fiber.props[name]; }); return dom; } function render(element, container) { // 創建根 fiber,設為下一次的單元任務 nextUnitOfWork = { dom: container, props: { children: [element] } }; } let nextUnitOfWork = null; function workLoop(deadline) { let shouldYield = false; while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); shouldYield = deadline.timeRemaining() < 1; } requestIdleCallback(workLoop); } // 一旦瀏覽器空閑,就觸發執行單元任務 requestIdleCallback(workLoop); function performUnitOfWork(fiber) { if (!fiber.dom) { fiber.dom = createDom(fiber); } // 子節點 DOM 插到父節點之后 if (fiber.parent) { fiber.parent.dom.appendChild(fiber.dom); } // 每個子元素創建新的 fiber const elements = fiber.props.children; let index = 0; let prevSibling = null; while (index < elements.length) { const element = elements[index]; const newFiber = { type: element.type, props: element.props, parent: fiber, dom: null }; // 根據上面的圖示,父節點只鏈接第一個子節點 if (index === 0) { fiber.child = newFiber; } else { // 兄節點鏈接弟節點 prevSibling.sibling = newFiber; } prevSibling = newFiber; index++; } // 返回下一個任務單元(fiber) // 有子節點直接返回 if (fiber.child) { return fiber.child; } // 沒有子節點則找兄弟節點,兄弟節點也沒有找父節點的兄弟節點, // 循環遍歷直至找到為止 let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling; } nextFibernextFiber = nextFiber.parent; } }
V: render 和 commit 階段
我們的代碼還有一個問題。
每完成一個任務單元都把節點添加到 DOM 上。請記住,瀏覽器是可以打斷渲染流程的,如果還沒渲染完整棵樹就把節點添加到 DOM,用戶會看到殘缺不全的 UI 界面,給人一種很不專業的印象,這肯定不是我們想要的。因此需要重構節點添加到 DOM 這部分代碼,整棵樹(fiber)渲染完成之后再一次性添加到 DOM,即 React commit 階段。
具體來說,去掉 performUnitOfWork 的 fiber.parent.dom.appendChild 代碼,換成如下代碼。
function createDom(fiber) { const dom = fiber.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(fiber.type) const isProperty = key => key !== "children" Object.keys(fiber.props) .filter(isProperty) .forEach(name => { dom[name] = fiber.props[name] }) return dom } // 新增函數,提交根結點到 DOM function commitRoot() { commitWork(wipRoot.child); wipRoot = null; } // 新增子函數 function commitWork(fiber) { if (!fiber) { return; } const domParent = fiber.parent.dom; domParent.appendChild(fiber.dom); // 遞歸子節點和兄弟節點 commitWork(fiber.child); commitWork(fiber.sibling); } function render(element, container) { // render 時記錄 wipRoot wipRoot = { dom: container, props: { children: [element], }, }; nextUnitOfWork = wipRoot; } let nextUnitOfWork = null; // 新增變量,跟蹤渲染進行中的根 fiber let wipRoot = null; function workLoop(deadline) { let shouldYield = false; while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ); shouldYield = deadline.timeRemaining() < 1; } // 當 nextUnitOfWork 為空則表示渲染 fiber 樹完成了, // 可以提交到 DOM 了 if (!nextUnitOfWork && wipRoot) { commitRoot(); } requestIdleCallback(workLoop); } // 一旦瀏覽器空閑,就觸發執行單元任務 requestIdleCallback(workLoop); function performUnitOfWork(fiber) { if (!fiber.dom) { fiber.dom = createDom(fiber); } const elements = fiber.props.children; let index = 0; let prevSibling = null; while (index < elements.length) { const element = elements[index]; const newFiber = { type: element.type, props: element.props, parent: fiber, dom: null, }; if (index === 0) { fiber.child = newFiber; } else { prevSibling.sibling = newFiber; } prevSibling = newFiber; index++; } if (fiber.child) { return fiber.child } let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling; } nextFibernextFiber = nextFiber.parent; } }
VI: 更新和刪除節點/Reconciliation
目前我們只添加節點到 DOM,還沒考慮更新和刪除節點的情況。要處理這2種情況,需要對比上次渲染的 fiber 和當前渲染的 fiber 的差異,根據差異決定是更新還是刪除節點。React 把這個過程叫 Reconciliation。
因此我們需要保存上一次渲染之后的 fiber 樹,我們把這棵樹叫 currentRoot。同時,給每個 fiber 節點添加 alternate 屬性,指向上一次渲染的 fiber。
代碼較多,建議按 render ? workLoop ? performUnitOfWork ? reconcileChildren ? workLoop ? commitRoot ? commitWork ? updateDom 順序閱讀。
function createDom(fiber) { const dom = fiber.type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(fiber.type); updateDom(dom, {}, fiber.props); return dom; } const isEvent = key => key.startsWith("on"); const isProperty = key => key !== "children" && !isEvent(key); const isNew = (prev, next) => key => prev[key] !== next[key]; const isGone = (prev, next) => key => !(key in next); // 新增函數,更新 DOM 節點屬性 function updateDom(dom, prevProps = {}, nextProps = {}) { // 以 “on” 開頭的屬性作為事件要特別處理 // 移除舊的或者變化了的的事件處理函數 Object.keys(prevProps) .filter(isEvent) .filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key)) .forEach(name => { const eventType = name.toLowerCase().substring(2); dom.removeEventListener(eventType, prevProps[name]); }); // 移除舊的屬性 Object.keys(prevProps) .filter(isProperty) .filter(isGone(prevProps, nextProps)) .forEach(name => { dom[name] = ""; }); // 添加或者更新屬性 Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { // React 規定 style 內聯樣式是駝峰命名的對象, // 根據規范給 style 每個屬性單獨賦值 if (name === "style") { Object.entries(nextProps[name]).forEach(([key, value]) => { dom.style[key] = value; }); } else { dom[name] = nextProps[name]; } }); // 添加新的事件處理函數 Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name.toLowerCase().substring(2); dom.addEventListener(eventType, nextProps[name]); }); } function commitRoot() { deletions.forEach(commitWork); commitWork(wipRoot.child); currentRoot = wipRoot; wipRoot = null; } 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); } function render(element, container) { wipRoot = { dom: container, props: { children: [element] }, alternate: currentRoot }; deletions = []; nextUnitOfWork = wipRoot; } let nextUnitOfWork = null; let currentRoot = null; let wipRoot = null; let deletions = null; function workLoop(deadline) { let shouldYield = false; while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); shouldYield = deadline.timeRemaining() < 1; } if (!nextUnitOfWork && wipRoot) { commitRoot(); } requestIdleCallback(workLoop); } requestIdleCallback(workLoop); function performUnitOfWork(fiber) { if (!fiber.dom) { fiber.dom = createDom(fiber); } const elements = fiber.props.children; // 原本添加 fiber 的邏輯挪到 reconcileChildren 函數 reconcileChildren(fiber, elements); if (fiber.child) { return fiber.child; } let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling; } nextFibernextFiber = nextFiber.parent; } } // 新增函數 function reconcileChildren(wipFiber, elements) { let index = 0; // 上次渲染完成之后的 fiber 節點 let oldFiber = wipFiber.alternate && wipFiber.alternate.child; let prevSibling = null; // 扁平化 props.children,處理函數組件的 children elementselements = elements.flat(); while (index < elements.length || oldFiber != null) { // 本次需要渲染的子元素 const element = elements[index]; let newFiber = null; // 比較當前和上一次渲染的 type,即 DOM tag 'div', // 暫不考慮自定義組件 const sameType = oldFiber && element && element.type === oldFiber.type; // 同類型節點,只需更新節點 props 即可 if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, // 復用舊節點的 DOM parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE" // 新增屬性,在提交/commit 階段使用 }; } // 不同類型節點且存在新的元素時,創建新的 DOM 節點 if (element && !sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT" // PLACEMENT 表示需要添加新的節點 }; } // 不同類型節點,且存在舊的 fiber 節點時, // 需要移除該節點 if (oldFiber && !sameType) { oldFiber.effectTag = "DELETION"; // 當最后提交 fiber 樹到 DOM 時,我們是從 wipRoot 開始的, // 此時沒有上一次的 fiber,所以這里用一個數組來跟蹤需要 // 刪除的節點 deletions.push(oldFiber); } if (oldFiber) { // 同步更新下一個舊 fiber 節點 oldFiberoldFiber = oldFiber.sibling; } if (index === 0) { wipFiber.child = newFiber; } else { prevSibling.sibling = newFiber; } prevSibling = newFiber; index++; } }
注意:這個過程中 React 還用了 key 來檢測數組元素變化了位置的情況,避免重復渲染以提高性能。簡化起見,本文未實現。
下面 CodeSandbox 代碼用了個小技巧,重復執行 render 實現更新界面的效果,動手改改試試。
redact-2
VII: 函數組件
目前我們還只考慮了直接渲染 DOM 標簽的情況,不支持組件,而組件是 React 是靈魂,下面我們來實現函數組件。
以一個非常簡單的組件代碼為例。
/** @jsx Redact.createElement */ function App(props) { return <h2>Hi {props.name}</h2>; }; // 等效 JS 代碼 ? function App(props) { return Redact.createElement( "h2", null, "Hi ", props.name ) } const element = <App name="foo" />; const container = document.getElementById("root"); Redact.render(element, container);
函數組件有2個不同點:
函數組件的 fiber 節點沒有對應 DOM
函數組件的 children 來自函數執行結果,而不是像標簽元素一樣直接從 props 獲取,因為 children 不只是函數組件使用時包含的子孫節點,還需要組合組件本身的結構
注意以下代碼省略了未改動部分。
function commitWork(fiber) { if (!fiber) { return; } // 當 fiber 是函數組件時節點不存在 DOM, // 故需要遍歷父節點以找到最近的有 DOM 的節點 let domParentFiber = fiber.parent; while (!domParentFiber.dom) { domParentFiberdomParentFiber = domParentFiber.parent; } const domParent = domParentFiber.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") { // 直接移除 DOM 替換成 commitDeletion 函數 commitDeletion(fiber, domParent); } commitWork(fiber.child); commitWork(fiber.sibling); } // 新增函數,移除 DOM 節點 function commitDeletion(fiber, domParent) { // 當 child 是函數組件時不存在 DOM, // 故需要遞歸遍歷子節點找到真正的 DOM if (fiber.dom) { domParent.removeChild(fiber.dom); } else { commitDeletion(fiber.child, domParent); } } function performUnitOfWork(fiber) { const isFunctionComponent = fiber.type instanceof Function; // 原本邏輯挪到 updateHostComponent 函數 if (isFunctionComponent) { updateFunctionComponent(fiber); } else { updateHostComponent(fiber); } if (fiber.child) { return fiber.child; } let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling; } nextFibernextFiber = nextFiber.parent; } } // 新增函數,處理函數組件 function updateFunctionComponent(fiber) { // 執行函數組件得到 children const children = [fiber.type(fiber.props)]; reconcileChildren(fiber, children); } // 新增函數,處理原生標簽組件 function updateHostComponent(fiber) { if (!fiber.dom) { fiber.dom = createDom(fiber); } reconcileChildren(fiber, fiber.props.children); }
VIII: 函數組件 Hooks
支持了函數組件,還需要支持組件狀態 / state 才能實現刷新界面。
我們的示例也跟著更新,用 hooks 實現經典的 counter,點擊計數器加1。
/** @jsx Redact.createElement */ function Counter() { const [state, setState] = Redact.useState(1) return ( <h2 onClick={() => setState(c => c + 1)}> Count: {state} </h2> ); } const element = <Counter />; const container = document.getElementById("root"); Redact.render(element, container);
注意以下代碼省略了未變化部分。
// 新增變量,渲染進行中的 fiber 節點 let wipFiber = null; // 新增變量,當前 hook 的索引 let hookIndex = null; function updateFunctionComponent(fiber) { // 更新進行中的 fiber 節點 wipFiber = fiber; // 重置 hook 索引 hookIndex = 0; // 新增 hooks 數組以支持同一個組件多次調用 `useState` wipFiber.hooks = []; const children = [fiber.type(fiber.props)]; reconcileChildren(fiber, children); } function useState(initial) { // alternate 保存了上一次渲染的 fiber 節點 const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; const hook = { // 第一次渲染使用入參,第二次渲染復用前一次的狀態 state: oldHook ? oldHook.state : initial, // 保存每次 setState 入參的隊列 queue: [] }; const actions = oldHook ? oldHook.queue : []; actions.forEach(action => { // 根據調用 setState 順序從前往后生成最新的 state hook.state = action instanceof Function ? action(hook.state) : action; }); // setState 函數用于更新 state,入參 action // 是新的 state 值或函數返回新的 state const setState = action => { hook.queue.push(action); // 下面這部分代碼和 render 函數很像, // 設置新的 wipRoot 和 nextUnitOfWork // 瀏覽器空閑時即開始重新渲染。 wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot }; nextUnitOfWork = wipRoot; deletions = []; }; // 保存本次 hook wipFiber.hooks.push(hook); hookIndex++; return [hook.state, setState]; }
完整 CodeSandbox 代碼如下,點擊 Count 試試:
redact-3
結語
除了幫助讀者理解 React 核心工作原理外,本文很多變量都和 React 官方代碼保持一致,比如,讀者在 React 應用的任何函數組件里斷點,再打開調試工作能看到下面這樣的調用棧:
updateFunctionComponent
performUnitOfWork
workLoop
注意本文是教學性質的,還缺少很多 React 的功能和性能優化。比如:在這些事情上 React 的表現和 Redact 不同。
Redact 在渲染階段遍歷了整棵樹,而 React 用了一些啟發性算法,可以直接跳過某些沒有變化的子樹,以提高性能。(比如 React 數組元素推薦帶 key,可以跳過無需更新的節點,參考官方文檔)
Redact 在 commit 階段遍歷整棵樹, React 用了一個鏈表保存變化了的 fiber,減少了很多不必要遍歷操作。
Redact 每次創建新的 fiber 樹時都是直接創建 fiber 對象節點,而 React 會復用上一個 fiber 對象,以節省創建對象的性能消耗。
Redact 如果在渲染階段收到新的更新會直接丟棄已渲染的樹,再從頭開始渲染。而 React 會用時間戳標記每次更新,以決定更新的優先級。
源碼還有很多優化等待讀者去發現。。。
感謝各位的閱讀,以上就是“從零開始學習React”的內容了,經過本文的學習后,相信大家對從零開始學習React這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。