vue2源码-八、依赖收集的过程

依赖收集的过程

  • 前言

    使用真实节点替换原始节点,主要涉及以下步骤:

    1.新老节点的更新方案。

    2.虚拟节点与真实节点映射。

    3.实现新老节点的替换。

  • 依赖收集

    已经完成了Vue的两大核心部分:响应式数据和数据渲染,即完成了整个Vue的初始化流程:

    new Vue()时,执行_init初始化,通过mountComponent做组件的挂载:

    1.vm._render:调用render方法,创建新节点。

    2.vm._render:更新逻辑,将虚拟节点渲染成真实DOM

    patch:根据虚拟节点生成真实节点,新节点替换老节点

    Vue特性:当响应式数据发生变化时,会触发对应视图的更新。

    举个例子:同一数据可能被放到多个视图中(页面或者组件)所共享,比如Vuex中的数据:

    A组件,使用了数据name;

    B组件,使用了数据name

    这样,A,B两个组件就都依赖了数据name,当数据发生变化时,两个组件都会触发对应视图更新操作。

    这就需要知道数据和视图间的对应关系,从而准确触发该数据对应的视图更新操作,从而设计模式上看就是观察者模式。

    重点:depwatcher

    Vue中,依赖收集的实现使用了观察者模式:

    • watcher函数:每个组件或者页面所对应的渲染函数
    • dep属性:每个数据都具有一个dep属性,用于记录使用该数据的组件或页面的视图渲染函数watcher

    当数据发生变化时,dep属性中存放的多个watcher将会被通知,watcher通过调用自身对应的更新方法update,完成页面的重新渲染:

    • name添加属性dep:用于收集组件A和组件B的渲染逻辑watcherAwatcherB
    • watcherAwatcherB添加各自的更新方法update
    • 当数据发生变化时,通知dep中存放的watcherAwatcherB触发各自的更新方法update

    之前的内容;

    • 由于vm._update(vm._render)执行了数据渲染和更新操作
    • 所以watcher中的update方法,便触发vm._update(vm._render())重新进行数据渲染和视图更新。
    • 所以,需要将vm._update(vm._render())改造为可以通过watcher调用的方法。

    最后:

    • 数据响应式过程中,为每个属性扩展dep,用于收集watcher,在数据渲染时记录watcher
    • 当同一数据在同一视图中被多次使用时,在dep中需要对watcher进行查重,确保watcher进行查重,确保相同watcher仅记录一次。
    • 防止只要数据变化就会渲染视图的情况:当数据在视图中没有被使用时,数据的变化不应触发watcher渲染,需要在视图渲染时进行依赖收集,知道哪些数据被“真正”使用了;

dep和watcher关联

  • watcher部分

    根据上篇

    • vm._render方法:调用render方法,生成虚拟节点。
    • vm._update方法:将虚拟节点更新到页面上。

    所以就是通过执行vm._update(vm._render())就能触发视图的更新。

    vue中,数据更新的原理如下:

    • 每个数据都有一个dep属性:记录使用改数据的组件或页面的视图渲染函数watcher
    • 当数据发生变化时:dep属性中存放的多个watcher将会被通知(观察者模式)。

    这里的watcher就相当于vm._update(vm._render())

    因此,需要将视图渲染逻辑vm._update(_render()),抽取为一个可单独调用的函数。

    抽取视图更新逻辑watcher

    将视图渲染逻辑抽取成为可调用函数,包装为function:

    export function mountComponent(vm, el) {
      // 1.调用render方法产生虚拟节点虚拟DOM
      vm.$el = el;
    
      const updateComponent = () => {
        vm._update(vm._render());
      };
    
      const watcher = new Watcher(vm, updateComponent, true);
      // vm._update(vm._render()); // vm.$options.render
      // 2.根据虚拟DOM产生真实DOM
      // 3.插入el元素中
    }
    

    接下来,只要能够通过watcher来调用执行updateComponent方法,就可以触发视图更新。

    创建watcher

    “数据改变,视图更新”,所以Watcher类应从属响应式模块;

    class Watcher {
    
      constructor(vm, fn, options){
        this.vm = vm;
        this.fn = fn;
        this.options = options;
    
        this.getter = fn; // fn 为页面渲染逻辑
        this.get();       // Watcher初始化时调用页面渲染逻辑
      }
    
      get(){
        this.getter();
      }
    }
    
    export default Watcher;
    

    收集依赖的必要性

    做法:

    • 有数据响应式原理可知,当响应式数据发生变化时,就会进入Object.definePropertyset方法。
    • 那么,此时在set方法中调用视图更新逻辑vm._update(vm.render())就能触发图的更新操作。

    问题:

    • 由于所有的响应式数据被修改时都会进入到set方法,这就将会导致未被视图使用的数据发生变化时也会触发页面的更新。
    • 这种做法会触发不必要的视图更新,造成多余的性能开销。

    针对上面,就需要进行依赖收集操作,为数据创建dep用来收集渲染watcher

  • Dep部分

    创建Dep类

    • 每一个数据都有一个dep属性,用于存放对应的渲染watcher
    • 在每一个watcher中,也可能存放多个dep

    所以:

    • dep类中,需要具有一个添加watcher的方法;
    • watcher类中,也需要有一个添加dep的方法。

    dep

    // src/observe/dep.js
    
    // dep 对象的唯一 id
    let id = 0;
    
    class Dep {
    
      constructor(){
        this.id = id++;
        this.subs = [];
      }
    
      // 保存数据的渲染 watcher
      depend(){
        this.subs.push(Dep.target)
      }
    }
    
    // 静态属性,用于记录当前 watcher
    Dep.target = null;  
    
    export default Dep
    

    为data中的属性添加dep

    function defineReactive(obj, key, value) {
      observe(value);
      let dep = new Dep();  // 为每个属性添加一个 dep
    
      Object.defineProperty(obj, key, {
        get() {
          return value;
        },
    
        set(newValue) {
          if (newValue === value) return
          observe(newValue);
          value = newValue;
        }
      })
    }
    

    修改watcher

    class Watcher {
      constructor(vm, fn, cb, options){
        this.vm = vm;
        this.fn = fn;
        this.cb = cb;
        this.options = options;
    
        this.getter = fn;
        this.get();
      }
    
      get(){
        Dep.target = this;  // 在触发视图渲染前,将 watcher 记录到 Dep.target 上
        this.getter();      // 调用页面渲染逻辑
        Dep.target = null;  // 渲染完成后,清除 Watcher 记录
      }
    }
    export default Watcher
    

    在数据渲染时,如果当前数据被视图所使用,当进入Object.definePropertyget方法时,Dep.target有值且为当前watcher对象,使用当前数据的dep对象记住此渲染watcher

    function defineReactive(obj, key, value) {
    
      observe(value);
      let dep = new Dep();
    
      Object.defineProperty(obj, key, {
        get() {
          // 如果 Dep.target 有值,将当前 watcher 保存到 dep
          if(Dep.target){
            dep.depend(); 
          }
          return value;
        },
    
        set(newValue) {
          if (newValue === value) return
          observe(newValue);
          value = newValue;
        }
      })
    }
    

视图更新部分

  • 前言

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

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

    查重watcher

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

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

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

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

    因此需要设置一个id作为标识符,每次new Watcherid自增,因此作为标识对watcher实例进行查重。

      constructor(vm, fn, options) {
        this.id = id++; // 创建时递增
        this.renderWatcher = options;
        this.getter = fn; // getter意味着调用这个函数可以发生取值操作
        this.deps = []; // 后续实现计算属性和一些青理工作需要用
        this.depsId = new Set();
        this.get();
      }
    

    让watcher也记住dep

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

    let id = 0;
    class Dep {
      constructor() {
        this.id = id++; // 属性dep要收集watcher
        this.subs = []; // 这里存放着当前属性对应的watcher有哪些
      }
      depend() {
        // 这里我们不希望放重复的watcher,而且刚才只是单向的关系 dep->watcher
        // watcher记录dep
        // this.subs.push(Dep.target);
        Dep.target.addDep(this); // 让watcher记住dep
      }
      // 让dep记住watcher-在watcher中被调用
      addSub(watcher) {
        this.subs.push(watcher);
      }
    
      notify() {
        this.subs.forEach((watcher) => {
          watcher.update(); // 告诉watcher去更新
        });
      }
    }
    
    Dep.target = null;
    
    export default Dep;
    

    这里,如果互相记住,watcher中要对dep查重,dep中也要对watcher查重;

    用这个方法,使depwatcher关联起来,只需要判断一次就可以了。

    import Dep from "./dep";
    
    let id = 0;
    
    // 1)当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target中
    // 2)调用_render()会取值走到get上
    
    // 不同的组件有不同的watcher,目前只有一个渲染根实例的
    class Watcher {
      constructor(vm, fn, options) {
        this.id = id++;
        this.renderWatcher = options;
        this.getter = fn; // getter意味着调用这个函数可以发生取值操作
        this.deps = []; // 后续实现计算属性和一些青理工作需要用
        this.depsId = new Set();
        this.get();
      }
    
      addDep(dep) {
        let id = dep.id;
        // dep查重
        if (!this.depsId.has(id)) {
          // 让watcher记住dep
          this.deps.push(dep);
          this.depsId.add(id);
          // 让dep也记住watcher
          dep.addSub(this); // watcher已经记住了dep并且去重了,此时让dep也记住了watcher
        }
      }
    
      get() {
        Dep.target = this; // 静态属性就只有一份
        this.getter(); // 会去vm上取值
        Dep.target = null; // 渲染完之后就清空
      }
      update() {
        // this.get(); // 重新渲染
        queueWatcher(this); // 先把当前的watcher暂存起来
      }
      run() {
        this.get();
      }
    }
    

    这样实现,会让depwatcher保持一种共存关系。

    如果watcher中存在dep,那么dep中一定存在watcher,反之,亦然。

    所以,只需要判断一次,就能够完成depwatcher查重。

    数据改变触发视图更新

    当视图改变的时候,会进入Object.definePropertyset方法。

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

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

    Dep中添加notify方法:

      notify() {
        this.subs.forEach((watcher) => {
          watcher.update(); // 告诉watcher去更新
        });
      }
    

    Watcher中添加update方法

      get(){
        Dep.target = this;
        this.getter();
        Dep.target = null;
      }
      // 执行视图渲染逻辑
      update(){
        this.get();
      }
    

    结尾

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

    视图初始化:

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

    数据更新时:

    • 当数据发生改变,会进入Object.definePropertyset方法。
    • set方法中,使dep中收集的全部watcher执行视图渲染操作watcher.get()
    • 在视图渲染前(this.getter方法执行前),通过dep.target记录当前渲染的watcher
    • 重复视图初始化流程
posted @   楸枰~  阅读(170)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示