一、简介
前段时间看到一个用33行代码就实现了一个非常基本的react代码。感觉还是蛮有趣的,代码如下:
其主要实现了两大功能:
① 生成虚拟DOM;
② 根据虚拟DOM渲染出真实的DOM;
无注释版:https://github.com/leontrolsk...
有注释版:https://github.com/leontrolsk...
二、代码分析
虽然代码总共才33行,但是写的非常简洁,可能不是一下就能看懂,我对此细细研读了一番,用更加明了的方式重新写了一下。主要就是对外暴露了React.createElement()和React.render()两个方法。
① 实现代码基本框架
// 定义一个React类并对外暴露
class React {
// 负责创建虚拟DOM
static createElement(...args) {
}
// 负责将虚拟DOM渲染成真实的DOM
static render(parent, v) {
}
}
export default React;
② 实现React.createElement()方法
createElement()方法主要就是解析传递过来的参数,然后解析出标签名、类名数组、属性对象、子节点数组。
- 解析标签名: 第一个参数为字符串中包含标签名,这个参数支持点的形式,如 div.redColor.bluebg,所以需要以点号进行分割成数组["div", "redColor", "bluebg"],将数组的第一个元素作为标签名,剩余的元素都作为元素的类名。
- 解析元素的属性对象: 通常传入的第二个参数对象为元素的属性对象,但是也可以不传或直接传入元素的子节点,所以需要对第二个参数进行判断,看一下是否是属性对象。判断的依据就是,如果第二个参数是null、string、number、array、vnode,那么就不是属性对象,而是子节点。
class React {
// 判断能不能渲染,不能渲染则为属性对象
static isRenderable(v) {
return v === null || ['string', 'number'].includes(typeof v) || v.__m || Array.isArray(v);
}
}
- 解析子节点: 解析完标签名和属性对象后,剩下的参数都是子节点,只不过参数有可能是一个数组,所以需要对其进行判断,如果是数组则需要遍历数组,然后再递归将其添加到children数组中。
class React {
// 负责创建虚拟DOM
static createElement(...args) {
// ① 将attrs初始化为一个空对象{},并解构出第一个参数head
let attrs = {};
let [head, ...tail] = args;
// ② 获取标签名和第一个参数中传递的类名
let [tag, ...classes] = head.split('.');
// ③ 获取传入的attrs对象,首先判断tail中的第一部分能不能渲染,如果不能渲染,说明是attrs对象,否则为子节点
if (tail.length && !this.isRenderable(tail[0])) { // 如果传入的第二个参数不能被渲染,那么第二个参数就是attrs对象
[attrs, ...tail] = tail; // 取出attrs对象
}
// ④ 解析attrs对象中包含的类名
if (attrs.class) { // 如果attrs属性对象中有class属性,那么将其中的类名放到classes中
classes = [...classes, ...attrs.class];
}
// ⑤ 移除attrs对象中的class属性
attrs = {...attrs};
delete attrs.class;
// ⑥ 初始化children为空数组[]
const children = [];
// ⑦ 定义一个addChildren()方法,将所有子节点加入到children数组中
const addChildren = (v) => {
if (v === null) { // 如果是null则直接返回,不将其加入到children数组中
return;
}
if (Array.isArray(v)) { // 如果是数组则遍历数组并调用addChildren()
v.map(addChildren);
} else {
children.push(v); // 非数组则直接将其添加到children数组中
}
}
addChildren(tail);
// ⑧ 返回虚拟DOM对象
return {
__m: true, // 是否是虚拟节点
tag: tag || 'div',
attrs, classes,
children
};
}
}
③ 实现React.render()方法
React.render()方法主要传入一个真实的父节点和对应的虚拟节点,将真实父节点当做旧节点,将虚拟节点作为新节点,然后对新旧节点进行比较,没有则创建,有则更新,同时更新属性,然后进行递归比较其子节点,直到没有子节点为止。
class React {
// 进行新旧节点的比较并渲染出真实DOM
static render(parent, v) {
// ① 取出真实节点下的所有真实子节点
const olds = parent.childNodes || [];
// ② 取出虚拟节点下的children属性对应的虚拟子节点
const news = v.children || [];
// ③ 移除超出的元素(如果旧的节点比新的节点多)
for (const _ of Array(Math.max(0, olds.length - news.length))) {
parent.removeChild(parent.lastChild);
}
// ④ 遍历所有虚拟子节点,没有子节点了则会结束render
news.length > 0 && news.forEach((child, i) => {
// 如果旧子节点不存在则创建一个
let el = olds[i] || this.makeEl(child);
if (!olds[i]) { // 如果旧节点不存在,则直接加入
parent.appendChild(el);
}
// 判断新旧节点是否匹配
const mismatch = (el.tagName || '') !== (child.tag || '').toUpperCase();
if (mismatch) { // 如果不匹配,则创建出新节点,并且用新节点去替换掉旧节点
(el = this.makeEl(child)) && parent.replaceChild(el, olds[i]);
}
// 更新节点属性
this.update(el, child);
// 递归更新子节点
this.render(el, child);
})
}
static makeEl(vnode) {
if (vnode.__m) { // 如果是元素节点
return document.createElement(vnode.tag);
} else { // 如果是文本节点
return document.createTextNode(vnode);
}
}
}
④ 实现update()方法
主要就是传入真实节点和虚拟节点,然后遍历虚拟节点的classes类名数组,将新的类名添加到真实节点上,遍历真实节点上的所有classList,然后移除虚拟DOM上不再使用的类名。遍历虚拟DOM节点上的attrs属性对象,并将新的属性添加到真实节点上,遍历真实DOM节点上的attributes,移除虚拟DOM上不再使用的属性。
class React {
static update(el, v) {
if (!v.__m) { // 如果是文本节点
return el.data === `${v}` || (el.data = v);
}
// 遍历虚拟节点上所有的class,并添加到classList中
for (const name of v.classes) {
if (!el.classList.contains(name)) {
el.classList.add(name);
}
}
// 遍历真实节点的classList,移除虚拟节点没有的class
for (const name of el.classList) {
if (!v.classes.includes(name)) {
el.classList.remove(name);
}
}
// 遍历虚拟节点的attrs属性对象
for (const name of Object.keys(v.attrs)) {
if (el[name] !== v.attrs[name]) { // 如果发生变化,则更新
el[name] = v.attrs[name]; // 更新
}
}
// 遍历真实节点的attributes,移除虚拟节点上不存在的attrs
for (const {name} of el.attributes) {
if (!Object.keys(v.attrs).includes(name) && name !== 'class') {
el.removeAttribute(name);
}
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。