-
为什么需要虚拟
DOM
?
浏览器的引擎工作流程,大致分5步:- 创建
DOM tree
:用HTML
分析器,分析HTML
元素,构建一颗DOM
树。 - 创建
Style Rules
:用CSS
分析器,分析CSS
文件和元素上的inline
样式,生成页面的样式表。 - 构建
Render tree
:将上面的DOM
树和样式表,关联起来,构建一颗Render
树。这一过程又称为Attachment
。每个DOM
节点都有attach
方法,接受样式信息,返回一个render
对象(又名renderer
)。这些render
对象最终会被构建成一颗Render
树。 - 布局
Layout
:浏览器开始布局,会为每个Render
树上的节点确定一个在显示屏上出现的精确坐标值。 - 绘制
Painting
:调用每个节点的paint
方法,让它们显示出来。
当用传统的源生
api
或jQuery
去操作DOM
时,浏览器会从构建DOM
树开始从头到尾执行一遍流程。比如当你在一次操作时,需要更新10个DOM
节点,理想状态是一次性构建完DOM
树,再执行后续操作。但浏览器没这么智能,收到第一个更新DOM
请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程。因此操作DOM
的代价是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验。
真实的DOM
节点,哪怕一个最简单的div
也包含着很多属性,虚拟DOM
就是为了解决这个浏览器性能问题而被设计出来的。let div = document.createElement('div'); for(let key in div) { console.log(key) }
- 创建
-
虚拟
DOM
是什么?
由于浏览器的标准过于复杂,自己使用js
的对象来描述真实dom
,这个js
对象,称为虚拟dom
。<div id="app"> <p class="item">节点1</p> <div class="item">节点2</div> </div> { tag: 'div', data: { id: 'app' }, children: [ { tag: 'p', data: { class: 'item' }, children: ['节点1'] }, { tag: 'div', data: { class: 'item' }, children: ['节点2'] } ] }
-
为什么需要虚拟
DOM
,它有什么好处?
Web
界面由DOM
树(数据结构)来构建,当其中一部分发生变化时,其实就是对应某个DOM
节点发生了变化。
虚拟DOM
就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有10次更新DOM
的动作,虚拟DOM
不会立即操作DOM
,而是将这10次更新的diff
内容保存到本地一个JS
对象中,最终将这个JS
对象一次性attch
到DOM
树上,再进行后续操作,避免大量无谓的计算量。
所以,用JS
对象模拟DOM
节点的好处是,页面的更新可以先全部反映在JS
对象(虚拟DOM
)上,操作内存中的JS
对象的速度显然要更快,等更新完成后,再将最终的JS
对象映射成真实的DOM
,交由浏览器去绘制。 -
如何实现虚拟
DOM
?
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.item {
font-size: 30px;
color: red;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="vdom.js"></script>
<script>
// 旧的 VNode
const prevVNode = createElement('div', null, [
createElement('div', { key: 'a' , style: {color:'blue', 'text-align': 'left'} }, '节点1'),
createElement('p', { key: 'b' ,'@click': () => alert('呵呵') }, '节点2'),
createElement('div', { key: 'c' }, '节点3'),
createElement('p', { key: 'd'}, '节点4'),
])
// 新的 VNode
const nextVNode = createElement('div', null, [
createElement('p', { key: 'd' }, '节点4'),
createElement('p', { key: 'a' , style: {color:'red'} }, '节点1'),
createElement('p', { key: 'f' }, '节点6'),
createElement('p', { key: 'e' , class: "item box" }, '节点5'),
createElement('div', { key: 'b' , '@click':() => alert('哈哈') }, '节点2'),
])
render(prevVNode, document.getElementById('app'))
// 2秒后更新
setTimeout(() => {
render(nextVNode, document.getElementById('app'))
}, 1000)
</script>
</body>
</html>
vdom.js:
const VNodeType = {
HTML: 'HTML',
COMPONENT: 'COMPONENT',
TEXT: 'TEXT'
}
const ChildType = {
EMPTY: 'EMPTY',
SINGLE: 'SINGLE',
MULTIPLE: 'MULTIPLE'
}
// 新建虚拟dom
// tag: 标签名;data: 属性;children: 子元素
function createElement(tag, data, children) {
// vnode 类型
let flags = null;
if (typeof tag === 'string') {
flags = VNodeType.HTML;
} else if (typeof tag === 'function') {
flags = VNodeType.COMPONENT;
} else {
flags = VNodeType.TEXT;
}
// children 类型
let childFlags = null;
if (Array.isArray(children)) {
if (!children.length) {
childFlags = ChildType.EMPTY;
} else if (children.length) {
// 多个子节点,且子节点使用key
childFlags = ChildType.MULTIPLE;
}
} else if (!children) {
// 没有子节点
childFlags = ChildType.EMPTY;
} else {
// 其他情况都作为文本节点处理,即单个子节点,会调用 createTextVNode 创建纯文本类型的 VNode
childFlags = ChildType.SINGLE;
children = createTextVnode(children);
}
// 返回 vnode,key 用来标识节点的唯一性
return { flags, tag, data, key: data && data.key, children, childFlags, el: null }
}
// 新建文本类型的 vnode
function createTextVnode(text) {
return {
flags: VNodeType.TEXT,
tag: null,
data: null,
children: text,
childFlags: ChildType.EMPTY
}
}
// 渲染虚拟dom
function render(vnode, container) {
// 区分首次渲染和再次渲染
const prevVNode = container.vnode;
if (prevVNode) {
// 有旧的 VNode,则调用 `patch` 函数打补丁
patch(prevVNode, vnode, container);
} else {
// 没有旧的 VNode,使用 `mount` 函数挂载全新的 VNode
mount(vnode, container);
}
// 将新的 VNode 添加到 container.vnode 属性下,这样下一次渲染时旧的 VNode 就存在了
container.vnode = vnode;
}
// 首次渲染(首次挂载)
function mount(vnode, container, flagNode) {
let { flags } = vnode;
if (flags === VNodeType.HTML) {
// 挂载普通标签
mountElement(vnode, container, flagNode);
} else if (flags === VNodeType.TEXT) {
// 挂载纯文本
mountText(vnode, container);
}
}
// 挂载普通标签
function mountElement(vnode, container, flagNode) {
let { tag, data, children, childFlags } = vnode;
let el = document.createElement(tag);
vnode.el = el;
if (data) {
for(let key in data) {
patchData(el, key, null, data[key])
}
}
if (childFlags === ChildType.SINGLE) {
mount(children, el);
} else if (childFlags === ChildType.MULTIPLE) {
children.forEach(item => {
mount(item, el);
});
}
flagNode ? container.insertBefore(el, flagNode) : container.appendChild(el);
}
// 挂载纯文本
function mountText(vnode, container) {
const el = document.createTextNode(vnode.children)
vnode.el = el
container.appendChild(el)
}
// 更新data
function patchData(el, key, prevValue, nextValue) {
switch (key) {
case 'style':
// 先增加新值
for(let k in nextValue) {
el.style[k] = nextValue[k];
}
for(let k in prevValue) {
// 再去掉旧值
if (!nextValue.hasOwnProperty(k)) {
el.style[k] = '';
}
}
break;
case 'className':
el.className = nextValue;
break;
default:
if (key[0] === '@') {
// 事件
// 移除旧事件
if (prevValue) {
el.removeEventListener(key.slice(1), prevValue);
}
// 添加新事件
if (nextValue) {
el.addEventListener(key.slice(1), nextValue);
}
} else {
// attr
el.setAttribute(key, nextValue);
}
break;
}
}
// 打补丁
function patch(prevVNode, nextVNode, container) {
const prevFlags = prevVNode.flags;
const nextFlags = nextVNode.flags;
if (prevFlags !== nextFlags) {
// 直接替换
replaceVNode(prevVNode, nextVNode, container);
} else if (nextFlags === VNodeType.HTML) {
// 普通标签
patchElement(prevVNode, nextVNode, container);
} else if (nextFlags === VNodeType.TEXT) {
// 纯文本
patchText(prevVNode, nextVNode, container);
}
}
// 替换节点
function replaceVNode(prevVNode, nextVNode, container) {
container.removeChild(prevVNode.el)
mount(nextVNode, container)
}
// 普通标签
function patchElement(prevVNode, nextVNode, container) {
// 如果新旧 VNode 描述的是不同的标签,则调用 replaceVNode 函数使用新的 VNode 替换旧的 VNode
if (prevVNode.tag !== nextVNode.tag) {
// 直接替换
replaceVNode(prevVNode, nextVNode, container);
return;
}
// 拿到 el 元素,且让 nextVNode.el 也引用该元素
const el = (nextVNode.el = prevVNode.el);
const prevData = prevVNode.data;
const nextData = nextVNode.data;
// 更新新的数据
if (nextData) {
for(let key in nextData) {
patchData(el, key, prevData[key], nextData[key])
}
}
// 删除旧的数据
if (prevData) {
for(let key in prevData) {
if (prevData[key] && !nextData.hasOwnProperty(key)) {
patchData(el, key, prevData[key], null)
}
}
}
// 调用 patchChildren 函数,递归更新的子节点
patchChildren(prevVNode.childFlags, nextVNode.childFlags, prevVNode.children, nextVNode.children, el);
}
// 更新子节点
// 旧的 VNode 子节点的类型; 新的 VNode 子节点的类型; 旧的 VNode 子节点; 新的 VNode 子节点; 当前标签元素,即这些子节点的父节点
function patchChildren(prevChildFlags, nextChildFlags, prevChildren, nextChildren, container) {
switch (prevChildFlags) {
// 旧的 children 是单个子节点
case ChildType.SINGLE:
switch (nextChildFlags) {
// 新的 children 是单个子节点
case ChildType.SINGLE:
patch(prevChildren, nextChildren, container);
break;
// 新的 children 没有子节点
case ChildType.EMPTY:
container.removeChild(prevChildren.el);
break;
// 新的 children 中有多个子节点
default:
container.removeChild(prevChildren.el);
nextChildren.forEach(item => {
mount(item, container);
})
break;
}
break;
// 旧的 children 没有子节点
case ChildType.EMPTY:
switch (nextChildFlags) {
// 新的 children 是单个子节点
case ChildType.SINGLE:
mount(nextChildren, container);
break;
// 新的 children 没有子节点
case ChildType.EMPTY:
break;
// 新的 children 中有多个子节点
default:
nextChildren.forEach(item => {
mount(item, container);
})
break;
}
break;
// 旧的 children 中有多个子节点
default:
switch (nextChildFlags) {
// 新的 children 是单个子节点
case ChildType.SINGLE:
prevChildren.forEach(item => {
container.removeChild(item.el);
})
mount(nextChildren, container);
break;
// 新的 children 没有子节点
case ChildType.EMPTY:
prevChildren.forEach(item => {
container.removeChild(item.el);
})
break;
// 新的 children 中有多个子节点
default:
patchMultipleToMultiple(prevChildren, nextChildren, container);
break;
}
break;
}
}
// 纯文本
function patchText(prevVNode, nextVNode, container) {
// 拿到文本节点 el,同时让 nextVNode.el 指向该文本节点
const el = (nextVNode.el = prevVNode.el);
// 只有当新旧文本内容不一致时才有必要更新
if (prevVNode.children !== nextVNode.children) {
el.nodeValue = nextVNode.children;
}
}
function patchMultipleToMultiple(prevChildren, nextChildren, container) {
let lastIndex = 0;
let hasFind = false;
let prevVNode = null;
// 处理新节点的节点
nextChildren.forEach((nextVNode, nextIndex) => {
hasFind = false;
for(let prevIndex = 0; prevIndex < prevChildren.length; prevIndex++) {
prevVNode = prevChildren[prevIndex];
if (nextVNode.key === prevVNode.key) {
hasFind = true;
if (prevIndex < lastIndex) {
// 需要移动
const flagNode = nextChildren[nextIndex - 1].el.nextSibling;
container.insertBefore(prevVNode.el, flagNode);
} else {
lastIndex = prevIndex;
}
patch(prevVNode, nextVNode, container);
break;
}
}
if (!hasFind) {
// 挂载新节点
const flagNode = !nextIndex ? prevChildren[0].el : nextChildren[nextIndex - 1].el.nextSibling;
mount(nextVNode, container, flagNode);
}
});
// 移除已经不存在的节点
prevChildren.forEach(prevVNode => {
let hasFind = nextChildren.find(nextVNode => nextVNode.key === prevVNode.key);
if (!hasFind) {
container.removeChild(prevVNode.el);
}
});
}
网友评论