从零手写简易Vue3(四)—— Virtual Dom

1,203 阅读4分钟

本文使用的vue版本为3.0.4

什么是Virtual Dom?

英文全称Virtual Document Object Model,直译:虚拟的文档对象模型。

简而言之,就是使用js 对象去描述一个dom节点,有很多相关的文章描述,这里不再赘述。

我们可以像这样理解一个Virtual Dom

<div id="app">my vdom</div>
const vdom = {
  tag:'div',
  text:'my vdom',
  attributes:{
    id:'app'
  },
  children:[]
}

而在vue中,有一种自己独特的Virtual Dom——VNode,在Virtual Dom的思想基础上,添加了一些vue运行编译所需要的属性,一个初始VNode模板在源码中是这样定义的:

// packages/runtime-core/src/vnode.ts
const vnode: VNode = {
    __v_isVNode: true, // 区分VNode与普通js object的标志
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    children: null,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
}

为什么要使用Virtual Dom?

一个面试的常见问题:请谈谈你对虚拟dom的理解

初级回答

操作虚拟dom比操作真实dom元素性能好。

请勿模仿!这样回答基本上等同于你告诉面试官:我不知道什么是虚拟dom。

进阶版

将dom元素抽象为js对象,结合dom diff算法在更新视图时起到性能优化的效果。

有点内味了,但是感觉还差点什么。

高级版

1.将页面元素抽象为对象,不再局限于浏览器的Dom,奠定了跨平台渲染的基础,打开了函数式UI编程的大门。

以通用的方式描述页面中的元素,根据不同的终端转化为不同的render function进行渲染

2.浏览器不会对Dom操作进行缓存 => 批量处理。意味着你操作了Dom多少次,浏览器就会渲染多少次。而引入了Virtual Dom的概念后,我们可以将多次Dom更新都先体现在Virtual Dom上,再统一映射到真实Dom节点,避免掉了一部分重复渲染工作。

Vue是怎么做的?

Vue中采用异步的方式更新Dome,当观察到数据变化,会开启一个异步队列(优先采用PromiseMutationObserver的方式开启微任务队列,如果浏览器不支持,则会选择setTimeout(function(){},0)的方式开启宏任务队列),待本次event loop结束后,将所有变化同步到VNode中,再执行VNode -> render function -> dom的流程。

*注:由于兼容性原因,MutationObserver的方式已被废弃

3.不仅如此,在将VNode转化为Dom元素时还采用了优化后的diff算法:不再递归遍历整颗树,而是采用深度优先的方式逐层比较同级的树节点;还可以为Dom元素设置key值,加强节点的可复用性。

diff算法中的一些细节:

1.在对比同级树节点时,会先判断两个树节点是否对应同一个Dom节点:

​ a.如果不对应,则会用新的树节点将老的树节点整个替换掉。

​ b.如果对应,则会根据节点的类型以及是否含有子节点等情况进行下一步处理。

2.对于循环出来的Dom元素(例如列表项li),为其设置key属性,生成key => index对,在其相对顺序发生变化时,可以最大限度的复用之前的元素。否则在发生例如节点内容未变化而只是顺序改变的情况时,只能对位处理,只要该位置的元素不同,就会进行 patch(对Dom进行增、删、改)。

3.补充上一条,除非你确定你的列表项顺序不会发生改变,也不会发生除了向列表尾部追加列表项以外的操作,否则不要使用索引index作为key。如果你的key依赖于列表顺序,那么在顺序发生变化以后,key值也会相应改变,key => idnex对就不再有意义。

由此可见,Virtula Dom的理想使用场景是:

  • Dom结构繁杂
  • 需要频繁的操作Dom
  • 多端渲染

反过来,这些情况下则不适用Virtual Dom

  • Dom结构简单
  • 几乎不需要对Dom进行修改

在页面的首次渲染中,相较于Dom.innerHTMLVirtual Dom多了一步转化VNode的过程,速度有可能会慢于Dom.innerHTML;而在后续的Dom更新过程中,每次都要执行diff计算,而且内存中要始终维护一份树结构的备份。

所以要仔细斟酌,使用Virtual Dom所带来的提升是否能cover住所牺牲的部分。