<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從零開始寫一個 Fiber 版的 React

          共 19502字,需瀏覽 40分鐘

           ·

          2020-02-21 23:27

          a3e42f60a48b86c45cbef2bb93beb9b9.webp


          作者:devrsi0nhttps://segmentfault.com/a/1190000021689852


          React 是目前最流行的前端框架,很多讀者用 React 很溜,但想要深入學(xué)習(xí) React 的原理就會被官方源碼倉庫浩瀚如煙的代碼繞的暈頭轉(zhuǎn)向。今天我們通過不依賴任何第三方庫的方式,拋棄邊界處理、性能優(yōu)化、安全性等弱相關(guān)代碼手寫一個基礎(chǔ)版的 React,供大家學(xué)習(xí)和理解 React 的核心原理。

          e6613357447bd65261d166c10d2b34ed.webp

          本文實(shí)現(xiàn)的是包含現(xiàn)代 React 最新特性?Hooks?和?Concurrent Mode?的版本,傳統(tǒng) class 組件的方式稍有不同,不影響理解核心原理。本文函數(shù)、變量等標(biāo)識符命名都和官方盡量貼近,方便以后深入官方源碼。

          建議桌面端瀏覽本文,并且跟著文章手動敲一遍代碼加深理解。

          目錄總覽

          • 0: 從一次最簡單的 React 渲染說起

          • I: 實(shí)現(xiàn) createElement 函數(shù)

          • II: 實(shí)現(xiàn) render 函數(shù)

          • III: 并發(fā)模式 / Concurrent Mode

          • IV: Fibers 數(shù)據(jù)結(jié)構(gòu)

          • V: render 和 commit 階段

          • VI: 更新和刪除節(jié)點(diǎn)/Reconciliation

          • VII: 函數(shù)組件

          • VIII: 函數(shù)組件 Hooks

          0: 從一次最簡單的 React 渲染說起
          const element = <h1 title="hello">Hello World!h1>;
          const container = document.getElementById("root");
          ReactDOM.render(element, container);

          上面這三行代碼是一個再簡單不過的 React 應(yīng)用:在?root?根結(jié)點(diǎn)上渲染一個?Hello World!?h1 節(jié)點(diǎn)。

          第一步的目標(biāo)是用原生 DOM 方式替換 React 代碼

          JSX

          熟悉 React 的讀者都知道,我們直接在組件渲染的時候返回一段類似 html 模版的結(jié)構(gòu),這個就是所謂的?JSX。JSX 本質(zhì)上還是 JS,是語法糖而不是 html 模版(相比 html 模版要學(xué)習(xí)千奇百怪的語法比如:{{#if value}},JSX 可以直接使用 JS 原生的?&& || map reduce?等語法更易學(xué)表達(dá)能力也更強(qiáng))。一般需要?babel?配合@babel/plugin-transform-react-jsx?插件(babel 轉(zhuǎn)換過程不是本文重點(diǎn),感興趣可以閱讀插件源碼)轉(zhuǎn)換成調(diào)用?React.createElement,函數(shù)入?yún)⑷缦拢?/p>

          React.createElement(
          type,
          [props],
          [...children]
          )

          例如上面的例子中的?

          Hello World!

          ,換成?createElement?調(diào)用就是:

          const element = React.createElement(
          'h1',
          { title: 'hello' },
          'Hello World!'
          );

          React.createElement?返回一個包含元素(element)信息的對象,即:

          const element = {
          type: "h1",
          props: {
          title: "hello",
          // createElement 第三個及之后參數(shù)移到 props.children
          children: "Hello World!",
          },
          };

          react 官方實(shí)現(xiàn)還包括了很多額外屬性,簡單起見本文未涉及,參看官方定義。

          這個對象描述了 React 創(chuàng)建一個節(jié)點(diǎn)(node)所需要的信息,type?就是 DOM 節(jié)點(diǎn)的名字,比如這里是?h1,也可以是函數(shù)組件,后面會講到。props?包含所有元素的屬性(比如 title)和特殊屬性 children,children 可以包含其他元素,從根到葉也就能構(gòu)成一顆完整的樹,也就是描述了整個 UI 界面。

          為了避免含義不清,“元素”特指 “React elements”,“節(jié)點(diǎn)”特指 “DOM elements”。

          ReactDOM.render

          下面替換掉?ReactDOM.render?調(diào)用,這里 React 會把元素更新到 DOM。

          const element = {
          type: "h1",
          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?創(chuàng)建節(jié)點(diǎn),再把非 children 屬性(這里是 title)賦值給節(jié)點(diǎn)。

          然后創(chuàng)建 children 節(jié)點(diǎn),由于 children 是字符串,故創(chuàng)建?textNode?節(jié)點(diǎn),并把字符串賦值給?nodeValue,這里之所以用?createTextNode?而不是?innerText,是為了方便之后統(tǒng)一處理。

          再把 children 節(jié)點(diǎn) text 插到元素節(jié)點(diǎn)的子節(jié)點(diǎn)上,最后把元素節(jié)點(diǎn)插到根結(jié)點(diǎn)即完成了這次 React 的替換。

          像上面代碼?element?這樣 JSX 轉(zhuǎn)成的描述 UI 界面的對象就是所謂的?虛擬 DOM,相對的?node?即?真實(shí) DOMrender/渲染?過程就是把虛擬 DOM 轉(zhuǎn)換成真實(shí) DOM 的過程。


          I: 實(shí)現(xiàn) createElement 函數(shù)

          第一步首先實(shí)現(xiàn)?createElement?函數(shù),把 JSX 轉(zhuǎn)換成 JS。以下面這個新的渲染為例,createElement?就是把 JSX 結(jié)構(gòu)轉(zhuǎn)成元素描述對象。

          const element = (
          <div id="foo">
          <a>bara
          >
          <b />
          div>
          );
          // 等價轉(zhuǎn)換 ?
          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 的元素對象,描述節(jié)點(diǎn)信息。

          // 這里用了最新 ECMAScript 剩余參數(shù)和展開語法(Rest parameter/Spread syntax),
          // 參考 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax
          // 注意:這里 children 始終是數(shù)組
          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 可能包含字符串或者數(shù)字這類基礎(chǔ)類型值,給這里值包裹成?TEXT_ELEMENT?特殊類型,方便后面統(tǒng)一處理。

          注意:React 并不會包裹字符串這類值,如果沒有 children 也不會創(chuàng)建空數(shù)組,這里簡單起見,統(tǒng)一這樣處理可以簡化我們的代碼。

          我們把本文的框架叫做?redact,以區(qū)別?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);

          但是我們還是習(xí)慣用 JSX 來寫組件,這里還能用嗎?答案是能的,只需要加一行注釋即可。

          /** @jsx Redact.createElement */
          const element = (
          <div id="foo">
          <a>bara
          >
          <b />
          div>
          );
          const container = document.getElementById("root");
          ReactDOM.render(element, container);

          注意第一行注釋?@jsx?告訴 babel 用?Redact.createElement?替換默認(rèn)的?React.createElement。或者直接修改?.babelrc?配置文件的?pragma?項(xiàng),就不用每次都添加注釋了。

          {
          "presets": [
          [
          "@babel/preset-react",
          {
          "pragma": "Redact.createElement",
          }
          ]
          ]
          }

          II: 實(shí)現(xiàn) render 函數(shù)

          實(shí)現(xiàn)我們的 render 函數(shù),目前只需要添加節(jié)點(diǎn)到 DOM,刪除和更新操作后面再加。

          function render(element, container) {
          // 創(chuàng)建節(jié)點(diǎn)
          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]
          });

          // 遞歸遍歷子節(jié)點(diǎn)
          element.props.children.forEach(child =>
          render(child, dom)
          );

          // 插入父節(jié)點(diǎn)
          container.appendChild(dom);
          }

          上面的代碼放在了 CodeSandbox(在線開發(fā)環(huán)境),項(xiàng)目基于?Create React App?模版,試一試改下面的代碼驗(yàn)證下。

          redact-1


          III: 并發(fā)模式 / Concurrent Mode

          在我們深入其他 React 功能之前,先對代碼重構(gòu),引入 React 最新的并發(fā)模式(截止本文發(fā)表該功能還未正式發(fā)布)。

          可能讀者會疑惑我們目前連最基本的組件狀態(tài)更新都還沒實(shí)現(xiàn)就先實(shí)現(xiàn)并發(fā)模式,其實(shí)目前代碼邏輯還十分簡單,現(xiàn)在重構(gòu),比之后實(shí)現(xiàn)所有功能再回頭要容易很多,所謂積重難返就是這個道理。

          有經(jīng)驗(yàn)的開發(fā)者很容易發(fā)現(xiàn)上面的?render?代碼有一個問題,渲染子節(jié)點(diǎn)時遞歸遍歷了整棵樹,當(dāng)我們頁面非常復(fù)雜時很容易阻塞主線程(和 stack over flow, 堆棧溢出),我們都知道每個頁面是單線程的(不考慮 worker 線程),主線程阻塞會導(dǎo)致頁面不能及時響應(yīng)高優(yōu)先級操作,如用戶點(diǎn)擊或者渲染動畫,頁面給用戶 “很卡,難用” 的負(fù)面印象,這肯定不是我們想要的。

          因此,理想情況下,我們應(yīng)該把?render?拆成更細(xì)分的單元,每完成一個單元的工作,允許瀏覽器打斷渲染響應(yīng)更高優(yōu)先級的的工作,這個過程即 “并發(fā)模式”。

          這里我們用?requestIdleCallback?這個瀏覽器 API 來實(shí)現(xiàn)。這個 API 有點(diǎn)類似?setTimeout,不過不是我們告訴瀏覽器什么時候執(zhí)行回調(diào)函數(shù),而是瀏覽器在線程空閑(idle)的時侯主動執(zhí)行回調(diào)函數(shù)。

          React 目前已經(jīng)不用這個 API?了,而是用?調(diào)度器/scheduler?這個包,自己實(shí)現(xiàn)調(diào)度算法。但它們核心思路是類似的,簡化起見用 requestIdleCallback 足矣。

          let nextUnitOfWork = null

          function workLoop(deadline) {
          let shouldYield = false
          while (nextUnitOfWork && !shouldYield) {
          nextUnitOfWork = performUnitOfWork(
          nextUnitOfWork
          )
          // 回調(diào)函數(shù)入?yún)?deadline 可以告訴我們在這個渲染周期還剩多少時間可用
          // 剩余時間小于1毫秒就退出回調(diào),等待瀏覽器再次空閑
          shouldYield = deadline.timeRemaining() < 1
          }
          requestIdleCallback(workLoop)
          }

          requestIdleCallback(workLoop)

          // 注意,這個函數(shù)執(zhí)行完本次單元任務(wù)之后要返回下一個單元任務(wù)
          function performUnitOfWork(nextUnitOfWork) {
          // TODO
          }
          IV: Fibers 數(shù)據(jù)結(jié)構(gòu)

          為了方便描述渲染樹和單元任務(wù),React 設(shè)計(jì)了一種數(shù)據(jù)結(jié)構(gòu) “fiber 樹”。每個元素都是一個 fiber,每個 fiber 就是一個單元任務(wù)。

          假如我們渲染如下這樣一棵樹:

          Redact.render(
          <div>
          <h1>
          <p />
          <a />
          h1
          >
          <h2 />
          div>,
          container
          )

          用 Fiber 樹來描述就是:

          3c262e8ebba08bdabe4b04c9a6b89fe2.webp

          在?render?函數(shù)我們創(chuàng)建根 fiber,再把它設(shè)為?nextUnitOfWork。在 workLoop 函數(shù)把?nextUnitOfWork?給?performUnitOfWork?執(zhí)行,主要包含以下三步:

          1. 把元素添加到 DOM

          2. 為元素的后代創(chuàng)建 fiber 節(jié)點(diǎn)

          3. 選擇下一個單元任務(wù),并返回

          為了完成這些目標(biāo)需要設(shè)計(jì)的數(shù)據(jù)結(jié)構(gòu)方便找到下一個任務(wù)單元。所以每個 fiber 直接鏈接它的第一個子節(jié)點(diǎn)(child),子節(jié)點(diǎn)鏈接它的兄弟節(jié)點(diǎn)(sibling),兄弟節(jié)點(diǎn)鏈接到父節(jié)點(diǎn)(parent)。?示意圖如下(注意不同節(jié)點(diǎn)之間的高亮箭頭):

          e2754616452ba12c40076272258702d1.webp

          當(dāng)我們完成了一個 fiber 的單元任務(wù),如果他有一個?子節(jié)點(diǎn)/child?則這個節(jié)點(diǎn)作為?nextUnitOfWork。如下圖所示,當(dāng)完成?div?單元任務(wù)之后,下一個單元任務(wù)就是?h1

          fdf4935ff344d8f6b90d788d566f7e2e.webp

          如果一個 fiber 沒有?child,我們用?兄弟節(jié)點(diǎn)/sibling?作為下一個任務(wù)單元。如下圖所示,p?節(jié)點(diǎn)沒有?child?而有?sibling,所以下一個任務(wù)單元是?a?節(jié)點(diǎn)。

          8e9a7443812e7f85af592f1a7112bf82.webp

          如果一個 fiber 既沒有?child?也沒有?sibling,則找到父節(jié)點(diǎn)的兄弟節(jié)點(diǎn),。如下圖所示的?a?和?h2

          1a45f1ef417bfee8035bbc66c61c6871.webp

          如果父節(jié)點(diǎn)沒有兄弟節(jié)點(diǎn),則繼續(xù)往上找,直到找到一個兄弟節(jié)點(diǎn)或者到達(dá) fiber 根結(jié)點(diǎn)。到達(dá)根結(jié)點(diǎn)即意味本次?render?任務(wù)全部完成。

          把這個思路用代碼表達(dá)如下:

          // 之前 render 的邏輯挪到這個函數(shù)
          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) {
          // 創(chuàng)建根 fiber,設(shè)為下一次的單元任務(wù)
          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);
          }
          // 一旦瀏覽器空閑,就觸發(fā)執(zhí)行單元任務(wù)
          requestIdleCallback(workLoop);
          function performUnitOfWork(fiber) {
          if (!fiber.dom) {
          fiber.dom = createDom(fiber);
          }

          // 子節(jié)點(diǎn) DOM 插到父節(jié)點(diǎn)之后
          if (fiber.parent) {
          fiber.parent.dom.appendChild(fiber.dom);
          }

          // 每個子元素創(chuàng)建新的 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
          };
          // 根據(jù)上面的圖示,父節(jié)點(diǎn)只鏈接第一個子節(jié)點(diǎn)
          if (index === 0) {
          fiber.child = newFiber;
          } else {
          // 兄節(jié)點(diǎn)鏈接弟節(jié)點(diǎn)
          prevSibling.sibling = newFiber;
          }

          prevSibling = newFiber;
          index++;
          }
          // 返回下一個任務(wù)單元(fiber)
          // 有子節(jié)點(diǎn)直接返回
          if (fiber.child) {
          return fiber.child;
          }
          // 沒有子節(jié)點(diǎn)則找兄弟節(jié)點(diǎn),兄弟節(jié)點(diǎn)也沒有找父節(jié)點(diǎn)的兄弟節(jié)點(diǎn),
          // 循環(huán)遍歷直至找到為止
          let nextFiber = fiber;
          while (nextFiber) {
          if (nextFiber.sibling) {
          return nextFiber.sibling;
          }
          nextFiber = nextFiber.parent;
          }
          }

          V: render 和 commit 階段

          我們的代碼還有一個問題。

          每完成一個任務(wù)單元都把節(jié)點(diǎn)添加到 DOM 上。請記住,瀏覽器是可以打斷渲染流程的,如果還沒渲染完整棵樹就把節(jié)點(diǎn)添加到 DOM,用戶會看到殘缺不全的 UI 界面,給人一種很不專業(yè)的印象,這肯定不是我們想要的。因此需要重構(gòu)節(jié)點(diǎn)添加到 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
          }

          // 新增函數(shù),提交根結(jié)點(diǎn)到 DOM
          function commitRoot() {
          commitWork(wipRoot.child);
          wipRoot = null;
          }

          // 新增子函數(shù)
          function commitWork(fiber) {
          if (!fiber) {
          return;
          }
          const domParent = fiber.parent.dom;
          domParent.appendChild(fiber.dom);
          // 遞歸子節(jié)點(diǎn)和兄弟節(jié)點(diǎn)
          commitWork(fiber.child);
          commitWork(fiber.sibling);
          }

          function render(element, container) {
          // render 時記錄 wipRoot
          wipRoot = {
          dom: container,
          props: {
          children: [element],
          },
          };
          nextUnitOfWork = wipRoot;
          }

          let nextUnitOfWork = null;
          // 新增變量,跟蹤渲染進(jìn)行中的根 fiber
          let wipRoot = null;

          function workLoop(deadline) {
          let shouldYield = false;
          while (nextUnitOfWork && !shouldYield) {
          nextUnitOfWork = performUnitOfWork(
          nextUnitOfWork
          );
          shouldYield = deadline.timeRemaining() < 1;
          }

          // 當(dāng) nextUnitOfWork 為空則表示渲染 fiber 樹完成了,
          // 可以提交到 DOM 了
          if (!nextUnitOfWork && wipRoot) {
          commitRoot();
          }
          requestIdleCallback(workLoop);
          }
          // 一旦瀏覽器空閑,就觸發(fā)執(zhí)行單元任務(wù)
          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;
          }
          nextFiber = nextFiber.parent;
          }
          }

          VI: 更新和刪除節(jié)點(diǎn)/Reconciliation

          目前我們只添加節(jié)點(diǎn)到 DOM,還沒考慮更新和刪除節(jié)點(diǎn)的情況。要處理這2種情況,需要對比上次渲染的 fiber 和當(dāng)前渲染的 fiber 的差異,根據(jù)差異決定是更新還是刪除節(jié)點(diǎn)。React 把這個過程叫?Reconciliation

          因此我們需要保存上一次渲染之后的 fiber 樹,我們把這棵樹叫?currentRoot。同時,給每個 fiber 節(jié)點(diǎn)添加?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);

          // 新增函數(shù),更新 DOM 節(jié)點(diǎn)屬性
          function updateDom(dom, prevProps = {}, nextProps = {}) {
          // 以 “on” 開頭的屬性作為事件要特別處理
          // 移除舊的或者變化了的的事件處理函數(shù)
          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 規(guī)定 style 內(nèi)聯(lián)樣式是駝峰命名的對象,
          // 根據(jù)規(guī)范給 style 每個屬性單獨(dú)賦值
          if (name === "style") {
          Object.entries(nextProps[name]).forEach(([key, value]) => {
          dom.style[key] = value;
          });
          } else {
          dom[name] = nextProps[name];
          }
          });

          // 添加新的事件處理函數(shù)
          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 函數(shù)
          reconcileChildren(fiber, elements);

          if (fiber.child) {
          return fiber.child;
          }
          let nextFiber = fiber;
          while (nextFiber) {
          if (nextFiber.sibling) {
          return nextFiber.sibling;
          }
          nextFiber = nextFiber.parent;
          }
          }

          // 新增函數(shù)
          function reconcileChildren(wipFiber, elements) {
          let index = 0;
          // 上次渲染完成之后的 fiber 節(jié)點(diǎn)
          let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
          let prevSibling = null;

          // 扁平化 props.children,處理函數(shù)組件的 children
          elements = elements.flat();

          while (index < elements.length || oldFiber != null) {
          // 本次需要渲染的子元素
          const element = elements[index];
          let newFiber = null;

          // 比較當(dāng)前和上一次渲染的 type,即 DOM tag 'div',
          // 暫不考慮自定義組件
          const sameType = oldFiber && element && element.type === oldFiber.type;

          // 同類型節(jié)點(diǎn),只需更新節(jié)點(diǎn) props 即可
          if (sameType) {
          newFiber = {
          type: oldFiber.type,
          props: element.props,
          dom: oldFiber.dom, // 復(fù)用舊節(jié)點(diǎn)的 DOM
          parent: wipFiber,
          alternate: oldFiber,
          effectTag: "UPDATE" // 新增屬性,在提交/commit 階段使用
          };
          }
          // 不同類型節(jié)點(diǎn)且存在新的元素時,創(chuàng)建新的 DOM 節(jié)點(diǎn)
          if (element && !sameType) {
          newFiber = {
          type: element.type,
          props: element.props,
          dom: null,
          parent: wipFiber,
          alternate: null,
          effectTag: "PLACEMENT" // PLACEMENT 表示需要添加新的節(jié)點(diǎn)
          };
          }
          // 不同類型節(jié)點(diǎn),且存在舊的 fiber 節(jié)點(diǎn)時,
          // 需要移除該節(jié)點(diǎn)
          if (oldFiber && !sameType) {
          oldFiber.effectTag = "DELETION";
          // 當(dāng)最后提交 fiber 樹到 DOM 時,我們是從 wipRoot 開始的,
          // 此時沒有上一次的 fiber,所以這里用一個數(shù)組來跟蹤需要
          // 刪除的節(jié)點(diǎn)
          deletions.push(oldFiber);
          }

          if (oldFiber) {
          // 同步更新下一個舊 fiber 節(jié)點(diǎn)
          oldFiber = oldFiber.sibling;
          }

          if (index === 0) {
          wipFiber.child = newFiber;
          } else {
          prevSibling.sibling = newFiber;
          }

          prevSibling = newFiber;
          index++;
          }
          }

          注意:這個過程中 React 還用了?key?來檢測數(shù)組元素變化了位置的情況,避免重復(fù)渲染以提高性能。簡化起見,本文未實(shí)現(xiàn)。

          下面 CodeSandbox 代碼用了個小技巧,重復(fù)執(zhí)行?render?實(shí)現(xiàn)更新界面的效果,動手改改試試。

          redact-2


          VII: 函數(shù)組件

          目前我們還只考慮了直接渲染 DOM 標(biāo)簽的情況,不支持組件,而組件是 React 是靈魂,下面我們來實(shí)現(xiàn)函數(shù)組件。

          以一個非常簡單的組件代碼為例。

          /** @jsx Redact.createElement */
          function App(props) {
          return <h1>Hi {props.name}h1>;
          };

          // 等效 JS 代碼 ?
          function App(props) {
          return Redact.createElement(
          "h1",
          null,
          "Hi ",
          props.name
          )
          }

          const element = <App name="foo" />;
          const container = document.getElementById("root");
          Redact.render(element, container);

          函數(shù)組件有2個不同點(diǎn):

          • 函數(shù)組件的 fiber 節(jié)點(diǎn)沒有對應(yīng) DOM

          • 函數(shù)組件的 children 來自函數(shù)執(zhí)行結(jié)果,而不是像標(biāo)簽元素一樣直接從 props 獲取,因?yàn)?children 不只是函數(shù)組件使用時包含的子孫節(jié)點(diǎn),還需要組合組件本身的結(jié)構(gòu)

          注意以下代碼省略了未改動部分。

          function commitWork(fiber) {
          if (!fiber) {
          return;
          }

          // 當(dāng) fiber 是函數(shù)組件時節(jié)點(diǎn)不存在 DOM,
          // 故需要遍歷父節(jié)點(diǎn)以找到最近的有 DOM 的節(jié)點(diǎn)
          let domParentFiber = fiber.parent;
          while (!domParentFiber.dom) {
          domParentFiber = 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 函數(shù)
          commitDeletion(fiber, domParent);
          }

          commitWork(fiber.child);
          commitWork(fiber.sibling);
          }

          // 新增函數(shù),移除 DOM 節(jié)點(diǎn)
          function commitDeletion(fiber, domParent) {
          // 當(dāng) child 是函數(shù)組件時不存在 DOM,
          // 故需要遞歸遍歷子節(jié)點(diǎn)找到真正的 DOM
          if (fiber.dom) {
          domParent.removeChild(fiber.dom);
          } else {
          commitDeletion(fiber.child, domParent);
          }
          }

          function performUnitOfWork(fiber) {
          const isFunctionComponent = fiber.type instanceof Function;
          // 原本邏輯挪到 updateHostComponent 函數(shù)
          if (isFunctionComponent) {
          updateFunctionComponent(fiber);
          } else {
          updateHostComponent(fiber);
          }
          if (fiber.child) {
          return fiber.child;
          }
          let nextFiber = fiber;
          while (nextFiber) {
          if (nextFiber.sibling) {
          return nextFiber.sibling;
          }
          nextFiber = nextFiber.parent;
          }
          }

          // 新增函數(shù),處理函數(shù)組件
          function updateFunctionComponent(fiber) {
          // 執(zhí)行函數(shù)組件得到 children
          const children = [fiber.type(fiber.props)];
          reconcileChildren(fiber, children);
          }

          // 新增函數(shù),處理原生標(biāo)簽組件
          function updateHostComponent(fiber) {
          if (!fiber.dom) {
          fiber.dom = createDom(fiber);
          }
          reconcileChildren(fiber, fiber.props.children);
          }

          VIII: 函數(shù)組件 Hooks

          支持了函數(shù)組件,還需要支持組件狀態(tài) / state 才能實(shí)現(xiàn)刷新界面。

          我們的示例也跟著更新,用 hooks 實(shí)現(xiàn)經(jīng)典的 counter,點(diǎn)擊計(jì)數(shù)器加1。

          /** @jsx Redact.createElement */
          function Counter() {
          const [state, setState] = Redact.useState(1)
          return (

          setState(c => c + 1)}>
          Count: {state}


          );
          }
          const element = ;
          const container = document.getElementById("root");
          Redact.render(element, container);

          注意以下代碼省略了未變化部分。

          // 新增變量,渲染進(jìn)行中的 fiber 節(jié)點(diǎn)
          let wipFiber = null;
          // 新增變量,當(dāng)前 hook 的索引
          let hookIndex = null;

          function updateFunctionComponent(fiber) {
          // 更新進(jìn)行中的 fiber 節(jié)點(diǎn)
          wipFiber = fiber;
          // 重置 hook 索引
          hookIndex = 0;
          // 新增 hooks 數(shù)組以支持同一個組件多次調(diào)用 `useState`
          wipFiber.hooks = [];
          const children = [fiber.type(fiber.props)];
          reconcileChildren(fiber, children);
          }

          function useState(initial) {
          // alternate 保存了上一次渲染的 fiber 節(jié)點(diǎn)
          const oldHook =
          wipFiber.alternate &&
          wipFiber.alternate.hooks &&
          wipFiber.alternate.hooks[hookIndex];
          const hook = {
          // 第一次渲染使用入?yún)ⅲ诙武秩緩?fù)用前一次的狀態(tài)
          state: oldHook ? oldHook.state : initial,
          // 保存每次 setState 入?yún)⒌年?duì)列
          queue: []
          };

          const actions = oldHook ? oldHook.queue : [];
          actions.forEach(action => {
          // 根據(jù)調(diào)用 setState 順序從前往后生成最新的 state
          hook.state = action instanceof Function ? action(hook.state) : action;
          });

          // setState 函數(shù)用于更新 state,入?yún)?action
          // 是新的 state 值或函數(shù)返回新的 state
          const setState = action => {
          hook.queue.push(action);
          // 下面這部分代碼和 render 函數(shù)很像,
          // 設(shè)置新的 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 代碼如下,點(diǎn)擊 Count 試試:

          redact-3


          結(jié)語

          除了幫助讀者理解 React 核心工作原理外,本文很多變量都和 React 官方代碼保持一致,比如,讀者在 React 應(yīng)用的任何函數(shù)組件里斷點(diǎn),再打開調(diào)試工作能看到下面這樣的調(diào)用棧:

          • updateFunctionComponent

          • performUnitOfWork

          • workLoop

          6e6f6675f354eb3657be8498c0b8f9c9.webp

          注意本文是教學(xué)性質(zhì)的,還缺少很多 React 的功能和性能優(yōu)化。比如:在這些事情上 React 的表現(xiàn)和 Redact 不同。

          • Redact 在渲染階段遍歷了整棵樹,而 React 用了一些啟發(fā)性算法,可以直接跳過某些沒有變化的子樹,以提高性能。(比如 React 數(shù)組元素推薦帶 key,可以跳過無需更新的節(jié)點(diǎn),參考官方文檔)

          • Redact 在 commit 階段遍歷整棵樹, React 用了一個鏈表保存變化了的 fiber,減少了很多不必要遍歷操作。

          • Redact 每次創(chuàng)建新的 fiber 樹時都是直接創(chuàng)建 fiber 對象節(jié)點(diǎn),而 React 會復(fù)用上一個 fiber 對象,以節(jié)省創(chuàng)建對象的性能消耗。

          • Redact 如果在渲染階段收到新的更新會直接丟棄已渲染的樹,再從頭開始渲染。而 React 會用時間戳標(biāo)記每次更新,以決定更新的優(yōu)先級。

          • 源碼還有很多優(yōu)化等待讀者去發(fā)現(xiàn)。。。



          推薦閱讀




          我的公眾號能帶來什么價值?(文末有送書規(guī)則,一定要看)

          每個前端工程師都應(yīng)該了解的圖片知識(長文建議收藏)

          為什么現(xiàn)在面試總是面試造火箭?

          瀏覽 38
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲精品三级在线观看 | 青草在线视频 | 老鸭窝毛片 | 在线播放日本 | 久草这里只有精品 |