【vue2原理】-第四篇,dep 和 watcher 的关联

64 阅读4分钟

介绍

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

一,Watcher 部分

1,watcher 的本质

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

本质上,vm._update(vm._render()) 就可以触发视图的更新

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

vm.name = "Brave Wang";   // 数据改变
vm._update(vm._render()); // 视图更新

在 Vue 中,数据更新:

  • 每个数据有一个 dep 属性:记录使用该数据的组件或页面的视图渲染函数 watcher

  • 当数据发生变化时,dep 属性中存放的多个 watcher 将会被通知,这里是观察者模式

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

2,抽取视图更新逻辑 watcher

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

export function mountComponent(vm) {  
    // 抽取成为一个可被调用的函数  
    let updateComponent = ()=>{    
        vm._update(vm._render());    
    }  
    updateComponent();
}

最终的目标是:让**updateComponent**方法通过 watcher 被调用

3,创建 Watcher 类

数据改变,视图更新,所以 watcher 应所属于响应式模块

创建 watcher 类:

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

将页面更新逻辑 updateComponent 注入到 Watcher 类中,

再考虑如何通过 watcher 调用页面更新方法 updateComponent

export function mountComponent(vm) {  
     let updateComponent = ()=>{    
        vm._update(vm._render());    
     }
      // 渲染 watcher :每个组件都有一个 watcher  
      new Watcher(vm, updateComponent, ()=>{    
        console.log('Watcher-update')  
      },true)
 }

4,依赖收集的必要性

数据改变时,会被劫持进入 Object.defineProperty 的 set 方法

那么,如果在此时调用了视图更新逻辑,是不是就可以做到“数据变化,视图更新”了?

Object.defineProperty(obj, key, {    
    get() {      
        return value;    
    },   
    set(newValue) {      
        if (newValue === value) return      
        vm._update(vm._render());  // 当数据变化时,触发视图更新      
        observe(newValue);     
        value = newValue;    
    }
})

这样做,虽然可以实现视图的更新

但是,有一个严重的问题:

  • 此时,由于所有的响应式数据被修改时都会进入 set 方法,
  • 导致未被视图使用的数据变化也会触发页面的更新,
  • 即这种做法会触发不必要的视图更新

所以,在视图渲染过程中,被使用的数据需要被记录下来,并且只针对这些数据的变化触发视图更新

这就需要做依赖收集,需要创建为属性创建 dep 用来收集渲染 watcher

二,Dep 部分

1,创建 Dep 类

前面说过:

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

所以,Dep 类中要有一个添加 watcher 的方法;Watcher 类中 也要有一个添加 dep 的方法;

当数据变化时,通知数据 dep 属性中的所有 watcher 执行视图更新,应用了观察者模式

为了标识 Dep 的唯一性,每次 new Dep 时添加一个唯一 id;

let id = 0;
class Dep {  
    constructor(){    
        this.id = id++;    
        this.subs = [];  
    }  // 保存数据的渲染 watcher  
    
    depend(){    
        this.subs.push(Dep.target)  
    }
}

Dep.target = null;  // 静态属性

export default Dep

2,为属性添加 dep 属性

Object.defineProperty 时会为每个数据添加属性,在此时为属性添加 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 中的 get 方法,即**vm._update(vm._render())**

此时,利用 JS 的单线程特性,在即将进行渲染前,记录当前渲染 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

在视图渲染的过程中,将会触发数据的取值,如:vm.name

此时,进入 Object.defineProperty 中 get 方法

所以,如果 get 方法中 Dep.target 有值(即当前 watcher),就让数据的 dep 记住渲染 watcher

function defineReactive(obj, key, value) {  
    observe(value);  
    let dep = new 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,未参与视图渲染的数据更新时,不会触发视图更新

总结

视图初始化时:

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

数据更新时:

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