Vue-Diff算法

模板转换成视图的过程

  1. Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树
  2. 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。

img

我们先对上图几个概念加以解释:

  • 渲染函数:渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。
  • VNode 虚拟节点:它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。
  • patch(也叫做patching算法):虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新,其实际作用是在现有DOM上进行修改来实现更新视图的目的。

虚拟Dom

什么是虚拟DOM

Virtual DOM 是一棵以 JavaScript 对象作为基础的树,每一个节点称为 VNode ,用对象属性来描述节点,实际上它是一层对真实 DOM 的抽象,最终可以通过渲染操作使这棵树映射到真实环境上,简单来说 Virtual DOM 就是一个 Js 对象,用以描述整个文档。

<ul id='myId'>
 <li>Item 1</li>
 <li>Item 2</li>
<ul>
{
 tag: 'ul'
   attributes: { id: 'myId' }
   children: [
  	//这里是 li
   ]
};

虚拟DOM优势

  1. 具备跨平台的优势

    由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力。

  2. 操作 DOM 慢,js运行效率高,提高效率。

    因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)

  3. 提升渲染性能

    Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。

虚拟DOM作用

虚拟DOM的最终目标是将虚拟节点渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的DOM操作而造成了性能上的浪费。

为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。

虚拟DOM在Vue.js主要做了两件事:

  • 提供与真实DOM节点所对应的虚拟节点vnode
  • 将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图

#VNode

什么是VNode

​ Vue.js 利用 createElement 方法创建 VNode。就是描述真实节点的js对象

  1. 其实vnode只是一个名字,本质上其实是Javascript中一个普通的对象,是从VNode类实例化的对象。我们用这个Javascript对象来描述一个真实DOM元素的话,那么该DOM元素上的所有属性在VNode这个对象上都存在对应的属性。

  2. vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。例如tag表示一个元素节点的名称,text表示一个文本节点的文本,children表示子节点等。

  3. vnode表示一个真实的DOM元素,所有真实的DOM节点都使用vnode创建并插入到页面中。(vnode–>DOM–>视图)

  4. vnode和视图是一一对应的。我们可以把vnode理解成Javascript对象版本的DOM元素。

  5. 渲染视图的过程是先创建vnode,然后再使用vnode去生成真实的DOM元素,最后插入页面渲染视图。

VNode的作用

由于每次渲染视图时都是先创建vnode,然后使用它创建真实DOM插入到页面中,所以可以将上一次渲染视图时所创建的vnode缓存起来,之后每当需要重新渲染视图时,将新创建的vnode和上一次缓存的vnode进行对比,查看它们之间有哪些不一样的地方,找出这些不一样的地方并基于此去修改真实的DOM。

VNode属性含义

tag: 当前节点的标签名
data: 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息
children: 当前节点的子节点,是一个数组
text: 当前节点的文本
elm: 当前虚拟节点对应的真实dom节点
ns: 当前节点的名字空间
context: 当前节点的编译作用域
functionalContext: 函数化组件作用域
key: 节点的key属性,被当作节点的标志,用以优化
componentOptions: 组件的option选项
componentInstance: 当前节点对应的组件的实例
parent: 当前节点的父节点
raw: 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false
isStatic: 是否为静态节点
isRootInsert: 是否作为跟节点插入
isComment: 是否为注释节点
isCloned: 是否为克隆节点
isOnce: 是否有v-once指令

栗子🌰

//html
<div class="test">
    <span class="demo">hello,VNode</span>
</div>
//vnode
{
    tag: 'div'
    data: {
        class: 'test'
    },
    children: [
        {
            tag: 'span',
            data: {
                class: 'demo'
            }
            text: 'hello,VNode'
        }
    ]
}

vue.js

img

snaddom

编译
h('a', { props: { href: 'http://www.baidu.com' }}, 'Hello word');
得到
{ "sel": "a", "data": { props: { href: 'http://www.baidu.com' } }, "text": "Hello word" }
真实的节点
<a href="http://www.baidu.com">Hello word</a>

Vnode的类型

vnode是Javascript中的一个对象,不同类型的vnode之间其实只是属性不同,准确地说是有效属性不同。因为当使用VNode类创建一个vnode时,通过参数为实例设置属性时,无效的属性会默认被赋值为undefined或false。对于vnode身上无效属性,直接忽略就好。

注释节点
export const createEmptyVNode = text =>{
    const node = new VNode();
    node.text = text; 
    node.isComment = true;
    return node
}
(1)一个注释节点只有两个有效属性-----text和isComment,其余属性全是默认的undefined或则false。

//对应的vnode如下:
{
    text:"注释节点",
    isComment:true
}
// 注释节点
文本节点
export function createTextVNode(val){
    return new VNode(undefined,undefined,undefined,String(val))
}
(1)文本类型的vnode被创建时,它只有一个text属性

//文本类型的vnode
{
    text:"Hello Berwin"
}
克隆节点

(1)克隆节点是将现有节点的属性赋值到新节点中,让新创建的节点和被克隆的节点的属性保持一致,从而实现克隆效果。

(2)作用:优化静态节点和插槽节点(slot node)。

(3)以静态节点为例,当组件内的某个状态发生变化后,当前组件会通过虚拟DOM重新渲染视图,静态节点因为它的内容不会改变,所以除了首次渲染需要执行渲染函数获取vnode之外,后续更新不需要执行渲染函数重新生成vnode。因此,这时就会使用创建克隆节点的方法将vnode克隆一份,使用克隆节点进行渲染。这样就不需要重新执行渲染函数生成新的静态节点的vnode,从而提升一定程度的性能。
(4)克隆现有节点时,只需要将现有节点的属性全部赋值到新节点中即可。

(5)克隆节点和被克隆节点之间的唯一区别是isCloned属性,克隆节点的isCloned为true,被克隆的原始节点的isCloned为false。

export function cloneVNode(vnode,deep){
    const cloned = new VNode(
        vnode.tag,
        vnode.data,
        vnode.children,
        vnode.text,
        vnode.elm,
        vnode.context,
        vnode.componentOptions,
        vnode.asyncFactory
    )
    cloned.ns = vnode.;
    cloned.isStatic = vnode.isStatic;
    cloned.key = vnode.key;
    cloned.isComment = vnode.isComment;
    cloned.isCloned = true;
    if(deep&&vnode.children){
        cloned.chlidren = cloneVNodes(vnode.children);
    }
    return cloned;
}
元素节点

(1)元素节点通常存在以下4种有效属性。

​ tag:tag是一个节点的名称,例如ul、p、li和div等。
​ data:该属性包含了一些节点上的数据,比如attrs、class和style等。
​ children:当前节点的子节点列表。
​ context:它是当前组件的Vue.js实例。

真实的元素节点

<p><span>Hello</span><span>Berwin</span></p>

对应的vnode

{
	children:[VNode,VNode],
	context:{...},
	data:{...},
	tag:"p",
	....
}
组件节点

componentOptions:组件节点的选项参数,其中包含propsData、tag和children等信息。
componentInstance:组件的实例,也就是Vue.js实例。事实上,在Vue.js种,每个组件都是一个Vue.js实例。

一个组件节点

<child></child>
对应的vnode如下:

{
	componentInstance:{...},
	componentOptions:{...},
	context:{...},
	data:{...},
	tag:"vue-component-1-child",
	....
}
函数式组件

函数式组件和组件节点类似,它有两个独有的属性functionalContext和functionalOptions。

{
	functionalContext:{...},
	functionalOptions:{...},
	context:{...},
	data:{...},
	tag:"div",
	....
}

Patch

流程图:

https://processon.com/diagrams

updateChildren

在这里插入图片描述

oldStartIdxnewStartIdxoldEndIdx以及newEndIdx分别是新老两个VNode两边的索引,同时oldStartVnodenewStartVnodeoldEndVnodenew EndVnode分别指向这几个索引对应的vnode。整个遍历需要在oldStartIdx小于oldEndIdx并且newStartIdx小于newEndIdx(这里为了简便,称sameVnode为相似)

  1. oldStartVnode不存在的时候,oldStartVnode向右移动,oldStartIdx1

  2. oldEndVnode不存在的时候,oldEndVnode向右移动,oldEndIdx1

  3. oldStartVnodenewStartVnode相似,oldStartVnodenewStartVnode都向右移动,oldStartIdxnewStartIdx都增加1
    在这里插入图片描述

  4. oldEndVnodenewEndVnode相似,oldEndVnodenewEndVnode都向左移动,oldEndIdxnewEndIdx都减1

在这里插入图片描述

  1. oldStartVnodenewEndVnode相似,则把oldStartVnode.elm移动到oldEndVnode.elm的节点后面。然后oldStartIdx向后移动一位,newEndIdx向前移动一位

在这里插入图片描述

  1. oldEndVnodenewStartVnode相似时,把oldEndVnode.elm插入到oldStartVnode.elm前面。同样的,oldEndIdx向前移动一位,newStartIdx向后移动一位。

在这里插入图片描述

当以上情况都不符合的时候

生成一个key与旧vnode对应的哈希表

//createKeyToOldIdx 函数,该函数的作用是 当 对比两个子节点数组时,建立 key-index映射代理遍历查找 sameNode.提高性能。
function createKeyToOldIdx (children, beginIdx, endIdx) {
    let i, key
    const map = {}
    for (i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key
        if (isDef(key)) map[key] = i
    }
    return map
}

最后生成的对象就是以childrenkey为属性,递增的数字为属性值的对象例如

children = [{
    key: 'key1'
}, {
    key: 'key2'
}]
// 最后生成的map
map = {
    key1: 0,
    key2: 1,
}

所以oldKeyToIdx就是key和旧vnodekey对应的哈希表 根据newStartVnodekey看能否找到对应的oldVnode

  • 如果oldVnode不存在,就创建一个新节点,newStartVnode向右移动

  • 如果找到节点:

    • 并且和newStartVnode相似。将map表中该位置的赋值undefined(用于保证key是唯一的)。同时将newStartVnode.elm插入啊到oldStartVnode.elm的前面,然后index向后移动一位

    • 如果不符合sameVnode,只能创建一个新节点插入到parentElm的子节点中,newStartIdx向后移动一位

      (https://juejin.cn/post/6850418110911119374)

结束循环后

oldStartIdx又大于oldEndIdx`,就将新节点中没有对比的节点加到队尾中

如果newStartIdx > newEndIdx,就说明还存在新节点,就将这些节点进行删除

相关流程图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值