美文网首页
【手把手教你搓Vue响应式原理】(五) Watcher 与 De

【手把手教你搓Vue响应式原理】(五) Watcher 与 De

作者: 辉夜真是太可爱啦 | 来源:发表于2022-02-14 20:50 被阅读0次

    大家好,我是 辉夜真是太可爱啦 。这是我最近在写的【手把手教你搓Vue响应式原理】系列,本文将一步步地为你解开vue响应式原理的面纱。由于本人也是在写这篇文章的过程中不断试错,不断学习改进的,所以,本文同样很适合和我一样的初学者。和 Vue 的设计理念如出一辙,那就是渐进增强

    上文链接

    【手把手教你搓Vue响应式原理】(一)初识Vue响应式

    【手把手教你搓Vue响应式原理】(二)深度监测对象全部属性

    【手把手教你搓Vue响应式原理】(三)observe 以及 ob

    【手把手教你搓Vue响应式原理】(四) 数组的响应式处理

    前言

    之前已经将数据劫持已经全部完成了。

    那么,接下来,主要的要点就是在于两点,依赖收集和触发依赖更新。

    它的意义主要在于控制哪些地方使用了这个变量,然后,按照最小的开销来更新视图

    首先,要先明白,依赖是什么,比方说在我们的模板中有 {{a}} ,那么,这个地方就有对于变量 a 的依赖。

    在模板编译的时候,就会触发 a 变量的 getter

    然后,当我们执行 a++; 的时候,那么,我们就要触发依赖的更新,当初模板中 {{a}} 的地方,就要更新,是吧!

    所以,我们都是getter 中收集依赖,在 setter 中触发依赖更新

    这一节的内容,主要就是用来专门讲清楚这两件事情。

    依赖收集和派发更新

    依赖收集和触发依赖更新主要由两个类来完成, DepWatcher

    image.png

    DepWatcher 在设计模式中,就是发布-订阅者的模式。

    而依赖,你可以理解为所谓的订阅者。

    • Dep

    Dep 说白了就是发布者,它的工作就是依赖管理,要知道哪些地方用到了这个变量,可能用到这个变量的地方有很多,所以,它会有多个订阅者。

    然后,每个变量都应该有属于自己的 Dep ,因为每个变量所在的依赖位置是不一样的,所以他们的订阅者也不一样。

    然后在变量更新之后,就去通知所有的订阅者(Watcher),我的变量更新了,你们该触发视图更新了。

    • Watcher

    Watcher 说白了就是订阅者,它接受 Dep 发过来的更新通知之后,就去执行视图更新了。

    它其实就是所谓的 watch 监听器,变量改变之后,执行一个回调函数。

    Dep

    初始化我们的 Dep 类

    我们先按照图例来创建我们的 Dep

    根据我们的需求:

    1. 首先,它要在初始化的时候,新建一个 subs 数组,用来存储依赖,也就是 Watcher 的实例
    class Dep{
      constructor() {
        // 用数组存储自己的订阅者   subs 是 subscribes 订阅者的意思
        // 这个数组里放的是 Watcher 的实例
        this.subs=[]
      }
    }
    
    1. 它需要有一个 depend() 方法,用于添加依赖,也就是将 Watcher 实例往 subs 数组中 push
    class Dep{
      // 添加依赖
      depend(){
        // 判断当前是否有需要监听的目标,Dep.target 会被 Wacher 赋值
        if(Dep.target){
          // 将监听的目标推进 subs 数组
          this.subs.push(Dep.target);
        }
      }
    }
    
    1. 它需要有一个 notify() 方法,用于通知 Wacher 数据更新了,调用 Wacher 的 update() 方法
    class Dep{
      // 通知所有订阅者
      notify(){
        // 浅克隆一份
        const subs=this.subs.slice();
        // 遍历
        for(let i=0,l=subs.length;i<l;i++){
          // 逐个更新
          subs[i].update();
        }
      }
    }
    

    使用我们的 Dep 类

    1. 每个属性都要有自己的 Dep

    Dep 我们在前面也说了,每个属性都应该有它自己的 Dep ,用来管理依赖。

    所以,首先,如果我们在 Observer 中创建 Dep,那不就可以了。毕竟 Observer 会遍历到每一个对象。

    class Observer{
      constructor(obj){
        this.dep=new Dep();
        // ...
      }
    }
    
    1. 在 getter 中收集依赖

    所以,很明显,我们可以在 defineReactive 的 get 中收集依赖

    因为有了 if(Dep.target) 的判断,所以,只有绑定 Watcher 的变量触发 getter 时,才会添加依赖

    function defineReactive(obj,key,val) {
      let dep=new Dep();
      let childOb;
      // 判断当前入参个数,两个的话直接返回当前层的对象
      if(arguments.length===2){
        val=obj[key];
        childOb = observe(val)
      }
      Object.defineProperty(obj,key,{
        get(){
          // Dep.target 是我们弄的唯一标识,当有这个标识的时候,添加依赖
          if(Dep.target){
            // 添加依赖
            dep.depend();
            // 如果有子属性,也要将它加入依赖
            if(childOb){
              // 给子属性添加依赖
              childOb.dep.depend();
            }
          }
          return val;
        },
      })
    }
    

    这个 Dep.target 其实就是 Watcher 的实例

    image.png
    1. 在 setter 中触发依赖更新

    所以,很明显,我们可以在 defineReactive 的 set 中收调用 notify() 方法告知 Watcher 实例,数据更新了。

    function defineReactive(obj,key,val) {
      let dep=new Dep();
      let childOb;
      // 判断当前入参个数,两个的话直接返回当前层的对象
      if(arguments.length===2){
        val=obj[key];
        childOb = observe(val)
      }
      Object.defineProperty(obj,key,{
        // ...
        set(newValue){
          val=newValue;
          childOb = observe(val)
          // notify 切忌 val=newValue 之后,不然在 callback 回调中一直是旧值
          dep.notify();
        }
      })
    }
    

    至此, Dep 的所有职责,我们已经帮它完成了。

    其实照道理应该有一个删除依赖,我们这里就不再扩展了。

    Watcher

    初始化我们的 Watcher 类

    首先, Watcher 实例应该大家会相对而言更加好理解点,因为,我们有一个 watch 侦听器,大家一定都很熟悉,这两个其实一样。

    我们先按照图例来创建我们的 Watcher

    根据我们的需求:

    1. 首先,它要在初始化的时候,需要传入目标对象 target , 属性名 expression , 回调函数 callback
    class Watcher{
      // target 目标对象
      // expression 属性名
      // callback 回调函数
      // value 属性的值
      constructor(target,expression,callback) {
        this.target=target;
        // parsePath 为一个高阶函数
        this.getter=parsePath(expression);
        this.callback=callback;
        // get为我们之后要写的获取值的方法
        this.value=this.get();
      }
    }
    

    这个 parsePath 需要单独拎出来说一下,比方说我们现在有这么一个对象

    let a={
      b:{
        c:{
          d:10
        }
      }
    }
    

    我们要监听到 a.b.c.d ,所以,我们需要下面的这种格式

    new Watcher(a,'b.c.d',val=>{
      console.log('ok啦',val);
    })
    

    所以,这个 get 很明显就有点难度了。 我们需要通过循环 拿到 a.b 然后 .c 然后 .d。

    我们将这个方法命名为 parsePath

    function parsePath(str){
      let segments = str.split('.');
      return obj=> {
          for(let i=0;i<segments.length;i++){
            if(!obj) return;
            obj=obj[segments[i]];
          }
          return obj;
      }
    }
    

    入参接受我们的 b.c.d ,我们可以看到 第一句执行之后 segments=['b','c','d'] ,然后进行第二层,这是返回了一个方法,按照循环,那就是 obj=obj.b => obj=obj.c => obj=obj.d ,所以,就是返回一个对象的 obj.b.c.d,相当于是遍历字符串中的属性树。

    1. 它需要有一个 get() 方法,用于获取当前的值,并将它更新,然后 return 返回
    class Watcher{
      // 获取当前的值,并将它更新,然后 return 返回
      get(){
        // 进入依赖收集阶段,将 Dep.target 设为 Watcher 实例本身
        Dep.target=this;
    
        // 当前对象
        const obj=this.target;
        let value;
        // 当对象不再使用的时候,我们需要将它清空
        try{
          value=this.getter(obj)
        }finally {
          Dep.target=null;
        }
        this.value=value
        return value;
      }
    }
    
    1. 它需要有一个 update() 方法,用于执行数据触发更新之后,保存新的值和旧的值,将它返回给 callback 回调函数
    class Watcher{
      // Dep发过来的通知,当前变量更新了,我们返回一个更新之后的回调函数
      update(){
        // this.value 由于还没触发更新,所以此时是旧的值
        const oldValue=this.value;
        // 通过我们的 getter 方法,直接获取最新的值
        const newValue=this.get();
        // 将新值和旧值返回给 callback 回调函数
        this.callback(newValue,oldValue);
      }
    }
    

    使用案例

    let a={
      b:{
        c:{
          d:10
        }
      }
    }
    
    observe(a);
    new Watcher(a,'b.c.d',(val,oldValue)=>{
      console.log('ok',val,oldValue);  
    })
    a.b.c.d=55;  // ok 55 10
    

    在执行 a.b.c.d=55; 的同时,我们的控制台就会输出 ok 55 10 。

    运行分析

    observe(a)

    1. 首先, observe(a) 会将 a 对象变为响应式对象

    new Watcher

    1. 执行 new Watcher 之后,就会调用 Watcher 类的 constructor 。此时 target 是 a , expression 是 'b.c.d', callback(val,oldValue)=>{console.log('ok',val,oldValue); })
    // target 目标对象
    // expression 属性名
    // callback 回调函数
    // value 属性的值
    constructor(target,expression,callback) {
      this.target=target;
      // parsePath 为一个高阶函数
      this.getter=parsePath(expression);
      this.callback=callback;
      // get为我们之后要写的获取值的方法
      this.value=this.get();
    }
    
    1. this.value=this.get() 又会执行 get() 方法, 此时 Dep.target 被赋值了,就是当前 Watcher 实例。
    get(){
      // 进入依赖收集阶段,将 Dep.target 设为 Watcher 实例本身
      Dep.target=this;
    
      // 当前对象
      const obj=this.target;
      let value;
      // 当对象不再使用的时候,我们需要将它清空
      try{
        value=this.getter(obj)
      }finally {
        Dep.target=null;
      }
      this.value=value
      return value;
    }
    
    1. value=this.getter(obj) 会触发 defineReactive 中的 get() , 因为 Dep.target 之前已经被赋值了,所以,现在有值,触发 dep.depend
    get(){
      // Dep.target 是我们弄的唯一标识,当有这个标识的时候,添加依赖
      if(Dep.target){
        dep.depend();   // b 在这里触发
        // 如果有子属性,也要将它加入依赖
        if(childOb){
          childOb.dep.depend();  // c d 在这里触发
        }
      }
      return val;
    },
    
    1. 将当前 Watcher 实例推进了 subs 数组中。
    // 添加依赖
    depend(){
      // 判断当前是否有需要监听的目标,Dep.target 会被 Wacher 赋值
      if(Dep.target){
        // 将 Watcher 实例添加进 subs
        this.subs.push(Dep.target)
      }
    }
    

    a.b.c.d=55;

    1. 执行代码 a.b.c.d 触发 defineReactive 中的 set 方法,然后执行 dep.notify();
    set(newValue){
      val=newValue;
      childOb = observe(val)
      // notify 切忌 val=newValue 之后,不然在 callback 回调中一直是旧值
      dep.notify();
    }
    
    1. 通过遍历 subs 列表,通知所有订阅者
    // 通知所有订阅者
    notify(){
      // 浅克隆一份
      const subs=this.subs.slice();
      // 遍历
      for(let i=0,l=subs.length;i<l;i++){
        // 逐个更新
        subs[i].update();
      }
    }
    
    1. 相应的订阅者执行 update() ,将新值和旧值获取,然后通过 callback 回调函数返回
    // Dep发过来的通知,当前变量更新了,我们返回一个更新之后的回调函数
    update(){
      // this.value 由于还没触发更新,所以此时是旧的值
      const oldValue=this.value;
      // 通过我们的 getter 方法,直接获取最新的值
      const newValue=this.get();
      // 将新值和旧值返回给 callback 回调函数
      this.callback(newValue,oldValue);
    }
    
    1. 最终 new Watcher 实例中的回调函数成功执行,并且成功拿到 valoldValue
    new Watcher(a,'b.c.d',(val,oldValue)=>{
      console.log('ok',val,oldValue);  // ok 10 5
    })
    

    所有代码

    // 拷贝一份数组的原型
    const arrayPrototype=Array.prototype;
    // 以 Array.prototype 为原型创建 arrayMethods 对象
    const arrayMethods=Object.create(arrayPrototype);
    
    // 需要改写的数组方法列表
    const methodsNeedChange=[
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse',
    ]
    
    for(let i=0;i<methodsNeedChange.length;i++){
      // 备份原来的方法
      const original=arrayMethods[methodsNeedChange[i]];
      // 定义新的方法
      def(arrayMethods,methodsNeedChange[i],function () {
        // 用来保存新插入的值
        let inserted=[];
        // 由于 arguments 对象是类数组,所以先通过扩展运算符转为数组之后,再进行操作。
        let args=[...arguments];
        // 先判断 是否是 push shift splice ,如果是的话,先取出插入的新值,后面进行 observeArray
        switch (methodsNeedChange[i]) {
          case 'push':
          case 'shift':
            inserted=args;
            break;
          case ' ':
            // splice(起始下标,删除个数,新添加的元素)
            inserted=args.slice(2);
        }
        // 先判断 inserted 里面有东西,才执行 observeArray
        inserted.length && observeArray(inserted);
        // 将备份的方法进行执行,毕竟不能丢失数组方法原本的功能执行
        original.apply(this,arguments)
        // 写监听到之后更新视图
      },false)
    }
    
    function defineReactive(obj,key,val) {
      let dep=new Dep();
      // eslint-disable-next-line no-unused-vars
      let childOb;
      // 判断当前入参个数,两个的话直接返回当前层的对象
      if(arguments.length===2){
        val=obj[key];
        childOb = observe(val)
      }
      Object.defineProperty(obj,key,{
        // 可枚举,默认为 false
        enumerable:true,
        // 属性的描述符能够被改变,或者是删除,默认为 false
        configurable:true,
        get(){
          // Dep.target 是我们弄的唯一标识,当有这个标识的时候,添加依赖
          if(Dep.target){
            dep.depend();
            // 如果有子属性,也要将它加入依赖
            if(childOb){
              childOb.dep.depend();
            }
          }
          return val;
        },
        set(newValue){
          val=newValue;
          childOb = observe(val)
          // notify 切忌 val=newValue 之后,不然在 callback 回调中一直是旧值
          dep.notify();
        }
      })
    }
    
    function def(obj,key,value,enumerable) {
      Object.defineProperty(obj,key,{
        value,
        //这个属性仅仅保存 Observer 实例,所以不需要遍历
        enumerable
      })
    }
    
    // 遍历对象当前层的所有属性,并且绑定 defineReactive
    class Observer{
      constructor(obj){
        this.dep=new Dep();
        def(obj,'__ob__',this,false)
        if (Array.isArray(obj)){
          // 遍历当前数组,给所有的元素绑定 observe 响应式
          observeArray(obj)
          // 将当前数组对象的原型链强行指向 arrayMethods
          Object.setPrototypeOf(obj,arrayMethods);
        }else{
          this.walk(obj);
        }
      }
      // 遍历对象的当前层的所有属性, 给他绑定 defineReactive 响应式
      walk(obj){
        let keys=Object.keys(obj);
        for(let i =0;i<keys.length;i++){
          defineReactive(obj,keys[i])
        }
      }
    }
    
    // 响应式的入口方法 ,主要用于先判断是否是对象 ,然后判断是否有 __ob__ 属性,没有的话,肯定没有 Observer 遍历过
    function observe(value) {
      // 判断传入的值是否是对象,不是对象直接返回,不进行后面的操作
      if(typeof value !== 'object') return;
      // 用来存储当前的 Observer 实例
      let ob;
      // 判定当前属性是否有 __ob__ ,并且该属性是否原型属于 Observer
      // eslint-disable-next-line no-prototype-builtins
      if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer){
        ob=value.__ob__;
      }else{
        // 没有 __ob__ 属性代表没有遍历过,先执行 new Observer(value)
        ob = new Observer(value);
      }
      return ob;
    }
    
    // 遍历数组,将他们 observe 进行响应式
    function observeArray(list) {
      for(let i=0,l=list.length;i<l;i++){
        observe(list[i])
      }
    }
    
    
    class Dep{
      constructor() {
        // 用数组存储自己的订阅者   subs 是 subscribes 订阅者的意思
        // 这个数组里放的是 Watcher 的实例
        this.subs=[]
      }
      // 添加依赖
      depend(){
        // 判断当前是否有需要监听的目标,Dep.target 会被 Wacher 赋值
        if(Dep.target){
          // 将 Watcher 实例添加进 subs
          this.subs.push(Dep.target)
        }
      }
      // 通知所有订阅者
      notify(){
        // 浅克隆一份
        const subs=this.subs.slice();
        // 遍历
        for(let i=0,l=subs.length;i<l;i++){
          // 逐个更新
          subs[i].update();
        }
      }
    }
    
    class Watcher{
      // target 目标对象
      // expression 属性名
      // callback 回调函数
      // value 属性的值
      constructor(target,expression,callback) {
        this.target=target;
        // parsePath 为一个高阶函数
        this.getter=parsePath(expression);
        this.callback=callback;
        // get为我们之后要写的获取值的方法
        this.value=this.get();
      }
      // Dep发过来的通知,当前变量更新了,我们返回一个更新之后的回调函数
      update(){
        // this.value 由于还没触发更新,所以此时是旧的值
        const oldValue=this.value;
        // 通过我们的 getter 方法,直接获取最新的值
        const newValue=this.get();
        // 将新值和旧值返回给 callback 回调函数
        this.callback(newValue,oldValue);
      }
      // 获取当前的值,并将它更新,然后 return 返回
      get(){
        // 进入依赖收集阶段,将 Dep.target 设为 Watcher 实例本身
        Dep.target=this;
    
        // 当前对象
        const obj=this.target;
        let value;
        // 当对象不再使用的时候,我们需要将它清空
        try{
          value=this.getter(obj)
        }finally {
          Dep.target=null;
        }
        this.value=value
        return value;
      }
    }
    
    function parsePath(str){
      let segments = str.split('.');
      return obj=> {
          for(let i=0;i<segments.length;i++){
            if(!obj) return;
            obj=obj[segments[i]];
          }
          return obj;
      }
    }
    
    let a={
      b:{
        c:{
          d:10
        }
      }
    }
    
    observe(a)
    new Watcher(a,'b.c.d',(val,oldValue)=>{
      console.log('ok',val,oldValue);  // ok 10 5
    })
    a.b.c.d=55;
    

    文章参考

    【尚硅谷】Vue源码解析之数据响应式原理

    相关文章

      网友评论

          本文标题:【手把手教你搓Vue响应式原理】(五) Watcher 与 De

          本文链接:https://www.haomeiwen.com/subject/dvdakrtx.html