Vue 2 阅读理解(十八)之响应式系统(四)Watcher

简介: Vue 2 阅读理解(十八)之响应式系统(四)Watcher

响应式系统(四)


关于 Vue 2 响应式系统的前三节已经讲述了 “数据劫持 Observer” 与 “依赖收集分发 Dep”,但是关于 “依赖收集分发” 部分还有还有 “观察者 Watcher” 没有解析。

上文也提到了,Dep 不能脱离 Watcher 单独使用。


1. Watcher 定义


该类定义位于 src/core/observer/watcher.ts,定义如下:


export default class Watcher implements DepTarget {
  vm?: Component | null
  expression: string
  cb: Function
  id: number
  deep: boolean
  user: boolean
  lazy: boolean
  sync: boolean
  dirty: boolean
  active: boolean
  deps: Array<Dep>
  newDeps: Array<Dep>
  depIds: SimpleSet
  newDepIds: SimpleSet
  before?: Function
  onStop?: Function
  noRecurse?: boolean
  getter: Function
  value: any
  post: boolean
  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    recordEffectScope(this, activeEffectScope || (vm ? vm._scope : undefined))
    if ((this.vm = vm)) {
      if (isRenderWatcher) {
        vm._watcher = this
      }
    }
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid
    this.active = true
    this.post = false
    this.dirty = this.lazy
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = ''
    if (isFunction(expOrFn)) {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      !this.getter && (this.getter = noop)
    }
    this.value = this.lazy ? undefined : this.get()
  }
  get() {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e: any) {
    } finally {
      this.deep && traverse(value)
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  addDep(dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
  cleanupDeps() {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp: any = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
  // ... 其他实例方法
}


2. 参数和属性分析


class Watcher 中定义了接近 20 个属性(排除了 dev 环境的 debug 支持),除了标识唯一性的 id 属性、当前组件实例的 vm 属性、观察变更时的回调 cb 之外,其他的属性大致可以进行以下分类:


  1. 观察对象相关属性,包括 expression,getter,value,deep,其中 expression 主要是用来进行开发模式下的错误提示,getter 用来获取当前时刻下 expOrFn 获取到的值,deep 则表示深度监听处理


  1. 观察对象改变时的回调函数执行控制,包括 deep,lazy,dirty,sync,post,active,before,user


  1. 其中 user 主要是用来处理回调函数执行时的错误信息处理,默认是 false,即不处理错误;


       b. before 则是一个可选的函数,会在 cb 回调函数执行前执行,这一点可以在 $mount 方法的定义中有明确体现(会在 before 中触发生命周期 beforeUpdate 的函数调用)

       c. active 用来控制是否可以触发,即是否执行 cb 回调


       d. dirty 则是控制 computed 计算属性的延迟更新,配合 lazy 一起实现;syncpost 用来确定 cb 执行时刻,是否是立即执行或者等待该次数据、试图更新结束之后再执行


  1. 依赖处理部分属性,包括 deps,newDeps,depIds,newDepIdsonStop,其中 onStop 用来处理该观察者依赖项被情况时执行的操作,其他几个属性则是用来收集当前状态解析/执行 expOrFn 时的依赖,并且针对性能做了一些优化处理(下面会讲)


3. 实例化与更新过程


3.1 属性初始化


new Watcher() 时,首先执行 recordEffectScope(),这个方法会将该 watcher 实例注册到 vm._scope.effects 数组中。


这里与 Vue 2.7 之前的版本有一些区别。在 Vue 2.6.14 及之前的版本中,组件内的 watcher 实例都是绑定在 vm._watchers 属性中的。


之后则是根据 options 参数进行属性初始化,这里有以下几点注意事项:


  1. depIdsnewDepIds 采用的是 ES6 的 Set 结构,在后续 addDep 添加依赖时可以通过 depIds.has() 来去除重复依赖


  1. getter 会被定义成一个函数,就算是一个 xxx.xxx.xxx 结构的字符串,也会通过 parsePath 解析成一个函数


  1. lazy 情况(非 computed 或者特殊声明)下,会执行 watcher.get() 来初始化当前时刻监听到的 expOrFn 的值


3.2 watcher.get() 收集依赖


在此过程中,首先就是通过 pushTarget(this)Dep.target 指向当前的 watcher 实例,保证在 watcher.get 的过程中收集到的依赖都是对应到该 watcher 实例的。

之后则是执行 this.getter.call(vm, vm),获取到 watcher 实例的监听对象 expOrFn 此时对应的值;并且在执行过程中,会进行数据读取,触发 Object.defineProperty 中定义的 getter,调用 dep.depend() 执行依赖收集。上一节也已经讲过,dep.depend() 其实就是调用 Dep.target(也就是此时的 watcher 实例)的 addDep(this) 方法,将自己(该数据对应的 dep 实例)插入到 watcher 的 newDeps,newDepIds 中,并且只有在 原有的 watcher.deps 中不存在这个 dep 实例,才会在 dep.subs 中插入该 watcher,这样可以避免 watcher 重复。


如果配置了深度监听(deep 为 true)时,还会通过 traverse(value) 遍历获取结果触发每一级子属性的 getter 来进行依赖收集。


最后则是调用 popTarget() 结束当前阶段的依赖收集,并调用 cleanupDeps()newDeps,newDepIds 的值复制给 deps, depIds 并清空。


网络异常,图片无法展示
|


当然,渲染 watcher(isRenderWatcher 为 true)此时对应的 expOrFn 为:


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


这个模板解析过程中解析到的变量都会添加到 renderWatcher.deps 中


3.3 cleanupDeps() 清除依赖


整个 cleanupDeps() 函数的执行过程,就是清除无用依赖,然后将 newDeps, newDepIds 的值赋值给原始的 deps, depIds,并把 newDeps, newDepIds 清空,方便下次执行 get() 和 addDep() 的执行。


上面定义里有体现,在 addDep(dep) 时,会首先判断 newDepIds 中不存在这个数据的依赖引用 dep,并将这个 dep 添加到 newDepIdsnewDeps 中;且当 depIds 中也不存在该 dep 时,才会执行 dep.addSub(),将 watcher 观察者添加到 dep 的通知对象 subs 中。


cleanupDeps() 中最先执行的 dep.removeSub(this),就是为了优化掉不需要被通知改变的一系列观察者。


最常见的场景就是使用 v-if,v-else-if,v-else 的场景,当节点对应的条件为 false 的时候,节点内部定义的一系列数据变化都是可以取消监听的,因为此时的节点都不会被渲染,也没有在 get() 中收集相关依赖,所以需要手动将原有的依赖从 dep.subs 中清理掉。


3.4 run() 与 update() 状态更新


在数据更新的时候,会通过 Observer - Object.defineProperty 定义的 setter 方法来处理,此时会触发 dep.notify()


dep.notify() 的逻辑也很简单,就是遍历 dep.subs 数组,依次执行 sub[i].update();上面的过程也可以看到,这里的 sub[i] 其实就是依赖这个数据的每一个 watcher 实例。


首先我们来看 update()


update() {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}


这里也比较清晰,就是三个判断:


  1. lazy 为 true,表示延迟执行,此时把脏值标志(也就是 computed 的惰性更新机制)设置为 true


  1. 如果 sync 为 true,表示立即同步执行回调,则直接执行 run() 方法


  1. 其他情况则使用 queueWatcher 将该次回调插入到更新后的执行队列中,待当前的组件实例更新结束之后再依次执行(当然,最后也是通过 watcher.run() 执行回调方法 cb)


然后就是核心方法之一 run()


run() {
  if (this.active) {
    const value = this.get()
    if (value !== this.value || isObject(value) || this.deep) {
      const oldValue = this.value
      this.value = value
      this.cb.call(this.vm, value, oldValue)
    }
  }
}


当然这里我省略了错误处理那部分内容。


这个方法也就处理了两个地方:


  1. 更新当前 watcher 的 value


  1. 执行 cb 函数


4. computed 支持


在上面的过程中,提到了 computed 配置产生的 watcher 实例

(computedWatcher),默认会将 lazy 设置为 true,那么此时在执行 watcher.update() 时是会直接退出的,怎么处理这个 lazy 和 dirty 呢?


所以 Watcher 又定义了另外两个方法:evaluate()depend(),其定义如下:


evaluate() {
  this.value = this.get()
  this.dirty = false
}
depend() {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}


evaluate:


在触发 computed.getter(即读取 computed 配置的计算属性时),才会通过该方法获取到 watcher 实例当前的值,并作为 getter 返回值


depend:


处理 computed.getter 方法中遇到的变量,并生成一个 computedWatcher 插入到所有变量对应的 dep 实例的 subs 数组中


这里的逻辑可能有点绕,笔者的理解和描述也可能有误差,希望大家多多包涵和指出错误😘


5. v3 语法支持


因为在 Vue 2.7 之后是适配了 Vue 3 的 setup 语法(组合式 api)的,而 Vue 3 中的 watchEffect 支持取消监听,所以 Watcher 又增加了一个取消依赖订阅的方法。


teardown:


teardown() {
  if (this.vm && !this.vm._isBeingDestroyed) {
    remove(this.vm._scope.effects, this)
  }
  if (this.active) {
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
    this.active = false
    if (this.onStop) {
      this.onStop()
    }
  }
}


作用如下:


  1. 在实例还存在且没有被销毁时,在 vm._scope.effects 将该 watcher 实例移除掉


  1. 如果该 watcher 实例是活动的(也就是没有配置取消监听),会依次在 watcher.deps 中清除掉当前 watcher 的依赖,并将 active 设置为 false


  1. 如果有配置停止监听时的处理方法,还会执行 onStop()


目录
相关文章
|
2天前
|
JavaScript API
Vue3的响应式原理
Vue 3 中的响应式原理是通过使用 ES6 的 `Proxy 对象`来实现的**。在 Vue 3 中,每个组件都有一个响应式代理对象,当组件中的数据发生变化时,代理对象会立即响应并更新视图。
|
2天前
|
JavaScript 前端开发
深入了解前端框架Vue.js的响应式原理
本文将深入探讨Vue.js前端框架的核心特性之一——响应式原理。通过分析Vue.js中的数据绑定、依赖追踪和虚拟DOM等机制,读者将对Vue.js的响应式系统有更深入的理解,从而能够更好地利用Vue.js构建灵活、高效的前端应用。
|
2天前
|
JavaScript 测试技术 开发者
Vue 3 Vuex:构建更强大的状态管理系统
Vue 3 Vuex:构建更强大的状态管理系统
25 1
|
2天前
|
JavaScript 前端开发 开发者
Vue的响应式原理:深入探索Vue的响应式系统与依赖追踪
【4月更文挑战第24天】Vue的响应式原理通过JavaScript getter/setter实现,当数据变化时自动更新视图。它创建Watcher对象收集依赖,并通过依赖追踪机制精确通知更新。当属性改变,setter触发更新相关Watcher,重新执行操作以反映数据最新状态。Vue的响应式系统结合依赖追踪,有效提高性能,简化复杂应用的开发,但对某些复杂数据结构需额外处理。
|
2天前
|
JavaScript 前端开发 UED
Vue工具和生态系统: Vue.js和服务器端渲染(SSR)有关系吗?请解释。
Vue.js是一个渐进式JavaScript框架,常用于开发单页面应用,但其首屏加载较慢影响用户体验和SEO。为解决此问题,Vue.js支持服务器端渲染(SSR),在服务器预生成HTML,加快首屏速度。Vue.js的SSR可手动实现或借助如Nuxt.js的第三方库简化流程。Nuxt.js是基于Vue.js的服务器端渲染框架,整合核心库并提供额外功能,帮助构建高效的应用,改善用户体验。
21 0
|
2天前
|
JavaScript 搜索推荐 前端开发
Vue工具和生态系统:Vue CLI是什么?它的作用是什么?
【4月更文挑战第17天】Vue CLI是官方的Vue.js开发加速器,它包含交互式项目模板和@vue/cli-service,基于webpack并预设配置。支持个性化配置和插件扩展,拥有大量官方插件,整合最佳前端工具。还提供图形化界面用于项目管理和创建。
11 0
|
2天前
|
Web App开发 JavaScript 开发者
Vue工具和生态系统:什么是Vue DevTools?如何使用它?
Vue Devtools是Vue.js官方的浏览器扩展,用于简化应用调试和优化。可在Chrome和Firefox等浏览器上安装,集成到开发者工具中。安装步骤包括下载源码、npm安装、修改manifest.json并加载编译后的扩展。启用后,开发者能查看Vue组件树,检查属性,并在允许的情况下编辑data,提升开发效率。
18 0
|
2天前
|
JavaScript 前端开发 开发者
浅谈Vue 3的响应式对象: ref和reactive
浅谈Vue 3的响应式对象: ref和reactive
|
2天前
Vue3 响应式数据 reactive使用
Vue3 响应式数据 reactive使用
|
2天前
|
JavaScript
Vue 响应式数据的判断
Vue 响应式数据的判断