React虚拟dom原理

“ 在使用React时,我们能经常听见虚拟dom,那虚拟dom是什么?它的原理又是什么?我们该如何来实现一个简版的虚拟dom呢?”

1. 什么是虚拟dom

其实虚拟dom不是什么何方神圣,它本质上就是js对象,该对象就是对dom的一种描述方式。换句话来说:js对象模拟dom结构,将dom的变化对比放到了js层来做。
如果你读过源码的话,肯定觉得要想实现虚拟dom对象是很简单的一件事情。首先我们需要知道JSX是什么?它其实只是语法糖而已,babel会将它编译为React.createElement函数调用;那createElement函数返回的是什么呢?做一个测试代码如下所示:

1
2
3
4
5
6
7
8
import React, {Component, createElement} from 'react';
const element = createElement('div',
{
id: 'my-id',
className: 'my-class'
},
'我是内容部分');
console.log('element', element);

得到的element值是一个对象,具体结果如下图所示:
element对象返回值图
element对象返回值图

在这里我就不具体讲述createElement方法的源码是什么了,感兴趣的小伙伴可以自行去官网下载React源码来看看。createElement(type, config, children)在这里我就简述一下它的作用:
(1)在调用createElement方法之后,会先处理key、ref、self、source属性;
(2)接着就是config属性的处理,此时会调用hasOwnProperty方法处理除了对象原型、RESERVED_PROPS(key、ref)上面的属性,将其拷贝到props对象中;
(3)处理createElement第三个参数以及第三个参数之后情况,当createElement形参只有三个时,将第三个参数直接赋值给children,当createElement形参大于三个时,遍历第三个参数及之后参数到一个新的数组中,然后赋值给children;
(4)接着查看type.defaultPorps是否存在,如果存在的话且在props中找到其对应字段的值为undefined时,就将其存入到props中;
(5)最后将处理完的数据作为参数返回给ReactElement方法中,其实这个ReactElement方法就是一个虚拟dom节点
ReactElement方法又做了什么处理?该方法就是我们所说的虚拟dom节点,它其实就是又把我们处理过的数据再进行了一次组合,比较特殊的是它在这里添加了$$typeof属性,然后最后返回element对象。

提到$$typeof属性,在这里延展一下为什么要使用它:其实$$typeof它的值是Symbol类型的,这样做的原因是因为服务器存在使用JSON作为文本返回的安全漏洞问题,也就是我们常常所说的xss跨站脚本攻击,这个时候由于JSON不支持Symbol类型,所以在虚拟dom中添加$$typeof使用Symbol标记我们的每一个React元素,React就会去检测每一个元素的$$typeof,如果该元素丢失或者无效就会拒绝处理该元素。

2. 如何实现简版的虚拟dom对象

在第一节中我们大致了解了createElement和ReactElement两个方法的过程,下面就来实现一下如何写一个简版的虚拟dom对象。在这里我们就只处理props和type,至于key、ref等字段你可以看完源码自己封装一波图片图片。

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
// 虚拟dom中的一个节点
function ReactElement (type, props) {
const element = {type, props};
return element;
}

function createElement (type, config = {}, children) {
const props = {};
for (let propsName in config) {
props[propsName] = config[propsName];
}
const childrenLen = arguments.length - 2; // 查看children长度,因为它可以是单个也可以是多个
if (childrenLen === 1) {
props.children = children;
}
// 有多个子元素
if (childrenLen > 1) {
const childArray = Array(childrenLen);
for (let i = 0; i < childrenLen; i++) {
childArray[i] = arguments[2 + i];
}
// 对于上面for循环,可以简写成下面一行
// props.children = Array.from(arguments).slice(2);
props.children = childArray;
}
return ReactElement(type, props);
}

const element = createElement("div", {
id: "my-id",
className: "my-class"
}, createElement("p", null, "我是内容p1"), createElement("p", null, "我是内容p2"));

console.log(element)\

得到element对象结果值如下图所示:
封装的element对象返回值
封装的element对象返回值