美文网首页让前端飞Web前端之路
【源码解读】通过分析 Vue computed 的实现,居然发现

【源码解读】通过分析 Vue computed 的实现,居然发现

作者: 前端develop | 来源:发表于2020-03-18 17:41 被阅读0次

Vue 的 computed 经常会用到,其中包含以下两个重点:

1、 computed 的计算结果会进行缓存;

2、只有在响应式依赖发生改变时才会重新计算结果。

接下从源码的出发,看看能不能验证这两个重点。为了能更好理解 computed 的实现,文章字数会比较多,请耐心阅读。

源码分析

// vue/src/core/instance/state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 初始化 props
  if (opts.props) initProps(vm, opts.props)
  // 初始化 methods
  if (opts.methods) initMethods(vm, opts.methods)
  // 初始化 data
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化 computed
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

从初始化状态的顺序可以看出,在翻转字符串的例子中会先初始化 data,再进行初始化 computed

data 初始化

先看看初始化 data 做了什么,initData 源码如下:

// vue/src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  // 兼容 对象或函数返回对象的写法
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  // 判断 data 是否为普通对象
  if (!isPlainObject(data)) {
    // data 不是普通对象,重新赋值为空对象,并在输出警告
    data = {}
    ...
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // data 的属性不能与 methods、 props 的属性重复
    if (process.env.NODE_ENV !== 'production') {
      // 重复 key,输出警告
      ...
    } else if (!isReserved(key)) {
      // 将每个 key 挂载到实例上,在组件内就可以用 this.key 取值
      proxy(vm, `_data`, key)
    }
  }
  // 监听 data
  // observe data
  observe(data, true /* asRootData */)
}

初始化 data,主要做了 3 点,1、属性名重复的判断;2、将属性挂载到 vm 上;3、监听 data。

接下来看看 observe 的实现,源码如下:

// vue/src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 非对象 或者是 VNode,直接 return
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // 存在 '__ob__' 属性,表示已经监听
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 开始创建监听
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

接下来则到了 Observer 类,源码如下:

// vue/src/core/observer/index.js
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 将 '__ob__' 挂载到 value 上,避免重复监听
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 将对象每个属性添加 getter、 setter
      defineReactive(obj, keys[i])
    }
  }
  
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
     // 对数组的每一项进行监听
      observe(items[i])
    }
  }
}

接下来会调用 defineReactive,源码如下:

// vue/src/core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // dep 用于依赖收集
  const dep = new Dep()

  ...

  // data 的值有可能包含数组、对象,在这里 data 的值进行监听
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // Dep.target 是一个静态属性
      // 给 data 的属性添加 getter 时,target 为 undefined,不会进行依赖收集
      // 当 computed 用了 data中的属性时时将会进行依赖收集,先跳过这部分,等到了 computed 再回来看
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      ...
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 当值发生变化时,通知所有订阅者进行更新
      dep.notify()
    }
  })
}

defineReactive 中用到了 Dep 用来进行依赖收集,接下来看看 Dep 的源码:

// vue/src/core/observer/dep.js
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  // 添加订阅者
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  // 删除订阅者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  // 将 Dep 实例传递给目标 Watcher 上,目标 Watcher 再通过 addSub 进行订阅
  depend () {
    // 只有目标 Watcher 存在才可以进行订阅
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 通知订阅者
  notify () {
    // 根据 Watcher id 进行排序,通知更新
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // 调用订阅 Watcher 的 update 方法进行更新
      subs[i].update()
    }
  }
}

Dep.target = null
const targetStack = []

// 添加目标 Watcher,并将 Dep.target 指向最新的 Wathcer
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

// 移除目标 Watcher,并将 Dep.target 指向 targetStack 的最后一个
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Dep 其实就是一个订阅发布模式,说明一下最主要的两个地方

1、pushTargetpopTarget

这两个方法中用到了 targetStack 堆栈,这样做就可以进行嵌套,比如在给某个 Watcher 收集依赖的时候,发现了新的 Watcher 需要收集依赖,这样就可以 target 指向新的 Watcher,先把新的 Watcher 收集完再 popTarget,再进行上一个 Watcher 的收集。

2、depend

depend 执行的是 Watcher 的 addDep 方法,看看 addDep 怎么写的

// vue/src/core/observer/watcher.js
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)
    }
  }
}

addDep 做了一些判断避免重复订阅,再调用 addSub 添加订阅。

再回过头来看看 initData

// vue/src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  ...
}

data 是一个函数时,会调用 getData 获取 data 函数的返回值,看看 getData 的实现。

// vue/src/core/instance/state.js
export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

可以看到在执行 data 函数前后,执行了 pushTargetpopTarget 的操作,因为 data 的属性并不依赖其他响应式变量、在设置 gettersetter 时,因为 dep.targetundefined 所以并不会收集依赖。

data 的初始化到这里就差不多了,接下来看看 computed 的初始化。

computed 初始化

同样的,先从 initComputed 方法开始

// vue/src/core/instance/state.js
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  // 创建空对象,绑定到 vm._computedWatchers 上
  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]
    // computed 如果是函数就当成 getter,如果是对象则取 get 方法
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // getter 不存在时,输出警告
    ...

    if (!isSSR) {
      // 为 computed 的每个属性创建 Watcher
      // Watcher 是引用变量,vm._computedWatchers 也会被修改
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // 此时 key 还没挂载到 vm
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // key 在 data 或者 props 存在,输出警告
    }
  }
}

initComputed 会给 computed 的每个属性创建 Watcher(服务端渲染不会创建 Watcher), 然后调用 defineComputed。先看看 new Watcher 的构造函数做了什么

// vue/sct/core/observe/watcher.js
constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  this.vm = vm
  if (isRenderWatcher) {
    // 渲染 Watcher
    vm._watcher = this
  }
  vm._watchers.push(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.dirty = this.lazy // for lazy watchers
  // 还有其他属性的赋值
  ...

  // parse expression for getter
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn
  } else {
    // 解析表达式,得到 getter 函数
    this.getter = parsePath(expOrFn)
    if (!this.getter) {
      this.getter = noop
      // getter 为空时,输出警告
      ...
    }
  }

  // lazy 为 true 时,将 value 赋值为 undefined,否则调用 get 函数计算 value
  this.value = this.lazy
    ? undefined
    : this.get()
}

看看 defineComputed 传了哪些参数给这个构造函数。

const computedWatcherOptions = { lazy: true }
...
  watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    computedWatcherOptions
  )
...

可以从上面看到 computed 属性创建 Watcher 时,lazytrue,也就是在 computed 中声明了属性也不使用,那么将不会计算该属性的结果,value 为 undefined。

顺便看下 Watcher 的 get 方法

// vue/sct/core/observe/watcher.js
get () {
  // 将该 Watcher push 到 Dep 的 targetStack 中,开启依赖收集的模式
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 执行 computed 中的 get 函数
    // 如果函数内使用了 data 中的属性,那么就会触发 defineProperty 中 get
    value = this.getter.call(vm, vm)
  } catch (e) {
    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
}

这里就可以看到在调用 get 函数时,会将当前的 Watcher 指定为 Dep.target,然后开始执行 computed 属性的 get 函数,如果 computed 属性的 get 函数内使用了 data 中的属性,那么就会触发 defineProperty 中的 getter。这就验证了开头说的第二点:只有在响应式依赖发生改变时才会重新计算结果。

// vue/src/core/observer/index.js
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    // 这个时候 target 为 computed 属性的 Watcher,然后将 data 属性的 dep 收集到 computed 属性的 Watcher 中
    if (Dep.target) {
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
    }
    return value
  },
  set: {
    ...
    // data 的属性发生变化,通知订阅者进行更新
    dep.notify()
  }
})

从这里可以看出 Vue 设计的非常巧妙,通过执行 computed 属性的 get 函数,就可以完成所有依赖的收集,当这些依赖发生变化时,又会通知 computed 属性的 Watcher 进行更新。

接着看回 defineComputed

// vue/src/core/instance/state.js
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 客户端渲染时,shouldCache 为 true,也就是对计算结果进行缓存。
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    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 (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    // 开发环境 computed 属性的 set 函数为空函数时,替换为输出警告的函数
    ...
  }
  // 将 computed 的属性挂载到 vm 上,这样就可以用 this.key 调用 computed 的属性
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

从这里可以看到,当对计算结果需要缓存,则会调用 createComputedGetter,如果计算结果不需要缓存,则会调用 createGetterInvoker

官方彩蛋

从这里还可以看到一个可以在开发时的小技巧,当 computed 的属性为对象时,还可以自定义是否需要缓存。

官方文档好像没提到这一点,可能是觉得不缓存就和 methods 一样,就没有提到,这可能就是彩蛋吧。

computed: {
  noCacheDemo: {
    get () { ... },
    set () { ... },
    cache: false
  }
}

回到正题,看看 createComputedGetter 做了什么。

// vue/src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    // watcher 为 initComputed 中创建的 watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // watcher 初始化时,dirty 的值与 lazy 相同,都为 true
      // 那么第一次获取 computed 属性的值将会执行 watcher.evaluate()
      // evaluate 中会将 dirty 置为 false
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 如果处于收集依赖的模式,调用 watcher 的 depend 进行依赖收集
      if (Dep.target) {
        watcher.depend()
      }
      // 返回 watcher.value,而不是执行 computed 属性的 get 函数计算结果
      return watcher.value
    }
  }
}

再看下 watcher 的 evaluate 函数

// vue/sct/core/observe/watcher.js
evaluate () {
  this.value = this.get()
  this.dirty = false
}

这里可以看到,如果 computed 的计算结果需要缓存时,在第一次使用 computed 属性时会执行 watcher 的 get 函数,在执行 computed 属性的函数的过程中完成依赖的收集,并将计算结果赋值给 watcher的 value 属性。

之后再调用 computed 的属性则会取 watcher.value 的值,而不用执行 computed 属性的 get 函数,就这样做到了缓存的效果。也就验证了开头提到的第一点:computed 的计算结果会进行缓存。

最后再看看不使用缓存时的做法,createGetterInvoker 函数

// vue/sct/core/instance/state.js
function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

其实做法非常简单,就是每次调用就执行 computed 属性的 get 函数。

总结

总结一下 computed 的实现过程,主要有以下几个方面:

1、给 computed 的每个属性创建 Watcher

2、第一个使用 computed 的属性时,将会执行该属性的 get 函数,并完成依赖收集,完后将结果保存在对应 Watcher 的 value 中,对计算结果进行缓存。

3、当依赖发生变化时,Dep 会发布通知,让订阅的 Watcher 进行更新的操作。

最后感谢各位小伙伴看到这里,Vue computed 的实现过程都过了一遍,希望能够对各位小伙伴有所帮助。

如果有讲的不对的地方,可以评论指出哦。如果还有不了解的地方,欢迎关注我的公众号给我留言哦。


如果你喜欢我的文章,希望可以关注我的公众号【前端develop】

前端develop

相关文章

网友评论

    本文标题:【源码解读】通过分析 Vue computed 的实现,居然发现

    本文链接:https://www.haomeiwen.com/subject/zdycyhtx.html