文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Vue中Watcher和Scheduler的实现原理是什么

2023-06-21 22:28

关注

这篇文章主要介绍“Vue中Watcher和Scheduler的实现原理是什么”,在日常操作中,相信很多人在Vue中Watcher和Scheduler的实现原理是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Vue中Watcher和Scheduler的实现原理是什么”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

Vue中Watcher和Scheduler的实现原理是什么

Vue通过数据侦测机制感知状态的变化,上一篇《Vue如何实现数据侦测》有提到Watcher对象,当数据更新有更新,例如当执行this.title = '监听我变化了没',在setter函数调用dep.notify通知watcher执行更新(具体执行watcher.update函数)。

那么Vue在何时创建Watcher,如何通过Scheduler来调度Watcher队列,watcher的更新最终如何体现到视图的渲染,本篇内容主要围绕这三个问题来介绍Vue的Watcher实现原理。

Vue中Watcher和Scheduler的实现原理是什么

1.何时创建Watcher

组件从创建到销毁会经历一系列生命周期,其中我们比较熟悉的有beforeMount、mounted、beforeUpdate、updated, 了解了生命周期,理解Watcher在何时被创建就会容易很多。Vue共三处地方会创建Watcher对象,mount事件、$watch函数、computed和watch属性, mount事件创建Watcher用于渲染通知,watch和computed创建的Watcher都用于监听用户自定义的属性变化。

1.1 mount事件

文件core/instance/lifecycle.js包含了Vue生命周期相关的函数,例如$forupdate、$destroy以及实例化Watcher的mountComponent函数,mountComponent函数在组件挂载完成执行$mount时触发,函数首先触发beforeMount钩子事件,在实例化Watcher时有传入before函数,before将触发beforeUpdate hook。当组件有属性更新时,watcher在更新(watcher.run)之前会触发beforeUpdate事件。isRenderWatcher表明创建的是渲染Watcher,直接挂在vm._watcher属性上,当强制执行$forceUpdate刷新渲染,会执行vm._watcher.update触发渲染过程以及对应的update hook。

 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 )    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函数

在组件中,除了使用watch、computed方法监听属性变化,Vue定义了$watch函数用于监听属性变化,例如当a.b.c嵌套属性变化,可以$watch来实现监听做后续处理,$watch相当于在组件中直接写watch属性的函数式写法,可支持在运行时动态的添加依赖监听,例如Vue源码中的keep-alive组件在mounted事件中使用$watch监听include、exclude属性变化。

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))    })  }

$watch函数和mountComponent函数的区别是,mountComponent用于渲染监听,会触发相关的hook事件,而$watch的职责比较专一,就处理expOrFn的监听。另外,$watch的cb参数可以是函数、对象或字符串,当为字符串时表示定义在Vue对象的函数名,例如在Vue组件中定义了nameChange函数,那么定义vm.$watch('name', 'nameChange')后,如果name有更新会触发Vue实体的nameChange函数。

// 监听属性变化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和computed属性

使用Vue开发组件,这两个属性一定不陌生,例如使用watch定义firstName、secondName属性的监听,使用computed定义fullName属性监听,当firstName和secondName更新时fullName也随之触发更新。

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'  }})

当我们在watch和computed定义了对属性的监听,Vue在何时将其转换为Watcher对象执行监听?Vue的构造函数会调用_init(options)执行初始化,源码core/components/instance/init.js文件定义了_init函数,执行了一些列初始化操作,例如初始化生命周期、事件、状态等,其中initState函数就包含了watch和computed的初始化。

// core/components/instance/init.js// Vue构造函数function Vue (options) {  this._init(options)}// core/components/instance/init.jsVue.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.jsexport 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属性

initComputed初始化computed属性,每一个Vue实体都包含_computedWatchers对象用于存储所有computed属性的watcher对象。首先遍历computed对象,为每个key创建一个新的Watcher对象,其lazy属性为true,表示Watcher会缓存计算值,如果依赖其依赖的属性(如firstName、secondName)没有更新,当前computed属性(例如fullName)也不会触发更新。computed中定义的属性可以通过this(例如this.fullName)访问,defineComputed将所有computed属性挂载到Vue实体上。

// 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函数将计算属性转换为{ get, set }形式,但计算属性不需要set,所以代码直接为其赋值了noop空函数。计算属性的get函数通过createComputedGetter封装,首先找到对应属性的watcher对象,如果watcher的dirty为true,表示依赖属性有更新,需要调用evaluate函数重新计算新值。

// 将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 () {        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创建一个微任务等待执行。

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事件。

 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('person', () => {  this.message = '年龄为:' + 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 {    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函数。

Vue中Watcher和Scheduler的实现原理是什么

当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()}

到此,关于“Vue中Watcher和Scheduler的实现原理是什么”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯