Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【源码】Computed 源码解析 #39

Open
YIngChenIt opened this issue Jul 3, 2020 · 0 comments
Open

【源码】Computed 源码解析 #39

YIngChenIt opened this issue Jul 3, 2020 · 0 comments

Comments

@YIngChenIt
Copy link
Owner

【源码】Computed 源码解析

前言

今天我们就不从基础用法开始讲了, 直接上源码, 通过这篇文章我们可以知道

  • computed是如何进行初始化的

  • computed是怎么进行计算的

  • computed是如何实现缓存的

  • computed内部是如何触发更新的

....

初始化

我们来看下源码的初始化

function Vue(){
    ...
    initState(this)
    ...
}

// vue/src/core/instance/state.js

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

从上述代码我们可以发现, 当我们new Vue() 的时候会调用initState()处理Vue的各种钩子, 其中对computed的处理在propsmethodsdata之后

而处理computed的方法是initComputed(), 我们接下来看下这个方法

initComputed

// vue/src/core/instance/state.js
// 以下代码有为了方便阅读, 省略了一些不必要的判断逻辑代码
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null) // 创建一个watchers对象,并挂载到vm._computedWatchers

  const isSSR = isServerRendering() // 判断当前环境是不是SSR渲染

  for (const key in computed) { // 循环获取到每个computed钩子上的函数或对象
    const userDef = computed[key] // 每个computed钩子上的函数或对象
    const getter = typeof userDef === 'function' ? userDef : userDef.get // 如果是对象则取get, 获取回调函数

    if (!isSSR) { // 如果不是SSR, 为每个 computed 配发 watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    if (!(key in vm)) { // 如果实例上没有同名, 则调用defineComputed
      defineComputed(vm, key, userDef)
    }
  }
}

其实initComputed主要做了几件事:

  • 为每个 computed 派发 watcher

  • 对每个 computeddefineComputed 处理

  • 收集所有 computedwatcher

我们接下来一一解释一下这几件事情

为每个 computed 派发 watcher

我们先来看下Watch到底是什么

// vue/src/core/observer/watcher.js
// 以下代码进行了省略
class Watcher {
  vm: Component;
  lazy: boolean;
  dirty: boolean;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    options?: ?Object,
  ) {
    if (options) {
      this.lazy = !!options.lazy
    }
    this.dirty = this.lazy    
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }

    this.value = this.lazy
      ? undefined
      : this.get()
  }

我们理一下上述代码做了啥

缓存getter

把用户设置的 computed-getter函数保存到 watcher.getter

缓存getter的计算结果

watcher.value 用于存放computed-getter运行的结果, 但是这里需要注意的是因为 lazy 的原因,不会新建实例并马上读取值

初始化 dirty 的值

在代码中我们可以发现this.dirty = this.lazy , 我们后序会讲到dirty是整个计算属性缓存的关键, 如果dirty为true,
表示所依赖的数据变化了, 不能使用缓存。而这一步赋值的意义在于,给dirty一个初始值,表示开始了缓存任务.
我们大致可以看到 computedwatcher 有什么关系了

至于如何通过dirty实现缓存,我们接下来说

对每个 computeddefineComputed 处理

我们来看下defineComputed的源码

// vue/src/core/instance/state.js
// 源码有省略
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  if (typeof userDef === 'function') { // 如果是函数
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key) // 调用createComputedGetter对getter进行包装
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else { // 如果是对象
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key) // 调用createComputedGetter对getter进行包装
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }

  Object.defineProperty(target, key, sharedPropertyDefinition) // target === vm 
}

在上诉代码中我们可以了解到

  • 通过 Object.defineProperty(vm, getter, cb)的方式让我们在实例上可以拿到getter

  • set 函数默认是空函数,如果用户设置,则使用用户设置

  • 通过 createComputedGetter包装getter

其实最重要的就是第三点了, 因为第三点实现了我们computed的核心逻辑, 即data发生改变引起computed发生改变, 最后刷新视图,我们来看下createComputedGetter的源码

// vue/src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key] // 拿到当前的watch
    if (watcher) {
      if (watcher.dirty) { // 第一次获取computed的时候 dirty为true 执行watcher.evaluate()
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

我们了解一下createComputedGetter()主要处理了什么

控制缓存

通过代码控制缓存

if (watcher.dirty) { 
    watcher.evaluate()
}

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

watcher.evaluate()的作用是计算出新的值,更新缓存的值, 然后设置dirty的值为false,表示缓存已经更新了

缓存的实现

目前我们知道dirty是控制缓存的关键,那么dirty是如何控制计算属性的缓存的么?

假设我们有一个计算属性A依赖于data中的B, 也就是B收集了A的watch,那么当B发生改变的时候会通知A进行更新,也就是调用了A的watch的update方法

// vue/src/core/observer/watcher.js
  update () {
    if (this.lazy) {
      this.dirty = true
    } 
    ....
  }

通过update方法将dirty设置为true, 就会调用watcher.evaluate()方法获取到最新的数据了

和data、视图建立联系

我们知道, 当computed所依赖的数据data发生改变的时候, 会触发computed的改变, 最后触发视图更新,那么这条链路是如何实现的呢?

关键在于下面的代码

if (watcher.dirty) { // 第一次获取computed的时候 dirty为true 执行watcher.evaluate()
    watcher.evaluate()
}
if (Dep.target) {
    watcher.depend()
}

evaluate () {
    this.value = this.get()
    this.dirty = false
}

当我们第一次获取computed的时候, 会调用Watch.get(), 我们来看下

get () {
    pushTarget(this)
    let value
    ...
    const vm = this.vm
    value = this.getter.call(vm, vm)
    popTarget()
    ...
    return value
}

连接的详情流程为:

  • 在页面更新读取computed的时候, Dep.target 会设置为 页面 watcher

  • 在我们读取computed,调用get()方法获取值的时候,先通过pushTarget(this)将当前的Dep.target 被设置为 computed-watcher

  • 然后this.getter.call(vm, vm)执行, 在执行的时候回调中对data进行读取,所以 computed-watcher 也会保存到 data 的依赖收集器 dep

  • 再然后通过 popTarget()释放Dep.target, 此时Dep.target的值还原为页面 watcher

  • 最后通过手动调用watcher.depend(), 将此时的Dep.target的值保存到data 的依赖收集器 dep

  • 此时 data 的依赖收集器 =【computed-watcher,页面watcher】

我们知道 data 发生改变的时候会正序遍历依赖收集器,所以就实现了data发生改变的时候, 会触发computed的改变, 最后触发视图更新,如下图

img

总结

通过对源码的阅读,我们知道计算属性的核心还是通过Object.defineProperty来实现的,它和method最本质的区别是它可以通过dirty实现缓存,另外计算属性更新的连接流程也是我们需要掌握的

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant