Build your own React(2)-createElement&&初级版render函数

136 阅读3分钟

原文链接:Build your own React

  • Step I: The  createElement Function ✅
  • Step II: The  render Function ✅
  • Step III: Concurrent Mode 并发模式
  • Step IV: Fibers
  • Step V: Render and Commit Phases
  • Step VI: Reconciliation
  • Step VII: Function Components
  • Step VIII: Hooks

** To avoid confusion, I’ll use “element” to refer to React elements and “node” for DOM elements.*

前言

上文中我们做了一些基本的准备工作,这节开始正式来编写我们自己的React(建议从上文开始看起)。

The createElement Function

上文中我们得知一个react元素被转译后实际上就是一个包含了type以及props的js对象,那么我们在这里就是要通过createElement这个函数创建这么一个对象。

对props使用扩展运算符,对children使用rest参数语法,这样children将使用是一个数组格式。我们可以编写第一版createElement Function了:

// react/createElement.js
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  };
}
​
export default createElement;
​
// react/index.js
// 统一管理createElement函数以及render函数
// 以便main.js中直接引入即可
import createElement from "./createElement";
​
export { createElement };

return示例:

createElement('div')  returns:
{
  "type": "div",
  "props": { "children": [] }
}
​
createElement("div", null, a) returns:
​
{
  "type": "div",
  "props": { "children": [a] }
}
​
createElement("div", null, a, b) returns:
​
{
  "type": "div",
  "props": { "children": [a, b] }
}

在上一节的示例中,我们知道子节点有可能是一个常量(字符串或者数字类型),所以我们在这里不考虑其他类型,如果子节点是文本节点的话,为其赋值一个特殊type值:TEXT_ELEMENT,接下来继续改造我们的createElement函数。

// react/createElement.js
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函数就完成了,编写一个例子看看返回的对象长什么样子:

// main.js
const element = createElement(
  "h1",
  { id: "title" },
  "hello world",
  createElement("a", { href: "https://xxx.com" }, "yyy")
);
​
output =>
​
{
  type: 'h1',
  props: {
    id: "title",
    children: [
      {type: 'TEXT_ELEMENT', props: {nodeValue: 'hello world', children: []}},
      {type: 'a', props: {href: "https://xxx.com", children: [
        {type: 'TEXT_ELEMENT', props: {nodeValue: 'yyy', children: []}}
      ]}}
    ]  
  }
}

大家可以多观察下返回的对象结构,最好做到熟记于心。

The render Function

第一节我们简单模拟了jsx到虚拟DOM的转换,如果想详细了解React源码中怎么处理的,可以参考jsx如何转成虚拟dom,那么继续往下进行,编写render函数把虚拟DOM渲染成真实DOM节点显示到页面上:

目前我们只关心新增节点,后面会介绍更新和删除DOM。

我们首先使用元素类型创建DOM节点,然后将节点插入到根节点中,使用递归处理子节点

// react/render.js
function render(element, container) {
  const dom = document.createElement(element.type);
  element.props.children.forEach((child) => render(child, dom));
  container.appendChild(dom);
}
​
export default render;
​
// react/index.js
import createElement from "./createElement";
import render from "./render";
​
export { createElement, render };

可以注意到,我们有些节点类型是TEXT_ELEMENT,所以这种类型我们要特殊处理下:

// react/render.js
- const dom = document.createElement(element.type);
+ const dom =
+     element.type === "TEXT_ELEMENT"
+       ? document.createTextNode("")
+       : document.createElement(element.type);

示例:

// main.js
import { createElement, render } from "./react";
const element = createElement(
  "h1",
  { id: "title" },
  "hello world",
  createElement("a", { href: "https://xxx.com" }, "yyy")
);
const container = document.getElementById("root");
render(element, container);

运行项目,可以看到页面虽然什么都没有,但是在chrome devtools Elements面板中,可以看到h1标签以及a标签都已经被渲染出来并且挂载到根节点中了:

render without props.png

接下来我们需要为各个节点添加其属性(props):

// react/render.js
+ Object.keys(element.props)
+     .filter((key) => key !== "children")
+     .forEach((name) => (dom[name] = element.props[name]));

观察页面:

render with props.png

至此,初级版render函数也成功完成了,render函数完整版:

function render(element, container) {
  const dom =
    element.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);
​
  Object.keys(element.props)
    .filter((key) => key !== "children")
    .forEach((name) => (dom[name] = element.props[name]));
​
  element.props.children.forEach((child) => render(child, dom));
  container.appendChild(dom);
}

总结

综上我们已经基本完成了createElement函数以及render函数,可是仔细想我们的render函数是存在一些问题的,由于递归渲染子节点不会中断,那么如果我们的DOM树过于庞大,想一想是不是就会一直占用我们浏览器的主线程,会影响一些其他例如输入输出事件等等。

这些问题将会在后续文章讲解如何解决,希望大家不要操之过急,先把这篇文章中基本的createElement函数,及其返回的对象格式,以及简单版本render函数理解透彻,才能更快的往下进行。