【手写 Vue2.x 源码】第二十八篇 - diff算法-问题分析与patch优化
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
一前言
首先对 6 月更文内容做一下简单的回顾
- Vue2.x 源码环境的搭建
- Vue2.x 初始化流程介绍
- 对象的单层、深层劫持
- 数组的单层、深层劫持
- 数据代理的实现
- 对象、数组数据变化的观测
- Vue 数据渲染流程介绍
- 模板生成 AST 语法树
- AST 语法树生成 render 函数
- render 函数生成 Vnode
- 根据 Vnode 创建真实节点
- 真实节点替换原始节点
- Vue2.x 依赖收集的流程分析
- 依赖收集和视图更新流程dep 和 watcher 关联
- 异步更新流程说明
- 数组的依赖收集
- Vue 生命周期和 Mixin 的实现
本篇开始继续Vue2.x源码的diff算法部分
二当前版本存在的问题
1初始化与更新流程分析
Vue 初始化会在挂载时调用 mountComponent 方法
// src/init.js
Vue.prototype.$mount = function (el) {
const vm = this;
const opts = vm.$options;
el = document.querySelector(el); // 获取真实的元素
vm.$el = el; // vm.$el 表示当前页面上的真实元素
// 如果没有 render, 看 template
if (!opts.render) {
// 如果没有 template, 采用元素内容
let template = opts.template;
if (!template) {
// 拿到整个元素标签,将模板编译为 render 函数
template = el.outerHTML;
}
let render = compileToFunction(template);
opts.render = render;
}
mountComponent(vm);
}
在 mountComponent 方法中会创建一个 watcher
// src/lifeCycle.js
export function mountComponent(vm) {
let updateComponent = ()=>{
vm._update(vm._render());
}
// 当视图渲染前调用钩子: beforeCreate
callHook(vm, 'beforeCreate');
// 渲染 watcher 每个组件都有一个 watcher
new Watcher(vm, updateComponent, ()=>{
// 视图更新后调用钩子: created
callHook(vm, 'created');
},true)
// 当视图挂载完成调用钩子: mounted
callHook(vm, 'mounted');
}
数据更新时会进入 set 方法
// src/observe/index.js
function defineReactive(obj, key, value) {
// childOb 是数据组进行观测后返回的结果内部 new Observe 只处理数组或对象类型
let childOb = observe(value);// 递归实现深层观测
let dep = new Dep(); // 为每个属性添加一个 dep
Object.defineProperty(obj, key, {
// get方法构成闭包取obj属性时需返回原值value
// value会查找上层作用域的value所以defineReactive函数不能被释放销毁
get() {
if(Dep.target){
// 对象属性的依赖收集
dep.depend();
// 数组或对象本身的依赖收集
if(childOb){ // 如果 childOb 有值说明数据是数组或对象类型
// observe 方法中会通过 new Observe 为数组或对象本身添加 dep 属性
childOb.dep.depend(); // 让数组和对象本身的 dep 记住当前 watcher
if(Array.isArray(value)){// 如果当前数据是数组类型
// 可能数组中继续嵌套数组需递归处理
dependArray(value)
}
}
}
return value;
},
set(newValue) { // 确保新对象为响应式数据如果新设置的值为对象需要再次进行劫持
console.log("修改了被观测属性 key = " + key + ", newValue = " + JSON.stringify(newValue))
if (newValue === value) return
observe(newValue); // observe方法如果是对象会 new Observer 深层观测
value = newValue;
dep.notify(); // 通知当前 dep 中收集的所有 watcher 依次执行视图更新
}
})
}
此时会调用 dep.notify() 通知对应的 watcher 调用 update 方法做更新
class Dep {
constructor(){
this.id = id++;
this.subs = [];
}
// 让 watcher 记住 dep查重再让 dep 记住 watcher
depend(){
Dep.target.addDep(this);
}
// 让 dep 记住 watcher - 在 watcher 中被调用
addSub(watcher){
this.subs.push(watcher);
}
// dep 中收集的全部 watcher 依次执行更新方法 update
notify(){
this.subs.forEach(watcher => watcher.update())
}
}
在 Watcher 类的 update 方法中调用了 queueWatcher 方法将 watcher 进行缓存、去重操作
// src/observe/watcher.js
class Watcher {
constructor(vm, fn, cb, options){
this.vm = vm;
this.fn = fn;
this.cb = cb;
this.options = options;
this.id = id++; // watcher 唯一标记
this.depsId = new Set(); // 用于当前 watcher 保存 dep 实例的唯一id
this.deps = []; // 用于当前 watcher 保存 dep 实例
this.getter = fn; // fn 为页面渲染逻辑
this.get();
}
addDep(dep){
let did = dep.id;
// dep 查重
if(!this.depsId.has(did)){
// 让 watcher 记住 dep
this.depsId.add(did);
this.deps.push(dep);
// 让 dep 也记住 watcher
dep.addSub(this);
}
}
get(){
Dep.target = this; // 在触发视图渲染前将 watcher 记录到 Dep.target 上
this.getter(); // 调用页面渲染逻辑
Dep.target = null; // 渲染完成后清除 Watcher 记录
}
update(){
console.log("watcher-update", "查重并缓存需要更新的 watcher")
queueWatcher(this);
}
run(){
console.log("watcher-run", "真正执行视图更新")
this.get();
}
}
queueWatcher 方法
// src/observe/scheduler.js
/**
* 将 watcher 进行查重并缓存最后统一执行更新
* @param {*} watcher 需更新的 watcher
*/
export function queueWatcher(watcher) {
let id = watcher.id;
if (has[id] == null) {
has[id] = true;
queue.push(watcher); // 缓存住watcher,后续统一处理
if (!pending) { // 等效于防抖
nextTick(flushschedulerQueue);
pending = true; // 首次进入被置为 true使微任务执行完成后宏任务才执行
}
}
}
/**
* 刷新队列执行所有 watcher.run 并将队列清空
*/
function flushschedulerQueue() {
// 更新前,执行生命周期beforeUpdate
queue.forEach(watcher => watcher.run()) // 依次触发视图更新
queue = []; // reset
has = {}; // reset
pending = false; // reset
// 更新完成,执行生命周期updated
}
flushschedulerQueue 方法执行时会调用 watcher 的 run 方法
run 内部调用watcher 的 get 方法get方法中记录当前 watcher 并调用 getter
this.getter 即 watcher 初始化时传入的视图更新方法 fn
即 updateComponent 视图渲染逻辑
// src/lifeCycle.js
export function mountComponent(vm) {
let updateComponent = ()=>{
vm._update(vm._render());
}
// 当视图渲染前调用钩子: beforeCreate
callHook(vm, 'beforeCreate');
// 渲染 watcher 每个组件都有一个 watcher
new Watcher(vm, updateComponent, ()=>{
// 视图更新后调用钩子: created
callHook(vm, 'created');
},true)
// 当视图挂载完成调用钩子: mounted
callHook(vm, 'mounted');
}
这样就会再次执行 updateComponent->vm._render
会根据当前的最新数据重新生成虚拟节点并且再次调用 update
// src/lifeCycle.js
export function lifeCycleMixin(Vue){
Vue.prototype._update = function (vnode) {
const vm = this;
// 传入当前真实元素vm.$el虚拟节点vnode返回新的真实元素
vm.$el = patch(vm.$el, vnode);
}
}
附一张 Vue 流程图
2问题分析与优化思路
update 方法会使用新的虚拟节点重新生成真实 dom并替换掉原来的dom
在 Vue 的实现中会做一次 diff 算法优化尽可能复用原有节点以提升渲染性能
所以patch方法即为重点优化对象
当前的 patch 方法仅考虑了初始化的情况还需要处理更新操作
patch 方法需要对新老虚拟节点进行一次比对尽可能复用原有节点以提升渲染性能
- 首次渲染根据虚拟节点生成真实节点替换掉原来的节点
- 更新渲染生成新的虚拟节点并和老的虚拟节点进行对比再渲染
三模拟新老虚拟节点比对
模拟两个虚拟节点的比对
- 生成虚拟节点1
- 生成虚拟节点2
- 调用 patch 方法进行新老虚拟节点比对
1生成第一个虚拟节点
首次生成虚拟节点后直接进行挂载
// src/index.js
// 1,生成第一个虚拟节点
// new Vue会对数据进行劫持
let vm1 = new Vue({
data(){
return {name:'Brave'}
}
})
// 将模板 render1 生成为 render 函数
let render1 = compileToFunction('<div>{{name}}</div>');// 调用 compileToFunction将模板生成 render 函数会解析模板最终包成一个 function
// 调用 render 函数产生虚拟节点
let oldVnode = render1.call(vm1) // oldVnode:第一次的虚拟节点
// 将虚拟节点生成真实节点
let el1 = createElm(oldVnode);
// 将真实节点渲染到页面上
document.body.appendChild(el1);
2生成第二个虚拟节点
// src/index.js
// 2生成第二个虚拟节点
let vm2 = new Vue({
data(){
return {name:'BraveWang'}
}
})
let render2 = compileToFunction('<p>{{name}}</p>');
let newVnode = render2.call(vm2);
// 延迟看效果初始化完成显示 el11 秒后移除 el1 显示 el2
setTimeout(()=>{
let el2 = createElm(newVnode);
document.body.removeChild(el1);
document.body.appendChild(el2);
}, 1000);
export default Vue;
3patch 方法比对新老虚拟节点
patch 方法将新老虚拟节点进行一次比对尽可能复用原有节点以提升渲染性能
节点复用逻辑标签名和key相同即判定可复用
// 如果标签名一样就复用
// 3,调用 patch 方法进行比对
setTimeout(()=>{
// 比对新老虚拟节点的差异尽可能复用原有节点以提升渲染性能
patch(oldVnode,newVnode);
}, 1000);
4查看新老节点
let vm = new Vue({
data(){
return {name:'Brave'}
}
})
let render = compileToFunction('<div>{{name}}</div>');/
let oldVnode = render.call(vm)
let el = createElm(oldVnode);
document.body.appendChild(el);
// 数据更新后再次调用 render 函数
vm.name = 'BraveWang';
let newVnode = render.call(vm);
setTimeout(()=>{
patch(oldVnode, newVnode);
}, 1000);
查看生成的两个真实节点
接下来开始改造patch方法以实现节点对比和复用
四patch 方法优化
1当前的 patch 方法
当前的 patch 方法仅考虑到初始化的情况所以每次都会直接替换掉老节点
export function patch(el, vnode) {
// 1根据虚拟节点创建真实节点
const elm = createElm(vnode);
// 2使用真实节点替换掉老节点
// 找到元素的父亲节点
const parentNode = el.parentNode;
// 找到老节点的下一个兄弟节点nextSibling 若不存在将返回 null
const nextSibling = el.nextSibling;
// 将新节点elm插入到老节点el的下一个兄弟节点nextSibling的前面
// 备注若nextSibling为 nullinsertBefore 等价与 appendChild
parentNode.insertBefore(elm, nextSibling);
// 删除老节点 el
parentNode.removeChild(el);
return elm;
}
2改造 patch 方法
当前patch方法的两个入参分别是元素和虚拟节点
将虚拟节点创建为真实节点直接进行元素替换完成数据更新
现在需要将新老虚拟节点进行比对尽可能复用原有节点提高渲染性能
所以patch方法需改造为入参是新老虚拟节点oldVnode、vnode
当前的 patch 方法仅考虑到初始化的情况
现在还需要支持数据更新的情况
export function patch(oldVnode, vnode) {
const elm = createElm(vnode);
const parentNode = oldVnode.parentNode;
parentNode.insertBefore(elm, oldVnode.nextSibling);
parentNode.removeChild(oldVnode);
return elm;
}
问题初渲染 OR 更新渲染
通过判断 oldVnode.nodeType 节点类型是否为真实节点
是真实节点需要进行新老虚拟节点比对
非真实节点即为真实dom时进行初渲染逻辑
改造完成后的 patch 方法
export function patch(oldVnode, vnode) {
const isRealElement = oldVnode.nodeType;
if(isRealElement){// 真实节点走老逻辑
const elm = createElm(vnode);
const parentNode = oldVnode.parentNode;;
parentNode.insertBefore(elm, oldVnode.nextSibling);
parentNode.removeChild(oldVnode);
return elm;
}else{// 虚拟节点做 diff 算法新老节点比对
console.log(oldVnode, vnode)
}
}
后边开始针对更新渲染的情况进行新老虚拟节点的比对即 diff 算法逻辑
五结尾
本篇diff算法问题分析与patch方法改造主要涉及以下几点
- 初始化与更新流程分析
- 问题分析与优化思路
- 新老虚拟节点比对模拟
- patch 方法改造
下篇diff 算法-节点比对
维护日志
20210802添加“四patch 方法优化”添加 Vue 执行流程图更新文章标题和摘要
20210806调整布局与格式修改部分错别字和歧义语句