诗号:半神半圣亦半仙,全儒全道是全贤,脑中真书藏万卷,掌握文武半边天。

该文代码均来自:Build your own React, 所以文中代码会保持和原作者定义一致。

代码脑图

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

/img/react/react-zero.svg

实现主要分为几个步骤

  1. createElement 函数实现

  2. render 函数实现

  3. 并发模式,渲染任务的执行 workLoop() 函数

  4. Fibers react 中通过 fiber 结构来链接 parent, first child, sibling 以及作为节 点的结构,类似 vue 的 VNode

  5. 渲染和 commit 阶段,为了解决渲染进程可能被浏览器中断的问题,采取的是延迟渲染, 即在所有的 fiber 处理完之后,在最后执行渲染操作。

  6. 更新,新增,删除操作

  7. 函数式组件实现

  8. 钩子函数的实现 useState

预览

react 使用实例:

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

首先 const element = <h1 title="foo">Hello</h1>; 属于 JSX 书写风格,这个会被转 换成 JS 代码:

1
2
3
4
5
// 参数:
// 1. type: 'h1' 标签名
// 2. props: {title: 'foo'} 为元素的属性对象
// 3. children: 'Hello' 为子元素
const element = React.createElement("h1", { title: "foo" }, "Hello");

其次是 ReactDOM.render(element, container) 进行渲染到真实DOM的操作,这个函数的 功能简述:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 1. 根据 element.type 去创建 off-dom 元素
const node = document.createElement(element.type);
// 2. 设置属性 props
node['title'] = element.props.title

// 3. 处理 children 子元素
const text = document.createTextNode('')
text['nodeValue'] = element.props.children

// 4. 最后更新到真实DOM树
node.appendChild(text)
container.appendChild(node)

所以 render 函数的功能总结下来就分为四个步骤:

  1. 拿到节点的 fiber 结构,创建 off-dom 元素

  2. 处理 element.props 属性(可能是动态,静态,也可能是事件属性)

  3. 处理 element.children 子元素

  4. 更新到真实的 DOM 树

具体的实现都是围绕这个点去完成的,

比如 1 会使用 fiber 结构来组织每个节点,并且每个节点结构一般会有三个引用: parent、first child、sibling 这三个链接这个整个fiber 树的,这也为了后面节点操作 时方便查找。

又比如 2 中对 props 的处理,会考虑是不是事件属性 onXxx 。

以及最后更新真实DOM的时机等待。

nodeValue: HTML DOM nodeValue Property

/img/tmp/dom-prop-nodeValue.png

完整代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// createElement: jsx -> js vnode 结构
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)
测试:

/img/react/react-render-brief.svg

createElement

JSX 实例:

1
2
3
4
5
6
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

转成 JS 后调用 createElement:

1
2
3
4
5
6
7
8
React.createElement(
  "div",
  {
    id: "foo",
  },
  React.createElement("a", null, "bar"),
  React.createElement("b")
);

一个节点在渲染到 DOM 之前都会是以一个VNode 形式存在,其中就包含最基本的 type, props 属性。

{type: 'div', props: { id: 'foo', children: ... } }

这和 vue vnode 结构是类似的,只不过 vue vnode 的 children 不是在 props 里面:

{type: 'div', props: {id: 'foo'}, children: [...] }

这里只要知道 createElement 目的是解析节点,返回一个节点结构对象,下面就可以开始尝 试实现 createElement 了

最简单的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 第三个参数开始都当做子元素
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  };
}

// 所以,上面的实例会有如下结构:
var div = {
  type: "div",
  props: { id: "foo", children: [a, b] },
};

var a = {
  type: "a",
  props: {
    children: ["bar"],
  },
};

var b = {
  type: "b",
  props: {
    children: [], // 没有的时候默认返回空数组
  },
};

这里面对于 children 有两种类型

  1. <a>bar</a> 的 children 只有 "bar" 是个纯文本类型

  2. <div>...</div> 的 children 有两个节点 a 和 b ,他们经过 createElement 之后 都是对象,所以这里需要进行判断下,纯文本去创建文本节点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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: {
      // node.nodeValue 属性可以设置文本节点的内容,类似 textContent
      nodeValue: text,
      children: [],
    },
  };
}

// 测试
const element = createElement(
  "div",
  { id: "foo" },
  createElement("a", null, "bar"),
  createElement("b")
);
console.log(
  "输出结构>>> \n",
  element,
  "\n > element children: \n",
  element.props.children,
  "\n > a children: \n",
  element.props.children[0].props.children
);

// 为了区别 React,这里采用文字作者的命名空间: Didact
const Didact = {
  createElement
}
输出结构>>>
 { type: 'div', props: { id: 'foo', children: [ [Object], [Object] ] } }
 > element children:
 [
  { type: 'a', props: { children: [Array] } },
  { type: 'b', props: { children: [] } }
]
 > a children:
 [ { type: 'TEXT_ELEMENT', props: { nodeValue: 'bar', children: [] } } ]
undefined

为了方便后面的测试,考虑到代码会慢慢变长问题,后面的代码会移到 /js/react/didact.js 中去。

之后测试方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import(process.env.BLOG_JS + "/react/didact.js").then(({ default: Didact }) => {
  console.log(Didact);
  // 这样照样可以完成上面的测试
  const element = Didact.createElement(
    "div",
    { id: "foo" },
    Didact.createElement("a", null, "bar"),
    Didact.createElement("b")
  );
  console.log(element);
});
undefined

{ createElement: [Function: createElement] }
{ type: 'div', props: { id: 'foo', children: [ [Object], [Object] ] } }

render

增加 render 函数,它的目的在一开始也说了,就是将 vnode 渲染到真实DOM,至于怎么渲 染,结构又是怎么处理的,这之后会慢慢的去完成。

1
2
3
4
5
6
7
8
9
const Didact = {
  createElement,
  render
}

// ReactDOM.render
function render(element, container) {
  // TODO
}

渲染当前树根节点和子节点元素渲染工作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ReactDOM.render
function render(element, container) {
  // 1. 创建当前树根节点元素
  const dom = document.createElement(element.type)

  // 2. 遍历所有的 children 创建子元素
  element.props.children.forEach(child => render(child, dom /*parent*/))

  container.appendChild(dom)
}

但是节点类型有可能是纯文本的,比如 createElement 一节 中的例子里面的 <a>bar</a> 就有一个纯文本的 "bar" 节点,这个节点经过 createElement 之后结构 是: {type: 'TEXT_ELEMENT', props: {...}} ,所以这里面只需要针对 TEXT_ELEMENT 做下特殊处理,如果是文本就创建一个空的文本节点来容乃该文本内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ReactDOM.render
function render(element, container) {
  // 1. 创建当前树根节点元素
-  const dom = document.createElement(element.type)
+  const dom =
+    element.type === "TEXT_ELEMENT"
+      ? document.createTextNode("")
+      : document.createElement(element.type);

  // 2. 遍历所有的 children 创建子元素
  element.props.children.forEach((child) => render(child, dom /*parent*/));

  container.appendChild(dom);
}

最后是 props 的处理,这里记得要排除 props.children

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// ReactDOM.render
function render(element, container) {
  // 1. 创建当前树根节点元素
  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]));

  // 2. 遍历所有的 children 创建子元素
  element.props.children.forEach((child) => render(child, dom /*parent*/));

  container.appendChild(dom);
}

最后该阶段完整的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
console.log("\n");
const Didact = {
  createElement,
  render,
};

// ReactDOM.render
function render(element, container) {
  // 1. 创建当前树根节点元素
  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]));

  // 2. 遍历所有的 children 创建子元素
  element.props.children.forEach((child) => render(child, dom /*parent*/));

  container.appendChild(dom);
}

// React.createElement
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, // 类似 textContent 可以修改节点文本内容的属性
      children: [],
    },
  };
}

测试:

测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
console.log(Didact);

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

Didact.render(element, container);

并发模式(workLoop())

注意看上一节最后的代码,在 render 中会递归调用来完成 children 的渲染工作,这里就 会出现一个问题,只要 render 一旦执行,在渲染完整棵树之前是不能停止的,否则将导致 页面不完整。

昨天微信里面看到一篇文章:

Event Loop 和 JS 引擎、渲染引擎的关系

这里面大概讲述了一些JS 和 渲染之间的一些关系,而这里的实现和这文章里讲述的一些原 理是相通的。

要解决 render 递归的问题,大致的思想是通过 Fiber 将渲染任务封装,在某一个空闲的 时刻去执行这些 Fibers 进行实际的渲染,这样不至于阻塞主任务的执行。

这里的每个 Fiber 也被称作 work unit,当一个 work unit 完成会自动找到下一个应该执 行的 work unit 也就是下一个 Fiber,为什么叫应该呢,因为每个 Fiber 上面不止有一个 引用指向下一个 Fiber,然后如何决定下一个 work unit 跟渲染的优先级有关系。

每个 Fiber 上面最多有三个引用 parent, first child, parent sibling 三个节点。

优先级是: first child > parent sibling > parent。

如何执行 work unit ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let nextUnitOfWork = null
function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    // 执行当前的 work unit 返回下一个将要执行的 work unit
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    // 没有空闲的时间了
    shouldYield = deadline.timeRemaining() < 1
  }
  // 下个空闲时间去执行下一次 work unit loop
  requestIdleCallback(workLoop)
}

function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

/img/js/request-idle-callback.png

这里使用 requestIdleCallback 这个函数会在渲染之前检查是不是有空闲的时间,如果 有则执行回调,或者超时了强制执行回调。

React 已经不用这个了,而是自己实现了 react/packages/scheduler at master · facebook/react 来管理 fiber 的执行时机。

Fibers

上一节在实现 workLoop() 提到了 work unit 即 fiber。

为了方便管理一个 work unit ,需要一个比较合理的结构,里面能保存一些相关的信息, 比如 parent, first child, parent sibling 引用, VNode 节点信息,等待。

这个结果就是: Fiber tree 由一个个 fiber 结构通过链接组成的树,其实就是类似 Vue 中的 VNode 节点树。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Didact.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)

有如下的结构,该结构让每个节点都最多持有三个引用,从而更方便的找到下一个 work unit:

/img/react/fiber-0.png

render 查找步骤拆解:

root -> <div> -> <h1> -> <p> 这是遵循 first child 优先级最高查找的结果,一旦这个 过程渲染完成,接下来应该找 <p> 的 sibling(因为 sibling 优先级高于 parent 低于 first child):

<a> -> <h2> 因为 <a> 既没有 first child 也没有 sibling 所以找 parent sibling 即 <h2> 然后继续找发现 <div> 并没有 sibling,一直如此知道根节点。

更新 render 函数,将 DOM 元素的创建从 render 中抽离出来成 createDom 函数,这里开始使用 fiber:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function createDom(fiber) {
  // 1. 创建当前树根节点元素
  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) => (fiber[name] = fiber.props[name]));

  return dom;
}

let nextUnitOfWork = null
function render(element, container) {
  // 构建 root fiber 作为第一个 nextUnitOfWork
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element]
    }
  }
}

在 render 一开始会创建一个 root fiber 并且将它作为第一个 nextUnitOfWork ,而后 面则是执行 performUnitOfWork 这里面主要完成三部分任务:

  1. 将 fiber 对应的 element 添加到 DOM

  2. 1 中的 element.children 创建 fibers

  3. 找到合适的下一个 work unit 返回

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function performUnitOfWork(fiber) {
  // 1. 创建 fiber dom 元素
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }

  // 2. 将 fiber dom 添加到 parent DOM 树中去
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }

  // 3. 处理 children
  const elements = fiber.props.children;
  let index = 0;
  let prevSibling = null;

  while (index < elements.length) {
    const element = elements[index];

    // 为每个 child 构建 fiber 结构
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber, // 指向父级 fiber 的引用
      dom: null, // 指向真实DOM元素的引用
    };

    if (indx === 0) {
      // 表示是 parent 的第一个 child,标记为 first child
      firber.child = newFiber; // 第一个引用,优先级最高
    } else {
      // 非第一次的时候,等于是节点的兄弟节点
      // 第二个引用,优先级低于 first child
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
    index++;
  }

  // 到这里 fiber 结构初始化完成,此时每个 fiber 也有了自己的
  // 三个引用 fiber.child, fiber.parent, fiber.sibling
  // 下面将要去找到当前 Fiber 的下一个 work unit,查找遵循优先级:
  // fiber.child > fiber.sibling > fiber.parent.sibling
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      // 第一次这里找的是当前节点的兄弟节点,如果没找到
      // 依着 fiber 树往上找 parent 的 sibling
      return nextFiber.sibling;
    }

    nextFiber = fiber.parent;
  }
}

Render and Commit 阶段

到现在为止,前面都只是在 fiber tree 基础上去做了处理(创建 DOM 元素,链接 fiber tree,找下一个 work unit),但实际并没有开始渲染。

并且这里 performUnitOfWork 实现中有个问题:

1
2
3
if (fiber.parent) {
  fiber.parent.dom.appendChild(fiber.dom);
}

即这里每次执行 work unit 的时候都会立即将 fiber.dom 添加到 parent 的 DOM 🌲中去, 然后这个操作很有可能在完成整棵树的渲染之前被浏览器给终止了。

怎么解决这个问题?

从根上去解决这个问题,即在 render 中不用一开始就去进行 append 操作,而是从 root 开始去跟踪整个树的结构变化,将 root 也封装成 fiber。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let wipRoot = null
function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element]
    }
  }

  nextUnitOfWork = wipRoot
}

然后,一旦 wookLoop() 的一次循环结束了就进行一次提交,去一次性完成渲染任务。

那怎么判断说一次循环结束了?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function commitRoot() {
  // 执行 DOM 渲染
  commitWork(wipRoot.child);
  // 提交完成之后要重置,等待下一次更新的任务
  wipRoot = null;
}

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

  // 这里顺序也是一样 fiber -> fiber.child -> fiber.sibling
  const domParent = fiber.parent.dom;
  domParent.appendChild(fiber.dom);
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    // 执行当前的 work unit 返回下一个将要执行的 work unit
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    // 没有空闲的时间了
    shouldYield = deadline.timeRemaining() < 1;
  }

  // 这里检测没有下一个 work unit 了,说明整个树遍历完成了
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }

  // 下个空闲时间去执行下一次 work unit loop
  requestIdleCallback(workLoop);
}

updating and deleting 更新和删除

这一节将讲述如何将 old fiber 和 new fiber 进行比较来判断是进行 update 还是 delete 操作。

所以 Fiber 里面需要对上一次的提交的 fiber tree 进行备份。

这里用 currentRoot 表示 new fiber 用 fiber.alternate 来保存 old fiber。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
+ let currentRoot = null
function commitRoot() {
  commitWork(wipRoot.child)
+  // 当前渲染的树进行备份
+  currentRoot = wipRoot
  wipRoot = null
}

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element]
    },
+    // 保存一份老树
+    alternate: currentRoot
  }

  nextUnitOfWork = wipRoot
}

performUnitOfWork 中创建 Fiber 的抽离到 reconcileChildren 中去方便添加更 新和删除操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null;

  while (index < elements.length) {
    // 这个 element 是将要更新到DOM中的节点
    const element = elements[index];
    let newFiber = null

    // 比较 oldFiber 和 element
    // 类型一样,属于更新
    const sameType = oldFiber && element && oldFiber.type === element.type
    // 下面的更新并非是直接立即更新,而是为 Fiber 赋予新的属性来标识该节点
    // 在 commit 阶段应该执行什么操作

    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.effecTag = 'DELETION'
      // 因为 commit 是从 root 从上往下提交的,且在提交阶段
      // 已经丢失了 old fiber,因为上面结构已经更新了,因此这里需要记录
      // 哪些节点需要删除
      deletion.push(oldFiber)
    }

    // 为每个 child 构建 fiber 结构
    newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber, // 指向父级 fiber 的引用
      dom: null, // 指向真实DOM元素的引用
    };

    if (indx === 0) {
      // 表示是 parent 的第一个 child,标记为 first child
      firber.child = newFiber; // 第一个引用,优先级最高
    } else {
      // 非第一次的时候,等于是节点的兄弟节点
      // 第二个引用,优先级低于 first child
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
    index++;
  }
}

然后修改 render ,初始化或重置 deletion:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let deletion = null
// ReactDOM.render
function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  };

  // 初始化或重置待删除的节点,因为 fiber tree 更新发生在 commit 之前
  // 因此在 commit 阶段 old fiber 已经被替换了,所以在 fiber tree 更新的
  // 时候就要将要删除的 old fiber 缓存起来
  deletion = [];
  nextUnitOfWork = wipRoot;
}

然后在提交阶段 commitRoot 中执行删除

1
2
3
4
5
6
function commitRoot() {
  deletion.forEach(commitWork) // 执行节点删除
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}

最后修改 commitWork 去处理新增的 fiber.effectTag 根据不同类型执行相应的增加、删 除、更新操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function commitWork(fiber) {
  if (!fiber) return;

  // 这里顺序也是一样 fiber -> fiber.child -> fiber.sibling
  const domParent = fiber.parent.dom;
  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    // 添加新节点
    domParent.appendChild(fiber.dom);
  } else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom);
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    // 更新节点
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  }

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

更新节点,需要对状态进行对比,进行更新:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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) {
  // 删除
  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]))
}

处理特殊属性:事件属性的处理,需要将原来的 handler 先移除再添加新的 handler。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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) {
  // 移除或更新 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]));

  // 新增事件属性
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach((name) => {
      const eventType = name.toLowerCase().substring(2);
      dom.addEventListener(eventType, nextProps[name]);
    });
}

修改 createDom 初创建 DOM 元素的时候执行一次 updateDom(dom, {}, fiber.props)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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) => (fiber[name] = fiber.props[name]));

+  updateDom(dom, {}, fiber.props)

  return dom
}

函数组件

如实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo"/>
// jsx -> js
function App(props) {
  return Didact.createElement('h1', null, "Hi ", props.name)
}
const element = Didact.createElement(App, {
  name: 'foo'
})

函数组件与普通组件不同点:

  1. 来自函数组件的 Fiber 没有 DOM 节点

  2. fiber.children 来自函数执行的结果,而不是直接从 props 中取

修改 performUnitOfWork 增加函数组件判断:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }

  // 到这里 fiber 结构初始化完成,此时每个 fiber 也有了自己的
  // 三个引用 fiber.child, fiber.parent, fiber.sibling
  // 下面将要去找到当前 Fiber 的下一个 work unit,查找遵循优先级:
  // fiber.child > fiber.sibling > fiber.parent.sibling
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      // 第一次这里找的是当前节点的兄弟节点,如果没找到
      // 依着 fiber 树往上找 parent 的 sibling
      return nextFiber.sibling;
    }

    nextFiber = nextFiber.parent;
  }
}

更新函数组件 updateFunctionComponent(fiber) :

1
2
3
4
5
function updateFunctionComponent(fiber) {
  // 执行函数组件得到 children
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

更新普通组件 updateHostComponent(fiber) 直接把 performUnitOfWork 中原来的处理 挪进来就 OK:

1
2
3
4
5
6
7
8
function updateHostComponent(fiber) {
  // 1. 创建 fiber dom 元素
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }

  reconcileChildren(fiber, fiber.props.children);
}

因为函数组件并没有 DOM 元素,所以 commit 阶段需要进行判断,如果没有就往上找父级 的 dom 元素作为 parent 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function commitWork(fiber) {
  if (!fiber) return;

  // 这里顺序也是一样 fiber -> fiber.child -> fiber.sibling
  const domParentFiber = fiber.parent;
  // 如果是函数组件是没有 dom 的,那么需要找到它的父级作为目标 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);
}

删除的时候也必须找到有 child 的 fiber, commitDeletion :

1
2
3
4
5
6
7
function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    commitDeletion(fiber.child, domParent)
  }
}

hooks

添加状态和 hooks。

在函数组件中我们可以通过 useState hook 来获取状态以及改变状态的函数 setState 让我们能在函数组件中来更新状态,从而来更新UI。

实例:

1
2
3
4
5
6
7
8
9
function Counter() {
const [state, setState] = useState(1)
  return (
    <h1 onClick={() => setState(c => c+1)}>
      Count: { state }
    </h1>
  )
}
const elment = <Counter/>

实现 useState 并且修改函数组件的更新函数 updateFunctionComponent 让状态的修改能 触发该函数执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function useState(initial) {
}

let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []

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

新增 fiber.hooks 目的是为了在同一个组件内可以多次调用 useState, 并且记录每个 hook 所在的索引 hookIndex 。

当组件调用 useState 时候,在里面要去检测是不是有 old hook,从 fiber.alternate 根据 hookIndex 去找。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];

  // 如果有老的 hook ,要将老的 hook state 更新过来
  const hook = {
    state: oldHook ? oldHook.state : initial,
    // 缓存状态更新的 action
    queue: [],
  };

  // 更新之前先执行 actions 更新状态,因此在状态返回之前是最新的
  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,
    };
    // 状态更新,插入新的 work unit
    nextUnitOfWork = wipRoot;
    deletion = [];
  };

  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

最终源码流程图

/img/react/react-src-code.svg

测试

diff 还没有深入实现,没做到 setState 只更新相应的文本 count = 0 组件。

还有待继续研究!!!

总结

通过作者的这个实现,对 react 的部分主要功能有了一定的认知,文内涉及的主要知识点:

  1. createElement 实现

  2. render 函数的实现

  3. fiber tree 的处理(初始化,更新,删除,新增等)

  4. commit 阶段处理,实际渲染DOM和 fiber tree 的遍历是分开的

  5. useState hook 函数实现的基础原理,通过 wipFiber + hookIndex 来实现函数组件和 useState 的连接,从而互相影响。