美文网首页
依赖收集、派发更新和Vue.set

依赖收集、派发更新和Vue.set

作者: 大猪蹄子_0f6b | 来源:发表于2020-03-12 16:03 被阅读0次

    defineReactive给数据添加了 getter 和 setter。
    在defineReactive内第一步是实例了Dep

    Dep是整个 getter 依赖收集的核⼼

    export default class Dep {
      static target: ?Watcher;
      id: number;
      subs: Array<Watcher>;
      constructor () {
        this.id = uid++
        this.subs = []
      }
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
      removeSub (sub: Watcher) {
        remove(this.subs, sub)
      }
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
      notify () {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    

    target 是⼀个全局唯⼀ Watcher ,这是⼀个⾮常巧妙的设计,因为在同⼀时间只能有⼀个全局的 Watcher 被计算,另外它的⾃⾝属性 subs 也是 Watcher 的数组。

    Watcher

    这个Class的构造函数中定义了一些和Dep相关的属性。

    • this.deps 和 this.newDeps 表⽰ Watcher 实例持有的 Dep 实例的数组;
    • this.depIds 和 this.newDepIds 分别代表 this.deps 和 this.newDeps 的 id Set。
    • Watcher还定义了⼀些原型的⽅法,和依赖收集相关的有 get 、 addDep 和 cleanupDeps⽅法
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    

    依赖收集

      //defineReactive方法内部
      let childOb = !shallow && observe(val)//可能还是个obj
      const dep = new Dep()
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            dep.depend()// 执行Watcher ? Dep.target.addDep(this)
            if (childOb) {
              childOb.dep.depend()
              //收集了依赖
              //执⾏ Vue.set 的时候通过 ob.dep.notify() 能够通知到 watcher
              //从⽽让添加新的属性到对象也可以检测到变化
              if (Array.isArray(value)) {
                dependArray(value)//把数组每个元素也去做依赖收集
              }
            }
          }
          return value
        }
        //...
      })
    

    在mount过程中,mountComponent函数有这么一段逻辑

    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    new Watcher(vm, updateComponent, noop, {
      before () {
        if (vm._isMounted) {
          callHook(vm, 'beforeUpdate')
        }
      }
    }, true /* isRenderWatcher */)
    
    
    • 当我们去实例化⼀个渲染 watcher 的时候,⾸先进⼊ watcher 的构造函数逻辑,实际上就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复⽤)
    • 接着调用vm._update,因为这个方法会生成渲染VNode,并且会对vm进行数据访问,因此会触发数据对象的getter,而每个getter都持有一个dep
    • 在触发getter的时候会调用dep.depend(),即执行Dep.target.addDep(this),在保证不会被重复添加的情况下,把当前的 watcher 订阅到这个数据持有的 dep 的 subs,this.subs.push(sub),⽬的是为后续数据变化时候能通知到哪些 subs 做准备。
    • traverse(value)递归触发子项getter
    • Dep.target = targetStack.pop(),把Dep.target恢复成上一个状态
    • this.cleanupDeps(),设计了在每次添加完新的订阅,会移除掉旧的订阅。(比如v-if渲染不同子模板a b,渲染a改a通知a的subs,切换渲染b去修改a,若会通知a的subs的回调就是浪费,因此要移除旧的订阅)

    派发更新

    setter 的逻辑有 2 个关键的点,⼀个是 childOb = !shallow && observe(newVal) ,如果 shallow为 false 的情况,会对新设置的值变成⼀个响应式对象;另⼀个是 dep.notify() ,通知所有的订阅者

      //defineReactive方法内部
      let childOb = !shallow && observe(val)//可能还是个obj
      const dep = new Dep()
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          }
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          dep.notify()
        }
        //...
      })
    

    在组件中对响应的数据做了修改,就会触发 setter 的逻辑,最终调用dep.notify(),即遍历subs调用每个watcher的update方法,会对watcher不同状态走不同逻辑,一般情况下会执行queueWatcher逻辑

    • 引入一个队列的概念,因为每次数据改变都触发watcher回调,把这些watcher添加到一个队列里,然后异步执行nextTick(flushSchedulerQueue)
      • 把传⼊的 cb 压⼊callbacks 数组,最后⼀次性在下一个tick执⾏
      • 因此数据的变化到 DOM 的重新渲染是⼀个异步过程,发⽣在下⼀个 tick。
    • flushSchedulerQueue会把queue队列做一个id从小到大的排序,原因如下
      • 1.组件的更新由⽗到⼦;因为⽗组件的创建过程是先于⼦的,所以 watcher 的创建也是先⽗后⼦,执⾏顺序也应该保持先⽗后⼦
      • 2.⽤户的⾃定义 watcher 要优先于渲染 watcher 执⾏;因为⽤户⾃定义 watcher 是在渲染watcher 之前创建的
      • 3.如果⼀个组件在⽗组件的 watcher 执⾏期间被销毁,那么它对应的 watcher 执⾏都可以被跳过,所以⽗组件的 watcher 应该先执⾏
    • 在排序后遍历拿到对应watcher,执行watcher.run()。在遍历的时候每次都会对queue.length求值,因为在run()的时候queue可能会发生变化
      • run函数传入watcher的回调函数,先this.get()求值,做判断满足条件执行watcher回调,并且传入newVal和oldVal
      • this.get()求值的时候会执行getter,触发组件重新渲染
    • 状态恢复,把流程控制变量设回初始值,把watcher队列情况。

    针对特殊情况

    给响应式对象添加新属性,使用Vue.set API

    set ⽅法接收 3个参数, target 可能是数组或者是普通对象, key 代表的是数组的下标或者是对象的键值, val 代表添加的值。

    • ⾸先判断如果 target 是数组且 key 是⼀个合法的下标,则通过 splice 去添加进数组然后返回(splice并非原生splice)
    • 若key 已经存在于 target 中,则直接赋值返回
    • 获取target.__ob__(Observer的实例),若不存在表明target飞响应式对象直接返回
    • 通过defineReactive(ob.value, key, val) 把新添加的属性变成响应式对象,然后再通过 ob.dep.notify() ⼿动的触发依赖通知。(因为在getter过程中有childOb的判断并且调用childOb.dep.depend()收集了依赖,因此能notify()通知)
    • 数组情况,在observe方法观察对象的时候对数组作了处理,对数组中所有能改变数组⾃⾝的⽅法,如push、pop 等这些⽅法进⾏重写。重写后的⽅法会先执⾏它们本⾝原有的逻辑,并对能增加数组⻓度的 3 个⽅法 push、unshift、splice ⽅法做了判断,获取到插⼊的值,然后把新添加的值变成⼀个响应式对象,并且再调⽤ ob.dep.notify() ⼿动触发依赖通知

    相关文章

      网友评论

          本文标题:依赖收集、派发更新和Vue.set

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