Home >Web Front-end >Vue.js >Learn more about Watcher and Scheduler in Vue

Learn more about Watcher and Scheduler in Vue

青灯夜游
青灯夜游forward
2021-12-01 19:58:293014browse

This article will take you to understand the Watcher and Scheduler in Vue, and introduce the implementation principle of Vue's Watcher. I hope it will be helpful to everyone.

Learn more about Watcher and Scheduler in Vue

Vue senses state changes through the data detection mechanism. The previous article "How Vue Implements Data Detection" mentioned the Watcher object. When the data is updated, there is an update, such as When executing this.title = 'Watch whether I have changed', the setter function calls dep.notify to notify the watcher to perform the update (specifically execute the watcher.update function).

So when does Vue create a Watcher, how to schedule the Watcher queue through the Scheduler, and how the update of the watcher is finally reflected in the rendering of the view. This article mainly focuses on these three issues to introduce the implementation principle of Vue's Watcher. [Related recommendations: "vue.js Tutorial"]

Learn more about Watcher and Scheduler in Vue

1. When to create the Watcher

component from creation to destruction After going through a series of life cycles, the ones we are more familiar with are beforeMount, mounted, beforeUpdate, and updated. After understanding the life cycle, it will be much easier to understand when the Watcher was created. Vue creates Watcher objects in three places, the mount event, $watch function, computed and watch properties. The mount event creates a Watcher for rendering notifications. The Watchers created by watch and computed are both used to monitor user-defined property changes.

Learn more about Watcher and Scheduler in Vue

1.1 mount event

The file core/instance/lifecycle.js contains functions related to the Vue life cycle, such as $forupdate, $destroy and instantiation Watcher's mountComponent function, the mountComponent function is triggered when the component is mounted and $mount is executed. The function first triggers the beforeMount hook event. When instantiating the Watcher, the before function is passed in, and before will trigger the beforeUpdate hook. When a component has a property update, the watcher will trigger the beforeUpdate event before updating (watcher.run). isRenderWatcher indicates that the rendering Watcher is created and is directly hung on the vm._watcher attribute. When $forceUpdate is forcibly executed to refresh the rendering, vm._watcher.update will be executed to trigger the rendering process and the corresponding update hook.

/**
 * 生命周期mount事件触发函数
 * @param {*} vm 
 * @param {*} el 
 * @param {*} hydrating 
 * @returns 
 */
 export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el

  callHook(vm, 'beforeMount')

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

  // 实例化Watcher对象,在Watcher构造函数中建立Watcher和vm的关系
  new Watcher(vm, updateComponent, noop, {
    // 在执行wather.run函数之前触发before hook事件
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
    // isRenderWatcher表示用于渲染的Watcher,在执行$forceupdate时会手动触发watcher.update
  }, true /* isRenderWatcher */)
  
  return vm
}

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    this.getter = expOrFn
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}

1.2.$watch function

In the component, in addition to using the watch and computed methods to monitor property changes, Vue defines the $watch function to monitor property changes, such as when a.b.c nested properties For changes, you can use $watch to implement monitoring and subsequent processing. $watch is equivalent to a functional way of writing the watch attribute directly in the component. It can support dynamic addition of dependent monitoring at runtime. For example, the keep-alive component in the Vue source code is mounted Use $watch in the event to monitor include and exclude attribute changes.

vm.$watch( expOrFn, callback, [options] )
参数:
    {string | Function} expOrFn
    {Function | Object} callback
    {Object} [options]
    {boolean} deep
    {boolean} immediate
返回值:{Function} unwatch

// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
  // 做点什么
})

// keep-alive.js文件
  mounted () {
    this.cacheVNode()
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  }

The difference between the $watch function and the mountComponent function is that mountComponent is used for rendering monitoring and will trigger related hook events, while $watch has a more specific responsibility and handles expOrFn monitoring. In addition, the cb parameter of $watch can be a function, object or string. When it is a string, it means the function name defined in the Vue object. For example, if the nameChange function is defined in the Vue component, then define vm.$watch('name' , 'nameChange'), if the name is updated, the nameChange function of the Vue entity will be triggered.

// 监听属性变化
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  // cb可能是纯JS对象,那么回调为cb.handler
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  const watcher = new Watcher(vm, expOrFn, cb, options)
  
  // 返回watch注销监听函数
  return function unwatchFn () {
    watcher.teardown()
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 当执行函数是一个对象的时候, 将 handler 的 handler调用给执行函数
    // 这里的 options 是 watch 函数的配置信息
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

1.3.watch and computed properties

Use Vue to develop components. These two properties must be familiar. For example, use watch to define the monitoring of the firstName and secondName properties, and use computed to define the monitoring of the fullName property. When firstName and secondName are updated, fullName will also trigger an update.

new Vue({
  el: '#app',
  data() {
    return {
        firstName: 'Li',
        secondName: 'Lei'
    }
  },
  watch: {
      secondName: function (newVal, oldVal) {
          console.log('second name changed: ' + newVal)
      }
  },
  computed: {
      fullName: function() {
          return this.firstName + this.secondName
      }
  },
  mounted() {
    this.firstName = 'Han'
    this.secondName = 'MeiMei'
  }
})

When we define monitoring of properties in watch and computed, when does Vue convert it into a Watcher object to perform monitoring? Vue's constructor will call _init(options) to perform initialization. The source code core/components/instance/init.js file defines the _init function and performs a series of initialization operations, such as initialization life cycle, events, status, etc., among which initState The function includes the initialization of watch and computed.

// core/components/instance/init.js
// Vue构造函数
function Vue (options) {
  this._init(options)
}

// core/components/instance/init.js
Vue.prototype._init = function (options?: Object) {
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')
}

// // core/components/state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  ...
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

1.3.1 computed attribute

initComputed initializes the computed attribute. Each Vue entity contains a _computedWatchers object that is used to store watcher objects for all computed attributes. First traverse the computed object and create a new Watcher object for each key. Its lazy attribute is true, which means that the Watcher will cache the calculated value. If the properties it depends on (such as firstName, secondName) are not updated, the current computed property (such as fullName) ) will not trigger an update. The properties defined in computed can be accessed through this (for example, this.fullName). defineComputed mounts all computed properties to the Vue entity.

// lazy为true表示需要缓存,一般只有computed属性才会用到
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
    const watchers = vm._computedWatchers = Object.create(null)

    for (const key in computed) {
      const userDef = computed[key]
      // 用户定义的执行函数可能是{ get: function() {} }形式
      const getter = typeof userDef === 'function' ? userDef : userDef.get
      // 为用户定义的每个computed属性创建watcher对象
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )

      // 组件自身的computed属性已经定义在组件原型链上,我们只需要定义实例化的computed属性。
      // 例如我们在computed定义了fullName,defineComputed会将其挂接到Vue对象的属性上
      if (!(key in vm)) {
        defineComputed(vm, key, userDef)
      }
}

defineComputed function converts calculated properties into {get, set} form, but calculated properties do not require set, so the code directly assigns a noop empty function to it. The get function of the calculated attribute is encapsulated by createComputedGetter. First, the watcher object of the corresponding attribute is found. If the dirty value of the watcher is true, it means that the dependent attribute has been updated, and the evaluate function needs to be called to recalculate the new value.

// 将computed定义的属性转换为{ get, set }形式并挂接到Vue实体上,这样就可以通过this.fullName形式调用
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? createComputedGetter
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }

  Object.defineProperty(target, key, sharedPropertyDefinition)
}

// 定义computed的专属getter函数
function createComputedGetter (key) {
  return function computedGetter () {
    // _computedWatchers上为每个computed属性定义了Watcher对象
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // dirty为true,表示依赖的属性有变化
      if (watcher.dirty) {
      // 重新计算值
        watcher.evaluate()
      }
      if (Dep.target) {
        // 将Dep.target(watcher)附加到当前watcher的依赖中
        watcher.depend()
      }
      return watcher.value
    }
  }
}

如果Dep.target有值,将其他依赖当前计算属性的Watcher(例如使用到fullName的依赖Watcher)附加到当前计算属性所依赖的属性的dep集合中。如下面的代码创建了对fullName计算属性的监听, 我们将其命名为watcher3。那么firstName和secondName的dep对象都会附加上watcher3观察者,只要其属性有任何变化,都会触发watcher3的update函数,重新读取fullName属性值。

vm.$watch('fullName', function (newVal, oldVal) {
  // 做点什么
})

1.3.2 watch属性

initWatch函数逻辑相对简单些,遍历每个属性的依赖项,如果依赖项为数组,则遍历数组,为每个依赖项单独创建Watcher观察者,createWatcher函数在前文中有提到,它使用$watch创建新的watcher实体。

// 初始化Watch属性
function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    // 如果对应属性key有多个依赖项,则遍历为每个依赖项创建watcher
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

2.Scheduler调度处理

Vue在core/observer/scheduler.js文件定义了调度函数,一共有两处使用,Watcher对象以及core/vdom/create-component.js文件。watcher对象在执行更新时,会被附加到调度队列中等待执行。create-component.js主要处理渲染过程,使用scheduler的主要作用是触发activated hook事件。这里重点阐述Watcher对Scheduler的使用。
当执行watcher的update函数,除了lazy(计算属性watcher)、sync(同步watcher),所有watcher都将调用queueWatcher函数附加到调度队列中。

export default class Watcher {
  /**
 * 通知订阅,如果依赖项有更新,该函数会被触发
 */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

queueWatcher函数定义如下,函数的目的是将watcher附加到调度队列中,对调度队列创建微任务(microTask),等待执行。关于microTask和macroTask的区别,看查看参考8“宏任务macroTask和微任务microTask的区别”。如果微任务flushSchedulerQueue还未执行(flushing为false),直接将watcher附加到queue即可。否则,还需判断当前微任务的执行进度,queue会按watcher的id做升序排序,保证先创建的watcher先执行。index为微任务中正在被执行的watcher索引,watcher将会插入到大于index且符合id升序排列的位置。最后队列执行函数flushSchedulerQueue将通过nextTick创建一个微任务等待执行。

/*
* 附加watcher到队列中,如果有重复的watcher直接跳过。
* 如果调度队列正在执行(flushing为true),将watcher放到合适的位置
*/
export function queueWatcher (watcher: Watcher) {
  // 所有watcher都有一个递增的唯一标识,
  const id = watcher.id
  // 如果watcher已经在队列中,不做处理
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // 如果队列还未执行,则直接附加到队列尾部
      queue.push(watcher)
    } else {
      // 如果正在执行,基于id将其附加到合适的位置。
      // index为当前正在执行的watcher索引,并且index之前的watcher都被执行了。
      // 先创建的watcher应该被先执行,和队列中的watcher比较id大小,插入到合适的位置。
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      // i的位置,表明 watcher[i - 1].id < watcher[i].id < watcher[i + 1].id
      queue.splice(i + 1, 0, watcher)
    }
    // 如果未排队,开始排队,nextick将执行调度队列。
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
 }

nextTick将会选择适合当前浏览器的微任务执行队列,例如MutationObserver、Promise、setImmediate。flushSchedulerQueue函数将遍历所有watcher并执行更新,首先需要将queue做升序排序,确保先创建的watcher先被执行,例如父组件的watcher优先于子组件执行。接着遍历queue队列,先触发watcher的before函数,例如前文中介绍mountComponent函数在创建watcher时会传入before事件,触发callHook(vm, 'beforeUpdate')。接下来就具体执行更新(watcher.run)操作。当队列执行完后,调用resetSchedulerState函数清空队列、重置执行状态。最后callActivatedHooks和callUpdatedHooks将触发对应的activated、updated hook事件。

/**
 * 遍历执行所有的watchers
 */
 function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // 遍历之前先排序队列
  // 排序的队列能确保:
  //    1.父组件先于子组件更新,因为父组件肯定先于子组件创建。
  //    2.组件自定义的watcher将先于渲染watcher执行,因为自定义watcher先于渲染watcher创建。
  //    3.如果组件在父组件执行wtcher期间destroyed了,它的watcher集合可以直接被跳过。
  queue.sort((a, b) => a.id - b.id)

  // 不要缓存length,因为在遍历queue执行wacher的同时,queue队列一直在调整。
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // 通过before可触发hook,例如执行beforeUpdated hook
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 执行watcher的更新
    watcher.run()
  }

  // 由于activatedChildren和queue两个队列一直在更新,因为需要拷贝处理
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  // 重置掉队队列状态
  resetSchedulerState()

  // 触发activated和updated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}

3.Watcher更新

调度队列会执行watcher的run函数触发更新,每个watcher有active状态,表明当前watcher是否处于激活状态,当组件执行$destroy函数,会调用watcher的teardown函数将active设置为false。在执行更新通知回调cb之前,有三个条件判断,首先判断值是否相等,对于简单值string或number类型的可直接判断;如果value为对象或需要深度遍历(deep为true),例如用户自定义了person属性,其值为对象{ age: number, sex: number },我们使用$watch('person', cb)监听了person属性,但当person.age发生变化时,cb不会被执行。如果改成$watch('person', cb, { deep: true }),任何嵌套的属性发生变化,cb都会被触发。满足三个条件其中之一,cb回调函数将被触发。

export default class Watcher {
  /**
 * 调度接口,将被调度器执行
 */
   run () {
    // 仅当watcher处于激活状态,才会执行更新通知
    // 当组件destroyed时,会调用watcher的teardown将其重置到非激活状态
    if (this.active) {
      // 调用get获取值
      const value = this.get()
      if (
        // 如果新计算的值更新了
        value !== this.value ||
        // 如果value为对象或数组,不管value和this.value相等否,则其深度watchers也应该被触发
        // 因为其嵌套属性可能发生变化了
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

this.$watch(&#39;person&#39;, () => {
  this.message = &#39;年龄为:&#39; + this.person.age
  }, 
  // 当deep为true,当age更新,回调会被触发;如果deep为false,age更新不会触发回调
  { deep: true }
)

run函数有调用get获取最新值,在get函数中,首先调用pushTarget函数将当前Watcher附加到全局Dep.target上,然后执行getter获取最新值。在finally模块中,如果deep为true,则调用traverse递归遍历最新的value,value可能为Object或者Array,所以需要遍历子属性并触发其getter函数,将其dep属性附加上Dep.target(当前Watcher),这样任何子属性的值发生变化都会通知到当前watcher,至于为什么,可以回顾下上篇《Vue如何实现数据状态的侦测》。

export default class Watcher {
  /**
* 执行getter,重新收集依赖项
*/
  get () {
    // 将当前Watcher附加到全局Dep.target上,并存储targetStack堆栈中
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行getter读取value
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // 如果deep为true,将遍历+递归value对象
      // 将所有嵌套属性的dep都附加上当前watcher,所有子属性对应的dep都会从push(Dep.target)
      if (this.deep) {
        // 递归遍历所有嵌套属性,并触发其getter,将其对应的dep附加当前watcher
        traverse(value)
      }
      // 退出堆栈
      popTarget()
      // 清理依赖
      this.cleanupDeps()
    }
    return value
  }
}

在get函数中为什么要执行traverse递归遍历子属性,我们可以通过实际的例子来说明,例如在data中定义了{ person: { age: 18, sex: 0, addr: { city: '北京', detail: '五道口' } }, Vue会调用observe将person转换为如下Observer对象,子属性(如果为对象)也会转换为Observer对象,简单属性都会定义get、set函数。

Learn more about Watcher and Scheduler in Vue

当watcher.get执行traverse函数时,会递归遍历子属性,当遍历到addr属性时,触发get函数,该函数将调用其dep.depend将当前Watcher附加到依赖项中,这样我们在执行执行this.person.age = 18,其set函数调用dep.notify触发watcher的update函数,实现person对象的监听。

get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val
  if (Dep.target) {
    dep.depend()
    ...
  }
  return value
}

set: function reactiveSetter (newVal) {
  ...
  dep.notify()
}

4.总结

本文首先介绍Watcher从创建到最终组件渲染经历的流程,然后就"何时创建Watcher"、"Scheduler调度处理"、"Watcher更新"三部分作了详细介绍。通过"何时创建Watcher"了解到Vue在哪些地方会创建Watcher对象,"Scheduler调度处理"介绍了Vue如何通过微任务来执行wacher队列的更新,"Watcher更新"简单讲述了每一个watcher如何执行更新。由于篇幅原因,watcher的更新最终如何体现到视图的渲染,将在下一篇做具体介绍。篇幅有点长,能看到这里,说明大家对Vue是真爱。

更多编程相关知识,请访问:编程入门!!

The above is the detailed content of Learn more about Watcher and Scheduler in Vue. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:juejin.cn. If there is any infringement, please contact admin@php.cn delete