1、什么是virtual dom
Virtual DOM是对DOM的抽象,本质上是JavaScript对象,这个对象就是更加轻量级的对DOM的描述.
2、为什么需要virtual dom
既然我们已经有了DOM,为什么还需要额外加一层抽象?
首先,我们都知道在前端性能优化的一个秘诀就是尽可能少地操作DOM,不仅仅是DOM相对较慢,更因为频繁变动DOM会造成浏览器的回流或者重回,这些都是性能的杀手,因此我们需要这一层抽象,在patch过程中尽可能地一次性将差异更新到DOM中,这样保证了DOM不会出现性能很差的情况.
其次,现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率.
最后,也是Virtual DOM最初的目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR(服务端渲染),那么一个方式就是借助Virtual DOM,因为Virtual DOM本身是JavaScript对象.
3、virtual dom的创建
我们已经知道Virtual DOM是对真实DOM的抽象,根据不同的需求我们可以做出不同的抽象,比如snabbdom.js的抽象方式是这样的.
当然,snabbdom.js由于是面向生产环境的库,所以做了大量的抽象各种,我们由于仅仅作为教程理解,因此采用最简单的抽象方法:
{
type, // String,DOM 节点的类型,如 'div'
data, // Object,包括 props,style等等 DOM 节点的各种属性
children // Array,子节点
}
在明确了我们抽象的Virtual DOM构造之后,我们就需要一个函数来创建Virtual DOM.
/**
* 生成 vnode
* @param {String} type 类型,如 'div'
* @param {String} key key vnode的唯一id
* @param {Object} data data,包括属性,事件等等
* @param {Array} children 子 vnode
* @param {String} text 文本
* @param {Element} elm 对应的 dom
* @return {Object} vnode
*/
function vnode(type, key, data, children, text, elm) {
const element = {
__type: VNODE_TYPE,
type, key, data, children, text, elm
}
return element
}
这个函数很简单,接受一定的参数,再根据这些参数返回一个对象,这个对象就是DOM的抽象.
4、Virtual DOM Tree的创建
上面我们已经声明了一个vnode函数用于单个Virtual DOM的创建工作,但是我们都知道DOM其实是一个Tree,我们接下来要做的就是声明一个函数用于创建DOM Tree的抽象 -- Virtual DOM Tree.
function h(type, config, ...children) {
const props = {}
let key = null
// 获取 key,填充 props 对象
if (config != null) {
if (hasValidKey(config)) {
key = '' + config.key
}
for (let propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS[propName]) {
props[propName] = config[propName]
}
}
}
return vnode(
type,
key,
props,
flattenArray(children).map(c => {
return isPrimitive(c) ? vnode(undefined, undefined, undefined, undefined, c) : c
})
)
}
5、virtual dom的更新
Virtual DOM 归根到底是JavaScript对象,我们得想办法将Virtual DOM与真实的DOM对应起来,也就是说,需要我们声明一个函数,此函数可以将vnode转化为真实DOM.
function createElm(vnode, insertedVnodeQueue) {
let data = vnode.data
let i
// 省略 hook 调用
let children = vnode.children
let type = vnode.type
/// 根据 type 来分别生成 DOM
// 处理 comment
if (type === 'comment') {
if (vnode.text == null) {
vnode.text = ''
}
vnode.elm = api.createComment(vnode.text)
}
// 处理其它 type
else if (type) {
const elm = vnode.elm = data.ns
? api.createElementNS(data.ns, type)
: api.createElement(type)
// 调用 create hook
for (let i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
// 分别处理 children 和 text。
// 这里隐含一个逻辑:vnode 的 children 和 text 不会/应该同时存在。
if (isArray(children)) {
// 递归 children,保证 vnode tree 中每个 vnode 都有自己对应的 dom;
// 即构建 vnode tree 对应的 dom tree。
children.forEach(ch => {
ch && api.appendChild(elm, createElm(ch, insertedVnodeQueue))
})
}
else if (isPrimitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text))
}
// 调用 create hook;为 insert hook 填充 insertedVnodeQueue。
i = vnode.data.hook
if (i) {
i.create && i.create(emptyNode, vnode)
i.insert && insertedVnodeQueue.push(vnode)
}
}
// 处理 text(text的 type 是空)
else {
vnode.elm = api.createTextNode(vnode.text)
}
return vnode.elm
}
function createElm(vnode, insertedVnodeQueue) {
let data = vnode.data
let i
// 省略 hook 调用
let children = vnode.children
let type = vnode.type
/// 根据 type 来分别生成 DOM
// 处理 comment
if (type === 'comment') {
if (vnode.text == null) {
vnode.text = ''
}
vnode.elm = api.createComment(vnode.text)
}
// 处理其它 type
else if (type) {
const elm = vnode.elm = data.ns
? api.createElementNS(data.ns, type)
: api.createElement(type)
// 调用 create hook
for (let i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
// 分别处理 children 和 text。
// 这里隐含一个逻辑:vnode 的 children 和 text 不会/应该同时存在。
if (isArray(children)) {
// 递归 children,保证 vnode tree 中每个 vnode 都有自己对应的 dom;
// 即构建 vnode tree 对应的 dom tree。
children.forEach(ch => {
ch && api.appendChild(elm, createElm(ch, insertedVnodeQueue))
})
}
else if (isPrimitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text))
}
// 调用 create hook;为 insert hook 填充 insertedVnodeQueue。
i = vnode.data.hook
if (i) {
i.create && i.create(emptyNode, vnode)
i.insert && insertedVnodeQueue.push(vnode)
}
}
// 处理 text(text的 type 是空)
else {
vnode.elm = api.createTextNode(vnode.text)
}
return vnode.elm
}
6、virtual dom的diff算法
主要使用的是:递归 + 双指针遍历
7、virtual dom的优化
上一节我们的Virtual DOM实现是参考了snabbdom.js的实现,当然Vue.js也同样参考了snabbdom.js,我们省略了大量边缘状态和svg等相关的代码,仅仅实现了其核心部分.
snabbdom.js已经是社区内主流的Virtual DOM实现了,vue 2.0阶段与snabbdom.js一样都采用了上面讲解的「双端比较算法」,那么有没有一些优化方案可以使其更快?
其实,社区内有更快的算法,例如inferno.js就号称最快react-like框架(虽然inferno.js性能强悍的原因不仅仅是算法,但是其diff算法的确是目前最快的),而vue 3.0就会借鉴inferno.js的算法进行优化.
我们可以等到Vue 3.0发布后再一探究竟,具体的优化思想可以先参考diff 算法原理概述,其中一个核心的思想就是利用LIS(最长递增子序列)的思想做动态规划,找到最小的移动次数.
例如以下两个新旧数组,React的算法会把 a, b, c 移动到他们的相应的位置 + 1共三步操作,而inferno.js则是直接将d移动到最前端这一步操作.
* A: [a b c d]
* B: [d a b c]
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/weixin_47450807/article/details/124912130
内容来源于网络,如有侵权,请联系作者删除!