编写简易react框架(一)

143 阅读4分钟

编写简易的 react 框架

目的

  1. 性能优化(例如:调度的优先级)
  2. 学习优秀的代码(设计理念、架构、协同规范、代码编写)
  3. 解决开发中遇到的疑难杂症,更好的去思考遇到问题

学习构建简易 React 几个学习阶段

  1. 介绍 createElement 与 render
  2. 实现 createElement
  3. 实现 render
  4. 介绍并发模式
  5. 实现 Fibers
  6. render 和 commit 阶段
  7. 实现协调
  8. 支持函数组件
  9. 实现 Hooks

阶段一(介绍 createElement 与 render)

热身回顾基本概念

我们先来回顾下一些基本概念,React、JSXDOM 元素

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

以这个仅有三行的 React App 为例,第一行定义了个 React element,第二行从 DOM 中拿到了一个节点,最后一行将 React element 渲染到容器中。

第一行我们定义了 JSX 元素。这不是合法的 JavaScript 代码,因此我们需要将其替换成合法的 JavaScript 代码。

JSX 通过构建工具 Babel 转换成 JS。这个转换过程很简单:将标签中的代码替换成 createElement,并把标签名、参数和子节点作为参数传入。

createElement

🔈:思路整理分为两部分,先从 js 对象生成 dom 结构去出发然后用 babel 去处理 jsx 语法


(一) 用 js 对象生成 dom 结构

  • 根DOM节点(为下文做铺垫)
<body>
  <div id="root"></div>
</body>
  • 先看下面这一则咱们编写的 js 对象,来让它实现生成 dom 结构
const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
};
  • 生成 dom 结构

HTML元素通常是由元素节点和文本节点组成,创建一个标题 (h1), 你必须创建 "h1" 元素和文本节点。

// Step1: 获取根节点用于最后挂载
const container = document.getElementById("root");

// Step2.利用上述 js 对象创建DOM元素,最终会生成 <h1></h1>
const node = document.createElement(element.type);
node['title'] = element.props.title;

// Step3.创建一个文本节点
const text = document.createTextNode("");
text['nodeValue'] = element.props.children; // nodeValue 属性返回或设置当前节点的值

// Step4: 创建出来的DOM节点,添加文本节点
node.appendChild(text);

// Step5: (done) 最后添加到在到根DOM节点 root 上
container.appendChild(node);
  • 最终呈现的效果
<body>
  <div id="root">
    <h1 title="foo">Hello</h1>
  </div>
</body>

如果你对上述的代码看不懂,不能马上理解,说明你对 javascript 三大组成部分 DOM 篇 还不太熟悉!!!

那就继续打卡对自己的技术盲区查漏补缺吧 ⇨ 浅入了解 DOM 篇


(二) 用 babel 处理 jsx 语法

Step1: 先编写一则 JSX 代码

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

第一行我们定义了 JSX 元素。这不是合法的 JavaScript 代码,因此我们需要将其替换成合法的 JavaScript 代码。

分析:JSX 通过 babel 转换成 js对象结构上有两种情况

  • 情况一

JSX 通过构建工具 Babel 转换成 JS。这个转换过程很简单:将标签中的代码替换成 createElement,并把标签名、参数和子节点作为参数传入。React.createElement 验证入参并生成了一个对象。因此我们将其替换成如下代码。

/**
 * React.createElement
 * @param  {type}  string  
 * @param  {props} object 
 */
const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

一个 JSX 元素就是一个带有 type 和 props 属性的对象(其实还有更多属性),但是我们这里只关心这两个属性。

type 属性是个字符串,指定了 DOM 节点的类型。type 就是JSX中的标签名,最终会当做 tagName 传给 document.createElement 创建 HTML 元素。它也可以是一个函数。

props 也是个对象,从 JSX 属性中接收所有 key、value,并且有一个特别的属性 children。

  • 情况二

在下面这个例子中 children 是一个字符串,但是在一般情况下,他会是一个由元素组成的数组。由此 element 的结构形成了一个树。

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

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

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

React 在 render 函数里会处理改变 DOM,先不了解render内部实现,利用上述示例 js 对象我们先手动更新下DOM

const node = document.createElement(element.type)
node["title"] = element.props.title

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

node.appendChild(text)

首先我们根据传入的 type 创建了一个 node ,在这个例子中是 'h1'

然后我们把 props 传递给 node,在这个例子中就是 ‘title’。

为了避免混淆,我会用 “element” 对象来代指React内部 JSX通过 Babel 转换过的 React.createElement 所需的对象结构

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

const container = document.getElementById("root")

// 接下来创建 node 的子节点。在这个例子中,子节点是字符串,因此我们需要创建一个 text 节点。
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);

使用 textNode 而不是 innerText 有助于我们稍后统一处理所有的 element。和我们给 h1 的 title 进行赋值一样,我们对 textNode(文本节点) 的 nodeValue(DOM节点) 进行赋值,像这样:props: { nodeValue: "Hello" }

最后,我们把 textNode 添加到 h1 里,把 h1 添加到 container 里。

在不使用 React 的情况下,我们成功渲染了和 React 相同的内容, 理解利用结合 Babel 把 JSX 语法转换为 JS 对象再渲染到根节点上的概念就结束了!