vue2-虚拟dom

201 阅读6分钟

vdom

什么是vdom?

用js模拟dom结构,计算出最小的变更,从而控制操作dom的次数。

结构如下:tag/sel标签,props/data{节点属性:类名 样式 事件},children{子元素}

虚拟dom的类库:snabbdom和virtual-dom

作用?

传统dom数据变化时,要不断操作dom,才能更新dom的数据,后面出现模板引擎,能一次性更新多个dom。但模板引擎不能追踪状态,引擎内某个数据变化时,依然要操作dom去重新渲染整个引擎。而vue react只需要更新数据,其内部通过vdom会自动操作dom,包括控制操作dom的频率。

虚拟dom可以跟踪当前dom状态,会根据当前数据生成一个描述当前dom结构的虚拟dom,数据变化时,再生成新的虚拟dom,两个虚拟dom保存变化前后的状态。通过diff算法,计算两个虚拟dom差异,得出更新的最优方法(哪些改变,就更新哪些)

生成过程

代码初次运行,生命周期走到created到beforeMount之间时,编译template模板成render函数。然后render函数运行时,h函数被调用,而h函数内调用了vnode函数生成虚拟dom,返回生成结果。虚拟dom首次生成。

之后,数据变化时会重新编译生成新vdom。新旧两个vdom对比。采用diff算法算出最优更新结果。

snabbdom库

1. h函数

在render函数内运行。vue生命周期在created–>beforeMount之间会模板编译成render函数,就是将模板编译成h函数所认可的格式放在render函数内,然后render函数运行时,会生成虚拟dom。h函数用函数重载的方式定义的。

render() 方法生成 vnode

App.vue是根组件,最开始的页面就显示在这里。Router-view路由的出口放在这里。默认路径是/

ps函数重载

定义多个重名函数,利用函数的参数个数以及参数类型区分。参数个数不同,参数类型不同时,函数内执行的代码会相应不同。

下面,图中第四种。

第一个参数sel 表示dom选择器,如: div#app.wrap ==》

第二个参数表示dom属性,是个对象如:{ class: ‘ipt’, value: ‘今天天气很好’ }

第三个参数表示子节点,子节点可以是一个子虚拟节点或者文本节点

const vdom = h('div', { class: 'vdom'}, [
  h('p', { class: 'text'}, ['hello word']),
  h('input', { class: 'ipt', value: '今天星期二' })
]) // 模板会编译成这种格式
console.log(vdom)

h函数内主要是执行vnode函数( 将h函数传的参数转为js对象(虚拟dom) )

h函数内部 return vnode(sel, data, children, text, elm ,key)

vnode函数内部 return {sel,data.children,text,elm,key}

  • sel 当前节点标签名
  • data 节点属性
  • children 子节点
  • text 当前节点下的文本
  • elm 当前虚拟节点下对应真实节点
  • key v-for或者组件也能设置key

注意:children text不能共存,子元素要么是文本要么是多个节点,

2. patch函数(根节点替换操作)

diff算法入口,直接比较两个vdom根节点是否相同,只触发一次。

2.1

现在已经通过h函数生成vnode, 这一步是将vnode渲染到真实的dom节点。

var container = document.getElementById('container')

patch(container,vnode)

第一个参数是vnode或element,若是element就创建空vnode关联到dom元素,一个vnode必须有个dom元素关联方便后续更新,vue使用的el挂载。

第二个参数是vnode

2.2

若两个参数都是vnode ;patch(vnode,newVnode)后续更新调用

若key sel不同,删除销毁重建,不继续比较。(降低复杂度)

若两个vnode 相同(key sel 相同),执行patchVnode(vnode,newVnode)进一步对比。

3. patchVnode函数(key sel相同时候,判断data text当前节点下的文本 children是否存在且相同)

text与children不共存, 有 children 属性的情况下,text 中的内容会转化为一个文本节点置入 children 数组中

首先oldvnode.elm赋值给newVnode.elm

开始执行oldVnode === vNode ,相等直接return, 否则进一步比较

1.newVnode.text === undefined newVnode.children !== undefined

  • newVnode没有子节点

    • oldVnode有子节点, 执行删除oldVnode子节点
    • oldVnode没有子节点,置空
    • oldVnode有text先清空,再执行添加节点操作
  • newVnode有子节点

    • oldVnode有子节点。深度对比执行updateChildren([],[])
    • oldVnode没有子节点 有text节点,先清空,再执行添加节点操作

2.newVnode.text !== undefined newVnode.children === undefined

  • oldVnode有children,删除旧子节点,设置vnode对象的真实dom的text值(使用setTextContent函数)
  • 其他直接设置vnode对象的真实dom的text值

4. updateChildren函数(子节点替换操作 双端指针 diff 算法)

while循环截止条件 oldStart>oldEnd&&newStart>newEnd

四种对比:若是相同节点(key sel相同),执行patchVnode(),索引累加或累减。

  • newStart vs oldStart
  • newEnd vs oldEnd
  • newStart vs oldEnd
  • newEnd vs oldStart

单独情况:以上四种都没命中, 看newVnodeC.key是否对应上oldVnodeC.key,没对应上就插入重建,对应上拿到key对应的新旧节点,看sel是否相同,相同的话执行patchVnode(),否则重建。

vdom核心是最大程度减少dom渲染范围:怎么减少--diff算法

时间复杂度

树的diff是n3:遍历A,遍历B,排序

改进(时间复杂度变为n)

  • 只比较同一层, 不跨级。同层的特点是判断是否有相同的父级。
  • 若tag不同直接删掉重建
  • tag key都相同就相同,不再比较

就是snabbdom库中patch patchVnode updateChildren函数比较过程。

diff采用先序深度优先遍历,比较某个节点时,如果该节点存在子节点,优先比较他的子节点,到所有子节点全部比较完成,才开始比较该节点的下一个同层级节点。首先比较的是根节点,

问题

diff算法,为什么还要数据劫持?getter,setter?

dom树非常复杂,每次小改动,就要通过diff算法去精确找改动的地方,计算量大.性能损耗

vue: MVVM原理,vue通过Object.defineproperty数据劫持,劫持到每个状态数据,加上getter,setter。并且创建一个发布者Dep, 同时给依赖这个状态数据的每个依赖者添加订阅者watcher。当数据变化时,触发对应的setter,Dep发布通知,通知每个订阅者watcher,然后watcher更新对应数据。

如果任何一个数据依赖都增加一个watcher。那么watcher数量非常庞大。细粒度太高,会带来内存和依赖关系维护的巨大消耗。这样情况下,vue通过响应式的getter,setter快速知道数据的变化发生在哪个组件中,然后组件内部再通过diff的方式去获取更详细的更新情况,并更新数据。

react:通过生命周期函数shouldComponentUpdate。手动在这个生命周期函数中判断当前组件的数据是否有发生变化,决定当前组件是否需要更新。没有发生状态数据变化的组件就不需要进行diff。从而缩小diff的范围。