vlambda博客
学习文章列表

深度思考 jsx、虚拟 dom

前言

在 react 中 jsx 和虚拟 dom 大家再熟悉不过了,本章带你探究 jsx 的工作机制、以及 fiber 出现 之前, 虚拟 dom 转真实 dom 的流程!

本章结构

jsx

what?(什么是 jsx?)

jsx 是 XML 形式的 javaScript 模版语法,在 react 中,我们可以通过 jsx 来代替常规的 javaScript,经过 babel 的编译之后,jsx 将会编译成 createElement 的形式,所以站在 react 的角度上来说,jsx 是 createElement 的语法糖

在 react 的官网上我们可以很直接地看到 jsx 和 createElement 的之间的转换关系

使用 jsx

深度思考 jsx、虚拟 dom

编译之后

深度思考 jsx、虚拟 dom

why?(为什么要使用 jsx ?)

前面我们提到 jsx 将会转化为 createElement,那么为什么还要有 jsx 呢?

1、开发效率:使用 jsx 编写模板简单快速

让我用 createElemet 写代码,这辈子都不可能的

2、类型安全:在编译过程中就能发现错误

createElement 是一个函数,在编写的过程中,我们需要传递各种类型的参数,如果不小心,很容易导致类型的错误

how ? (jsx 的工作流是什么?)

在 react17 之前

我们直接在 jsx 中随便打一个断点,观察 jsx 的执行

深度思考 jsx、虚拟 dom
深度思考 jsx、虚拟 dom
深度思考 jsx、虚拟 dom

我们发现最终调用了 熟悉的 createElemet,因此这就可以解释为什么在 react 中写 jsx 的时候必须要引入 react,因为 jsx 经过解析之后变成了 createElement 的形式,createElement 最终执行 ReactElement 来返回虚拟 dom

深度思考 jsx、虚拟 dom
深度思考 jsx、虚拟 dom

在 react17 中

react17 很大的一个变化就是我们在使用 jsx 的时候可以不引入 react,因为 react17 中的 jsx 不会 转换为 createElement,而是直接从 react 的 package 中引入新的入口函数并调用

同样我们在 react17 的环境下在 jsx 中打一个断点来观察

我们发现执行了一个 jsxDEV 的函数,这个 jsxDEV 就是在开发环境下解析 jsx 的函数

深度思考 jsx、虚拟 dom

去到 jsxDEV 里面,最后执行了和在 createElement 一致的函数 ReactElement

深度思考 jsx、虚拟 dom

经过 createElement 或者经过 react17 解析后的 jsx 最终变成了什么呢?

我们直接打印出来,这就是我们经常挂载嘴边的虚拟 dom(vnode)

虚拟 dom

what?(什么是虚拟 dom)

我们前面打印出来的 jsx 最终结果就是一个虚拟 dom,虚拟 dom 是一个 js 对象,它有 type、props、children 等属性,于是我们总结,虚拟 dom 是一个表示真实 dom 特性的 js 对象

why? (为什么要使用虚拟 dom)

我们知道在 vue 中,为了减少 Watcher(监听)提升性能,引入 diff 算法而引入的虚拟 dom,react 中一直就存在虚拟 dom 的概念,为什么 react 要存在虚拟 dom 呢?

  • 传统的 dom 操作

在 JQ 横行的年代,任何的视图变化,都是我们直接操作 dom 元素来实现了,但是当应用越来越大的时候,大批量的 dom 操作使得能成本变得越来越大,很容易造成视图的卡顿,导致用户体验变差。

  • 为什么不 diff 真实 dom 呢?

我们知道虚拟 dom 中有 diff 算法,比较后只将不一致的部分虚拟 dom 进行更新,然后将部分的变更同步到真实 dom,这就是性能提升的重要原因,那为什么不在真实 dom 中也使用 diff 算法呢?

我们创建一个真实 dom 元素,然后将其打印出来观察

const div = document.createElement("div");
let dom = "";
for (const key in div) {
dom += ` ${key}`;
}
console.log("dom", dom);

可以看到一个真实 dom 的信息含量特别大,如果对一个真实 dom 进行 diff,那成本可能会比直接操作真实 dom 的还大

于是我们总结,使用虚拟 dom 的根本目的是为了优化传统真实 dom 操作带来的性能问题

how? (虚拟 dom 的工作原理)

  • 从整个工作流来讲

在程序第一次执行的时候,我们会直接将虚拟 dom 转换为真实 dom,渲染在界面上,并且缓存第一次的虚拟 dom,在之后的每一次更新阶段,都重新生成一份虚拟 dom,将两份虚拟 dom 进行比较,将不同的地方进行更新,并且将部分的更新同步到真实 dom,之后的每一次更新都和上一次缓存的虚拟 dom 进行 diff

  • 从代码层面来讲

我们尝试使用以上 jsx 编译之后的最终 虚拟 dom ,将其转换为真实 dom,我们先只实现初次渲染

模仿 react 的主入口,我们来实现 render 函数,正常在页面显示 jsx 对应的元素就大功告成

function FunctionComponent(props) {
return (
<div className="FunctionComponent">
<p>{props.name}</p>
<button
onClick={() =>
{
console.log("omg"); //sy-log
}}
>
click
</button>
</div>

);
}

class ClassComponent extends Component {
render() {
return (
<div className="border">
<p>{this.props.name}</p>
</div>

);
}
}

const jsx = (
<div className="parent">
<FunctionComponent name="function" />
<ClassComponent name="class" />
<span>1</span>
<span>2</span>
<div className="child">
<span>3</span>
<span>4</span>
</div>
</div>

);

ReactDOM.render(jsx, document.getElementById("root"));

首先我们先来实现 class 组件中的 Componet 构造函数, 核心部分就是绑定 props,然后在原型上给一个 class 组件的标志 isReactComponent

function Component(props) {
this.props = props;
}

Component.prototype.isReactComponent = {};

编写 render 函数,作为入口,render 函数主要做了两件事

1、将虚拟 dom 转换为真实 dom

2、将真实 dom 更新在 container 上

function render(vnode, container) {
// 1. vnode->node
const node = createNode(vnode);
// 把vnode更新到container
container.appendChild(node);
}

使用 createNode 将 vnode => 真实 dom, 这里面主要实现了原生标签的转换,函数式组件、class 组件以及文本节点的转换

updateHostComponent 转换原生标签

updateClassComponent 转换 class 组件

updateFunctionComponent 转化函数组件

createTextNode 转换文本节点

function createNode(vnode) {
let node;
const { type } = vnode;
if (typeof type === "string") {
node = updateHostComponent(vnode);
} else if (typeof type === "function") {
node = type.prototype.isReactComponent
? updateClassComponent(vnode)
: updateFunctionComponent(vnode);
} else {
node = document.createTextNode(vnode);
}

return node;
}

updateHostComponent 实现, 主要是创建元素、协调子元素以及更新元素的属性

function updateHostComponent(vnode) {
const { type, props } = vnode;
const node = document.createElement(type);
reconcileChildren(node, props.children);
updateNode(node, props);
return node;
}

reconcileChildren 对子元素进行遍历,挨个执行 render,在 fiber 出现之前我们采取这种方式来模拟,我们在这里的重点先模拟整个初始化渲染的流程,在源码中协调的部分是很复杂的,我们后续会结合 fiber 架构进行讲解

function reconcileChildren(parentNode, children) {
const newChildren = Array.isArray(children) ? children : [children];

for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
render(child, parentNode);
}
}

updateNode 主要实现元素属性、事件的绑定

function updateNode(node, nextVal) {
Object.keys(nextVal)
.filter((k) => k !== "children")
.forEach((k) => {
if (k.startsWith("on")) {
// 事件
let eventName = k.slice(2).toLocaleLowerCase();
node.addEventListener(eventName, nextVal[k]);
} else {
node[k] = nextVal[k];
}
});
}

以上,我们实现了原生标签的渲染,初次渲染的核心的实现已经完成了,接下来就是对 class 和 function 组件的渲染,其实它们本质最后都是会转换为原生标签的方式

updateClassComponent

function updateClassComponent(vnode) {
const { type, props } = vnode;
const instance = new type(props);
const child = instance.render();
// child -> node
const node = createNode(child);
return node;
}

updateFunctionComponent

function updateFunctionComponent(vnode) {
const { type, props } = vnode;
const child = type(props);
const node = createNode(child);
return node;
}

面试题

说说你对 jsx 的理解?

首先我们说说 jsx 是什么?jsx 是 XML 形式的 javaScript 模版语法,是用来表示元素、元素属性、绑定事件等的一种方式语法糖

jsx 有一些好处,使用 jsx 可以让我们更加高效地写代码,而且 jsx 有错误检查机制,可以避免直接写 js 传参的不规范的一些问题

在 react 的版本迭代过程中,jsx 会存在一些区别

在 react17 之前,jsx 会被 babel 编译成 React.createElement 的形式,因此之前在 react 中写 jsx 的时候必须要导入 react

在 react17,jsx 的编译交给了 react 的 package 中引入的新的入口函数

最终 jsx 经过处理之后,产出的是一个虚拟 dom

React17 的 jsx 发生了什么变化?

在 react17 中,因为 jsx 不会被解析为 Reac.createElement,而是直接从 react 的 package 中引入新的入口函数并调用

所以我们不需要引入 react 也能够编写 jsx

虚拟 dom 是什么?

首先虚拟 dom 本质上是一个描述真实 dom 的 js 对象,它是一种表现形式,只要能够正确地表示真实 dom 的信息,我们能够将其归纳为虚拟 dom

为什么要存在虚拟 dom 呢,由于原生的大批量 dom 操作会导致页面的性能不好,造成浏览器的卡顿,这对用户体验是很差的,所以引入了虚拟 dom 进行 diff 比较来更新变化的部分,为什么不对真实的 dom 进行 diff 呢,因为真实的 dom 节点含有大量的内容,进行 diff 的成本很高

虚拟 dom 整个工作的原理是在程序第一次执行的时候,我们会直接将虚拟 dom 转换为真实 dom,渲染在界面上,并且缓存第一次的虚拟 dom,在之后的每一次更新阶段,都重新生成一份虚拟 dom,将两份虚拟 dom 进行比较,将不同的地方进行更新,并且将部分的更新同步到真实 dom,之后的每一次变更都会和上一次缓存的虚拟 dom 进行 diff 比较,总的来说就是缓存、比较、部分更新以及部分同步虚拟 dom

你知道 fiber 架构吗?

fiber 本质上也是一个虚拟 dom,因为它也是真实 dom 的一种 js 表现形式,唯一特殊的就是这个 js 的数据结构是一个链表,下一个 react 的章节我们将讲解 fiber 架构!

我们将持续更新大前端框架(vue、react等)原理剖析、核心面试题讲解、技术探究等!!!
点赞👍、在看😉、关注😌,三连搞起,这都是对我们的鼓励 和支持,谢谢~