sunnyxujian's blog sunnyxujian's blog
首页
  • JavaScript
  • CSS
  • HTML和浏览器
  • Vue
  • Vue3
  • React
  • 基础知识
  • webpack
  • rollup
  • vite
  • 其他
  • Nodejs
  • 网络
  • TypeScript
  • 架构设计
  • 性能优化
  • 数据结构和算法
  • JavaScript教程 笔记
  • ES6 教程 笔记
  • TypeScript 从零实现 axios
  • Git学习 笔记
  • 渲染器原理
  • TypeScript 笔记
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • 网站
  • 资源
  • Vue资源
  • 友情链接
  • 分类
  • 标签
  • 归档
  • 友链
  • 关于
GitHub (opens new window)

xujian

劝君惜取少年时。
首页
  • JavaScript
  • CSS
  • HTML和浏览器
  • Vue
  • Vue3
  • React
  • 基础知识
  • webpack
  • rollup
  • vite
  • 其他
  • Nodejs
  • 网络
  • TypeScript
  • 架构设计
  • 性能优化
  • 数据结构和算法
  • JavaScript教程 笔记
  • ES6 教程 笔记
  • TypeScript 从零实现 axios
  • Git学习 笔记
  • 渲染器原理
  • TypeScript 笔记
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • 网站
  • 资源
  • Vue资源
  • 友情链接
  • 分类
  • 标签
  • 归档
  • 友链
  • 关于
GitHub (opens new window)
  • Vue

  • Vue3

  • React

    • 手写Mini React

      • Mini React 之声明式渲染框架
      • Mini React 之整体架构介绍
      • Mini React 之JXS介绍及实现
      • Mini React 之渲染器
        • 1. 渲染器宏观逻辑介绍
        • 2. 实现基础渲染器
          • 2.1 渲染标签和文本
          • 2.2 渲染属性和子节点
          • 2.3 简单事件绑定
        • 3. 总结
      • Mini React 之组件实现
    • 由浅到深的React合成事件
    • setState的执行机制
  • 框架
  • React
  • 手写Mini React
xujian
2022-06-15

Mini React 之渲染器

# 1. 渲染器宏观逻辑介绍

在上一篇文章中,我们实现了如何将jsx编译成React.createElement(...),执行后得到虚拟 DOM。这一章我们循序渐进的讲一下渲染器的基本工作原理,也就是runtime-dom那部分。简单说渲染器就是将 虚拟 DOM 渲染成特定平台下真实 DOM 的工具(本质是一个函数,通常叫 render)。渲染器的工作流程分为两个阶段:mount 和 compareTwoVdom,第一次渲染,由于没有旧的 VNode,直接将新的 VNode 创建成真实的 DOM挂载到指定的容器内,这个过程叫做 mount; 下一次再渲染的时候会生成新的 VNode,则会使用新的 VNode 与上一次生成的旧 VNode 进行对比,以最少的性能开销完成对真实 DOM 的更新,这个过程就叫 “打补丁”,也就是diff的部分。

#

渲染器是React最核心最终要的功能,因为它除了将虚拟 DOM渲染成真实DOM只外,还承担着以下重要角色:

  • 控制组件生命周期钩子的调用: 在整个渲染周期中包含了大量的 DOM 操作、组件的挂载、卸载,控制着组件的生命周期钩子调用的时机。
  • 包含最核心的 Diff 算法: Diff 算法是渲染器的核心特性之一,正是由于 Diff 算法的存在才使得 Virtual DOM 如此成功。
  • 与特定类型的渲染有直接关系: React的Ref、Context、Memo、Portal等组件的挂载是跟渲染器有密不可分的关系,他们需要要在适当的时机处理,一些需要在真实DOM存在之后才能执行的操作(如 ref)也应该在这个时候进行的。
  • 自定义渲染器: 自定义渲染器的本质就是把特定平台操作“DOM”的方法从核心算法中抽离,并提供可配置的方案。

# 2. 实现基础渲染器

# 2.1 渲染标签和文本

通常渲染器接收两个参数,第一个参数是将要被渲染的 VNode 对象,第二个参数是一个用来承载内容的容器(container),通常叫挂载点。接下来我们新建一个文件src/runtime-dom.js,主要负责将虚拟dom渲染成真实dom。如下代码所示:

src/runtime-dom.js

import { REACT_TEXT } from "./element";

function render(vdom, container) {
  mount(vdom, container);
}
function mount(vdom, container) {
  let newDOM = createDOM(vdom);
  container.appendChild(newDOM);
}

function createDOM(vdom) {
  let dom;//真实DOM元素
  let { type } = vdom;
  if (type === REACT_TEXT) {
    dom = document.createTextNode(props);//props是个字符串,不是一个DOM节点
  } else {
    dom = document.createElement(type);
  }
  return dom;
}

const ReactDOM = {
  render
}
export default ReactDOM;
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

上面的代码实现了2个函数,render函数就是我们的渲染器(渲染器后续还要实现异步渲染逻辑),然后render里面调了mount函数里面调createDOM方法将vnode生成成真实dom,得到真实dom追加到container容器之中,最后将render函数导出。

createDOM函数,函数里面实现了标签节点和文本节点的创建。声明一个dom变量用于存放真实dom,取vdom的类型(type),判断如果是REACT_TEXT说明是文本节点,创建文本节点并赋值,否则vnode是HTML标签,根据type创建对应标签即可。

现在我们验证一下这个渲染器,在mian.js里面导入ReactDom,调用render函数渲染之前的jsx,看看渲染结果:

mian.js

import React from "./src/index";
import ReactDOM from "./src/react-dom"

const jsxELe = (
  <h1 className="title" style={{ color: "red" }}>
    h1的文本内容
    <div onClick={() => alert("hi~")}>
      <p>p的文本内容</p>
    </div>
  </h1>
);

ReactDOM.render(jsxELe, document.getElementById('root'))
1
2
3
4
5
6
7
8
9
10
11
12
13

得到下面的结果:

render1

我们看到成功render了,但是只渲染了一个h1标签,子节点并没有渲染出来。

# 2.2 渲染属性和子节点

上面只渲染了最外层标签和文本,还未对节点属性和子节点进行处理,下面我们来改造一下createDOM这个函数,增加相关处理逻辑:

function createDOM(vdom) {
  let { type, props } = vdom;
  let dom;//真实DOM元素
  if (type === REACT_TEXT) {
    dom = document.createTextNode(props);//props是个字符串,不是一个DOM节点
  } else {
    dom = document.createElement(type);
  }
  // 如果存在属性,则需要添加属性
  if (props) {
    //更新属性 DOM 属性
    updateProps(dom, props);
    // 只有一个子节点的情况
    if (typeof props.children === 'object' && props.children.type) {
      mount(props.children, dom)
    // 有多个子节点的情况, 需要递归挂载子节点
    } else if (Array.isArray(props.children)) {
      reconcileChildren(props.children, dom);
    }
  }
  return dom;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

上面的代码增加了对props的处理,在上一章实现jsx里面,我们把子节点赋值到虚拟dom的props的children属性中,所以处理及节点属性也包含了子节点的挂载。增加了updateProps属性更新函数和reconcileChildren递归创建子元素这两个函数。代码如下

function reconcileChildren(children, parentDOM) {
  for (let i = 0; i < children.length; i++) {
    mount(children[i], parentDOM) // 递归挂载
  }
}
function updateProps(dom, newProps = {}) {
  for (let key in newProps) {
    if (key === 'children') {
      // 如果是`children`属性就跳过,因为外面已经做过处理了
      continue;
    } else if (key === 'style') {
      // 如果是style属性就需要特殊处理一下
      let styleObj = newProps[key];
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr];
      }
    } else {
      // 虚拟DOM属性一般来刚好和dom的属性相同的,都是小驼峰命名(className),直接赋值即可 
      dom[key] = newProps[key];
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

reconcileChildren函数就是简单的递归创建子元素,我们着重的说一下updateProps这个函数的逻辑:这里要对style属性进行特殊处理一下,因为style比较特殊,它的命令是代码是element.style.xx = xx,所以我们取出style对象,将它混入到之前的style对象上。然后其他属性,jsx特意设计成跟原声dom属性相同,所以直接赋值就行。

完美~ 终于渲染出来了:

render1

# 2.3 简单事件绑定

别高兴的太早,我们在子元素上注册了onClick事件,这时点击该元素没有起作用,因为我们还没有对事件进行处理。

<div onClick={() => alert("hi~")}>
  <p>p的文本内容</p>
</div>  
1
2
3

下面我们来对事件进行简单的注册处理:

function updateProps(dom, newProps = {}) {
  for (let key in newProps) {
    if (key === 'children') {
      // 如果是`children`属性就跳过,因为外面已经做过处理了
      continue;
    } else if (key === 'style') {
      // 如果是style属性就需要特殊处理一下
      let styleObj = newProps[key];
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr];
      }
    } else if (/^on[A-Z].*/.test(key)) { // 暂时的事件处理 事件的key一般是onXxx
      dom[key.toLowerCase()] = newProps[key];
    } else {
      // 虚拟DOM属性一般来刚好和dom的属性相同的,都是小驼峰命名(className),直接赋值即可 
      dom[key] = newProps[key];
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

我们来看一下结果,事件也能正常注册了:

render3

在虚拟dom里面,事件的key一般是onXxx所以我们直接把on的属性当成事件处理即可。但是像dom[key.toLowerCase()] = newProps[key]这么直接处理肯定是不行的,我们都知道React采用的是合成事件而非原生事件,后续说到合成事件的时候再进行优化。

# 3. 总结

我们首先介绍了渲染器的本质和作用,渲染器其实就是一个函数,目的是为了将虚拟dom渲染成真实dom并挂载到指定的节点上。然后我们说了渲染工作的两种状态,首次渲染(render)和重新渲染(rerender),render的时候是递归创建所有的真实dom,rerender的时候是diff新旧vnode对真实dom进行最小化更新。然后阐述了渲染器的一些重要意义,例如实现组件生命周期,自定义渲染,实现memo,context,等特殊优化及渲染等。后面开始实现基本的文本和标签渲染,还有属性绑定和事件绑定,这样一个最简单最基础的渲染器就完成了。

下一章我们来聊一下React另一个核心特性:组件化。

编辑 (opens new window)
上次更新: 2022/06/30, 18:16:38
Mini React 之JXS介绍及实现
Mini React 之组件实现

← Mini React 之JXS介绍及实现 Mini React 之组件实现→

最近更新
01
😍Vue3和React都在用的函数式编程,学起来~
02-03
02
链表
11-24
03
基础类型
11-18
更多文章>
Theme by Vdoing | Copyright © 2020-2023 xujian | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式