vue计算属性computed源码解析笔记

本文详细介绍了Vue.js中计算属性的工作原理,从初始化、getter的创建到依赖收集和缓存机制。通过源码分析,解释了计算属性如何响应数据变化并触发视图更新,以及不同版本Vue在更新计算属性时的差异,揭示了Vue的高性能渲染策略。
摘要由CSDN通过智能技术生成

学习笔记,持续补充

1. 使用示例 (官网)
var vm = new Vue({
  data: { a: 1 },
  computed: {
    // 计算属性的 getter
    // 仅读取
    aDouble: function () {
      // `this` 指向 vm 实例
      return this.a * 2
    },
    // 读取和设置
    aPlus: {
      get: function () {
        return this.a + 1
      },
      set: function (v) {
        this.a = v - 1
      }
    }
  }
})
vm.aPlus   // => 2
vm.aPlus = 3
vm.a       // => 2
vm.aDouble // => 4

计算属性的结果会被缓存,除非依赖的响应式 property 变化才会重新计算。注意,如果某个依赖 (比如非响应式 property) 在该实例范畴之外,则计算属性是不会被更新的。

这是官网给的有关计算属性的说明,至于计算属性到底是个什么,为什么会被缓存呢?我们继续往下看

2. 源码解析(2.5.17beta版,多计算,少渲染)

这里结合一个场景例子,直观的分析一下计算属性的实现

var vm = new Vue({
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

1. initComputed()

首先,在vue初始化过程中,在 initState ( ) 中会对computed计算属性进行初始化处理,判断如果定义了计算属性,会执行

initComputed()方法,如下

if (opts.computed) initComputed(vm, opts.computed)

看一下这个 initComputed()方法,主要分为四步

  • 创建一个空对象,用来保存watcher
  • 对computed计算属性遍历,拿到定义的每一个计算属性
  • 为每一个计算属性创建一个watcher
  • 对遍历出的每一个计算属性做判断,如果不是vm上的属性,执行defineComputed ( )方法,如果是vm上的属性,判断当前计算属性名是不是已经在data和props定义了

可以看一下下面代码及相关注释

const computedWatcherOptions = { computed: true } // 实例化Watcher时的配置项

function initComputed (vm: Component, computed: Object) {
  // 第一步. 创建空对象 watchers,vm._computedWatchers
  const watchers = vm._computedWatchers = Object.create(null)
  // 判断是否是服务端渲染(SSR),这里我们分析的是浏览器环境
  const isSSR = isServerRendering()
  // 第二步. 对computed计算属性遍历
  for (const key in computed) {
    const userDef = computed[key] // 拿到定义的每一个计算属性的value
    // 判断上边拿到的计算属的value,是否是function,如果是function,直接赋值给getter,不是的话将计算属性的get赋值给getter(看下官网计算属性定义的方式,---->示例)
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // 如果计算属性没定义getter,给出警告
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // 第三步. 为每一个getter创建一个watcher(可以称为computed watcher,在下面对实例化computed watcher进行了分析)
      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. 组件定义的计算属性已经在组件原型上定义了
    
    // 第四步. 判断key,如果不是vm的属性,执行defineComputed( )做响应式处理
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 如果key已经在data,或者props中定义了,给出警告
      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)
      }
    }
  }
}

下面我们分析一下以上四步中所做的一些处理

2. defineComputed()

这个方法,他的目的就是利用Object.defineProperty()给每一个计算属性添加getter和setter,使之响应式

// 定义一个对象,当作Object.defineProperty()的参数
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop, // 空函数
  set: noop
}
export function defineComputed (
  target: any,
  key: string, // 定义的计算属性名
  userDef: Object | Function // 计算属性所做的处理
) {
  // 下面的操作就是给定义的这个 sharedPropertyDefinition 对象添加get和set处理函数
  const shouldCache = !isServerRendering() // 非服务器端渲染
  if (typeof userDef === 'function') { // 是函数
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else { // 是对象
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.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对计算属性进行处理
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

这里我们先忽略计算属性对set函数的处理,在平时的开发场景中,计算属性有 setter 的情况比较少,主要分析一下getter, 上面对于get函数的定义,在我们平常使用的浏览器环境中会进入createComputedGetter ( ),我们来看一下这个函数做了什么,继续往下

3. createComputedGetter()

最终getter对应的是createComputedGetter()的返回值,如下

function createComputedGetter (key) {
  return function computedGetter () {
    // 这里会去获取this._computedWatchers,这个是上面在initComputed中定义在vm 上的对象,可以通过this访问到,这个对象保存的是每一个计算属性的watcher,下面定义的这个 watcher,就是取到的每个计算属性的watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 调用watcher.depend(),返回watcher.evaluate(),这是什么??看下面 4
      watcher.depend()
      return watcher.evaluate()
    }
  }
}

对于返回的这个函数,如果会调用depend ( )和evaluate ( )方法,这俩方法定义在Watcher这个类中,如下

4. 计算属性的new Watcher()

Watcher是定义的一个类,下面我直接把定义Watcher的代码拿了过来,有点多,大家可以看下,和计算属性相关的,我会随后拿出来分析

/* @flow */

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError
} from '../util/index'

import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import type { SimpleSet } from '../util/index'

let uid = 0

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  computed: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  dep: Dep;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = 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) {
      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
  }

  /**
   * Add a dependency to this directive.
   */
  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)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }

  getAndInvoke (cb: Function) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }

  /**
   * Evaluate and return the value of the watcher.
   * This only gets called for computed property watchers.
   */
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

  /**
   * Depend on this watcher. Only for computed property watchers.
   */
  depend () {
    if (this.dep && Dep.target) {
      this.dep.depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

在上面执行initComputed( )的时候,在我们所说的第三步中,遍历实例化创建了计算属性的watcher (computed watcher)

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

简化一下new Watcher,我们看一下

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  
  // ...
  
  if (this.computed) { // 判断this.computed
    this.value = undefined
    this.dep = new Dep()
  } else {
    this.value = this.get() // 调用get()求值
  }
}

这里可以看到,计算属性的watcher并不会立即求值,而是判断了this.computed,同时实例化了一个dep实例。

在vue渲染数据到页面这一过程中,如果访问到定义的计算属性时,就会触发计算属性的getter( getter对应的是 上边所说的createComputedGetter( )函数的返回值 ),拿到计算属性对应的watcher,然后执行Watcher中的depend 方法,并且返回evaluate方法处理后的值

  if (watcher) {
      watcher.depend()
      return watcher.evaluate()
    }

depend 方法

/**
  * Depend on this watcher. Only for computed property watchers.
  */
depend () {
  if (this.dep && Dep.target) {
    this.dep.depend()
  }
}

注意,这时候的 Dep.target 是渲染 watcher ,所以 this.dep.depend( ) 相当于渲染 watcher 订阅了这个 computed watcher 的变化。

然后再执行watcher.evaluate ( ) 求值

evaluate( )这个函数 首先判断this.dirty,如果为true,会调用get ( ) 求值,get ( )也定义在Watcher这个类中,他的核心就是

value = this.getter.call(vm, vm) ,实际上就是调用计算属性中定义的getter函数,求值, 并把this.dirty设置为false,最后返回所求的值

/**
  * Evaluate and return the value of the watcher.
  * This only gets called for computed property watchers.
  */
evaluate () {
  if (this.dirty) {
    this.value = this.get()
    this.dirty = false
  }
  return this.value
}

这里需要特别注意的是,由于 this.firstNamethis.lastName 都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher

5. 修改计算属性所依赖的数据

一旦我们对计算属性所依赖的数据进行修改, 会触发setter过程,通知所有订阅他变化的watcher更新( dep.notify ( ) )

 notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

如上,遍历循环,并执行watcher的update ( )方法

 update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

对于计算属性的watcher,他实际上有两种模式,lazy和active,如果this.dep.subs.length === 0说明没有人去订阅这个计算属性watcher的变化,把this.dirty设为true,使只有当下次再访问这个计算属性的时候才会重新求值。

在我们所举例的场景下,渲染 watcher 订阅了这个 computed watcher 的变化,那么它会执行:

 this.getAndInvoke(() => {
      this.dep.notify()
   })
getAndInvoke (cb: Function) {
    const value = this.get() // 会重新计算
    // 新旧值会进行比较,如果变化了 ,才会执行传入的回调函数
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }

getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher重新渲染

这里对计算属性的新旧值会进行比较,这样保证了,当计算属性依赖的值发生变化时,他会被重新计算,但不一定会重新渲染,只有计算出来的新值相对于旧值改变了,才会触发渲染watcher重新渲染,本质上这是vue的一种优化,多计算,少更新

3. 源码解析(2.6.13版本,多渲染,少计算)

在watch的update方法中,没有getAndInvoke这一方法,不会比较计算属性计算出的新旧值,不管相等否,都会触发updateComponent去update更新渲染

Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

看之前版本(2.5.17beta)

Watcher.prototype.update = function update () {
    var this$1 = this;

  /* istanbul ignore else */
  if (this.computed) {
    // A computed property watcher has two modes: lazy and activated.
    // It initializes as lazy by default, and only becomes activated when
    // it is depended on by at least one subscriber, which is typically
    // another computed property or a component's render function.
    if (this.dep.subs.length === 0) {
      // In lazy mode, we don't want to perform computations until necessary,
      // so we simply mark the watcher as dirty. The actual computation is
      // performed just-in-time in this.evaluate() when the computed property
      // is accessed.
      this.dirty = true;
    } else {
      // In activated mode, we want to proactively perform the computation
      // but only notify our subscribers when the value has indeed changed.
      this.getAndInvoke(function () {
        this$1.dep.notify();
      });
    }
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

看一下getAndInvoke ( )方法

Watcher.prototype.getAndInvoke = function getAndInvoke (cb) {
  var value = this.get();
  // 做了判断,如果计算出的新值和旧值不一样 的话,才会继续执行,如果一样,就不会继续,,不会再次渲染
  if ( 
    value !== this.value ||   
    // Deep watchers and watchers on Object/Arrays should fire even
    // when the value is the same, because the value may
    // have mutated.
    isObject(value) ||
    this.deep
  ) {
    // set new value
    var oldValue = this.value;
    this.value = value;
    this.dirty = false;
    if (this.user) {
      try {
        cb.call(this.vm, value, oldValue);
      } catch (e) {
        handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
      }
    } else {
      cb.call(this.vm, value, oldValue);
    }
  }
};
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值