Vue源码学习7-响应式原理-computed

  • 内容
  • 评论
  • 相关

前言

computed(计算属性)是Vue中比较好玩的一个东西,他在定义的时候和 data或者props 完全不同,但它的使用方式又和 data/props 没啥区别。

它的出现是其实 Vue 为了避免开发者在模板中写太多的复杂运算逻辑,将这些逻辑都转移都js代码中

--以下所有代码来自Vue 2.6.11 版本

--以下的vm都代指vue实例

贴源码

通过第一篇文章可以了解到,initComputed 是发生在 initState 逻辑中,且初始化的顺序是排在 props / methods / data 之后,watch 之前的,为啥在watch之前?因为watch除了可以监听 data/props 的数据,还可以监听 computed,要知道 计算属性 背后其实是一个函数,它是如何做到像 data/props 那样使用的,看看源码就知道了。

这是一段完整的代码,来源 src/core/instance/state.js,它是从 initState 进来的,第一个入参是 vm,第二个入参就是我们自己在 computed 中写的计算函数了,贴个实例代码:

data:{
  person:'Tifa'
},
computed:{
  person1(){
    return 'my name is ' + this.person;
  }
}

1:看到源码,直接看 for循环,一个简单的遍历,忽略一些逻辑,走到 new Watcher ,重点来了,这里的 Watcher 入参和前面文章讲的那个 render-watcher 的入参是不一样的(具体有哪些差异私下简单看看就行了)。

2:getter,求值函数,Vue的 computed 提供了两种写法(具体怎么去看文档 )总之  ,这个 getter 就用来取值的

3:computedWatcherOptions 是一个公共的对象,和 initComputed 方法定义在一个文件中的,可以提供给所有 计算属性 使用,它只定义了一个属性  lazy : true。我们查看 Watcher 的源码,第四个入参 options 不为空且 this.lazy 被标记为 true;接着省略大段代码,走到最后的求值,因为lazy为true,所以这个计算属性 this.person1 现在的值是 undefined

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

4:求值完毕,再次回到 initComputed 方法中,接着一个简单的判断 key 是否重复

5:defineComputed(代码贴在了最下方) 它是干嘛的?简单来说,就是给通过Object.defineProperty 给 vm 添加新字段,并且修改该字段的 get 函数,这个 get 函数正是在 defineComputed 中创建的。进入 defineComputed 方法,在这里 if 逻辑能够直接进入,接着就是通过 createComputedGetter(代码也在最下方) 创建一个 get 函数,并且赋值给  sharedPropertyDefinition.get

6:sharedPropertyDefinition 是什么?它是一个共享对象,初始化计算属性时共享的一个对象,结构如下,就是用来提供给 Object.defineProperty 使用的

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

7:最后 Object.defineProperty 执行完毕之后,vm上就多一个叫 person1 的字段,且现在的值还是为 undefined

(跳过下面一段源码继续看文章)

const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  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]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      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)) {
      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)
      }
    }
  }
}
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  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) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

计算属性的值从哪儿来?

看到这里,initComputed 的逻辑已经走完了,但是 vm.person1 的值还是undefined,如果我们在代码中的任何一个地方都不去获取这个值,那么它永远都是undefined,当我们在代码 或者 在模板 中获取这个值的时候,它才会进行第一次求值的操作。

接下来是重点,请仔细查看源码

1:首先,我们在 created 中打印这个值 ,它会触发 get 逻辑,也就是 createComputedGetter 这个方法创建的 get 函数,注意,computedGetter  这个函数的是通过 vm 触发的,所以它的this也是指向的 vm,通过key获取到对应的 computed-watcher,正常情况下这个watcher是可以获取到的(服务端渲染的情况下是没有的)

created(){
  console.log(this.person1);
}

2:watcher.dirty,这个字段的值为 true,是在 Watcher 的构造函数中赋值的,进入 evaluate 方法执行 get 求值,接着就是我们熟悉的操作,压栈-依赖收集-出栈-清理依赖,发生在render-watcher身上的事情,也发生在了 computed-watcher上

3:我们直接看到 Watcher 的 get 方法,还记得这个 getter 从哪儿来的吗?它在构造函数中赋值,这个 getter 正是我们自己写的计算函数,就如下面示例代码所写的那样

computed:{
  person1(){
    return 'my name is ' + this.person; 
  } 
}

4:执行 getter ,就触发 this.person 的get 逻辑,它的逻辑我们在上篇文章的 依赖收集 讲的很清楚了,此时 Dep.target 正是刚刚才压栈的 computed-watcher,然后它们就可以建立依赖关系了。

5:顺利求出了 value 的值,又返回 evaluate 方法,watcher.value 被赋值了,它就是 person1 现在最新的值,console.log(this.person1) 也就能打印出这个值了。dirty在此时被改为了 false,回头看看 computedGetter 中的那段 watcher.dirty 的判断,由此我们可以知道 person1 在第一次求值结束之后 value会被缓存下来,后面再多次获取 person1 的时候始终都会取到缓存的值,只要 person1 依赖的 person 没有发生变化,person1 便不会发生任何变化

6:还是看到 computedGetter  方法,有一段 Dep.target 的判断,由上面求值的逻辑可以知道,computed-watcher已经出栈了,此时的 Dep.target 是谁的watcher呢?如果,获取 person1 的值仅仅发生在代码中,如下列代码所示,那么此时Dep.target为undefined,至于这里为什么是 undefined,打开源码去看看callHook这个方法就知道了。

created(){
  console.log(this.person1)
}

如果,此时在模板中 使用了这个计算属性({{ person1 }}),在 vm.render的过程中也会触发get ,并且此时的Dep.target正是 render-watcher,然后就能进入 watcher.depend,打开 Watcher 的源码查看 depend 方法,遍历 deps,我们知道这个deps保存的正是 computed-watcher搜集的依赖,挨个儿执行depend 方法,然后dep对象就能和当前处于栈顶的 render-watcher 建立依赖,这也就是为什么当我们在代码中改变 person 的值的时候,依赖 person1 的视图也会触发变化。

当 person 的值改变时,computed-watcher是可以接收到通知的,并且执行 update 方法,此时由于 computed-watcher 的 lazy 为 true,就会走第一段 if逻辑,this.dirty = true,我们在上面的 computedGetter  已经看到过 dirty 这个东西了,它被设置为 true,就表示 computed-watcher 取消 value的缓存了,所以当 render-watcher 执行 vm.render 的时候,依赖会重新收集,person1 的 value 也会被重新计算,视图也会展示出最新的值。

总结

至此,computed的原理已经讲完了,下篇我们接着讲 watch 的原理。

评论

0条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注