Vue源码之computed

computed的使用

在理解源码之前,我们可以先回忆一下 computed 的使用方法

js 复制代码
<template>
  <div>
    <h2>{{ reverseMsg }}</h2>
    <h2>{{ fullName }}</h2>
  </div>
</template>
<script>
export default {
  data () {
    return {
      msg: 'hello, vue',
      firstName: 'Jack',
      lastName: 'Cheng'
    }
  },
  computed: {
    reverseMsg () {
      return this.msg.split('').reverse().join('')
    },
    fullName: {
      get () {
        return this.firstName + ' ' + this.lastName
      },
      set (newVal) {
        [this.firstName, this.lastName] = newVal.split(' ')
      }
    }
  }
}
</script>

computed 是一个对象,属性值可以使函数,也可以是对象,如果是对象的话,需要有一个get方法,获取计算值。 计算属性默认是只读的,一般情况下,我们用不到 set 方法,这里就不过多解释了。

computed源码分析

接下来,我们就根据上面的例子,来探索一下源码实现的过程。

initState 函数

入口是从 initState 函数开始,大家可以在源码中直接搜索这个函数即可。

js 复制代码
export function initState(vm: Component) {
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)

  // Composition API
  initSetup(vm)

  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    const ob = observe((vm._data = {}))
    ob && ob.vmCount++
  }
  // 📢注意:在这里我们去对computed进行初始化,该函数内其它代码可以不用看。
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initComputed 函数

接下来继续看一下 initComputed 这个函数做了些什么。

js 复制代码
const computedWatcherOptions = { lazy: true }

function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  // 📢:这个 watchers 是用来收集每一个 computed 的 watcher 实例
  // 📢:这里涉及到了一个浅拷贝,使 vm._computedWatchers 的值和 watchers 的值始终保持一致
  const watchers = (vm._computedWatchers = Object.create(null))
  // computed properties are just getters during SSR
  const isSSR = isServerRendering() // 是否为SSR服务端渲染

  // 📢:这里会遍历computed对象上的所有属性,获取属性值 userDef
  // 📢:通过上面举得例子,这里的key会依次为:reverseMsg、fullName
  for (const key in computed) {
    const userDef = computed[key]
    // 📢:isFunction 只是判断属性值是否为函数,这里就是为什么我上面的例子会用两种方法获取计算属性值
    const getter = isFunction(userDef) ? userDef : userDef.get
    // 不用看
    if (__DEV__ && getter == null) {
      warn(`Getter is missing for computed property "${key}".`, vm)
    }
    // 如果不是服务端渲染会创建一个Watcher的实例
    if (!isSSR) {
      // create internal watcher for the computed property.
      // 📢注意:这里就是我们要给每一个计算属性创建一个 watcher 实例,
      // 后面我们可以具体看一下这个watcher 内部具体做了什么事情
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions // 📢:值为:{ lazy: true }
      )
    }

    // 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)) {
       // 📢注意:每一个计算属性都调用了 defineComputed 方法,这里主要就是实现了计算属性的初始化以及判断是否走缓存,后面会具体讲到内部的实现
      defineComputed(vm, key, userDef)
    } else if (__DEV__) {
      // 不用看
      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)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(
          `The computed property "${key}" is already defined as a method.`,
          vm
        )
      }
    }
  }
}

总结一下,initComputed 这个函数其实主要就是给每一依赖属性创建一个 watcher 实例,并维护在 vm._computedWatchers 上。同时,每一个依赖属性都调用了一次 defineComputed 方法

Watcher构造函数

由于 Watcher 构造函数上面的代码比较多,不利于分析,这里只把相关代码摘取出来,其余不用看的代码会先删掉,这样可以更直观容易理解。

js 复制代码
export default class Watcher {
  constructor(  // 📢:这里定义了参数及类型
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function, // 📢:值为:function () {}
    options?: WatcherOptions | null, // 📢:值为:{ lazy: true }
    isRenderWatcher?: boolean
  ) {
    if ((this.vm = vm) && isRenderWatcher) {
      vm._watcher = this
    }
    // options
    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 // uid for batching
    this.active = true
    this.post = false
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = __DEV__ ? expOrFn.toString() : ''
    // parse expression for getter
    if (isFunction(expOrFn)) {
      this.getter = expOrFn // 📢:给每一个依赖属性添加getter方法
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
      }
    }
    // 📢:这里有个小Tip,初始时,lazy为true,所以,不会调用get,实现了懒执行
    // 可以理解,创建实例时,如果没有读取 computed 中的计算属性,则不会进行计算
    this.value = this.lazy ? undefined : this.get()
  }
  
  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get() {
    // 📢:这个方法就是依赖收集的核心  
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e: any) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  
   /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  // 📢:更改this.value的值,再将this.dirty设置为false
  evaluate() {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend() {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

Watcher 构造函数上,有两个方法:evaluatedepend,这两个方法在 defineComputed 这里会用到。

为了加深理解,我们拿我们之前定义的 computed 中的 reverseMsg 依赖属性举例,通过 Watcher 实例化后,我们得到的是这样的一个对象:

js 复制代码
{
    reverseMsg: {
        values: undefined, // 计算所得值
        deep: false,
        lazy: true,
        dirty: true // dirty表示是否从缓存中取值
        getter() {
            return this.msg.split('').reverse().join('')
        },
        // 这里主要就是调用this.getter方法,并且将得到的值返回
        // 也就是执行 this.msg.split('').reverse().join('')
        get() { 
            pushTarget(this)
            let value
            const vm = this.vm
            try {
              value = this.getter.call(vm, vm)
            } catch (e: any) {
              if (this.user) {
                handleError(e, vm, `getter for watcher "${this.expression}"`)
              } else {
                throw e
              }
            } finally {
              // "touch" every property so they are all tracked as
              // dependencies for deep watching
              if (this.deep) {
                traverse(value)
              }
              popTarget()
              this.cleanupDeps()
            }
            return value
        },
        update() {
            // 📢:这里的lazy为true,后面的逻辑不用看
            if (this.lazy) {
              this.dirty = true
            } else if (this.sync) {
              this.run()
            } else {
              queueWatcher(this)
            }
        },
        evaluate() {
            this.value = this.get()
            this.dirty = false
        },
        depend() {
            let i = this.deps.length
            while (i--) {
              this.deps[i].depend()
            }
        }
    }
}

另外这个Wacher构造函数,除了在计算属性这里会创建 计算watcher实例(computed watcher),在页面渲染的时候还会创建 渲染watcher实例(render watcher),具体可以在源码里查一下 mountComponent 这个方法,在这个方法里也进行了new Watcher。后面讲到依赖收集的时候会涉及到。在监听watch属性上面也创建了用户监听watcher

所以总共有三个地方会创建这个watcher实例。

defineComputed 方法

initComputed 这个方法中,每一个依赖属性都调用了 defineComputed 方法,这个方法的核心是调用了 createComputedGetter ,后面我们再继续看一下具体做了什么。

js 复制代码
export function defineComputed(
  target: any, // 📢:传值为vm
  key: string,
  userDef: Record<string, any> | (() => any)
) {
  const shouldCache = !isServerRendering()
  if (isFunction(userDef)) {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (__DEV__ && sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  // 📢:这里实现的目的是将计算属性绑定在vm上
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

createComputedGetter 方法

js 复制代码
function createComputedGetter(key) {
  return function computedGetter() {
    // 📢:_computedWatchers这个是不是眼熟,这里就是获取我们通过 Watcher 去实例化的对象,
    // 可以回去再看一下之前举得例子, reverseMsg 这个实例化后的对象
    // this._computedWatchers[key] => reverseMsg
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 📢:这里的 dirty 用来标记所依赖的值是否发生了变化,
      // 如果为 true,则不走缓存,会调用 evaluate 方法,重新计算获取计算属性值
      // 如果为 false,则直接取 watcher.value (缓存)中的值
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 📢:Dep.target 上的值具体可以看下面 pushTarget 、popTarget 方法
      if (Dep.target) {
        if (__DEV__ && Dep.target.onTrack) {
          Dep.target.onTrack({
            effect: Dep.target,
            target: this,
            type: TrackOpTypes.GET,
            key
          })
        }
        watcher.depend()
      }
      // 📢:最后将这个计算属性值返回,结束!
      return watcher.value
    }
  }
}

pushTarget 、popTarget方法

在上面的 Watcher 构造函数中,只要调用了get方法,就会执行这个pushTarget方法,并且会将当前的watcher实例作为参数target。

之前有提到过,有渲染watcher,计算watcher以及用户监听watcher,下面就分析一下这个过程:

⭐️ 在渲染的时候,会调用watcher上的get方法,也就会调用 pushTarget 方法,此时Dep.target = 渲染watchertargetStack = [ 渲染watcher ]

⭐️ 之后读取计算属性时,创建计算watcher,此时Dep.target = 计算watchertargetStack = [ 渲染watcher, 计算watcher ]

⭐️ 在获取计算属性时,会读取它的依赖值,基于Vue的响应式原理,就会执行依赖的getter函数,Dep.target上的addDep方法,则会将该依赖值收集到计算watcher上的deps中,同时又将计算watcher收集到该依赖值的deps中。

⭐️ 读取完依赖值后,会调用popTarget方法, 而此时Dep.target = 渲染watchertargetStack = [ 渲染watcher ]

⭐️ 再次触发watcher.depend,依赖值又收集到了渲染watcher

js 复制代码
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []

export function pushTarget(target?: DepTarget | null) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

总结

Vue 的 computed 是基于 Vue 的响应式原理来实现的。

通过创建 watcher 实例以及结合发布订阅模式实现了依赖收集。 当依赖的数据变化了,就会触发setter,同时将dirty变为true,计算watcher就会重新调用依赖数据的getter,获取最新的值,并将计算好的值缓存到watcher.value中。

相关推荐
一枚小小程序员哈2 小时前
基于Vue的个人博客网站的设计与实现/基于node.js的博客系统的设计与实现#express框架、vscode
vue.js·node.js·express
定栓2 小时前
vue3入门-v-model、ref和reactive讲解
前端·javascript·vue.js
LIUENG3 小时前
Vue3 响应式原理
前端·vue.js
wycode4 小时前
Vue2实践(3)之用component做一个动态表单(二)
前端·javascript·vue.js
wycode5 小时前
Vue2实践(2)之用component做一个动态表单(一)
前端·javascript·vue.js
第七种黄昏5 小时前
Vue3 中的 ref、模板引用和 defineExpose 详解
前端·javascript·vue.js
pepedd8646 小时前
还在开发vue2老项目吗?本文带你梳理vue版本区别
前端·vue.js·trae
前端缘梦6 小时前
深入理解 Vue 中的虚拟 DOM:原理与实战价值
前端·vue.js·面试
HWL56796 小时前
pnpm(Performant npm)的安装
前端·vue.js·npm·node.js
柯南95277 小时前
Vue 3 reactive.ts 源码理解
vue.js