细节流程图

初始化

计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中,执行了 if (opts.computed) initComputed(vm, opts.computed),initComputed 的定义在 src/core/instance/state.js 中:

const computedWatcherOptions = { computed: 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)
}
}
}
}

函数首先创建 vm._computedWatchers 为一个空对象,接着对 computed 对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个 userDef 对应的 getter 函数,拿不到则在开发环境下报警告。接下来为每一个 getter 创建一个 watcher,这个 watcher 和渲染 watcher 有一点很大的不同,它是一个 computed watcher,因为 const computedWatcherOptions = { computed: true }。computed watcher 和普通 watcher 的差别我稍后会介绍。最后对判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef),否则判断计算属性对于的 key 是否已经被 data 或者 prop 所占用,如果是的话则在开发环境报相应的警告。

接下来需要重点关注 defineComputed 的实现:

export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
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(target, key, sharedPropertyDefinition)
}

这段逻辑很简单,其实就是利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key) 的返回值,来看一下它的定义:

function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}
}

createComputedGetter 返回一个函数 computedGetter,它就是计算属性对应的 getter。

整个计算属性的初始化过程到此结束,我们知道计算属性是一个 computed watcher,它和普通的 watcher 有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析 computed watcher 的实现。

例子

以上关于计算属性相关初始化工作已经完成了,初始化计算属性的过程中主要创建了计算属性观察者以及将计算属性定义到组件实例对象上,接下来我们将通过一些例子来分析计算属性是如何实现的,假设我们有如下代码:

data () {
return {
a: 1
}
},
computed: {
compA () {
return this.a + 1
}
}

如上代码中,我们定义了本地数据 data,它拥有一个响应式的属性 a,我们还定义了计算属性 compA,它的值将依据 a 的值来计算求得。另外我们假设有如下模板:

<div>{{compA}}</div>

模板中我们使用到了计算属性,我们知道模板会被编译成渲染函数,渲染函数的执行将触发计算属性 compA 的 get 拦截器函数,那么 compA 的拦截器函数是什么呢?就是我们前面分析的 sharedPropertyDefinition.get 函数,我们知道在非服务端渲染的情况下,这个函数为:

sharedPropertyDefinition.get = function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}

也就是说当 compA 属性被读取时,computedGetter 函数将会执行,在 computedGetter 函数内部,首先定义了 watcher 常量,它的值为计算属性 compA 的观察者对象,紧接着如果该观察者对象存在,则会分别执行观察者对象的 depend 方法和 evaluate 方法。

我们首先找到 Watcher 类的 depend 方法,如下:

depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}

depend 方法的内容很简单,检查 this.dep 和 Dep.target 是否全部有值,如果都有值的情况下便会执行 this.dep.depend 方法。这里我们首先要知道 this.dep 属性是什么,实际上计算属性的观察者与其他观察者对象不同,不同之处首先会体现在创建观察者实例对象的时候,如下是 Watcher 类的 constructor 方法中的一段代码:

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// 省略...
> if (this.computed) {
> this.value = undefined
> this.dep = new Dep()
> } else {
this.value = this.get()
}
}

如上高亮代码所示,当创建计算属性观察者对象时,由于第四个选项参数中 options.computed 为真,所以计算属性观察者对象的 this.computed 属性的值也会为真,所以对于计算属性的观察者来讲,在创建时会执行 if 条件分支内的代码,而对于其他观察者对象则会执行 else 分支内的代码。同时我们能够看到在 else 分支内直接调用 this.get() 方法求值,而 if 分支内并没有调用 this.get() 方法求值,而是定义了 this.dep 属性,它的值是一个新创建的 Dep 实例对象。这说明计算属性的观察者是一个惰性求值的观察者。

现在我们再回到 Watcher 类的 depend 方法中:

depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}

此时我们已经知道了 this.dep 属性是一个 Dep 实例对象,所以 this.dep.depend() 这句代码的作用就是用来收集依赖。那么它收集到的东西是什么呢?这就要看 Dep.target 属性的值是什么了,我们回想一下整个过程:首先渲染函数的执行会读取计算属性 compA 的值,从而触发计算属性 compA 的 get 拦截器函数,最终调用了 this.dep.depend() 方法收集依赖。这个过程中的关键一步就是渲染函数的执行,我们知道在渲染函数执行之前 Dep.target 的值必然是 渲染函数的观察者对象。所以计算属性观察者对象的 this.dep 属性中所收集的就是渲染函数的观察者对象。

记得此时计算属性观察者对象的 this.dep 中所收集的是渲染函数观察者对象,假设我们把渲染函数观察者对象称为 renderWatcher,那么:

this.dep.subs = [renderWatcher]

这样 computedGetter 函数中的 watcher.depend() 语句我们就讲解完了,但 computedGetter 函数还没执行完,接下来要执行的是 watcher.evaluate() 语句:

sharedPropertyDefinition.get = function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}

我们找到 Watcher 类的 evaluate 方法看看它做了哪些事情,如下:

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

我们知道计算属性的观察者是惰性求值,所以在创建计算属性观察者时除了 watcher.computed 属性为 true 之外,watcher.dirty 属性的值也为 true,代表着当前观察者对象没有被求值,而 evaluate 方法的作用就是用来手动求值的。可以看到在 evaluate 方法内部对 this.dirty 属性做了真假判断,如果为真则调用观察者对象的 this.get 方法求值,同时将this.dirty 属性重置为 false。最后将求得的值返回:return this.value。

这段代码的关键在于求值的这句代码,如下高亮部分所示:

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

我们在计算属性的初始化一节中讲过了,在创建计算属性观察者对象时传递给 Watcher 类的第二个参数为 getter 常量,它的值就是开发者在定义计算属性时的函数(或 userDef.get),如下高亮代码所示:

function initComputed (vm: Component, computed: Object) {
// 省略...
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 省略... if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
} // 省略...
}
}

所以在 evaluate 方法中求值的那句代码最终所执行的求值函数就是用户定义的计算属性的 get 函数。举个例子,假设我们这样定义计算属性:

computed: {
compA () {
return this.a +1
}
}

那么对于计算属性 compA 来讲,执行其计算属性观察者对象的 wather.evaluate 方法求值时,本质上就是执行如下函数进行求值:

compA () {
return this.a +1
}

大家想一想这个函数的执行会发生什么事情?我们知道数据对象的 a 属性是响应式的,所以如上函数的执行将会触发属性 a 的 get 拦截器函数。所以这会导致属性 a 将会收集到一个依赖,这个依赖实际上就是计算属性的观察者对象。

现在思路大概明朗了,如果计算属性 compA 依赖了数据对象的 a 属性,那么属性 a 将收集计算属性 compA 的 计算属性观察者对象,而 计算属性观察者对象 将收集 渲染函数观察者对象,整个路线是这样的:

假如此时我们修改响应式属性 a 的值,那么将触发属性 a 所收集的所有依赖,这其中包括计算属性的观察者。我们知道触发某个响应式属性的依赖实际上就是执行该属性所收集到的所有观察者的 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)
}
}

如上高亮代码所示,由于响应式数据收集到了计算属性观察者对象,所以当计算属性观察者对象的 update 方法被执行时,如上 if 语句块的代码将被执行,因为 this.computed 属性为真。接着检查了 this.dep.subs.length === 0 的真假,我们知道既然是计算属性的观察者,那么 this.dep 中将收集渲染函数作为依赖(或其他观察该计算属性变化的观察者对象作为依赖),所以当依赖的数量不为 0 时,在 else 语句块内会调用 this.dep.notify() 方法继续触发响应,这会导致 this.dep.subs 属性中收集到的所有观察者对象的更新,如果此时 this.dep.subs 中包含渲染函数的观察者,那么这就会导致重新渲染,最终完成视图的更新。

以上就是计算属性的实现思路,本质上计算属性观察者对象就是一个桥梁,它搭建在响应式数据与渲染函数观察者中间,另外大家注意上面的代码中并非直接调用 this.dep.notify() 方法触发响应,而是将这个方法作为 this.getAndInvoke 方法的回调去执行的,为什么这么做呢?那是因为 this.getAndInvoke 方法会重新求值并对比新旧值是否相同,如果满足相同条件则不会触发响应,只有当值确实变化时才会触发响应,这就是文档中的描述,现在你明白了吧:

通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化。

七、vue计算属性的更多相关文章

  1. Vue计算属性

    github地址:https://github.com/lily1010/vue_learn/tree/master/lesson06 一 计算属性定位 当一些数据需要根据其它数据变化时,这时候就需要 ...

  2. 在做vue计算属性,v-for处理数组时遇到的一个bug

    bug: You may have an infinite update loop in a component render function 无限循环 需要处理的数组(在 ** ssq **里): ...

  3. vue教程2-03 vue计算属性的使用 computed

    vue教程2-03 vue计算属性的使用 computed computed:{ b:function(){ //默认调用get return 值 } } ---------------------- ...

  4. vue 计算属性 实例选项 生命周期

    vue 计算属性: computed:{} 写在new vue()的属性,只要参与运算,数据不发生变化时,次计算只会执行一次,结果缓存,之后的计算会直接从缓存里去结果.如果其中的值发生变化(不管几个) ...

  5. Vue计算属性缓存(computed) vs 方法

    Vue计算属性缓存(computed) vs 方法 实例 <div id="example"> <p>Original message: "{{ ...

  6. vue 计算属性实现过滤关键词

    效果 html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <m ...

  7. Vue 计算属性 && 监视属性

    1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 & ...

  8. 第三节:Vue计算属性

    计算属性就是当其依赖的属性的值发生变化的时候,这个属性的值就会自动更新. 例子: <!DOCTYPE html> <html> <head> <meta ch ...

  9. Vue#计算属性

    在模板中表达式非常便利,但是它们实际上只用于简单的操作.模板是为了描述视图的结构.在模板中放入太多的逻辑会让模板过重且难以维护.这就是为什么 Vue.js 将绑定表达式限制为一个表达式.如果需要多于一 ...

随机推荐

  1. IOS开发中缓存策略

    为了节约流量,同时也是为了更好的用户体验,目前很多应用都使用本地缓存机制,其中以网易新闻的缓存功能最为出色.我自己的应用也想加入本地缓存的功能,于是我从网上查阅了相关的资料,发现总体上说有两种方法.一 ...

  2. PHP:(一)安装并使用PHP

    php的安装分为两个部分:环境安装配置和开发工具 一.集成环境安装配置 (一)安装 选择:wampserver或者xampp 我采用的是xampp. 在http://www.sourceforce.n ...

  3. 深入理解java虚拟机学习笔记(一)JVM内存模型

    上周末搬家后,家里的宽带一直没弄好,跟电信客服反映了N遍了终于约了个师傅明天早上来迁移宽带,可以结束一个多星期没网的痛苦日子了.这段时间也是各种忙,都一个星期没更新博客了,再不写之前那种状态和激情都要 ...

  4. MySQL主从复制读写分离如何提高从库性能-实战

    在做主从读写分离时候,需要注意主从的一些不同参数设置,来提高从库的性能,提高应用读取数据的速度,这样做很有必要的. 做读写分离复制主从参数不同设置如下(需要根据自己应用实际情况来设置): parmet ...

  5. hadoop-2.0.0-cdh4.1.2升级到hadoop-2.7.2

    升级前准备: 如果是 centos6.x的系统得升级glibc和pam包 在/etc/ld.so.conf 文件里添加 /usr/src/jdk1.6.0_23/jre/lib/amd64/serve ...

  6. JQ常用方法(哈哈)

    1ajax请求 $(function(){   $("#send").click(function(){     $.ajax({     type:"get" ...

  7. 【LeetCode #179】Largest Number 解题报告

    原题链接:Largest Number 题目描述: Given a list of non negative integers, arrange them such that they form th ...

  8. PHP使用redis(一)

    1,connect 描述:实例连接到一个Redis.参数:host: string,port: int返回值:BOOL 成功返回:TRUE;失败返回:FALSE <?php  $redis = ...

  9. html ajax请求 php 下拉 加载更多数据 (也可点击按钮加载更多)

    <input type="hidden" class="total_num" id="total" value="{$tot ...

  10. 【转载】Callable、FutureTask中阻塞超时返回的坑点

    本文转载自:http://www.cnblogs.com/starcrm/p/5010863.html 案例1: package com.net.thread.future; import java. ...