创建一个自己的 React 版本

本文翻译自:https://pomb.us/build-your-ow...

创建一个自己的 React 版本

从头开始,下面这些是我们将要添加到我们的 React 版本中的内容。

  • Step 1: createElement 函数
  • Step 2: render函数
  • Step 3: Concurrent Mode 并发模式
  • Step 4: Fibers
  • Step 5: Render and Commit Phases 渲染和提交阶段
  • Step 6: Reconciliation 协调
  • Step 7: Function Components 函数组件
  • Step 8: Hooks

Step 0 : 回顾

首先让我们回顾一些基本概念。如果您已经对 ReactJSXDOM 元素的工作方式有了很好的了解,则可以跳过此步骤。

我们将使用此 React 应用程序

只需三行代码。第一行定义一个 React 元素。第二行从 DOM 获取一个节点。最后一行将 React 元素渲染到容器中。

让我们删除所有 React 特定的代码,然后将其替换为原始 JavaScript

// 第一行定义一个React元素
const element = 

Hello

; // 第二行从DOM获取一个节点 const container = document.getElementById("root"); // 最后一行将React元素渲染到容器中 ReactDOM.render(element, container);
在第一行中,我们具有用 JSX定义的元素。它甚至不是有效的 JavaScript,因此要用标准 JS取代它,首先我们需要用有效 JS取代它。

JSX通过Babel等构建工具转换为JS。转换通常很简单:使用对createElement的调用来替换标签内的代码,并将标签名称,道具和子代作为参数传递。

React.createElement根据其参数创建一个对象。除了进行一些验证之外,这就是它所做全部工作。因此,我们可以安全地将函数调用替换为其输出。

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

这就是一个元素,一个具有两个属性的对象:typeprops(它有更多的属性,但是我们只关心这两个属性):

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

类型type是一个字符串,用于指定我们要创建的DOM节点的类型,它是您要创建 HTML 元素时传递给document.createElementtagName。它也可以是一个函数,但我们将其留给步骤 7。

props是另一个对象,它具有JSX属性中的所有键和值。它还有一个特殊的属性:children

在这种情况下,children是字符串,但通常是包含更多元素的数组。这就是为什么元素也是树的原因。

我们需要替换的另一部分React代码是对ReactDOM.render的调用。

renderReact更改DOM的地方,所以让我们自己进行更新。

首先,我们使用元素类型(在本例中为h1)创建一个node *。

然后,我们将所有元素props分配给该节点。这里只是标题。

*为避免混淆,我将使用“element”来指代React elements,并使用“node”来指代DOM elements

// 首先,我们使用元素类型(在本例中为`h1`)创建一个`node` *。
const node = document.createElement(element.type);
// 然后,我们将所有元素`props`分配给该节点。这里只是标题。
node["title"] = element.props.title;

然后,我们为孩子创建节点。我们只有一个字符串作为孩子,所以我们创建一个文本节点。

const text = document.createTextNode("");
text["nodeValue"] = element.props.children;

使用textNode而不是设置innerText将使我们以后以相同的方式对待所有元素。还请注意我们是如何像设置h1标题一样设置nodeValue,就像字符串中带有props一样:{nodeValue:“ hello”}

最后,我们将textNode附加到h1并将h1附加到container

node.appendChild(text);
container.appendChild(node);

现在,我们拥有与以前相同的应用程序,但是没有使用React

// 1. 创建一个元素,这个元素是具有两个属性的对象:type和props(它有更多的属性,但是我们只关心这两个属性)
const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello"
  }
};

// 2. 从DOM获取一个节点
const container = document.getElementById("root");

// 3. 使用 type 创建一个`node` 。然后,我们将所有元素`props`分配给该节点。这里只是标题。
const node = document.createElement(element.type);
node["title"] = elememt.props.title;

// 4. 创建子节点(使用`textNode`而不是设置`innerText`将使我们以后以相同的方式对待所有元素)
const text = document.createTextNode("");
text["nodeValue"] = element.props.children;

// 5. 将`textNode`附加到`h1`并将`h1`附加到`container`
node.appendChild(text);
container.appendChild(node);

Step 1: createElement 函数

让我们从另一个应用程序开始。这次,我们将用自己的React版本替换React代码。

我们将从编写自己的createElement开始。

让我们将JSX转换为JS,以便可以看到createElement的调用。

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

正如我们在上一步Step 0中看到的,元素是具有typeprops的对象。我们的函数唯一需要做的就是创建该对象。

const element = React.createElement(
  "div",
  { id: "foo" },
  React.createElement("a", null, "bar"),
  React.createElement("b")
);

我们对props使用spread operator,对children使用rest parameter语法,这样children属性将始终是数组。

扩展运算符回顾:
// 此处使用扩展运算符可以直接将数组作为参数传入
function foo(name, ...arr) {
  return {
    name,
    arr
  };
}

const testArr = [1, 3, 5, 6, 2, 5];
foo("Bob", ...testArr);
/*
结果:
{
  name: "Bob",
  arr: [1,3,5,6,2,5]
}
*/

例如:
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] }
}

那么:

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

children组也可以包含原始值,例如字符串或数字。因此,我们会将不是对象的所有内容包装在其自己的元素中,并为其创建特殊类型:TEXT_ELEMENT

当没有children时,React不会包装原始值或创建空数组,但是我们这样做是因为它可以简化我们的代码,对于我们的库,我们更喜欢简单代码而不是高性能代码。

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

我们仍然在使用ReactcreateElement

为了代替它,我们给我们自己的库取一个名字。我们需要一个听起来像React,但又不同的名字。

我们叫它Didact

但是我们仍然想在这里使用 JSX。我们如何告诉babel 使用 DidactcreateElement 代替 React 的?

const Didact = {
  createElement
};

const element = Didact.createElement(
  "div",
  { id: "foo" },
  Didact.createElement("a", null, "bar"),
  Didact.createElement("b")
);

如果我们有这样的注解,当babel转译JSX时,它将使用我们定义的函数。

/** @jsx Didact.createElement */

// 得到:
const element = (
  
bar
);

Step 2: render 函数

接下来,我们需要编写我们的ReactDOM.render函数版本。

ReactDOM.render(element, container);

目前,我们只关心向 DOM 添加内容。我们稍后将处理更新和删除。

function render(element, container) {
  // TODO create dom nodes
}
​
const Didact = {
  createElement,
  render,
}

/** @jsx Didact.createElement */
const element = (
  
bar
) ​ const container = document.getElementById("root") Didact.render(element, container)

我们首先使用元素类型创建DOM节点,然后将新节点附加到容器container中。

function render(element, container) {
  const dom = document.createElement(element.type)
​
  container.appendChild(dom)
}

我们递归地为每个children做同样的事情。

function render(element, container) {
  const dom = document.createElement(element.type)
​
  element.props.children.forEach(child =>
    render(child, dom)
  )
​
  container.appendChild(dom)
}

我们还需要处理文本元素,如果元素类型为TEXT_ELEMENT,我们将创建文本节点而不是常规节点。

function render(element, container) {
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)
​
  element.props.children.forEach(child =>
    render(child, dom)
  )
​
  container.appendChild(dom)
}

我们在这里要做的最后一件事是将元素props分配给节点。

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

就是这样。现在,我们有了一个可以将JSX呈现到DOM的库。


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: [],
    },
  }
}
​
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)
}
​
const Didact = {
  createElement,
  render,
}
​
/** @jsx Didact.createElement */
const element = (
  
bar
) const container = document.getElementById("root") Didact.render(element, container)

Step 3: Concurrent Mode(并发模式)

但是……在开始添加更多代码之前,我们需要重构。

此递归调用存在问题。

function render(element, container) {
  element.props.children.forEach(child => render(child, dom));
}
一旦开始渲染后,直到渲染完完整的元素树后,我们才会停止。 如果元素树很大,则可能会阻塞主线程太长时间。 而且,如果浏览器需要执行诸如处理用户输入或使动画保持平滑等高优先级的工作,则它必须等到渲染完成为止。

因此,我们将工作分成几个小单元,在完成每个单元后,如果需要执行其他任何操作,我们将让浏览器中断渲染。

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进行循环。您可以将requestIdleCallback视为setTimeout,但浏览器将在主线程空闲时运行回调,而不是告诉我们何时运行。

React不再使用 requestIdleCallback现在,它使用调度程序包。但是对于此用例,它在概念上是相同的。

requestIdleCallback还为我们提供了截止日期参数。我们可以使用它来检查我们有多少时间,直到浏览器需要再次控制。

截至 2019 年 11 月,并发模式在React中还不稳定。循环的稳定版本看起来像这样:

while (nextUnitOfWork) {
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}

要开始使用循环,我们需要设置第一个工作单元,然后编写一个performUnitOfWork函数,该函数不仅执行工作,还返回下一个工作单元。

Step 4: Fibers

要组织工作单元,我们需要一个数据结构:一棵构造树(fiber tree)。

我们将为每个元素分配一根fiber,并且每根fiber将成为一个工作单元。

举一个例子:

假设我们要渲染一个像这样的元素树:

Didact.render(
  ,
  container
);

在渲染中,我们将创建root fiber并将其设置为nextUnitOfWork。剩下的工作将在performUnitOfWork函数上进行,我们将为每根fiber做三件事:

  1. 将元素element添加到DOM
  2. 为元素element的子代children创建fiber
  3. 选择下一个工作单元(the next unit of work)
    创建一个自己的 React 版本_第1张图片

该数据结构的目标之一是使查找下一个工作单元变得容易。这就是为什么每个fiber都链接到其第一个子节点,下一个兄弟姐妹和父节点。

当我们在一个fiber上完成了工作,如果这个fiber有一个child,那么这个fiber将会是下一个工作单元(the next unit of work)。

在我们的示例中,当完成 div fiber 上的工作,下一个工作单元将是h1 fiber。

如果这个fiber没有child, 用 sibling(兄弟) 作为下一个工作单元。

比如,p fiber 没有子节点·,那么在p fiber 完成后,移动到a fiber。

如果fiber既没有孩子child也没有兄弟姐妹sibling,那么我们去“叔叔”:父母的兄弟姐妹。就像示例中的ah2 fiber 一样。

另外,如果父母没有兄弟姐妹,我们会不断检查父母,直到找到有兄弟姐妹的父母,或者直到找到根。如果到达根目录,则意味着我们已经完成了此渲染的所有工作。

现在,将其放入代码中。

首先,让我们从 render 函数中删除此代码。

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)
}
​
let nextUnitOfWork = null

我们将创建DOM节点的部分保留在其自身的功能中,稍后我们将使用它。

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) {
  // TODO set next unit of work
}
​
let nextUnitOfWork = null

render 函数中,我们将nextUnitOfWork设置为 fiber tree 的根。

function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  }
}
​
let nextUnitOfWork = null

然后,当浏览器准备就绪时,它将调用我们的workLoop,我们将开始在根上工作。

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}
​
requestIdleCallback(workLoop)
​
function performUnitOfWork(fiber) {
  // TODO add dom node
  // TODO create new fibers
  // TODO return next unit of work
}

首先,我们创建一个新节点 node 并将其添加到 DOM

我们在 fibre.dom 属性中跟踪 DOM 节点。

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
​
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
​
  // TODO create new fibers
  // TODO return next unit of work
}

然后,为每个孩子 child 创建一个新的 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,
    }
  }

然后将其添加到 fiber tree 中,将其设置为子代 child 或者兄弟 sibling ,具体取决于它是否是第一个子代 child

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
  }

这就是我们的 performUnitOfWork

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
​
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
​
  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
  }
}

Step 5: Render and Commit Phases 渲染和提交阶段

我们这里还有另一个问题。

每次处理元素时,我们都会向 DOM 添加一个新节点。 而且,请记住,在完成渲染整个树之前,浏览器可能会中断我们的工作。 在这种情况下,用户将看到不完整的 UI 。 我们不想要那样。

function performUnitOfWork(fiber) {
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
​}

因此,我们需要从此处删除更改 DOM 的部分。

function performUnitOfWork(fiber) {
  
​}

相反,我们将跟踪 fiber tree 的根 root 。我们称其为进行中的根或 wipRoot

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}
​
let nextUnitOfWork = null
let wipRoot = null

一旦完成所有工作(因为没有下一个工作单元),我们便将整个 fiber tree 提交给 DOM

function commitRoot() {
  // TODO add nodes to dom
}
​
function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}
​
let nextUnitOfWork = null
let wipRoot = 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)

我们在 commitRoot 函数中做到这一点。在这里,我们将所有节点递归附加到 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)
}

Step 6: Reconciliation

到目前为止,我们仅向 DOM 添加了内容,但是更新或删除节点又如何呢?

这就是我们现在要做的,我们需要将在 render 函数上得到的元素与我们提交给 DOM 的最后一棵 fiber tree 进行比较。

因此,在完成提交之后,我们需要保存对“我们提交给 DOM 的最后一棵 fiber tree ”的引用。 我们称它为 currentRoot

我们还将 alternate 属性添加到每根 fiber 。 此属性是到旧 fiber 的链接,旧 fiber 是我们在上一个提交阶段向 DOM 提交的 fiber

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

现在,让我们从 performUnitOfWork 中提取代码来创建新的 fibers...

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

到新的 reconcileChildren 函数:

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
​
  const elements = fiber.props.children
  reconcileChildren(fiber, elements)
​
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}
​
function reconcileChildren(wipFiber, elements) {}

在这里,我们将旧 fibers 与新元素 elements 进行协调

function reconcileChildren(wipFiber, elements) {}

我们同时遍历 old fiberchildrenwipFiber.alternate)和要协调的元素数组。

如果我们忽略了同时迭代数组和链接列表所需的所有样板,那么在此期间,我们剩下的最重要的是: oldFiberelementelement 是我们要渲染到 DOM 的东西,而 oldFiber 是我们上次渲染的东西。

我们需要对它们进行比较,以了解是否需要对 DOM 进行任何更改。

function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber =
    wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null
​
  while (
    index < elements.length ||
    oldFiber != null
  ) {
    const element = elements[index]
    let newFiber = null
​
    // TODO compare oldFiber to element
​ }
}

为了比较它们,我们使用以下类型:

  • 如果 old fibernew element 具有相同的类型 type ,我们可以保留 DOM 节点,仅使用新的 props 进行更新。
  • 如果类型 type 不同并且有一个新元素 new element,则意味着我们需要创建一个新的 DOM 节点。
  • 如果类型 types 不同且有 old fiber ,则需要删除旧节点。
const sameType =
    oldFiber &&
    element &&
    element.type == oldFiber.type
​
if (sameType) {
  // TODO update the node
}
if (element && !sameType) {
  // TODO add this node
}
if (oldFiber && !sameType) {
  // TODO delete the oldFiber's node
}

在这里,React 也使用密钥,这样可以实现更好的协调。例如,它检测子元素何时更改元素数组中的位置。

old fibernew element 具有相同的类型 type时,我们创建新的 fiber,以使保持 DOM 节点远离 old fiber,保持 props 远离元素 element

我们还向 fiber 添加了一个新属性:effectTag。 稍后,我们将在提交阶段使用此属性。

const sameType =
  oldFiber &&
  element &&
  element.type == oldFiber.type
​
if (sameType) {
  newFiber = {
    type: oldFiber.type,
    props: element.props,
    dom: oldFiber.dom,
    parent: wipFiber,
    alternate: oldFiber,
    effectTag: "UPDATE",
  }
}

然后,对于元素需要新 DOM 节点的情况,我们使用 PLACEMENT 效果标签 effectTag 来标记新的 fiber

if (element && !sameType) {
  newFiber = {
    type: element.type,
    props: element.props,
    dom: null,
    parent: wipFiber,
    alternate: null,
    effectTag: "PLACEMENT",
  }
}

对于需要删除节点的情况,我们没有新的 fiber ,因此我们将效果标签 effectTag 添加到oldFiber

但是,当我们将 fiber tree 提交给 DOM 时,我们是从正在进行的根 root 开始的,它没有 oldFiber。

if (oldFiber && !sameType) {
  oldFiber.effectTag = "DELETION"
  deletions.push(oldFiber)
}

因此,我们需要一个数组来跟踪要删除的节点。

function render(element, container) {
  // ...
  deletions = []
}
// ...
let deletions = null

然后,当我们将更改提交到 DOM 时,我们也使用该数组中的 fiber

function commitRoot() {
  deletions.forEach(commitWork)
  // ...
}

现在,让我们更改 commitWork 函数以处理新的 effectTags

function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

如果 fiber 具有 PLACEMENT 效果标签,则与之前相同,将 DOM 节点附加到父 fiber 的节点上。如果是 DELETION ,则执行相反的操作,删除该子项。如果是 UPDATE ,我们需要使用更改的 props 来更新现有的 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)
  }

我们将在此 updateDom 函数中进行操作。

function updateDom(dom, prevProps, nextProps) {
  // TODO
}

我们将 oldFiber 中的 propsnewFiber 中的 props 进行比较,删除不再使用的 props,并设置新的或更改的 props

const isProperty = key => key !== "children"
const isNew = (prev, next) => key =>
  prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
  // 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]
    })
}

我们需要更新的一种特殊 prop 是事件侦听器,因此,如果 prop 名称以“ on”前缀开头,我们将以不同的方式处理它们。

const isEvent = key => key.startsWith("on")
const isProperty = key =>
  key !== "children" && !isEvent(key)

如果事件处理程序发生更改,我们将从节点中将其删除。

//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]
      )
    })

然后,我们添加新的处理程序。

 // 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]
      )
    })
}

Step 7: Function Components 函数组件

我们需要添加的下一件事是对函数组件的支持。

首先,让我们更改示例。我们将使用此简单的函数组件,该组件将返回 h1 元素。

/** @jsx Didact.createElement */
function App(props) {
  return 

Hi {props.name}

} const element = 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",
})

函数组件在两个方面有所不同:

  • 功能组件中的 fiber 没有 DOM 节点
  • children 来自于函数的调用,而不是直接来自于 props
function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
​
  const elements = fiber.props.children
  reconcileChildren(fiber, elements)
}

我们检查 fiber 类型是否为函数,并根据此函数使用其他更新函数。

updateHostComponent 中,我们执行与以前相同的操作。

function performUnitOfWork(fiber) {
  const isFunctionComponent =
    fiber.type instanceof Function
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }
  // ...
}
​
function updateFunctionComponent(fiber) {
  // TODO
}
​
function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  reconcileChildren(fiber, fiber.props.children)
}

updateFunctionComponent 中,我们运行该函数以获取子代 children

对于我们的示例,这里的 fibre.typeApp 函数,当我们运行它时,它返回 h1 元素。

然后,一旦有了 children ,对帐便以相同的方式进行,我们不需要在那里进行任何更改。

function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

我们需要更改的是 commitWork 函数。

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

现在我们有了没有 DOM 节点的 fiber ,我们需要更改两件事。

首先,要找到 DOM 节点的父节点,我们需要沿着 fiber tree 向上移动,直到找到带有 DOM 节点的 fiber

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

在删除节点时,我们还需要继续操作,直到找到具有 DOM 节点的子节点为止。

else if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent)
  }
function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    commitDeletion(fiber.child, domParent)
  }
}

Step 8: Hooks

最后一步。现在我们有了函数组件,我们还要添加状态 state

/** @jsx Didact.createElement */
function App(props) {
  return 

Hi {props.name}

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

让我们将示例更改为经典计数器组件。每次单击它,状态都会增加一。

请注意,我们正在使用 Didact.useState 获取和更新计数器值。

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

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

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

在这里,我们从示例中调用 Counter 函数。在该函数内部,我们称为 useState

function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
​
function useState(initial) {
  // TODO
}

我们需要在调用函数组件之前初始化一些全局变量,以便可以在 useState 函数中使用它们。

首先,我们将 work 设置在进行中的 fiber

我们还向 fiber 添加了一个 hooks 数组,以支持在同一组件中多次调用 useState 。并且我们跟踪当前的钩子索引。

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

当函数组件调用 useState 时,我们检查是否有旧的钩子。我们使用钩子索引检查 fiber 的替代项 alternate

如果我们有旧的 hook ,则将状态从旧的 hook 复制到新的 hook ,否则,我们将初始化状态。

然后,将新的 hook 添加到 fiber ,将 hook 索引加1,然后返回状态。

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 还应该返回一个更新状态的函数,因此我们定义了一个 setState 函数,该函数接收一个动作(对于 Counter 示例,该动作是将状态加1的函数)。

我们将该动作推送到添加到挂钩中的队列中。

然后,我们执行与渲染功能中类似的操作,将新的进行中的工作根设置为下一个工作单元,以便工作循环可以开始新的渲染阶段。

const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [],
  }
​
  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]

但是我们尚未执行该操作。

下次渲染组件时,我们会从旧的挂钩队列中获取所有动作,然后将它们逐一应用于新的挂钩状态,因此当我们返回更新后的状态。

const actions = oldHook ? oldHook.queue : []
  actions.forEach(action => {
    hook.state = action(hook.state)
  })

就这样。我们已经构建了自己的React版本。

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: [],
    },
  }
}
​
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)
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]
      )
    })
}
​
function commitRoot() {
  deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}
​
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
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) {
  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)
}
​
function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber =
    wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null
​
  while (
    index < elements.length ||
    oldFiber != null
  ) {
    const element = elements[index]
    let newFiber = null
​
    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type
​
    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)
    }
​
    if (oldFiber) {
      oldFiber = oldFiber.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 (
    

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

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

结语

除了帮助您了解 React 的工作原理外,本文的目的之一是使您更轻松地深入 React 代码库。 这就是为什么我们几乎在所有地方都使用相同的变量和函数名称的原因。

例如,如果您在真正的 React 应用程序的功能组件之一中添加断点,则调用堆栈应显示:

  • workLoop
  • performUnitOfWork
  • updateFunctionComponent

我们没有包括很多 React 功能和优化。例如,以下是 React 可以做的一些事情:

  • Didact 中,我们在渲染阶段遍历整棵树。相反, React 遵循一些提示和试探法,以跳过没有任何更改的整个子树。
  • 我们还在提交阶段遍历整棵树。 React 仅保留有影响的 fiber 并仅访问那些 fiber 的链表。
  • 每次我们构建一个新的进行中的工作树时,都会为每根 fiber 创建新的对象。 React 回收了先前树木中的 fiber
  • Didact 在渲染阶段收到新的更新时,它将丢弃进行中的工作树,然后从根重新开始。 React 使用过期时间戳标记每个更新,并使用它来决定哪个更新具有更高的优先级。
  • ......

你可能感兴趣的:(javascript,react.js,前端)