这一篇博客讲讲 Vue2 中的 watch 和 computed 是如何实现的,其实实现思路和 Vue3 中也差不多,大家可以和我的上一篇博客进行一个横向的对比。
在 Vue3 中,依赖收集的对象是 ReactiveEffect 类的实例,而在 Vue2 中,依赖收集的对象是 Watcher 类的实例,这两者所起的作用是差不多的,不过 Vue2 中的 Watcher 相较于 ReactiveEffect 封装了更多的逻辑,没有 Vue3 中逻辑拆分的清晰。
除此之外,Vue3 中的响应式数据是通过 Proxy 实现的,而 Vue2 中的响应式数据是通过 Object.defineProperty 实现的,不过这一点并不影响原理的理解,都是在 getter 中进行依赖的收集,在 setter 中触发依赖的执行。
watch 的作用是对某个响应式数据进行监控,当响应式数据发生变化时,触发对应的回调函数。
这个功能实现的重点是对监控的响应式数据进行读取操作,让读取操作触发依赖的收集,后续监控的数据发生,再进而触发回调函数的执行。大家可以发现,功能的实现思路和 Vue3 是一样的。
实现源码如下所示:
- // 初始化侦听属性
- function initWatch (vm: Component, watch: Object) {
- // 遍历用户定义的 watch 对象
- for (const key in watch) {
- // 获取当前 watch 对象的处理器
- // 处理器的类型可以是:函数、字符串、对象、数组
- // 如果是 函数、字符串、对象 类型的话,此时侦听的对象和处理器是一一对应的关系
- // 如果是 数组 类型的话,此时侦听的对象和处理器是一对多的关系
- const handler = watch[key]
- // 在这里处理是数组类型的情况
- // 在 createWatcher 方法中,handler 只会是 函数、字符串、对象 类型的
- if (Array.isArray(handler)) {
- // 所以,如果 handler 是一个数组的话,需要遍历 handler 数组,为每一个处理器都执行一下 createWatcher
- for (let i = 0; i < handler.length; i++) {
- createWatcher(vm, key, handler[i])
- }
- } else {
- createWatcher(vm, key, handler)
- }
- }
- }
首先,拿到用户定义的 watch 组件选项,进行 watch 的初始化,initWatch 主要进行了 handler 是数组情况的处理。接下来,看 createWatcher 函数。
- function createWatcher (
- vm: Component,
- keyOrFn: string | Function,
- // handler 只会是 函数、字符串、对象 类型的
- handler: any,
- options?: Object
- ) {
- // 如果 handler 是对象类型的话,需要进行下数据整形,确保 handler 指向处理函数,options 指向配置对象
- if (isPlainObject(handler)) {
- options = handler
- handler = handler.handler
- }
- // 如果 handler 是字符串类型的话,从 vm 实例中获取到对应的处理函数
- if (typeof handler === 'string') {
- handler = vm[handler]
- }
- // 调用 vm.$watch 实现侦听属性的功能
- // 代码执行到这里,handler 只能是函数类型的
- // 如果 vm 中的 key 发生了变化的话,会执行 handler 回调函数
- return vm.$watch(keyOrFn, handler, options)
- }
createWatcher 函数进行了 handler 是对象和字符串的处理,处理的最终效果是 handler 一定是 watch 的回调函数,最后调用了 this.$watch() 方法。
- Vue.prototype.$watch = function (
- expOrFn: string | Function,
- cb: any,
- options?: Object
- ): Function {
- const vm: Component = this
-
- // 因为 $watch 函数既能够被框架内部调用,也能够被用户调用,所以代码执行到这里,要进行
- // cb 是对象的判断,如果 cb 是对象的话,则调用 createWatcher 进行处理。
- if (isPlainObject(cb)) {
- return createWatcher(vm, expOrFn, cb, options)
- }
- options = options || {}
- options.user = true
- // vm.$watch 方法的核心,借助 Watcher 实现功能
- const watcher = new Watcher(vm, expOrFn, cb, options)
- if (options.immediate) {
- // 如果 immediate 为 true 的话,立即执行回调函数
- cb.call(vm, watcher.value)
- }
- return function unwatchFn () {
- watcher.teardown()
- }
- }
$watch 函数内部调用 new Watcher() 实现功能,我们接下来看 Watch 类。
- export default class Watcher {
- constructor (
- vm: Component,
- expOrFn: string | Function,
- cb: Function,
- options?: Object
- ) {
- this.vm = vm
- this.cb = cb
- // getter 属性必须是一个函数,并且函数中有对使用到的值的读取操作(用于触发数据的 getter 函数,在 getter 函数中进行该数据依赖的收集)
- if (typeof expOrFn === 'function') {
- this.getter = expOrFn
- } else {
- // 而如果是一个字符串类型的话,例如:"a.b.c.d",是一个数据的路径
- // 就将 parsePath(expOrFn) 赋值给 this.getter,
- // parsePath 能够读取这个路径字符串对应的数据(一样能触发 getter,触发数据的 getter 是关键)
- this.getter = parsePath(expOrFn)
- }
- this.value = this.get()
- }
-
- get () {
- // 将自身实例赋值到 Dep.target 这个静态属性上(保证全局都能拿到这个 watcher 实例),
- // 使得 getter 函数使用数据的 Dep 实例能够拿到这个 Watcher 实例,进行依赖的收集。
- // pushTarget 操作很重要
- pushTarget(this)
- let value
- const vm = this.vm
- try {
- // 执行 getter 函数,该函数执行时,会对响应式的数据进行读取操作,这个读取操作能够触发数据的 getter,
- // 在 getter 中会将 Dep.target 这个 Watcher 实例存储到该数据的 Dep 实例中,以此就完成了依赖的收集
- // 依赖收集需要执行 addDep() 方法完成
- value = this.getter.call(vm, vm)
- } catch (e) {
- ......
- }
- // 将 expOrFn 对应的值返回出去
- return value
- }
- }
在 Watcher 类的 constructor 中,我们定义了一个 getter 函数,这个函数的作用是对响应式数据进行读取操作,后续执行了 this.get() 函数,并把返回的值保存到 this.value 上。
在 get 函数中,首先是将自身(this)放到全局作用域中,然后执行 getter 函数,getter 函数的执行会触发响应式数据的依赖收集操作。
接下来,响应式数据的变更会触发 Watcher 类中的 update 方法,源码如下所示:
- export default class Watcher {
- update () {
- /* istanbul ignore else */
- if (this.lazy) {
- // lazy 属性为 true,说明当前的 watcher 实例是针对计算属性的,又因为依赖的数据发生了变化,此时需要将 dirty 设为 true
- this.dirty = true
- } else if (this.sync) {
- this.run()
- } else {
- // 如果当前的 watcher 实例不是立即触发的话,需要将当前的 watcher 实例添加到 watcher 缓存数组中
- queueWatcher(this)
- }
- }
- }
代码会执行到 queueWatcher(this),它的作用是利用事件循环异步执行接下来的操作,加下来会执行 Watcher 类的 run 函数。
- run () {
- if (this.active) {
- const value = this.get()
- // 下面进行回调函数 cb 的处理
- 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
- this.cb.call(this.vm, value, oldValue)
- }
- }
run函数首先获取监控数据的新值和旧值,然后触发执行 this.cb 函数就可。
ok,至此 watch 的功能就实现了。
computed 功能的实现还是借助了 Watcher 类,我们先来看与 computed 功能相关的 Watcher 类中三个属性的作用,分别是:
computed 功能的实现主要关注以下两点:
接下来,看源码:
- // 初始化计算属性
- function initComputed (vm: Component, computed: Object) {
- // 每一个计算属性都有一个对应的 Watcher 实例
- // 所以 vm 会有一个 _computedWatchers 属性,专门用来保存这个 Watcher 实例
- const watchers = vm._computedWatchers = Object.create(null)
- // computed properties are just getters during SSR
- const isSSR = isServerRendering()
-
- // 遍历我们定义的计算属性,进行处理。
- for (const key in computed) {
- // 获取当前计算属性的定义
- const userDef = computed[key]
- // 获取该计算属性的 getter。因为计算属性既可以是函数类型,也可以是对象类型,所以在此需要处理一下。
- const getter = typeof userDef === 'function' ? userDef : userDef.get
- if (process.env.NODE_ENV !== 'production' && getter == null) {
- // 如果 getter 不存在的话,在此打印出警告
- warn(
- `Getter is missing for computed property "${key}".`,
- vm
- )
- }
-
- if (!isSSR) {
- // create internal watcher for the computed property.
- // 创建该计算属性对应的 Watcher 实例,计算属性的 Watcher 并不会立即执行 watcher.get(),是一个 lazy Watcher
- watchers[key] = new Watcher(
- vm,
- getter || noop,
- noop,
- computedWatcherOptions
- )
- }
-
- // component-defined computed properties are already defined on the
- // component prototype. We only need to define computed properties defined
- // at instantiation here.
- if (!(key in vm)) {
- // 如果计算属性的 key,在 data、prop 中不存在的话,在 vm 上进行定义
- defineComputed(vm, key, userDef)
- } else if (process.env.NODE_ENV !== 'production') {
- // 否则的话,则会打印出相应的警告
- if (key in vm.$data) {
- warn(`The computed property "${key}" is already defined in data.`, vm)
- } else if (vm.$options.props && key in vm.$options.props) {
- warn(`The computed property "${key}" is already defined as a prop.`, vm)
- }
- }
- }
- }
initComputed 根据用户定义的 computed 配置选项进行计算属性的初始化,初始化分为两步,第一步创建每个计算属性对应的 Watcher 实例,第二步是将计算属性定义到组件实例上,这样我们就可以在组件中通过 this.[计算属性 key] 获取计算属性的值了,接下来看 defineComputed 函数。
- export function defineComputed (
- target: any,
- key: string,
- userDef: Object | Function
- ) {
- // 在浏览器的环境下,shouldCache 为 true
- const shouldCache = !isServerRendering()
- // const sharedPropertyDefinition = {
- // enumerable: true,
- // configurable: true,
- // get: noop,
- // set: noop
- // }
- // sharedPropertyDefinition 就是一个共享的对象属性定义,我们需要为这个对象设置 get 和 set 方法
- // 下面的代码就是用于给这个对象设置 get 和 set 方法
- if (typeof userDef === 'function') {
- sharedPropertyDefinition.get = shouldCache
- // createComputedGetter 方法能够返回一个方法,返回的方法具有缓存的作用
- ? createComputedGetter(key)
- : userDef
- sharedPropertyDefinition.set = noop
- } else {
- sharedPropertyDefinition.get = userDef.get
- ? shouldCache && userDef.cache !== false
- ? createComputedGetter(key)
- : userDef.get
- : noop
- sharedPropertyDefinition.set = userDef.set
- ? userDef.set
- : noop
- }
- if (process.env.NODE_ENV !== 'production' &&
- sharedPropertyDefinition.set === noop) {
- sharedPropertyDefinition.set = function () {
- warn(
- `Computed property "${key}" was assigned to but it has no setter.`,
- this
- )
- }
- }
- // 借助 Object.defineProperty,向 target 上设置属性
- Object.defineProperty(target, key, sharedPropertyDefinition)
- }
defineComputed 函数的内部通过 Object.defineProperty() 函数将计算属性设置到组件实例上,这里主要看 sharedPropertyDefinition.get 函数,看看它是如何获取计算属性值的,接下来看 createComputedGetter 函数。
- // createComputedGetter 方法能够返回一个方法,返回的方法具有缓存的作用
- function createComputedGetter (key) {
- // 返回的方法会作为 get。具有缓存结果值的作用,实现的依据是 Watcher 实例的 dirty 属性,
- return function computedGetter () {
- // this 就是 vm
- // 拿到当前计算属性对应的 Watcher 实例
- const watcher = this._computedWatchers && this._computedWatchers[key]
- if (watcher) {
- // dirty 属性是一个标志位:标志着这个 Watcher 所依赖的数据有没有变化
- // 如果 Watcher 所依赖的数据没有变化的话,也就不用重新计算值(watcher.value),直接返回 watcher.value 即可
- // 如果 watcher.dirty 为 true 的话,说明 watcher.value 还没有计算或者依赖的数据变化了,此时就需要重新计算
- if (watcher.dirty) {
- watcher.evaluate()
- }
- // 下面代码的作用是:让组件的渲染 Watcher 监控 当前计算属性watcher所监控的数据。
- // 实现的效果:计算属性 watcher 所依赖的数据发生了变化的话,会触发组件的重新渲染。
- // 底层表现就是:当前计算属性的 Watcher 实例所依赖数据的 dep 实例的 subs 数组保存 组件的渲染 Watcher
- // 此时的 Dep.target 是指渲染 Watcher 实例
- if (Dep.target) {
- watcher.depend()
- }
- return watcher.value
- }
- }
- }
计算属性的 getter 函数首先获取到对应的 Watcher 实例,然后判断 Watcher 实例的 dirty 是不是为 true,如果为 true 的话,就执行 watcher.evaluate 函数进行重新求值,evaluate 函数的源码如下所示:
- evaluate () {
- this.value = this.get()
- this.dirty = false
- }
evaluate 函数执行 this.get 函数,这个函数会执行用户定义的计算属性的 getter 函数,这个 getter 函数会读取依赖的响应式数据,进行依赖收集,并返回最新的计算属性值,计算出来的值保存到 this.value,然后将 dirty 属性设置为 false。
computedGetter 函数最后返回 watch.value。
接下来,如果计算属性依赖的响应式数据发生变化的话,会触发执行对应 Watcher 实例的 update 方法,源码如下所示:
- update () {
- /* istanbul ignore else */
- if (this.lazy) {
- // lazy 属性为 true,说明当前的 watcher 实例是针对计算属性的,又因为依赖的数据发生了变化,此时需要将 dirty 设为 true
- this.dirty = true
- } else if (this.sync) {
- this.run()
- } else {
- // 如果当前的 watcher 实例不是立即触发的话,需要将当前的 watcher 实例添加到 watcher 缓存数组中
- queueWatcher(this)
- }
- }
因为计算属性 watcher 实例的 lazy 属性是 true,所以 update 方法直接将 dirty 属性设置为 true 即可。
ok,Vue2 中的 watch 和 computed 的实现思路就是这样了,是不是发现和 Vue3 中的实现思路也差不多。
接下来,说说 Vue3 中 ref 函数是如何实现的。