最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 数据响应式原理 - 03

    正文概述 掘金(亦黑迷失)   2021-06-11   608

    这是我参与更文挑战的第5天,活动详情查看: 更文挑战

    在上一篇中,我们实现了数组的响应式处理,本篇将对依赖收集和 Watcher 类进行介绍。

    收集依赖

    前面我们已经将数据变成了响应式数据,但是怎么让用户定义的方法可以在这些数据改变时调用呢?
    这就要用到 Watcher,new Watcher 时传入的参数里会有个 callback,这个 callback 就可以是用户定义的方法,通过 watcher 那些响应式数据,当数据改变时,调用 callback。

    那么为什么 watcher 的数据改变时能够调用 callback 呢?
    这就涉及到依赖的收集,首先这些数据已经是 observe 处理了,也就是说已经是响应式的了。在 Watcher 的构造函数中会去获取要订阅的数据的值,这就会触发数据的 getter,一旦触发 getter 就会把这个 watcher 实例收集到一个数组 subs 里,一旦这个数据被改动,就会触发 setter,然后在 setter 里会循环 subs 数组,一个个去通知,执行 update 方法,通过 update 方法里最终触发 callback。

    依赖是什么?

    • 需要用到数据的地方称为依赖。在 vue2.x 中,用到数据的组件是依赖。当数据变化时通知组件,在组件内通过虚拟 dom 进行 diff 算法
    • 在 getter 中收集依赖,在 setter 中触发依赖

    Dep 类

    Dep 类用来封装依赖收集的代码,管理依赖

    // Dep.js
    export default class Dep {
      constructor(arg) {
         // 用数组存储自己的订阅者, 数组里是 Watcher 实例
         this.subs = []
      }
      
      // 添加订阅
      addSub(sub) {
        this.subs.push(sub)
      }
      
      // 添加依赖
      depend() {
        // Dep.target 就是我们指定的一个全局唯一位置,换成 window.target 也一样
        if (Dep.target) {
          this.addSub(Dep.target)
        }
      }
      
      // 通知更新
      notify() {
        const subs = this.subs.slice() // 浅克隆
        subs.forEach(item => {
          item.update()
        })
      }
    }
    

    每个 Observer 实例中都有一个 Dep 的实例

    在 Observer 类的constructor 函数中 const dep = new Dep()

    // Observer.js
    ...
    import Dep from './Dep.js'
    export default class Observer {
      constructor(value) {
        this.dep = new Dep() // 本次笔记的案例中,这里其实不写也可以
        ...
      }
      ...
    }
    

    还有个地方也会创建 Dep 实例,就是在 defineReactive 里

    目的是在于可以在被侦测的对象 setter 时去发通知 dep.notify()

    // defineReactive.js
    import Dep from './Dep.js'
    export default function defineReactive(data, key, value) {
      const dep = new Dep()
      ...
      Object.defineProperty(data, key, {
        ...
        set(newValue) {
          ...
          // 在 setter 中触发依赖
          dep.notify()
        }
      })
    }
    

    Dep 使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有 Watcher 都通知一遍
    这样,一旦去修改 obj,比如 obj.b = 3,就会执行 Dep 的 notify 方法。当然,通过 7 种能改变数组本身的方法改变数组时,也需要能够通知,所以在 array.js 改写 7 中方法时也加上 ob.dep.notify()

    // array.js 
    ...
    methodsCouldChange.forEach(item => {
      ...
      def(arrayMethods, item, function() { 
        ...
        ob.dep.notify()
        ...
      }, false)
    })
    ...
    

    Watcher 类

    先说目的

    我们最终目的是在 index.js 新建一个 watcher 类的实例去监控我们指定的对象的指定的属性,并希望在 new Watcher() 的第 3 个参数,回调函数里得到对象属性修改前后的值,这样就可以去做一些我们想做的事情了。

    // index.js
    import observe from './observe.js'
    import Watcher from './Watcher.js'
    
    let obj = {
      a: {
        m: {
          n: 1
        }
      }
    }
    observe(obj)
    new Watcher(obj, 'a.m.n', (val, oldValue) => {
      console.log('watcher', val, oldValue)
    })
    obj.a.m.n = 2
    

    期望得的到结果是

    数据响应式原理 - 03
    也就是说只要我 new 了一个 Watcher 实例,并把想要监控的属性(a.m.n)和对象(obj)传进去,那么在 Watcher 的第 3 个参数,也就是个回调函数里就能得到 obj.a.m.n 的新旧属性,并能继续做一些事情,比如进行 diff 算法等等。下面开始书写 Watcher 类:

    新建 Watcher.js 文件

    // Watcher.js
    import Dep from './Dep.js'
    let uid = 0
    export default class Watcher {
      constructor(target, expression, callback) {
        this.id = uid++ // 让每个 watcher 实例有一个自己的 id
        this.target = target // target 为新建实例时传入的要监控的对象(obj)
        this.getter = parsePath(expression) // getter 会是一个函数, 在下面定义的 get 里调用
        this.callback = callback // callback 就是传入的回调函数
        this.val = this.get() // 获取对象 target 的 expression 属性的值
      }
    
      // 数据更新触发
      update() {
        this.run()
      }
    
      get() {
        // 将 Dep.target 赋值为 new 的这个 Watcher 实例本身,代表进入依赖收集阶段
        Dep.target = this
        const obj = this.target
        
        let value
        try {
          /*
            注意,一旦这里去获取 obj 的 expression 属性的值, 
            因为 obj 已经被 observe 了,所以就会触发 defineReactive, 
            Object.defineProperty 里的 get() 就会被触发
          */
          value = this.getter(obj)
        } finally { // 在 try 语句块之后执行, 无论是否有异常抛出或捕获都将执行
          Dep.target = null // 退出依赖收集
        }
        return value
      }
    
      run() {
        this.getAndInvoke(this.callback)
      }
    
      getAndInvoke(cb) {
        const newValue = this.get()
        if (newValue !== this.val || typeof newValue === 'object') {
          const oldValue = this.val
          cb.call(this.target, newValue, oldValue)
        }
      }
    }
    
    // 传入一个属性字符串比如 a.m.n, 然后返回一个函数(getter),给这个函数传入 obj, 则可以得到 obj.a.m.n 的值
    const parsePath = function(str) {
      const segments = str.split('.')
      return function(obj) {
        const value = segments.reduce((accumulator, currentValue) => {
          return accumulator = accumulator[currentValue]
        }, obj)
        return value
      }
    }
    

    至此,我们先停下来理清下思路:为了实现开始的那个目的,我们新建了 Watcher 类,当我们在 index.js 进行 new Watcher(obj, 'a.m.n', (val, oldValue) => {console.log('watcher', val, oldValue)}) 时,就会执行 Watcher 类的构造函数,其中有这么一句 this.val = this.get(),这是条关键语句,在 get() 函数中,主要做了 2 件事:

    1. 通过 Dep.target = this 开始收集依赖,给全局变量 Dep.target 赋值,值为这个 Watcher 实例本身。
    2. 通过 value = this.getter(obj) 去查找 obj.a.m.n 的值,因为之前已经通过 observe(obj) 让 obj 的每一个属性的 getter 和 setter 都是被监听的,所以这就触发了 obj.a.m.n 的 getter。

    那么,我们就可以对 defineReactive.js 做如下更改

    // defineReactive.js
    ...
    export default function defineReactive(data, key, value) {
      ...
      Object.defineProperty(data, key, {
        ...
        get() {
          // 如果处于依赖收集阶段(在 getter 中收集依赖)
          if (Dep.target) {
            dep.depend()
          }
          return value
        },
        ...
      })
    }
    

    依赖就是 Watcher,只有 Watcher 触发的 getter 才会收集依赖,哪个 Watcher 触发了 getter,就把哪个 Watcher 收集到 Dep 中。

    至此,数据响应式原理的内容学习笔记分享完毕,难免有所纰漏之处,还请斧正。

    数据响应式原理 - 03

    数据响应式原理 - 03


    起源地下载网 » 数据响应式原理 - 03

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元