Virtual Dom转化为真实DOM

在 React 中,Virtual DOM(虚拟 DOM)是一个轻量级的 JavaScript 对象,它是真实 DOM 的一个表示。React 使用这个表示来优化 DOM 操作,提高性能。渲染 Virtual DOM 到真实 DOM 的过程通常是自动完成的,但理解这个过程可以帮助你更好地理解 React 的工作机制。

React 使用以下步骤将 Virtual DOM 渲染成真实 DOM:

  1. 组件更新:当一个 React 组件的状态(state)或属性(props)发生变化时,React 会更新该组件的 Virtual DOM。

  2. 生成 Virtual DOM:React 会调用组件的 render 方法,根据组件的最新状态和属性来生成一个新的 Virtual DOM 树。

  3. Diff 算法:React 使用 Diff 算法比较新的 Virtual DOM 树与上一次的 Virtual DOM 树的差异。这个过程称为重新协调(reconciliation)。

  4. 确定 DOM 更新:Diff 算法确定了在 Virtual DOM 树中哪些地方发生了变化。React 将这些变化(或“补丁”)转换为对真实 DOM 的操作指令。

  5. 真实 DOM 更新:React 批量执行这些 DOM 操作指令,更新真实的 DOM 树。React 会尽量最小化操作真实 DOM 的次数,因为这些操作通常是性能开销最大的。

  6. 引用更新:一旦真实 DOM 更新完毕,如果有必要,React 会更新组件的 refs。

这个过程在 React 应用程序中是自动发生的,作为开发者,你通常只需要关注组件的状态和属性,并编写对应的 render 方法。React 会负责将你的 Virtual DOM 高效地渲染至真实的 DOM。

如果你正在使用 React 的类组件,这个过程基本上是由 ReactDOM.render() 方法触发的,它将组件挂载到某个 DOM 元素上。对于函数组件,这个过程是由 React Hooks 如 useStateuseEffect 来管理状态和副作用的。

在 React 16 及以后的版本中,这个过程得到了进一步的优化,引入了 Fiber 架构来更好地处理更新过程中的异步任务、分割可中断的工作等,以实现更高效的性能表现。

手动实现简单的render方法

封装一个对象迭代方法

for/in循环的弊端

for/in 循环在JavaScript中是用来遍历对象属性的一种方法,但它有几个弊端需要注意:

  1. 迭代继承的属性: for/in 循环不仅遍历对象自身的属性,还会遍历其原型链上继承来的可枚举属性,这可能会引入意外的属性,特别是当使用第三方库或者框架时,可能会有很多继承的属性加入到对象上。

  2. 仅限可枚举属性: for/in 仅遍历对象的可枚举属性,这意味着使用 Object.defineProperty() 方法定义的不可枚举属性将不会被遍历。

  3. 忽略 Symbol 属性: for/in 循环不会遍历使用 Symbol 作为键的属性,而这些属性可能代表了对象的重要部分。

  4. 性能问题: 在某些情况下,尤其是对象属性数量庞大时,for/in 的性能可能不如其他方法,比如 Object.keys()Object.entries() 配合 forEach()for/of 循环。

1
2
3
4
5
6
7
8
9
10
// 一般来说内置的属性都是不可枚举的,自定义的属性都是可枚举的,(枚举:可以被for/in、Object.keys、JSON.stringify列举出来的属性),但可以通过Object.defineProperty()方法来定义属性的可枚举性
Array.prototype.Test = 4; // 为数组在原型上定义一个属性,这个属性是公有的、可枚举的
let arr = [1, 2];
arr[Symbol('test')] = 3;

console.log(arr); // [1, 2, Symbol(test): 3]

for (const key in arr) {
console.log(key); // 0 1 Test 遍历不出Symbol属性
}

优化方案

可以使用Reflect.ownKeys()方法来获取对象的所有属性,包括Symbol属性

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
/**
* @Description:
* @param {Object} obj
* @param {Function} callback
* @return {void}
* @Date: 2023-11-28 17:09:07
*/
const each = (obj, callback) => {
if (obj === null || typeof obj !== 'object') throw new TypeError('obj must be an object');
if (typeof callback !== 'function') throw new TypeError('callback must be a function');
let keys = Reflect.ownKeys(obj);
keys.forEach(key => {
callback(key, obj[key]);
})
}

Array.prototype.Test = 4; // 为数组在原型上定义一个属性,这个属性是公有的、可枚举的
let arr = [1, 2];
arr[Symbol('test')] = 3;

each(arr, (key, value) => {
console.log(key, value);
})
// 0 1
// 1 2
// length 2
// Symbol(test) 3

封装render方法

render方法会有两个参数:vNode(virtual Dom),container(挂载虚拟dom的节点),vNode数据结构大致为 {type: 'div', props: {id: 'app', children: []}},我们需要的是type和props

思路:
  1. 根据type创建真实dom
  2. 遍历props,为当前dom添加对应属性

js通过dom[key] = valuesetAttribute 都可以用来为DOM元素设置属性,但它们在一些方面有所不同:

  1. 属性类型:

    • dom[key] = value 是直接在DOM对象上设置一个属性或方法。这种方式设置的属性是JavaScript对象的属性。
    • setAttribute 是设置DOM元素的HTML属性,它会影响到元素的HTML结构,并且最终的效果可以通过元素的outerHTML属性看到。
  2. 属性名的限制:

    • 使用 dom[key] = value 时,key 对应的通常是元素的属性名,比如 id, title 或事件处理函数如 onclick 等。这种方式不适合设置非标准属性,因为它可能不会正确反映在DOM上。
    • setAttribute 可以设置任何属性,包括自定义属性。如果设置的是非标准的属性,它会以自定义属性的形式出现在元素的标记上,例如 data-* 属性。
  3. 布尔属性:

    • 使用 dom[key] = value 可以设置布尔属性,例如 checked, disabled 等。设置为 truefalse 会正确地改变元素的状态。
    • setAttribute 则需要传递一个字符串,对于布尔属性来说,你通常会传递 "true""false",但实际上任何非空字符串都表示 true,包括字符串 "false"。为了删除一个布尔属性,你需要使用 removeAttribute
  4. 细微的行为差异:

    • dom[key] = value 设置的属性值可以直接通过JavaScript访问并且修改,这个变化是动态的,并且立即反映在对象上。
    • setAttribute 更改的是HTML属性,这可能会导致某些属性有不同的表现,例如,将 input 元素的 value 属性通过 setAttribute 设置,可能不会改变其当前显示的内容,因为 value 属性设置的是默认值。

在大多数情况下,这两种方法可以互换使用,但是推荐使用 dom[key] = value 来设置标准的属性和事件处理函数,而使用 setAttribute 来设置自定义属性或者当你需要操作的属性在JavaScript对象中不是一个简单的属性时。

Render方法封装

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* @Description:
* @param {Object} vNode
* @param {HTMLElement} container
* @return {void}
* @Date: 2023-11-29 10:18:36
*/
export function render(vNode, container) {
// vNode的数据结构 {type: 'div', props: {id: 'app', children: []}},我们需要的是type和props
let { type, props } = vNode;
if (typeof type === 'string') {
// 创建对应的标签
let ele = document.createElement(type);
// 遍历props,将属性添加到标签上
each(props, (key, value) => {
// key是className时,value是类名
if (key === 'className') {
ele[key] = value;
return;
}
// key是style时,value是一个对象,需要遍历对象,将样式添加到标签上
if (key === 'style') {
each(value, (styleName, styleValue) => {
ele['style'][styleName] = styleValue;
})
return;
}
// key是children时,value是一个数组,需要遍历数组,将子元素添加到标签上
if (key === 'children') {
// 子节点可能是文本节点,也可能是元素节点
let children = value;
// 如果只有一个子节点,children是一个文本节点,需要将其转换为数组
if (typeof children === 'string') children = [children];
children.forEach(child => {
// 子节点是文本元素,直接插入到标签中
if (/(string|number)/.test(typeof child)) {
child = document.createTextNode(child);
ele.appendChild(child);
return;
}
// 子节点是virtual dom,递归调用render方法,将子节点插入到标签中
render(child, ele);
})
}
})
// 将vNode挂载到container上,virtual dom -> dom
container.appendChild(ele);
}
}

测试Render方法

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
35
36
import { render } from './jsxHandler';

render(
{
type: 'div',
props: {
children: [
'Outer Content',
{
type: 'div',
props: {
children: [
'Middle Content',
{
type: 'div',
props: {
children: 'Inner Content'
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element')
}
]
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element')
}
]
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element')
},
document.getElementById('root')
);