Vue 源码解析(二):依赖更新(Watcher对象,nextTick和更新队列)

第一篇,新建watcher对象时运行的getter函数,会调用data属性的get修饰器从而触发dep.depend()函数,完成了watcher与dep依赖的收集。让watcher和data建立了联系。那么这一次,让我们看一下依赖收集完成之后,是如何完成依赖更新的。

让我们从还是在initData() 方法里面的defineReactive()方法再次开始吧!

export function defineReactive (obj: Object, key: string, val: any, customSetter?: Function) {
    
    
  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    
    
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
    
    
      ...
      return value
    },
    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 = observe(newVal)
      dep.notify() // <-- 依赖更新的重点
    }
  })
}

这次让我们看一下set修饰器的作用,在代码中出现 this.XXX = XXX 这样的语句时。set修饰器的方法就会被触发。首先通过getter得到旧值,判断旧值与新值是否相同,如果相同就不触发更新。然后使用新值替换旧值,再次递归观察新值,最后调用 dep.notify() 通知监听器依赖更新了。

export default class Dep {
    
    
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  // 通知所有监听的watcher更新
  notify () {
    
    
    // stabilize the subscriber list first
    const subs = this.subs.slice() // 浅复制subs数组
    for (let i = 0, l = subs.length; i < l; i++) {
    
    
      subs[i].update()
    }
  }
}
export default class Watcher {
    
    
  update () {
    
    
    /* istanbul ignore else */
    if (this.lazy) {
    
    
        this.dirty = true
    } else if (this.sync) {
    
     // 默认值是false
        this.run()
    } else {
    
    
        queueWatcher(this) // <-- 会加入watcher更新队列
    }
  }
}

这个notify()函数很简单,就是运行了subs数组里面的每个Watcher对象的update()函数,调用监听自己属性的所以监听器的更新函数。

而这个update()函数也很简单,根据是否同步走不同的方法,一般sync的默认值都是false,所以我们会走queueWatcher()函数。当然run()函数迟早也会运行的。所以我们就会看一下run()函数。

run () {
    
    
  if (this.active) {
    
    
    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
      if (this.user) {
    
    
        try {
    
    
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
    
    
          handleError(e, this.vm, `callback for watcher "${
      
      this.expression}"`)
        }
      } else {
    
    
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

我们看到 run() 函数首先重新运行了this.get()函数得到了新的watcher监听的值。然后调用this.cb函数将Watcher的新值和旧值都传了进去。这个cb函数就是我们自己定义的监听函数,所以我们在写的时候可以使用新值和旧值去写一些我们自己的逻辑。

const queue: Array<Watcher> = []
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 >= 0 && queue[i].id > watcher.id) {
    
    
              i--
          }
          queue.splice(Math.max(i, index) + 1, 0, watcher)
      }
      // queue the flush
      if (!waiting) {
    
    
          waiting = true
          nextTick(flushSchedulerQueue)
      }
  }
}

queueWatcher() 函数,如果没有在更新,则直接把watcher对象放入队列里面就行了。但是如果正在更新,上次我们讲过,watcher的更新顺序很重要,因为在watcher之间也存在依赖关系,所以后面的watcher是有可能依赖前面的watcher的。而决定watcher前后的就是id,所以使用在队列中比较id的大小,插入到适合的位置。

最后,调用flushSchedulerQueue函数以一定的周期(nextTick)更新队列。

function flushSchedulerQueue () {
    
    
  flushing = true
  let watcher, id, vm
  queue.sort((a, b) => a.id - b.id)  // 1、重新排序

  for (index = 0; index < queue.length; index++) {
    
    
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.run() // 2、运行更新方法

    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
    
    
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > config._maxUpdateCount) {
    
      // 3、判断是否触发循环更新
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${
      
      watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // reset scheduler before updated hook called
  const oldQueue = queue.slice()
  resetSchedulerState()  // 4、重置更新队列

  // call updated hooks
  index = oldQueue.length
  while (index--) {
    
    
    watcher = oldQueue[index]
    vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
    
    
      callHook(vm, 'updated') // 5、调用updated钩子
    }
  }
}
function resetSchedulerState () {
    
    
  queue.length = 0
  has = {
    
    }
  if (process.env.NODE_ENV !== 'production') {
    
    
    circular = {
    
    }
  }
  waiting = flushing = false
}

更新队列的 flushSchedulerQueue 函数一共有5步。分别是先对watcher进行排序,然后循环调用watcher的run()函数,完成更新。同时判断是否循环更新, config._maxUpdateCount 的值是100,接着更新完之后,会重置更新队列。最后判断 更新的watcher里面是否有视图的watcher对象,如果有调用 updated 钩子。

Guess you like

Origin blog.csdn.net/cuipp0509/article/details/117254924