手写vue-diff算法(一)

Vue的初始化涉及创建实例、模板编译、依赖收集和渲染。在数据更新时,setter会触发依赖的watcher执行更新,通过patch方法进行DOM的替换。文章还介绍了Vue的diff算法在更新渲染中的作用,以及patch方法的优化目标——尽可能复用原有节点以提升性能。
摘要由CSDN通过智能技术生成

Vue初始化流程

1.Vue流程图

在这里插入图片描述
Vue流程图:

  • Vue的初始化流程,默认会创建一个Vue实例,执行初始化、挂载、模板编译操作,模板被编译成为render函数;
  • render函数初始化时会执行取值操作,从而进入getter方法对当前组件进行依赖收集,收集渲染Watcher
  • 当用户修改数据时,进入setter方法就会通知对应的渲染Watcher执行更新操作;
  • 当前,视图更新操作的实现,是通过patch方法直接进行替换完成的,野蛮且暴力;
// src/lifeCycle.js

export function lifeCycleMixin(Vue){
  
  Vue.prototype._update = function (vnode) {
    const vm = this;
    // 生成新的真实节点,直接将老节点全部替换掉,可以做性能优化
    vm.$el = patch(vm.$el, vnode);
  }
}

2.初始化与更新流程分析

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');
}

当数据更新时,会进入defineProperty的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方法更新视图:

// src/obseve/dep.js

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);
  }
}

update方法会使用新的虚拟节点重新生成真实dom,并替换掉原来的dom

在Vue的实现中,会做一次diff算法优化:尽可能复用原有节点,以提升渲染性能

所以,patch方法即为重点优化对象:

  • 当前的 patch 方法,仅考虑了初始化的情况,还需要处理更新操作,

  • patch 方法需要对新老虚拟节点进行一次比对,尽可能复用原有节点,以提升渲染性能;

  • 首次渲染,根据虚拟节点生成真实节点,替换掉原来的节点;

  • 更新渲染,生成新的虚拟节点,并与老的虚拟节点进行对比,再渲染;

实现diff算法

1.模拟虚拟节点对比

// diff算法是一个平级比较的过程,父亲和父亲比,儿子和儿子比
// 测试用 
let render1 = compileToFunction(`<ul  a="1" style="color:blue">
    <li key="a">a</li>
    <li>b</li>
    <li>c</li>
    <li key="d">d</li>
</ul>`);
let vm1 = new Vue({ data: { name: 'zf' } })
let prevVnode = render1.call(vm1)
let el = createElm(prevVnode)
document.body.appendChild(el)
let render2 = compileToFunction(`<ul  a="1"  style="color:red;">
    <li key="e">e</li>
    <li>m</li>
    <li>p</li>
    <li key="q">q</li>
    
</ul>`);
let vm2 = new Vue({ data: { name: 'zf' } })
let nextVnode = render2.call(vm2)
let el2 = createElm(nextVnode)

2.调用patch优化

setTimeout(() => {
  patch(prevVnode, nextVnode)
}, 1000)

3.目前patch方法

3.1当前版本:

patch方法写的是初渲染流程,仅考虑初始化情况,,直接将新节点替换掉老节点
通过oldVnode.nodeType节点类型判断,如果为真实节点,执行初渲染流程,如果是非真实节点,执行更新逻辑

export function patch(oldVNode, vnode) {
    // 写的是初渲染流程
    const isRealElement = oldVNode.nodeType;
    if (isRealElement) {
        const elm = oldVNode; // 获取真实元素
        const parentElm = elm.parentNode; // 拿到父元素
        let newElm = createElm(vnode);
        parentElm.insertBefore(newElm, elm.nextSibling);
        parentElm.removeChild(elm); // 删除老节点

        return newElm;
    } else {
        // diff算法
        
    } 
}

3.2实现目标

使用diff算法,尽可能复用老节点

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值