前端DOM操作变革史
本文就前端DOM操作从命令式到声明式做一个简单的梳理和总结,同时提供一些直接DOM操作的方式
前言
纵观前端脚本的工作流程就不难发现,DOM操作几乎是一切前端脚本逻辑的输出终点,无论功能逻辑如何复杂,最后都需要通过DOM操作作用到具体的上下文中,这个功能才算真正成型。并且,DOM操作本身作为前端相当耗费性能的行为之一,对DOM的优化如何有时候能够很大程度上决定页面性能如何。而事实也正是,DOM操作在前端的发展中得到了史无前例的重视,每一代技术革新基本都伴随着一次DOM操作的变更,因此,本文就想基于这一现象,对前端DOM操作的变革做一个梳理和总结
从命令式到声明式
命令式VS声明式
如果要用一个最直观的点到点的变化整体概括前端DOM操作的总体变化的话,那一定是从命令式到声明式了:
- 命令式:直接定位
DOM元素,精确地一步一步告诉浏览器该怎么操作,首先获取DOM,然后修改它的样式属性,接着继续操作其它DOM… - 声明式:并不直接操作
DOM,而是通过声明状态的方式,让浏览器自动修改DOM状态,也就是并不关心怎么做,而是声明式地描述状态,后续只要修改状态,DOM便会自动同步改变,例如Vue中便是通过data来描述状态,等到编译时再将数据注入到模板中,从而实现状态的同步
第一阶段:原生JS时代
该阶段是命令式DOM操作的代表时代,在这个时代的DOM操作具有诸多痛点,包括:
- 冗长的操作
API:createElement/appendChild/setAttribute等等,往往一个功能反复调用DOM语句,十分冗杂 - 全局变量污染
- 无组件化概念,代码复用困难
- 直接操作
DOM导致性能低下(频繁重排重绘)
下面是使用原生JS操作DOM的例子:1
2
3
4
5
6
7
8
9
10
11
12// 典型DOM操作流程
const div = document.createElement('div');
div.id = 'myDiv';
div.className = 'box';
div.appendChild(document.createTextNode('Hello World'));
document.body.appendChild(div);
// 事件绑定
div.onclick = function() {
this.style.color = 'red';
// 兼容性问题:IE用attachEvent,其他用addEventListener
};
第二阶段:JQuery时代
Jquery的出现优化了原生JS操作DOM时冗长的代码调用问题,同时提供了一些内置的封装好的方法和函数,使得复用性大大提升,但JQuery的工作流程仍然是命令式的,只是封装了一些原本冗余的JS代码,在大量调用中仍会导致状态混乱等问题1
2
3
4
5
6
7
8
9
10
11
12// 等效jQuery操作
// 用$()来直接获取DOM元素
$('body').append(
$('<div>', {
id: 'myDiv',
'class': 'box',
text: 'Hello World',
click: function() {
$(this).css('color', 'red');
}
})
);
第三阶段:现代框架时代
从AngularJS到React和Vue.js,这些现代框架提供了更简洁的语法,同时标志着前端的DOM操作开始真正进入声明式的时代。
在这一阶段,两种现在广为人知的前端架构模式开始流行,分别是:
- MVC(Model-View-Controller):模型——视图——控制器,MVC模式将
DOM操作从业务逻辑中分离出来,将DOM操作的职责交给视图层,而视图层则通过控制器来控制模型,控制器通过DOM操作来修改视图层的状态,同时控制器也通过DOM操作来监听视图层的状态变化,从而实现数据的双向绑定。这一模式以React为代表(JSX)。 - MVVM(Model-View-ViewModel):模型——视图——视图模型,MVVM模式与MVC模式类似,也是将
DOM操作从业务逻辑中分离出来,将DOM操作的职责交给视图层,而视图层则通过视图模型来控制模型,视图模型通过DOM操作来修改视图层的状态,同时视图模型也通过DOM操作来监听视图层的状态变化,从而实现数据的双向绑定。这一模式以Vue.js为代表。
可以看到,两种模式都具有以下共性,即(MV)模型——视图,再通过(C/VM)控制器或视图模型来控制DOM,虽然控制方式略有差异,就如React用JSX控制DOM,而Vue.js则用template的双向绑定原理控制DOM,但两者都是典型的声明式DOM操作模式,有别于命令式的顺序执行DOM操作,不关注”如何做?”,只关注”什么状态?”。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// React 组件(MVC模式)
function List({ items }) {
return (
<ul>
{items.map(item => (
<Item key={item.id} item={item} />
))}
</ul>
);
}
// Vue 模板(MVVM模式)
<div v-for="item in items">
{{ item.title }}
<button @click="remove(item)">×</button>
</div>
另外值得提出的一点是,从
JQuery到React/Vue的发展是存在一个过渡期的,该阶段诞生了大量框架,并且MVC/MVVM模式也是在框架的迭代中逐渐发展而成的
第四阶段:框架技术深度演进
即使现代框架的DOM操作已经远远优于开始使用原生JS的时代,但相关技术还在不断演进,同时源源不断有新的概念被提出,旨在持续优化DOM性能
虚拟DOM技术
虚拟DOM技术是Vue等现代框架的渲染模式,其核心是用一套数据结构来表示真实的DOM树,这个数据结构对象包括节点名称、节点属性、参数,子节点等,于是,后续的节点变更都直接体现在这个虚拟节点中,而不需要直接操作真实的DOM树,框架内部引擎会自动比较真实节点树和虚拟树,这个过程称作diff,从而高效地实现DOM的更新,之后再将虚拟DOM反映到真实的DOM树中,这个过程称作节点的“挂载”,而实现挂载操作的算法称为patch。
以下是Vue官方案例中一个虚拟节点的数据结构:1
2
3
4
5
6
7
8
9const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* 更多 vnode */
]
}
diff算法
节点树patch
tip:Vue中的异步DOM更新
tip:Vue3.6引入Vapor概念
tip:React的fiber架构下的时间切片
常见的直接DOM操作方法实例总结
原生js
原生js中提供最直观的DOM操作方法:1
2
3let app = document.querySelector('#app')
app.classList.add('red')
app.classList.remove('red')
Vue
声明式框架减少了直接的DOM操作,但仍然提供了一些直接操作DOM的方法,例如:
extend构造器
通过extend构造器可以创建一个新的组件,然后通过new关键字实例化,最后通过$mount方法挂载到目标节点上1
2
3const MyComponent = Vue.extend(Component);
const vm = new MyComponent();
vm.$mount('#app');
Ref
通过ref接口可以获取到组件的DOM节点,从而实现对DOM节点的操作,一般适用于组件内部1
2
3
4
5
6
7
8
9
10
11
12
13<template>
<input ref="inputRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
const inputRef = ref(null);
onMounted(() => {
inputRef.value.focus();
});
</script>
React
React中同样提供Ref钩子,用于获取组件的DOM节点1
2
3
4
5
6
7
8
9
10
11
12import React, { createRef, useEffect } from 'react';
function App() {
const boxRef = createRef();
useEffect(() => {
boxRef.current.style.color = 'green';
}, []);
return <div ref={boxRef}>Hello React</div>;
}



