Maison > interface Web > Voir.js > En savoir plus sur Watcher et Scheduler dans Vue

En savoir plus sur Watcher et Scheduler dans Vue

青灯夜游
Libérer: 2021-12-01 19:59:03
avant
3011 Les gens l'ont consulté

Cet article vous amènera à comprendre le Watcher et le Scheduler dans Vue, et à présenter le principe de mise en œuvre du Watcher de Vue. J'espère qu'il vous sera utile.

En savoir plus sur Watcher et Scheduler dans Vue

Vue détecte les changements d'état via le mécanisme de détection des données. L'article précédent "Comment Vue implémente la détection des données" mentionnait l'objet Watcher Lorsque les données sont mises à jour, par exemple, lorsque this.title = '. Écouter mes modifications est exécuté. OK', appelez dep.notify dans la fonction setter pour demander à l'observateur d'effectuer la mise à jour (exécutez spécifiquement la fonction watcher.update).

Alors, quand Vue crée-t-il un Watcher, comment planifier la file d'attente du Watcher via le Scheduler, et comment la mise à jour du watcher se reflète finalement dans le rendu de la vue. Cet article se concentre principalement sur ces trois questions pour présenter le principe de. Implémentation Watcher de Vue. [Recommandations associées : "vue.js Tutorial"]

En savoir plus sur Watcher et Scheduler dans Vue

1. Quand créer un composant Watcher

passera par une série de cycles de vie de la création à la destruction, parmi lesquels nous sommes plus familiers avec beforeMount, monté, avant la mise à jour, mis à jour, la compréhension du cycle de vie permet de comprendre beaucoup plus facilement quand le Watcher est créé. Vue crée des objets Watcher à trois endroits, l'événement mount, la fonction $watch, les propriétés calculées et watch. L'événement mount crée un Watcher pour le rendu des notifications. Les Watchers créés par watch et calculés sont tous deux utilisés pour surveiller les modifications de propriétés définies par l'utilisateur.

En savoir plus sur Watcher et Scheduler dans Vue

Événement de montage 1.1

Le fichier core/instance/lifecycle.js contient des fonctions liées au cycle de vie de Vue, telles que $forupdate, $destroy et la fonction mountComponent qui instancie le Watcher. La fonction mountComponent est exécutée après. le composant est monté $ Déclenché lors du montage. La fonction déclenche d'abord l'événement de hook beforeMount Lors de l'instanciation de Watcher, la fonction before est transmise et before déclenchera le hook beforeUpdate. Lorsqu'un composant a une mise à jour de propriété, le watcher déclenchera l'événement beforeUpdate avant la mise à jour (watcher.run). isRenderWatcher indique que le rendu Watcher est créé et est directement accroché à l'attribut vm._watcher Lorsque $forceUpdate est exécuté de force pour actualiser le rendu, vm._watcher.update sera exécuté pour déclencher le processus de rendu et le hook de mise à jour correspondant.

/**
 * 生命周期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()
  }
}
Copier après la connexion

1.2.$watch function

Dans le composant, en plus d'utiliser les méthodes watch et calculées pour surveiller les modifications de propriétés, Vue définit la fonction $watch pour surveiller les modifications de propriétés. Par exemple, lorsque les propriétés imbriquées a.b.c changent, $watch. peut être utilisé pour implémenter la surveillance.Pour le traitement ultérieur, $watch équivaut à une méthode d'écriture fonctionnelle consistant à écrire directement l'attribut watch dans le composant, qui peut prendre en charge l'ajout dynamique de la surveillance des dépendances au moment de l'exécution, par exemple, le composant keep-alive dans. le code source de Vue utilise $watch pour surveiller l'inclusion dans l'événement monté. Les modifications de l'attribut d'exclusion.

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))
    })
  }
Copier après la connexion

La différence entre la fonction $watch et la fonction mountComponent est que mountComponent est utilisé pour la surveillance du rendu et déclenchera les événements de hook associés, tandis que $watch a une responsabilité plus spécifique et gère la surveillance expOrFn. De plus, le paramètre cb de $watch peut être une fonction, un objet ou une chaîne. Lorsqu'il s'agit d'une chaîne, il représente le nom de la fonction défini dans l'objet Vue. Par exemple, si la fonction nameChange est définie dans le composant Vue, alors. définir vm.$watch('name' , 'nameChange'), si le nom est mis à jour, la fonction nameChange de l'entité Vue sera déclenchée.

// 监听属性变化
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)
}
Copier après la connexion

1.3. watch et propriétés calculées

Utilisez Vue pour développer des composants. Par exemple, utilisez watch pour définir la surveillance des propriétés firstName et secondName, et utilisez calculator pour définir la surveillance du fullName. Lorsque firstName et secondName sont mis à jour, fullName sera également mis à jour.

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'
  }
})
Copier après la connexion

Lorsque nous définissons la surveillance des propriétés dans watch et calculée, quand Vue les convertit-il en un objet Watcher pour effectuer la surveillance ? Le constructeur de Vue appellera _init(options) pour effectuer l'initialisation. Le fichier de code source core/components/instance/init.js définit la fonction _init, qui effectue une série d'opérations d'initialisation, telles que le cycle de vie de l'initialisation, les événements, le statut, etc. , parmi lesquels initState La fonction inclut l'initialisation de la montre et du calcul.

// 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)
  }
}
Copier après la connexion

1.3.1 attribut calculé

initComputed initialise l'attribut calculé. Chaque entité Vue contient un objet _computedWatchers pour stocker les objets d'observation pour tous les attributs calculés. Parcourez d'abord l'objet calculé et créez un nouvel objet Watcher pour chaque clé. Son attribut paresseux est vrai, ce qui signifie que le Watcher mettra en cache la valeur calculée si les propriétés dont il dépend (telles que firstName, secondName) ne sont pas mises à jour. la propriété calculée actuelle (telle que fullName) ) ne déclenchera pas de mise à jour. Les propriétés définies dans computing sont accessibles via ceci (par exemple, this.fullName). DefinComputed monte toutes les propriétés calculées sur l'entité Vue. La fonction

// 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)
      }
}
Copier après la connexion

defineComputed convertit les propriétés calculées en forme {get, set}, mais les propriétés calculées ne nécessitent pas de définition, donc le code lui attribue directement une fonction vide noop. La fonction get de l'attribut calculé est encapsulée par createComputedGetter. Tout d'abord, l'objet watcher de l'attribut correspondant est trouvé. Si la valeur sale de l'observateur est vraie, cela signifie que l'attribut dépendant a été mis à jour et que la fonction d'évaluation doit le faire. être appelé pour recalculer la nouvelle valeur.

// 将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
    }
  }
}
Copier après la connexion

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

vm.$watch('fullName', function (newVal, oldVal) {
  // 做点什么
})
Copier après la connexion

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)
    }
  }
}
Copier après la connexion

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)
    }
  }
}
Copier après la connexion

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)
    }
  }
 }
Copier après la connexion

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)
}
Copier après la connexion

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 }
)
Copier après la connexion

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
  }
}
Copier après la connexion

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

En savoir plus sur Watcher et Scheduler dans 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()
}
Copier après la connexion

4.总结

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

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:juejin.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal