從零開始構建React

之前學習React的時候看到一篇文章《Build Your Own React》, 不論從質量還是更新速度上, 都非常的不錯, 現將它翻譯一下, 同時也加深自己對React的理解. 感興趣的朋友可以進入 傳送門 查看原文.

正文開始

[toc]

第零步: 知識回顧

首先我們來回顧一下React的一些基本概念. 如果你已經對React, JSX和DOM元素之間是如何交互的有了較好的理解, 那么可以跳過這一步.

我們使用這個僅有三行代碼的React應用小程序. 第一行代碼定義了一個React元素, 第二行代碼使用原生Js得到一個DOM節(jié)點, 最后一行代碼將React元素渲染到container容器中.

我們用最普通的Js來替換掉所有React特有的代碼

const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)

第一行代碼是用JSX定義的React元素, 這根本不是合法的Js. 為了將代碼轉換為純Js, 我們首先將它替換成合法的Js代碼.

JSX可以通過Babel等構件工具轉換成Js. 轉換過程通常也比較簡單, 就是將標簽內容整個替換為調用createElement函數, 然后將標簽名, props屬性和children屬性作為參數傳進去.

const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
)

React.createElement根據傳入的參數創(chuàng)建一個對象. 源碼中無非就是多了一些驗證操作, 所以我們直接把結果拿過來替換掉這個函數沒有任何問題.

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

這就是一個React元素, 它就是一個有type和props屬性的對象(當然源碼中肯定有更多屬性, 但是我們只關心這兩個).

type屬性的值是一個字符串, 它指定了我們想要創(chuàng)建的DOM節(jié)點的類型, 它就是你想要創(chuàng)建HTML元素時傳入document.createElement函數的元素標簽名. type屬性也可以是一個函數, 這個我們留到第七步再講.

props屬性是一個對象, 它的內容都從JSX代碼中的attributes(HTML中書寫的屬性叫做attributes)得來, 它還有一個比較特殊的屬性children.

在這個例子中, children是一個字符串, 但是通常情況下它是一個數組.

另外一個我們需要替換的React代碼是ReactDOM.render. render是React更改DOM的地方, 所以現在改為由我們自己來做對DOM的操作.

首先我們用type指定元素類型來創(chuàng)建一個節(jié)點, 也就是h1元素. 然后再將所有的props屬性添加到節(jié)點中, 這里我們僅有title屬性.

為了避免混淆, 下文中的"元素"指的是React元素, "節(jié)點"指DOM元素.

接下來我們?yōu)閏hildren創(chuàng)建節(jié)點. 由于我們的children只是一個字符串, 所以我們創(chuàng)建文本節(jié)點.

const node = document.createElement(element.type)
node["title"] = element.props.title
?
const text = document.createTextNode("")
text["nodeValue"] = element.props.children

之所以要創(chuàng)建textNode而不是去設置某元素的innerText, 是因為這樣可以使我們在將來可以以一種相同的方式來處理所有的元素. 同樣需要注意到, 我們設置nodeValue的方式也是跟我們設置h1的title屬性是一樣的, 這就好像字符串也有一個屬性props: {nodeValue: "hello"}.

最后, 我們把文本節(jié)點掛載到h1上, 然后把h1元素掛載到容器container上. 現在我們的程序就跟之前有一樣的效果了.

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}
?
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)

第一步: 搞定createElement函數

這次我們重新整一個程序, 而且要用我們自己的React來替換官方的React. 我們從重寫createElement函數開始.

首先還是把JSX轉換成Js, 來瞅一眼官方是怎么調用createElemnt的.

const element = React.createElement(
  "div",
  { id: "foo" },
  React.createElement("a", null, "bar"),
  React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

從上一步中我們看到了一個React元素其實就是一個有typeprops屬性的對象. 我們自己的createElement函數要做事也就是創(chuàng)建這么一個對象.

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  }
}

我們使用擴展運算符將props展開, 使用剩余參數的語法將children搜集到一起, 這樣children屬性就一定會是數組了.

例如, createElement("div")返回:

{
  "type": "div",
  "props": { "children": [] }
}

createElement("div", null, a)返回:

{
  "type": "div",
  "props": { "children": [a] }
}

createElement("div", null, a, b)返回:

{
  "type": "div",
  "props": { "children": [a, b] }
}

children數組也可能包含像字符串, 數字這樣的原始值. 所以, 我們將元素中所有不是對象的東西全部包裹起來, 為他們創(chuàng)建一個名為TEXT_ELEMENT的特殊類型.

源碼中, 當沒有children時React并沒有去創(chuàng)建一個空數組, 也沒有包裹原始值, 但是我們偷懶了, 于是我們就是這么干了! 我們的代碼就是要簡單, 不需要表現多完美性能多好.

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: [],
    },
  }
}

現在的createElement還是React提供的, 為了要替換掉它, 我們還要給我們自己的庫起個名字. 我們需要起個有逼格的名字, 比如Didact. 于是乎, 代碼就變成了這樣:

nst Didact = {
  createElement,
}
?
const element = Didact.createElement(
  "div",
  { id: "foo" },
  Didact.createElement("a", null, "bar"),
  Didact.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

但是我們仍需要使用JSX啊, 那我們怎么告訴babel使用Didact的createElement呢? 就像這樣加個注釋就行了.

const Didact = {
  createElement,
}
?
/** @jsx Didact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

const container = document.getElementById("root")
ReactDOM.render(element, container)

第二步: 搞定render函數

接下來, 我們要來重寫我們自己的ReactDOM.render函數.

目前我們只關心往DOM里頭加東西, 至于怎么更新和刪除后面再說.

我們先用React元素的type類型, 創(chuàng)建DOM節(jié)點, 然后把這個新的節(jié)點加到container容器里.
對于子節(jié)點遞歸的做同樣的事情.
同時我們需要處理文本元素, 如果元素類型是TEXT_ELEMENT我們就要創(chuàng)建文本節(jié)點.
最后我們將元素屬性添加到節(jié)點上.

于是大功告成, 我們就有了一個能把JSX渲染到DOM元素里的庫了.
這還不趕快到cdoesandbox裝一波逼.

function render(element, container) {
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)
?
  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)
}

第三步: 搞定并發(fā)模式

不幸的事總是來得那么突然, 我們碼更多代碼之前, 可能需要重構一下. 我們的遞歸調用有問題啊! 一旦我們開始渲染, 那么在整個元素樹渲染完成前是不會罷手的. 如果元素樹賊jb大, 它搞不好就會把主線程阻塞很久. 如果這個時候瀏覽器要處理一些高優(yōu)先級的事物比如處理用戶輸入啊, 執(zhí)行流暢的動畫啊, 它就得等我們渲染完才能搞.

因此我們需要把工作拆分成一個一個的小單元, 每個單元完成時如果瀏覽器有急事我們都讓它能夠中斷渲染.


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(nextUnitOfWork) {
  // TODO
}

我們使用requestIdleCallback來進行循環(huán). 你可以把requestIdleCallback想象成是setTimeout, 區(qū)別就是它是由瀏覽器在主線程空閑的時候自動來執(zhí)行的.

React已經不再使用requestIdleCallback了. 它們現在使用scheduler包. 但是別想太多, 它們概念上差不多就行了.

requestIdleCallback會給回調函數傳入一個deadline參數. 我們可以使用它來在判斷瀏覽器橫插一腳之前我們還有多少時間來執(zhí)行我們的代碼.

為了啟動循環(huán), 我們要設置第一個工作單元是啥, 然后把performUnitOfWork函數給碼上. 這函數不僅要執(zhí)行它的工作, 還要能夠返回下一個工作單元.

第四步: 搞定Fibers

為了能夠很好的組織工作單元, 我們需要引入一種數據結構: fiber樹.

每個React元素對應一個fiber, 每個fiber就是一個工作單元.

Fiber樹的樣子:


微信截圖_20200515160430.png

每一個fiber對象, 包含的屬性有:

  • type: 即React元素的type, 表示元素類型
  • props: 即React元素的props
  • parent: 父級的fiber對象
  • dom: 保存了fiber對象對應的真實DOM元素
  • child: 此fiber對象的第一個子元素fiber對象
  • sibling: 此fiber對象的下一個相鄰兄弟fiber對象

讓老夫來給你們舉個栗子:

假設我們要渲染一顆醬的React元素樹

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

render中我們就需要創(chuàng)建根fiber并將它設置為nextUnitOfWork. 余下的事情就在performUnitOfWork函數里做. 在函數中, 針對每個fiber我們要做3件事:

  1. 將元素掛載到DOM節(jié)點
  2. 為元素的children創(chuàng)建fiber
  3. 選擇并返回下一個工作單元

采用這樣的數據結構的一個目標就是使我們更容易的找到下一個工作單元. 因此, 每個fiber都有一個鏈接指向它的第一個子fiber節(jié)點, 它的下一個兄弟fiber節(jié)點, 和它的父fiber節(jié)點.

當我們結束完一個fiber的工作時, 如果它有子元素, 那么那個fiber就是下一個工作單元.
就我們的例子來說, 當我們結束div fiber的時候, 下一個工作單元將是h1 fiber.

如果fiber沒有子元素, 我們就把它的兄弟元素作為下一個工作單元. 比如, p fiber沒有子元素, 因此我們在結束p之后把a fiber作為下一個工作單元.

如果fiber既沒有子元素也沒有兄弟元素咋整? 那我們就找它"叔", 也就是它"爹"的"兄弟". 比如a元素和h2元素.

那么如果它爹沒有兄弟咋整? 我們一直往上找, 直到找到有兄弟的"爹"或者找到了根元素. 如果我們回到了根元素, 就意味著這次render的工作已經全部搞定了.

下面, 我們把上面一坨廢話轉換成代碼.

首先, 把render函數改一下.

我們把創(chuàng)建DOM節(jié)點的代碼抽離到一個函數中, 待會要用.

render函數中, 我們將nextUnitOfWork設置成fiber樹根節(jié)點.

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) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  }
}

這樣, 當瀏覽器ok之后, 它就會調用workLoop函數. 在performUnitOfWork函數中, 首先我們創(chuàng)建新節(jié)點并掛載到DOM元素上. 我們將DOM節(jié)點保存在fiber.dom屬性中.

然后我們?yōu)槊恳粋€child創(chuàng)建新的fiber對象.

接著我們將它加入到fiber樹中. 如果它是第一個子元素, 就設置它為child, 如果不是則設置為sibling.

最后我們來尋找下一個工作單元. 首先我們找子元素, 沒有的話找sibling, 再沒有找它"叔", 以此類推.

那么這就是我們自己的performUnitOfWork函數了.



function performUnitOfWork(fiber) {
  // 第一件事: 掛載到dom節(jié)點
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

  // 第二件事: 為fiber的children創(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,
    }

    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
  }
}

第五步: 搞定render和commit階段

現在我們又碰到了一個問題. 我們每次針對fiber進行操作的時候, 都會將新的節(jié)點掛載到DOM上. 然而, 瀏覽器是有可能在我們完成整個fiber樹渲染之前中斷我們這個工作過程的, 那這樣的話用戶豈不是能看到一個半成品界面了?

于是乎, 我們應該將函數中變更DOM元素的代碼移除.

我們把fiber樹的根節(jié)點保存起來, 將它稱之為wipRoot(work in progress root).

一旦我們完成了所有fiber創(chuàng)建工作, 也就是說當沒有下一個工作單元返回的時候, 我們再將整個fiber樹更新到真實DOM元素上.

我們把這個更新工作放在commitRoot函數內. 我們遞歸的把所有的節(jié)點掛載到真實DOM上.

function commitRoot() {
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  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) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  }
  nextUnitOfWork = wipRoot
}
?
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null

第六步: 搞定調和(校對)Reconciliation

目前為止, 我們僅僅是往DOM上添加一些內容, 那更新和刪除DOM元素咋整呢?

這就是我們現在要做的事, 我們要比較這次從render函數里接收到的元素和上次已經渲染到真實DOM里的舊fiber樹之間的差異.

因此我們肯定是需要把"舊fiber樹"給保存起來. 我們把它稱為currentRoot. 啥時候來保存呢? 就在commit之后吧.

我們同樣要給每一個fiber增加一個alternate屬性, 這個屬性就指向這個fiber對應的那個"舊的fiber對象".

現在我們把performUnitOfWork函數中, 創(chuàng)建新fiber對象的那部分代碼給抽離出來. 抽離到reconcileChildren函數里好了.

在這個函數里, 我們將新元素與舊的fiber"校對"一下.

我們需要同時迭代舊fiber的子元素和這次想要校對的元素數組. 代碼中最重要的就是while循環(huán)中的oldFiberelement. element是我們這次想要渲染到DOM中的內容, oldFiber是我們上次渲染的內容.

我們通過比較它們倆來確定我們要不要對DOM進行更新.

我們使用type屬性來比較:

  • 如果舊fiber和新元素type相同, 我們就保留這個DOM節(jié)點, 僅僅更新一下它的屬性
  • 如果type不同, 只有新的element, 那就說明element是新的, 我們將要創(chuàng)建新的DOM節(jié)點
  • 如果type不同, 只有oldFiber, 那么說明這次渲染沒有這個DOM節(jié)點了, 需要移除

在校對這部分, React官方使用了key值, 來更高效的校對, 比如某個元素是不是僅僅換了個次序. 為了偷懶, 為了簡單, 我們肯定是不搞這玩意兒的

  • 當舊fiber和element type相同, 我們就創(chuàng)建一個新的fiber對象保存舊fiber中的DOM節(jié)點, 同時也保存來自element的props屬性.與此同時, 我們?yōu)閒iber打上一個標簽, 即effectTag屬性, 并設置為UPDATE. 這個屬性在后面的commit階段里要用到.

  • 當有新元素, 需要創(chuàng)建新的DOM節(jié)點時, 我們把effectTag設置為PLACEMENT

  • 當需要刪除節(jié)點時, 由于我們沒有創(chuàng)建新fiber, 我們把effectTag加到舊的fiber對象上.

但是我們commit時, 我們用的是work in progress root, 是沒有舊fiber的. 因此我們需要用數組保存一下要被移除的節(jié)點.

function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;

  // 循環(huán)對比 新的elements和上一次的fiber對象
  // elements表示新傳遞需要render的子元素;oldFiber表示之前那次渲染后的fiber對象
  while (index < elements.length || oldFiber !== null) {

    const element = elements[index];
    let newFiber = null;

    const sameType = oldFiber && element && element.type == oldFiber.type;

    // 比較兩個節(jié)點的類型
    // 1. 如果element和oldfiber的類型相同,那么保留dom,只更新屬性
    // 2. 如果type不同,并且有element,那么需要新增dom
    // 3. 如果type不同,并且有oldfiber,那么需要刪除原先的dom
    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: 'UPDATE',
      };
    }
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: 'PLACEMENT',
      };
    }
    if (oldFiber && !sameType) {
      oldFiber.effectTag = 'DELETION';
      deletions.push(oldFiber);
    }

    // 本次比較后,下次循環(huán)的oldFiber對象,重新賦值為oldFiber的兄弟fiber對象
    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    // 本次比較后,將本次的fiber對象存入變量prevSibling
    // 如果是第一個子元素,那么就將它設置為上級fiber對象的child
    // 如果不是第一個子元素,那么將它掛載到上一個fiber對象的[sibling]屬性上
    if (index === 0) {
      wipFiber.child = newFiber
    } else if (element) {
      prevSibling.sibling = newFiber
    }
    prevSibling = newFiber
    index++
  }
}

此時, rendercommitRoot部分的代碼為:

function commitRoot() {
  deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}

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

接下來我們需要更改commitWork函數來真正的處理effectTag.

  • 如果fiber對象有PLACEMENT標簽, 那么就將DOM元素掛載到它的父元素上
  • 如果是DELETION標簽, 那么就移除這個DOM
  • 如果是UPDATE標簽, 我們就在原始的DOM元素上更改一下屬性, 更改操作放到updateDom函數中
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)
}

我們比較新舊兩個fiber對象的props屬性, 移除掉新fiber中已經沒有了的屬性, 設置新增的或者有變化的屬性.

其中一種比較特殊的屬性是事件的監(jiān)聽函數. 因此如果屬性名以"on"前綴開頭, 我們就當它是事件監(jiān)聽函數, 特殊處理.

如果事件監(jiān)聽函數改變了, 我們就移除舊的, 加上新的.

updateDom部分代碼:

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)
function updateDom(dom, prevProps, nextProps) {
    //Remove old or changed event listeners
  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]
      )
    })
    
  // Remove old properties
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = ""
    })
?
  // Set new or changed properties
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })
    
    // Add event listeners
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener(
        eventType,
        nextProps[name]
      )
    })
}

第七步: 搞定函數組件

下一步, 我們來增加對函數組件的支持.

首先我們改一下例子, 我們使用一個返回h1元素的函數組件.

/** @jsx Didact.createElement */
function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)

如果把其中的JSX轉換成Js:

function App(props) {
  return Didact.createElement(
    "h1",
    null,
    "Hi ",
    props.name
  )
}
const element = Didact.createElement(App, {
  name: "foo",
})

函數組件有兩個方面是不同的:

  • 由函數組件創(chuàng)建的fiber對象沒有DOM節(jié)點
  • 它的children是來自于函數的運行結果, 并不是直接來自于props

我們需要判斷fiber對象的type是不是一個函數, 然后再決定使用哪個函數來處理.

updateHostComponent函數中我們跟之前一樣處理, 而在updateFunctionComponent函數中, 我們運行函數來得到children.

function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
?
function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  reconcileChildren(fiber, fiber.props.children)
}

得到了children之后, 校對(reconciliation)的工作跟之前是一樣的. 需要修改的是commitWork函數. 對于沒有DOM的fiber對象, 我們需要更改兩個東西.

首先, 我們要一直沿著fiber樹向上查找直到找到有DOM節(jié)點的fiber對象.
然后, 當要移除節(jié)點時, 我們同樣需要一直向上查找.

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

    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') {
        commitDeletion(fiber, domParent);
    }
    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

function commitDeletion(fiber, domParent) {
    if (fiber.dom) {
        domParent.removeChild(fiber.dom);
    } else {
        commitDeletion(fiber.child, domParent);
    }
}

第八步: 搞定Hooks

現在到了最后一步了, 既然有了函數組件, 那我們也給它加上狀態(tài).
我們把例子換成經典的counter組件. 每次點擊, 它都加1. 我們使用Didact.useState來獲取和更新counter的值.

const Didact = {
  reateElement,
  render,
  useState,
}
?
/** @jsx Didact.createElement */
function Counter() {
  const [state, setState] = Didact.useState(1)
  return (
    <h1 onClick={() => setState(c => c + 1)}>
      Count: {state}
    </h1>
  )
}
const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)

updateFunctionComponent里就是我們調用counter函數的地方.

在調用函數組件前我們需要初始化一些全局變量, 這樣我們可以在useState里用到它們.
首先設置work in progress fiber. 然后為fiber增加一個hooks屬性, 這個屬性是個數組, 處理在同一個函數組件中多次調用useState的情況. 然后我們記錄當前hook的index.

let wipFiber = null
let hookIndex = null
?
function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
?
function useState(initial) {
  // TODO
}

當函數組件調用useState時, 我們先通過alternate指向的舊fiber以及當前hook的index判斷是不是之前有這么個hook. 如果有的話, 我們就把舊狀態(tài)值拷貝到新hook里. 然后我們再把新hook加到fiber上, 將hook的index加1, 然后返回這個狀態(tài).

useState函數調用之后應該返回一個函數來更新狀態(tài)值. 因此我們定義一個函數setState, 它接收一個參數action.(以Counter的例子來說, action參數就是將狀態(tài)加1的函數)
將action加入到hook的queue數組中.

不過我們還沒有運行action參數呢. 我們在下次渲染組件的時候運行, 我們舊的hook的queue數組中得到的所有action依次運行, 并將結果添加到新hook的state中, 這樣當我們return時state就是最新的了.

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  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]
}

行了, 神功告成! 我們已經寫完了我們自己的React. 現在可以放到github上再裝一波比了~

分段貼代碼怕造成遺漏, 最后再重新貼依次全部代碼.


/**
 * 創(chuàng)建react元素
 * @param {*} type 元素類型
 * @param {*} props 元素屬性
 * @param  {...any} children 子元素,用剩余參數搜集到children中
 * @return {*} 返回值是一個對象,包含type和props屬性
 */
function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            // children如果是對象,那么就原封不動保留,如果不是則當成字符串,創(chuàng)建一個文本節(jié)點
            children: children.map(child => {
                return typeof child === 'object' ? child : createTextElement(child)
            }),
        }
    }
}

function createTextElement(text) {
    return {
        type: 'TEXT_ELEMENT',
        props: {
            nodeValue: text,
            children: [],
        }
    }
}

/**
 * 根據react的fiber對象,生成實際dom元素
 * @param {*} fiber 一個對象,包含type、child、parent、props、dom屬性
 */
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。包括移除舊的屬性和事件監(jiān)聽、設置或者改變新的屬性
 * @param {*} dom dom元素
 * @param {*} prevProps 上一次的屬性
 * @param {*} nextProps 下一次需要更新的屬性
 */
function updateDom(dom, prevProps, nextProps) {
    // 移除舊的或者是被改變的event listeners
    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 => {
            dom[name] = nextProps[name];
        });

    // 添加新的事件監(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樹的更新
 */
function commitRoot() {
    deletions.forEach(commitWork);
    commitWork(wipRoot.child);
    // 將上一次的fiber樹保存起來
    currentRoot = wipRoot;
    wipRoot = null;
}

/**
 * 進行實際的dom掛載、更新或者移除操作
 * @param {*} fiber fiber對象
 */
function commitWork(fiber) {
    if (!fiber) {
        return;
    }

    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') {
        commitDeletion(fiber, domParent);
    }
    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

function commitDeletion(fiber, domParent) {
    if (fiber.dom) {
        domParent.removeChild(fiber.dom);
    } else {
        commitDeletion(fiber.child, domParent);
    }
}

function render(element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element]
        },
        alternate: currentRoot,
    }
    deletions = [];
    nextUnitOfWork = wipRoot;
}

let nextUnitOfWork = null;
let currentRoot = null;
let wipRoot = null; // working in progress(wip) root
let deletions = null;

/**
 * 工作循環(huán)。工作循環(huán)的內容為:經由初始的fiber對象,不停的生成新的fiber對象
 * 如果沒有下一個工作單元了,表示fiber樹的創(chuàng)建完成,提交整個fiber樹,進行下一步
 * @param {*} deadline 瀏覽器傳入的一個參數對象,通過該對象的timeRemaining方法,可以查看瀏覽器空閑時間還有多久
 */
function workLoop(deadline) {
    let shouldYield = false;
    while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        shouldYield = deadline.timeRemaining() < 1;
    }

    // 有正在working的,但是沒有下一個工作單元了,提交
    // 也就是說,假如是因為瀏覽器空閑時間不足導致退出上面while循環(huán),那么nextUnitofWork是有值的
    // 此時就不執(zhí)行commitRoot函數進行dom的實際更改操作
    if (!nextUnitOfWork && wipRoot) {
        commitRoot();
    }

    requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

/**
 * 進行一個工作單元的任務,每個工作單元的任務有:
 * 1. 創(chuàng)建真實dom元素
 * 2. 根據當前fiber創(chuàng)建新的fiber
 * 3. 返回下一個工作單元
 * @param {*} fiber fiber對象。render執(zhí)行后,初始fiber對象為:
 * {
 *    dom: container,
 *    props: {
 *        children: [element]
 *    },
 *    alternate: null,          
 * }
 */
function performUnitOfWork(fiber) {
  const isFunctionComponent =
    fiber.type instanceof Function
  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
  }
}

let wipFiber = null;
let hookIndex = null;

function updateFunctionComponent(fiber) {
    wipFiber = fiber;
    hookIndex = 0;
    wipFiber.hooks = [];
    const children = [fiber.type(fiber.props)];
    reconcileChildren(fiber, children);
}

function useState(initial) {
    const oldHook = wipFiber.alternate &&
        wipFiber.alternate.hooks &&
        wipFiber.alternate.hooks[hookIndex];
    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];
}

function updateHostComponent(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }
    reconcileChildren(fiber, fiber.props.children)
}

/**
 * 生成當前對象的直接子元素的fiber對象
 * @param {*} wipFiber 當前的fiber對象
 * @param {*} elements 當前fiber對象的子元素
 */
function reconcileChildren(wipFiber, elements) {
    let index = 0;
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
    let prevSibling = null;

    // 循環(huán)對比 新的elements和上一次的fiber對象
    // elements表示新傳遞需要render的子元素;oldFiber表示之前那次渲染后的fiber對象
    while (index < elements.length || oldFiber !== null) {

        const element = elements[index];
        let newFiber = null;

        const sameType = oldFiber && element && element.type == oldFiber.type;

        // 比較兩個節(jié)點的類型
        // 1. 如果element和oldfiber的類型相同,那么保留dom,只更新屬性
        // 2. 如果type不同,并且有element,那么需要新增dom
        // 3. 如果type不同,并且有oldfiber,那么需要刪除原先的dom
        if (sameType) {
            newFiber = {
                type: oldFiber.type,
                props: element.props,
                dom: oldFiber.dom,
                parent: wipFiber,
                alternate: oldFiber,
                effectTag: 'UPDATE',
            };
        }
        if (element && !sameType) {
            newFiber = {
                type: element.type,
                props: element.props,
                dom: null,
                parent: wipFiber,
                alternate: null,
                effectTag: 'PLACEMENT',
            };
        }
        if (oldFiber && !sameType) {
            oldFiber.effectTag = 'DELETION';
            deletions.push(oldFiber);
        }

        // 本次比較后,下次循環(huán)的oldFiber對象,重新復制為oldFiber的兄弟fiber對象
        if (oldFiber) {
            oldFiber = oldFiber.sibling;
        }

        // 本次比較后,將本次的fiber對象存入變量prevSibling
        // 如果是第一個子元素,那么就將它設置為上級fiber對象的child
        // 如果不是第一個子元素,那么將它掛載到上一個fiber對象的[sibling]屬性上
        if (index === 0) {
            wipFiber.child = newFiber
        } else if (element) {
            prevSibling.sibling = newFiber
        }
        prevSibling = newFiber
        index++
    }
}

/********************* 以下是測試部分 *********************/

const Didact = {
    createElement,
    render,
    useState,
}

/** @jsx Didact.createElement */
function Counter() {
    const [state, setState] = Didact.useState(1)
    return (
        <h1 onClick={() => {
            setState(c => c + 1);
        }}>
            Count: {state}
        </h1>
    )
}
const element = <Counter />

const container = document.getElementById("root")
Didact.render(element, container)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容