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中。

相关推荐
别拿曾经看以后~22 分钟前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
Devil枫7 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
GIS程序媛—椰子8 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
毕业设计制作和分享9 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
程序媛小果9 小时前
基于java+SpringBoot+Vue的旅游管理系统设计与实现
java·vue.js·spring boot
从兄9 小时前
vue 使用docx-preview 预览替换文档内的特定变量
javascript·vue.js·ecmascript
凉辰10 小时前
设计模式 策略模式 场景Vue (技术提升)
vue.js·设计模式·策略模式
薛一半12 小时前
PC端查看历史消息,鼠标向上滚动加载数据时页面停留在上次查看的位置
前端·javascript·vue.js
MarcoPage12 小时前
第十九课 Vue组件中的方法
前端·javascript·vue.js