记录Computed源码分析

1,582 阅读6分钟

之前看了很多文章通过源码分析computed的原理,借此机会把自己对源码所理解的,所以这篇记录大多数都是对源码批注的形式写下来的,当做自己的学习笔记。写的不好还请担待。

了解Computed(计算属性)

vue中有个常用的方法Computed就是计算属性
平时我们在获取后台返回的数据后可能会对某些数据进行加工直接放在模板中太多的逻辑会让模板过重且难以维护。例如:

<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 message 的翻转字符串。当你想要在模板中多次引用此处的翻转字符串时,就会更加难以处理。

所以,对于任何复杂逻辑,你都可以使用计算属性。

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }
})

computed介绍

在vue中的computed声明了计算属性,初始化vue对象的时候会直接挂载到vue实例上,可以用this.访问,同时计算属性有一个特点,他会缓存数据。当计算属性中所依赖的数据没有变化的时候计算属性不会重新计算return的值。当计算属性中依赖的数据项有某一项变化的时候他会重新计算并返回新值。

computed与methods区别

由于计算属性会缓存所以调用computed不会重复的get数据并且计算,但是调用methods中的方法,每调用一次就会重新执行一次,所以使用computed的性能会高于methods

computed实现原理

刚刚简单的介绍了computed,那么他是如何实现的?我们通过源码来探究一下。 (如果不了解vue响应式原理的,请移步这里) vue初始化的时候分别对data,computed,watcher 执行相应的方法。源码地址

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  ---省去其他无用代码---
  //这里对computed执行了initComputed();
  if (opts.computed) initComputed(vm, opts.computed)
}

const computedWatcherOptions = { lazy: true }

下面是initComputed方法===============
function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()
  -----省略无用代码
  for (const key in computed) {
  //遍历了computed对每一个都进行了处理
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (!isSSR) {
    //这里new Watcher,调用属性的时候,会调用get,dep回收集相应的订阅者
      watchers[key] = new Watcher( vm, getter || noop, noop,computedWatcherOptions)
    }
    //判断声明的key不在vue实例中存在
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
     -----省略无用代码
    }
  }
}

这里初始化computed执行了initComputed方法,遍历computed 对每个key执行了defineComputed()生成了订阅者Watcher 同时传入了四个参数,我们看下getter,和computeeWatcherOptions,当computed里面写的是函数的时候,这个getter就是声明的函数,如果是对象,那么就取get方法,

computed:{
    sum(){
        return this.a+this.b;
    },
    add:{
        get:function(){
            return:this.a+this.b;
        }
    }
    
}

computedWatcherOptions是个对象{lazy:true},这个参数会就是控制计算属性懒加载的。我们接下来去看下 defineComputed(vm, key, userDef)这个方法做了什么。 defineComputed源码如下:

//这里声明了一个属性描述符,
//这里会用到sharedPropertyDefinition: Object.defineProperty(target, key, sharedPropertyDefinition)
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
//这里判断是不是服务端渲染,通常不是ssr
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
    //不是ssr执行了createComputedGetter(key),创建了新的get方法赋值给了sharedPropertyDefinition.get
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
  //这里也执行了createComputedGetter()创建了个getter方法。
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
 ----删除无用的------------------
  Object.defineProperty(target, key, sharedPropertyDefinition)
 //最后对computed的key添加属性描述符,重写了get和set
 //当我们调用computed的时候调用了get方法收集了相应的依赖
}

==========createComputedGetter源码============
//createComputedGetter会返回一个新get的函数
//当再代码中调用computed的时候就会调用get,
//Object.defineProperty(target, key, sharedPropertyDefinition)这里sharedPropertyDefinition的get就是这个函数返回的computedGetter
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    //当在代码中使用computed后这里判断是不是初始化时候添加过的watcher
    if (watcher) {
    //当watcher.dirty是true的时候调用了evaluate(),我们去下面看下evaluate
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
      //收集使用此计算属性的订阅者
        watcher.depend()
      }
      //这里返回计算后的值。
      return watcher.value
    }
  }
}
//上面说到初始化computed的时候会new Watcher()还传入了{lazy:true}
//所以当前这个computed的watcher.dirty=lazy=true

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    vm._watchers.push(this)
    // options
    if (options) {
      this.lazy = !!options.lazy
    } 
    this.getter = expOrFn
   //这里this.dirty=true
    this.dirty = this.lazy // for lazy watchers
    
    evaluate () {
    //调用了evaluate()方法。调用了get方法,缓存了自己
    //watcher实例中的value就是计算后的值,然后把dirty变成false.
    //所以这个计算属性实例访问计算后得值就是watcher.value
    this.value = this.get()
    this.dirty = false
  }
  //get方法缓存了自己,调用了传过来的getter方法返回了计算后的值
  //这个getter方法就是在计算属性中声明的函数
              <!--computed:{-->
              <!--    sum(){-->
              <!--        return this.a+this.b-->
              <!--    }-->
              <!-- }-->
   //这个sum函数使用了 data中声明的a变量,和b变量这里就就会调用这两个变量的属性描述符中的get方法,这两个变量就会把
     当前的计算属性这个订阅者收集起来。然后返回了计算后的值,这个watcher实例中的value就是计算后的值
    get () {
    pushTarget(this)
    let value
    const vm = this.vm
      value = this.getter.call(vm, vm)
    return value
  }

当计算属性中依赖的数据变化的时候就会触发set方法,通知更新,update中会把this.dirty设置为true,当再次调用computed的时候就会重新计算返回新的值.

 //当计算属性依赖的数据变化的时候就会触发set方法通知订阅者更新,就会调用update
  //因为this.lazy是初始化计算属性才传入进来的
  //这里判断this.lazy是true改变this.dirty=true;
  //必须是计算属性中用到的数据变化得时候才会改变this.dirty=trueupdate () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  //所以当重新触发计算属性的get方法后
  还会执行这里
   return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    //当在代码中使用computed后这里判断是不是初始化时候添加过的watcher
    if (watcher) {
    //当watcher.dirty是true的时候才去计算
    //为false的时候就直接返回原来的值
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
      //收集使用此计算属性的订阅者
        watcher.depend()
      }
      //这里返回计算后的值。
      return watcher.value
    }
  }

小结

我们在computed里声明了多个函数或者是对象,如果是函数则取本身作为getter函数,如果是对象则取对象中的get函数作为getter

computed:{
                  sum(){
                      return this.a+this.b
                  },
                  add(){
                      return this.aaa+this.bbb
                  },
                  name:{
                  get:function(){
                      return this.aa+this.bb
                  }
                  }
               }

初始化computed的时候遍历所有的computed,对每个key都创建了个watcher,传入了一个特殊的参数lazy:true,然后调用了自己的getter方法这样就收集了这个计算属性依赖的所有data;那么所依赖的data会收集这个订阅者(当前计算属性) 初始化的同时会针对computed中的key添加属性描述符创建了独有的get方法,当调用计算属性的时候,这个get判断dirty是否为真,为真的时候代表这个计算属性需要重新计算,如果为false则直接返回value。当依赖的data 变化的时候回触发数据的set方法调用update()通知更新,会把dirty设置成true,所以computed 就会重新计算这个值。

写的不好、欢迎指正