##
diff算法主要的来源是在更新组件的过程中触发的,
更新组件主要做两件事情:更新 props 和更新子节点。
- 更新 props,这里的 patchProps 函数就是在更新 DOM 节点的 class、style、event 以及其它的一些 DOM 属性等。
- 而更新子节点:
子节点 vnode 可能会有三种情况:纯文本、vnode 数组和空。
而触发diff核心算法的是新旧子节点都是数组的情况,就需要做完整的 diff 新旧子节点了,这是最复杂的情况,内部运用了核心 diff 算法。
新旧子节点都是数组时,无非就是通过更新、删除、添加和移动节点来完成。而核心 diff 算法,就是在已知旧子节点的 DOM 结构、vnode 和新子节点的 vnode 情况下,以较低的成本完成子节点的更新为目的,求解生成新子节点 DOM 的系列操作。
- diff的核心算法主要是做以下几种处理:
在整个 diff 的过程,我们需要维护几个变量:头部的索引 i、旧子节点的尾部索引 e1和新子节点的尾部索引 e2。
- 同步头部节点
同步头部节点就是从头部开始,依次对比新节点和旧节点,如果它们相同的则执行 patch 更新节点;如果不同或者索引 i 大于索引 e1 或者 e2,则同步过程结束。
i 是 2,e1 是 3,e2 是 4。
- 同步尾部节点
同步尾部节点就是从尾部开始,依次对比新节点和旧节点,如果相同的则执行 patch 更新节点;如果不同或者索引 i 大于索引 e1 或者 e2,则同步过程结束。
i 是 2,e1 是 1,e2 是 2。
判断: 新子节点有剩余要添加的新节点;
如果索引 i 大于尾部索引 e1 且 i 小于 e2,那么从索引 i 开始到索引 e2 之间,我们直接挂载新子树这部分的节点。判断:旧子节点有剩余要删除的多余节点;
如果索引 i 大于尾部索引 e2,那么从索引 i 开始到索引 e1 之间,我们直接删除旧子树这部分的节点。处理: 未知子序列。
其实无论多复杂的情况,最终无非都是通过更新、删除、添加、移动这些动作来操作节点
更新和移除旧节点:
正序遍历旧子序列,找到匹配的节点更新,删除不在新子序列中的节点,判断是否有移动节点
移动和挂载新节点
倒序的方式遍历新子序列,因为倒序遍历可以方便我们使用最后更新的节点作为锚点。在倒序的过程中,锚点指向上一个更新的节点,然后判断 newIndexToOldIndexMap[i] 是否为 0,如果是则表示这是新节点,就需要挂载它;接着判断是否存在节点移动的情况,如果存在的话则看节点的索引是不是在最长递增子序列中,如果在则倒序最长递增子序列,否则把它移动到锚点的前面。
3.0新增:最长递增子序列
其他:
Vue 3 diff 算法的主要优势是设计了 Block 的概念,在编译阶段对静态模板分析,生成 Block tree,收集动态更新的节点,然后在 patch 阶段就可以只比对 Block tree 中的节点,达到提升 diff 性能的目的,这块内容在后续编译章节会提到。而核心 diff 算法,也就是去头尾的最长递增子序列算法和双端比较算法就性能而言差别并不大。
vue3.0响应式内部实现原理?
响应式原理本质上是:当数据变化后会自动执行某个函数,映射到组件的实现就是,当数据变化后,会自动触发组件的重新渲染。
2.0
在2.0中: Object.defineProperty API 劫持数据的变化,在数据被访问的时候收集依赖,然后在数据被修改的时候通知依赖更新。
2.0中 Watcher 就是依赖,有专门针对组件渲染的 render watcher,组件在render的时候会访问模版中的数据,触发getter把render watcher作为依赖收集,并和数据建立联系,然后是通过派发通知,当我们对数据进行修改的时候,触发setter函数,通知render watcher更新,从而触发了组件的重新渲染。
3.0
Vue.js 3.0 的 reactive API 就是通过 Proxy 劫持数据,
proxy api有几个处理器函数:
- 访问对象属性会触发 get 函数;
- 设置对象属性会触发 set 函数;
- 删除对象属性会触发 deleteProperty 函数;
- in 操作符会触发 has 函数;
- 通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数。
无论命中哪个处理器函数,它都会做依赖收集和派发通知。
依赖收集:get 函数:
依赖收集发生在数据访问的阶段,这个响应式对象属性被访问的时候就会执行 get 函数,get 函数主要做了四件事情
- 判断:对传入的参数key 做了代理
判断响应式对象是否存在 __v_raw 属性,如果存在就返回这个响应式对象本身。 - 判断是不是数组。
如果传入的参数target 是数组的话,并且我们去访问了target.includes、indexOf 或者 lastIndexOf 等方法就会触发一个函数:来实现数组的这些方法,并对数组的每个元素做依赖收集。
因为一旦数组的元素被修改,数组的这几个 API 的返回结果都可能发生变化,所以我们需要跟踪数组每个元素的变化。
这里也会用到Reflect.get来求求值,对数组内的元素做依赖收集。
- 通过 Reflect.get 求值=res
求得的值如果是:对象或数组,则递归执行 reactive 把 res 变成响应式对象
- 然后会执行 track 函数收集依赖。
依赖收集的作用就是为了实现响应式的核心作用,当数据变化的时候么,可以自动执行一些函数,触发组件的自动渲染。
收集的依赖就是数据变化后执行的副作用函数。
实现:
把 target 作为原始的数据,key 作为访问的属性。我们创建了全局的 targetMap 作为原始数据对象的 Map,它的键是 target,值是 depsMap,作为依赖的 Map;这个 depsMap 的键是 target 的 key,值是 dep 集合,dep 集合中存储的是依赖的副作用函数。
所以每次 track ,就是把当前激活的副作用函数 activeEffect 作为依赖,然后收集到 target 相关的 depsMap 对应 key 下的依赖集合 dep 中。
派发通知:set 函数:
派发通知发生在数据更新的阶段 ,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性更新的时候就会执行 set 函数。set主要就做两件事情:
- 首先通过 Reflect.set 求值
- 然后通过 trigger 函数派发通知
- 通过 targetMap 拿到 target 对应的依赖集合 depsMap;
- 创建运行的 effects 集合;
- 根据 key 从 depsMap 中找到对应的 effects 添加到 effects 集合;
- 遍历 effects 执行相关的副作用函数。
所以每次 trigger 函数就是根据 target 和 key ,从 targetMap 中找到相关的所有副作用函数遍历执行一遍。
- 依据 key 是否存在于 target 上来确定通知类型,即新增还是修改。
总结:
Vue.js 3.0 在响应式的实现思路和 Vue.js 2.x 差别并不大,主要就是 劫持数据的方式改成用 Proxy 实现 , 以及收集的依赖由 watcher 实例变成了组件副作用渲染函数 。
讲讲3.0的优化?
- 3.0更推荐用户主动定义响应式对象,而不是都通过内部黑盒处理,这样用户可以更加明确哪些数据是响应式的
举个例子:
在2.0中我们只要在data、props、computed 中定义数据,Vue源码内部会在组件初始化的过程中自动把它变成就是响应式的,这是一个相对黑盒的过程。
而在created中定义的数据,不是响应式的,就只能用来共享上下文数据。
=> 在3.0中新增的 Composition API,可以利用setup函数中使用reactive API手动的变成响应式的。
- 3.0更推荐用户主动定义响应式对象,而不是都通过内部黑盒处理,这样用户可以更加明确哪些数据是响应式的
- 因为 Proxy 劫持的是对象本身,并不能劫持子对象的变化,这点和 Object.defineProperty API 一致。但是 Object.defineProperty 是在初始化阶段,即定义劫持对象的时候就已经递归执行了,而 Proxy 是在对象属性被访问的时候才递归执行下一步 reactive,这其实是一种延时定义子对象响应式的实现,在性能上会有较大的提升.