美文网首页
Vue源码分析—响应式原理(三)

Vue源码分析—响应式原理(三)

作者: oWSQo | 来源:发表于2019-07-19 10:26 被阅读0次

    派发更新

    响应式数据依赖收集过程,收集的目的就是为了当我们修改数据的时候,可以对相关的依赖派发更新,我们来详细分析这个过程。

    我们先来回顾一下setter部分的逻辑:

    /**
     * Define a reactive property on an Object.
     */
    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      const dep = new Dep()
    
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
    
      // cater for pre-defined getter/setters
      const getter = property && property.get
      const setter = property && property.set
      if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
      }
    
      let childOb = !shallow && observe(val)
      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的逻辑有2个关键的点,一个是childOb = !shallow && observe(newVal),如果shallow为 false 的情况,会对新设置的值变成一个响应式对象;另一个是dep.notify(),通知所有的订阅者,接下来我们分析整个派发更新的过程。

    过程分析

    当我们在组件中对响应的数据做了修改,就会触发setter的逻辑,最后调用dep.notify()方法, 它是Dep的一个实例方法,定义在src/core/observer/dep.js 中:

    class Dep {
      // ...
      notify () {
      // stabilize the subscriber list first
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    

    这里的逻辑非常简单,遍历所有的subs,也就是Watcher的实例数组,然后调用每一个watcherupdate方法,它的定义在src/core/observer/watcher.js中:

    class Watcher {
      // ...
      update () {
        /* istanbul ignore else */
        if (this.computed) {
          // A computed property watcher has two modes: lazy and activated.
          // It initializes as lazy by default, and only becomes activated when
          // it is depended on by at least one subscriber, which is typically
          // another computed property or a component's render function.
          if (this.dep.subs.length === 0) {
            // In lazy mode, we don't want to perform computations until necessary,
            // so we simply mark the watcher as dirty. The actual computation is
            // performed just-in-time in this.evaluate() when the computed property
            // is accessed.
            this.dirty = true
          } else {
            // In activated mode, we want to proactively perform the computation
            // but only notify our subscribers when the value has indeed changed.
            this.getAndInvoke(() => {
              this.dep.notify()
            })
          }
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    }  
    

    这里对于Watcher的不同状态,会执行不同的逻辑,在一般组件数据更新的场景,会走到最后一个queueWatcher(this)的逻辑,queueWatcher的定义在src/core/observer/scheduler.js中:

    const queue: Array<Watcher> = []
    let has: { [key: number]: ?true } = {}
    let waiting = false
    let flushing = false
    /**
     * Push a watcher into the watcher queue.
     * Jobs with duplicate IDs will be skipped unless it's
     * pushed when the queue is being flushed.
     */
    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id
      if (has[id] == null) {
        has[id] = true
        if (!flushing) {
          queue.push(watcher)
        } else {
          // if already flushing, splice the watcher based on its id
          // if already past its id, it will be run next immediately.
          let i = queue.length - 1
          while (i > index && queue[i].id > watcher.id) {
            i--
          }
          queue.splice(i + 1, 0, watcher)
        }
        // queue the flush
        if (!waiting) {
          waiting = true
          nextTick(flushSchedulerQueue)
        }
      }
    }
    

    这里引入了一个队列的概念,这也是Vue在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发watcher的回调,而是把这些watcher先添加到一个队列里,然后在nextTick后执行flushSchedulerQueue

    这里有几个细节要注意一下,首先用has对象保证同一个Watcher 只添加一次;接着对flushing的判断;最后通过waiting保证对nextTick(flushSchedulerQueue)的调用逻辑只有一次,nextTick目前就可以理解它是在下一个tick,也就是异步的去执行flushSchedulerQueue

    接下来我们来看flushSchedulerQueue的实现,它的定义在src/core/observer/scheduler.js中。

    let flushing = false
    let index = 0
    /**
     * Flush both queues and run the watchers.
     */
    function flushSchedulerQueue () {
      flushing = true
      let watcher, id
    
      // Sort queue before flush.
      // This ensures that:
      // 1\. Components are updated from parent to child. (because parent is always
      //    created before the child)
      // 2\. A component's user watchers are run before its render watcher (because
      //    user watchers are created before the render watcher)
      // 3\. If a component is destroyed during a parent component's watcher run,
      //    its watchers can be skipped.
      queue.sort((a, b) => a.id - b.id)
    
      // do not cache length because more watchers might be pushed
      // as we run existing watchers
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
          watcher.before()
        }
        id = watcher.id
        has[id] = null
        watcher.run()
        // in dev build, check and stop circular updates.
        if (process.env.NODE_ENV !== 'production' && has[id] != null) {
          circular[id] = (circular[id] || 0) + 1
          if (circular[id] > MAX_UPDATE_COUNT) {
            warn(
              'You may have an infinite update loop ' + (
                watcher.user
                  ? `in watcher with expression "${watcher.expression}"`
                  : `in a component render function.`
              ),
              watcher.vm
            )
            break
          }
        }
      }
    
      // keep copies of post queues before resetting state
      const activatedQueue = activatedChildren.slice()
      const updatedQueue = queue.slice()
    
      resetSchedulerState()
    
      // call component updated and activated hooks
      callActivatedHooks(activatedQueue)
      callUpdatedHooks(updatedQueue)
    
      // devtool hook
      /* istanbul ignore if */
      if (devtools && config.devtools) {
        devtools.emit('flush')
      }
    }
    

    这里有几个重要的逻辑要梳理一下,对于一些分支逻辑如keep-alive组件相关和之前提到过的 updated 钩子函数的执行会略过。

    • 队列排序
      queue.sort((a, b) => a.id - b.id)对队列做了从小到大的排序,这么做主要有以下要确保以下几点:
      1. 组件的更新由父到子;因为父组件的创建过程是先于子的,所以watcher的创建也是先父后子,执行顺序也应该保持先父后子。
        2.用户的自定义watcher要优先于渲染watcher执行;因为用户自定义watcher是在渲染watcher之前创建的。
        3.如果一个组件在父组件的watcher执行期间被销毁,那么它对应的watcher执行都可以被跳过,所以父组件的watcher应该先执行。
    • 队列遍历
      在对queue排序后,接着就是要对它做遍历,拿到对应的watcher,执行watcher.run()。这里需要注意一个细节,在遍历的时候每次都会对queue.length求值,因为在watcher.run()的时候,很可能用户会再次添加新的watcher,这样会再次执行到queueWatcher,如下:
    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id
      if (has[id] == null) {
        has[id] = true
        if (!flushing) {
          queue.push(watcher)
        } else {
          // if already flushing, splice the watcher based on its id
          // if already past its id, it will be run next immediately.
          let i = queue.length - 1
          while (i > index && queue[i].id > watcher.id) {
            i--
          }
          queue.splice(i + 1, 0, watcher)
        }
        // ...
      }
    }
    

    可以看到,这时候flushingtrue,就会执行到else的逻辑,然后就会从后往前找,找到第一个待插入watcherid比当前队列中watcherid大的位置。把watcher按照id的插入到队列中,因此queue的长度发生了变化。

    • 状态恢复
      这个过程就是执行resetSchedulerState函数,它的定义在src/core/observer/scheduler.js中。
    const queue: Array<Watcher> = []
    let has: { [key: number]: ?true } = {}
    let circular: { [key: number]: number } = {}
    let waiting = false
    let flushing = false
    let index = 0
    /**
     * Reset the scheduler's state.
     */
    function resetSchedulerState () {
      index = queue.length = activatedChildren.length = 0
      has = {}
      if (process.env.NODE_ENV !== 'production') {
        circular = {}
      }
      waiting = flushing = false
    }
    

    逻辑非常简单,就是把这些控制流程状态的一些变量恢复到初始值,把 watcher 队列清空。

    接下来我们继续分析watcher.run()的逻辑,它的定义在src/core/observer/watcher.js中。

    class Watcher {
      /**
       * Scheduler job interface.
       * Will be called by the scheduler.
       */
      run () {
        if (this.active) {
          this.getAndInvoke(this.cb)
        }
      }
    
      getAndInvoke (cb: Function) {
        const value = this.get()
        if (
          value !== this.value ||
          // Deep watchers and watchers on Object/Arrays should fire even
          // when the value is the same, because the value may
          // have mutated.
          isObject(value) ||
          this.deep
        ) {
          // set new value
          const oldValue = this.value
          this.value = value
          this.dirty = false
          if (this.user) {
            try {
              cb.call(this.vm, value, oldValue)
            } catch (e) {
              handleError(e, this.vm, `callback for watcher "${this.expression}"`)
            }
          } else {
            cb.call(this.vm, value, oldValue)
          }
        }
      }
    }
    

    run函数实际上就是执行this.getAndInvoke方法,并传入watcher的回调函数。getAndInvoke 函数逻辑也很简单,先通过this.get()得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep模式任何一个条件,则执行watcher的回调,注意回调函数执行的时候会把第一个和第二个参数传入新值value和旧值oldValue,这就是当我们添加自定义watcher的时候能在回调函数的参数中拿到新旧值的原因。

    那么对于渲染watcher而言,它在执行this.get()方法求值的时候,会执行getter方法:

    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    

    所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行 patch 的过程,但它和首次渲染有所不同。

    总结

    Vue数据修改派发更新的过程,实际上就是当数据发生变化的时候,触发 setter逻辑,把在依赖过程中订阅的的所有观察者,也就是watcher,都触发它们的update过程,这个过程又利用了队列做了进一步优化,在nextTick后执行所有watcherrun,最后执行它们的回调函数。nextTickVue一个比较核心的实现了,接下来我们重点分析它的实现。

    相关文章

      网友评论

          本文标题:Vue源码分析—响应式原理(三)

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