对vue响应式原理&diff的理解

113 阅读6分钟

Vue2

首先,使用了发布订阅模式,有三个核心实现类:Observer、Dep 、Watcher。

Observer的作用是给对象的属性添加setter和getter,用于依赖收集和派发更新

Dep用于收集当前响应式对象的依赖关系,每个对象及其子对象都有一个Dep实例,里面有个subs数组,用来存放Watcher实例。当数据有变化时,就会通过dep.notify()通知各个watcher进行更新

Watcher则是观察者对象,用来触发更新回调。实例分为三种,分别是渲染watcher、计算属性watcher、侦听器watcher。

接下来说说Watcher和Dep的关系

初始化时实例化了dep,并且向dep.subs中添加了订阅者,dep通过notify()遍历了dep.subs,通知每个watcher调用update()进行更新。

流程

响应式原理就是当创建Vue实例时,会遍历data中的属性,利用Object.defineProperty为属性添加getter和setter,目的是对数据的读取进行劫持。getter用来依赖收集,setter用来派发更新,并且在内部追踪依赖,在属性被访问时监听和修改时通知变化。

每个组件实例会有相对应的watcher实例,会在组件渲染时记录依赖的所有属性。当依赖项被修改时,setter方法会通知依赖,并且相对应的watcher实例进行更新,从而使相对应的组件重新渲染。

总结

Vue采用数据劫持结合发布订阅模式,通过Object.defineProperty来劫持各个属性的setter和getter,在数据变化时通知订阅者,触发响应的监听回调进行更新

Vue3

和Vue2中使用Object.defineProperty劫持各个属性不同,Vue3使用了ES6的proxy来做代理,实现响应式。

在reactive函数中返回了一个Proxy,代理了对target对象的存取。首先在get返回之前,自动调用track()将effect存到对应的位置。当数据属性被修改时,set会自动触发trigger(),调用了相对应的effect,重新计算实现了自动响应。

在ref函数中使用了getter和setter实现,模仿了Proxy的get和set。Ref专门用来处理原始类型的响应式,因为reactive还会添加更多处理流程,对处理原始类型是一种多余的负担。

Computed本质上就是封装了ref方法,用effect封装着来调用getter,将结果设给result的同时,也将eff保存在targetMap的对应位置,实现了computed的响应式

vue diff

前置知识

Vue在初始化页面时,将当前的真实DOM转换为虚拟DOM(Virtual DOM),并将其保存为oldVnode。当某个数据变化后,会生成一个新的虚拟DOM,成为vnode,然后将vnode和oldVnode进行比较,找出需要更新的地方,然后在对应的真实DOM上进行修改。当修改结束后,就将vnode赋值给oldVnode存起来,作为下次更新比较的参照物。新旧vnode的比较,就是我们常说的diff算法。

什么是虚拟DOM?

曾经打印过真实DOM,它实质上是个对象,但它的元素是非常多的。因为,在真实DOM下,我们不太敢去直接操作。这个时候就需要虚拟DOM了,它也是一个对象,而VDOM其实是将真实DOM的数据抽取出来,以对象的形式模拟树形结构,使其更加简洁明了。 在Vue中,有个render函数,返回的VNode就是一个虚拟DOM

render函数是怎么生成虚拟DOM?

image.png

image.png

重点

当组件创建和更新时,会执行内部的update函数,该函数使用render函数生成虚拟DOM,将新旧两树进行对比,找到差异点,最终更新到真实DOM 对比差异的过程叫diff,vue在内部通过一个patch函数完成,Patch函数可以让虚拟节点上树 在对比中,采用深度优先、同级比较的方式,不会跨层结构进行比较 在判断两个节点是否相同时,通过虚拟节点的key和tag进行判断

具体来说,首先对根节点进行比较,如果相同则将旧节点关联的真实DOM的引用挂到新节点上,然后根据需要更新属性到真实DOM,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实DOM,同时挂到对应虚拟节点上,然后移除旧的DOM。在对比其子节点数组时,对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢进行对比,这样做的目的是尽量复用真实DOM,尽量少去销毁和创建真实DOM。如果发现相同,则进入和根节点一样的对比流程,如果发现不相同,则移动真实DOM到合适的位置。这样一直递归遍历下去,直到整棵树完成对比。

特征
1.Diff是发生在新旧虚拟DOM上的
2.同层比较,遍历逐层比较

oldVnode和newVnode的sel和key是否都相同,如果不是,暴力删除旧的插入新的;如果是,继续判断

oldVnode和newVnode是不是内存中的同一个对象,如果是,什么都不处理;如果不是,继续判断newVnode有没有text属性(即有没有children)。

newVnode有text(没有children)的话,判断oldVnode的text或者children和newVnode的text是否相同?相同的话,什么都不做;不同的话,把旧elm的innerHTML改成newVnode的text(此时不用管oldVnode有的是text还是children,替换就完事儿了)

newVnode没有text(有children)的话,再看oldVnode有没有text。oldVnode有text,就清空oldVnode中的text,并且把newVnode的children添加到DOM中。没有text,说明oldVnode和newVnode都有children,就是最复杂的情况,要进行最优雅的diff

 

Diff算法优化策略:四种命中查找

①新前与旧前

②新后与旧后

③新后与旧前(此种发生了,新前指向的节点,移动到旧后之后)

④新前与旧后(此种发生了,新前指向的节点,移动到旧前之前)

命中一种就不再继续命中判断,没命中就用下一种命中方式判断;如果都没命中,在旧节点里循环查找: map[key]  缓存

四个指针:新前/旧前/新后/旧后

 

新增的情况:

While(新前《=新后&&旧前《=旧后){}

如果是旧节点先循环完毕,说明新节点中有要插入的节点

删除的情况:

如果是新节点先循环完毕,如果老节点中还有剩余节点,说明它们是要被删除的节点

复杂的情况(移动位置/删除):

当④新前与旧后命中的时候,旧后被命中节点(即新前指向的这个节点)标记为undefined,此时要移动节点。移动新前指向的这个节点(没被undefined前的克隆版)到老节点的旧前的前面。新前指针后移,旧后指针不变。如果都没命中,循环查找旧节点,没有找到,说明是新插入的。新插入的这个节点复制放到老节点的旧前的前面。新节点先循环完毕,如果老节点中还有剩余节点,说明它们是要被删除的节点   undefined节点表示虚拟节点已经被干掉了,但是插入到真实DOM中了