【手写 Vue2.x 源码】第二十三篇 - 依赖收集 - 视图更新部分

438 阅读4分钟

一,前言

上篇,主要介绍了依赖收集过程中 dep 和 watcher 关联:

利用 js 单线程特性,在 Watcher 类中 get 方法即将触发视图更新前,
利用全局的类静态属性 Dep.target 记录 Watcher 实例
并且,在视图渲染的取值过程中,在 Object.defineProperty 的 get 方法中,让数据的 dep 记住渲染watcher
从而,实现了 dep 与 watcher 相关联,只有参与视图渲染的数据发生变化才会触发视图的更新

本篇,继续依赖收集的视图更新部分


二,实现视图更新逻辑

1.查重 watcher

问题:同一数据在视图中多次使用会怎样?

按照当前逻辑,同一数据在一个视图中被多次使用时,相同watcher会在dep中会被重复保存多次:

<div id="app">
  <li>{{name}}</li>
  <li>{{name}}</li>
  <li>{{name}}</li>
</div>

这时,在name属性的dep中,将会保存三个相同的渲染watcher,所以,需要对watcher进行查重:

`Dep`类的做法类似:为`Watcher`添加一个 id 作为唯一标识,
每次`new Watcher``id`自增,以此作为标识对`watcher`实例进行查重;
// src/observe/watcher.js

let id = 0;            // watcher 的唯一标识

class Watcher {

  constructor(vm, fn, cb, options){
    this.vm = vm;
    this.fn = fn;
    this.cb = cb;
    this.options = options;
    this.id = id++;    // 创建时自增  

    this.getter = fn;
    this.get();
  }
  
  get(){
    Dep.target = this;
    this.getter();
    Dep.target = null;
  }
}

export default Watcher

2. 让 watcher 也记住 dep

前面,让数据的dep记住了渲染watcher 同样的,watcher也有必要记住dep

// src/observe/dep.js

let id = 0;

class Dep {

  constructor(){
    this.id = id++;
    this.subs = [];
  }
  
  // 让 watcher 记住 dep(查重),再让 dep 记住 watcher
  depend(){
    // 相当于 watcher.addDep:使当前 watcher 记住 dep
    Dep.target.addDep(this);  
  }
  
  // 让 dep 记住 watcher - 在 watcher 中被调用
  addSub(watcher){
    this.subs.push(watcher);
  }
}

// 静态属性,用于记录当前 watcher
Dep.target = null;

export default Dep;

那么,为什么要这样实现呢?

如果要互相记住,watcher 中要对 dep 做查重;dep 中也要对 watcher 做查重;
用这种方法,使 dep 和 watcher 关联在一起后,只需要判断一次就可以了
// src/observe/watcher.js

import Dep from "./dep";

let id = 0;

class Watcher {

  constructor(vm, fn, cb, options){
    this.vm = vm;
    this.fn = fn;
    this.cb = cb;
    this.options = options;
    
    this.id = id++;
    this.depsId = new Set();  // 用于当前 watcher 保存 dep 实例的唯一id
    this.deps = [];           // 用于当前 watcher 保存 dep 实例
    this.getter = 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;
    this.getter();
    Dep.target = null;
  }
}

export default Watcher;

这种实现方式,会让depwatcher保持一种“共存”关系:

如果 watcher 中存过 dep;那么 dep 中一定存过 watcher ;
如果 watcher 中没存过 dep;那么 dep 中一定也没存过 watcher ;
所以,只需要判断一次,就能够完成 dep 和 watcher 的查重;

3. 数据改变,触发视图更新

当视图更新时,会进入Object.definePropertyset方法,

因此,需要在set方法中,通知dep中所有收集的watcher执行视图更新方法:

// src/observe/index.js

function defineReactive(obj, key, value) {

  observe(value);
  let dep = new Dep();  // 为每个属性添加一个 dep
  
  Object.defineProperty(obj, key, {
  
    get() {
      if(Dep.target){
        dep.depend();
      }
      return value;
    },
    
    set(newValue) {
      if (newValue === value) return
      observe(newValue);
      value = newValue;
      // 通知当前 dep 中收集的所有 watcher 依次执行视图更新
      dep.notify(); 
    }
  })
}

4. Dep 中添加 notify 方法:

// src/observe/dep.js

let id = 0;

class Dep {

  constructor(){
    this.id = id++;
    this.subs = [];
  }
  
  depend(){
    Dep.target.addDep(this);  
  }
  
  addSub(watcher){
    this.subs.push(watcher);
  }
  
  // dep 中收集的全部 watcher 依次执行更新方法 update
  notify(){
    this.subs.forEach(watcher => watcher.update())
  }
}

export default Dep;

5. Watcher 中添加 update 方法:

// src/observe/watcher.js

import Dep from "./dep";

let id = 0;
class Watcher {

  constructor(vm, fn, cb, options){
    this.vm = vm;
    this.fn = fn;
    this.cb = cb;
    this.options = options;

    this.id = id++;
    this.depsId = new Set();
    this.deps = [];
    this.getter = fn;
    this.get();
  }
  
  addDep(dep){
    let did = dep.id;
    if(!this.depsId.has(did)){
      this.depsId.add(did);
      this.deps.push(dep);
      dep.addSub(this); 
    }
  }
  
  get(){
    Dep.target = this;
    this.getter();
    Dep.target = null;
  }
  
  // 执行视图渲染逻辑
  update(){
    this.get();
  }
}

export default Watcher;

6. 问题

多次频繁更新同一数据,会使视图频繁进行重新渲染操作


let vm = new Vue({
  el: '#app',
  data() {
    return { name: "Brave" , age: 123}
  }
}); 

vm.name = "Brave Wang";
vm.name = "Brave";
vm.name = "Brave Wang";
vm.name = "Brave";
vm.name = "Brave Wang";
vm.name = "Brave";

虽然name的值变化了6次,但数据最终并没有发生变化,这里就需要改为异步更新的机制;


三,结尾

本篇,介绍了 Vue依赖收集的视图更新部分,主要涉及以下几点:

视图初始化时:

  • render方法中会进行取值操作,进入 Object.defineProperty 的 get 方法
  • get 方法中为数据添加 dep,并记录当前的渲染 watcher
  • 记录方式:watcher查重并记住 dep,dep 再记住 watcher

数据更新时:

  • 当数据发生改,会进入 Object.defineProperty 的 set 方法
  • 在 set 方法中,使 dep 中收集的全部 watcher 执行视图渲染操作 watcher.get()
  • 在视图渲染前(this.getter方法执行前),通过 dep.target 记录当前的渲染 watcher
  • 重复视图初始化流程

下一篇:Vue 异步更新


维护日志:

  • 20210802:修改文章摘要;
  • 20230203:添加部分内容描述,添加内容中的代码高亮;
  • 20230207:优化部分内容描述,更新文章摘要;