七、vue计算属性
细节流程图
初始化
计算属性的初始化是发生在 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计算属性的更多相关文章
- Vue计算属性
github地址:https://github.com/lily1010/vue_learn/tree/master/lesson06 一 计算属性定位 当一些数据需要根据其它数据变化时,这时候就需要 ...
- 在做vue计算属性,v-for处理数组时遇到的一个bug
bug: You may have an infinite update loop in a component render function 无限循环 需要处理的数组(在 ** ssq **里): ...
- vue教程2-03 vue计算属性的使用 computed
vue教程2-03 vue计算属性的使用 computed computed:{ b:function(){ //默认调用get return 值 } } ---------------------- ...
- vue 计算属性 实例选项 生命周期
vue 计算属性: computed:{} 写在new vue()的属性,只要参与运算,数据不发生变化时,次计算只会执行一次,结果缓存,之后的计算会直接从缓存里去结果.如果其中的值发生变化(不管几个) ...
- Vue计算属性缓存(computed) vs 方法
Vue计算属性缓存(computed) vs 方法 实例 <div id="example"> <p>Original message: "{{ ...
- vue 计算属性实现过滤关键词
效果 html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <m ...
- Vue 计算属性 && 监视属性
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 & ...
- 第三节:Vue计算属性
计算属性就是当其依赖的属性的值发生变化的时候,这个属性的值就会自动更新. 例子: <!DOCTYPE html> <html> <head> <meta ch ...
- Vue#计算属性
在模板中表达式非常便利,但是它们实际上只用于简单的操作.模板是为了描述视图的结构.在模板中放入太多的逻辑会让模板过重且难以维护.这就是为什么 Vue.js 将绑定表达式限制为一个表达式.如果需要多于一 ...
随机推荐
- (转)ActionContext和ServletActionContext
前面已经了解到ActionContext是Action执行时的上下文,里面存放着Action在执行时需要用到的对象,我们也称之为广义值栈. Struts2在每次执行Action之前都会创建新的Acti ...
- java 集合 HashSet 实现随机双色球 HashSet addAll() 实现去重后合并 HashSet对象去重 复写 HashCode()方法和equals方法 ArrayList去重
package com.swift.lianxi; import java.util.HashSet; import java.util.Random; /*训练知识点:HashSet 训练描述 双色 ...
- django连接mysql数据库配置,出现no module named mysqldb报错
作为一个菜鸟运维也是要有梦想的,万一学会了python走向人生巅峰了呢.好吧,都是瞎想,今天主要介绍下django配置,最近也开始摸索这个牛b框架了,当然大佬肯定不屑一顾,都是照顾照顾我们这些菜鸟初学 ...
- mysql5.6 配置 文件
mysql 3306 主库配置文件 [client] port = 3306 default-character-set=utf8mb4 socket = /ssd/mysql/3306/tmp/my ...
- 「译」setState如何知道它该做什么?
本文翻译自:How Does setState Know What to Do? 原作者:Dan Abramov 如果有任何版权问题,请联系shuirong1997@icloud.com 当你在组件中 ...
- 内置函数系列之 filter
filter 过滤 基本语法: s = filter(function,iterable) 将可迭代对象的每一个元素,传进函数中,根据函数中的判断条件,返回True或False 返回True的是保留的 ...
- Django项目发布到Apache2.4配置mod_wsgi,解决遭遇的各种坑。
环境: Apache2.4 32bit Python 3.7.1 (v3.7.1:260ec2c36a, Oct 20 2018, 14:05:16) [MSC v.1915 32 bit (Inte ...
- you do not permission to access / no this server
最近在学习Linux下的httpd服务,自己写了一个虚拟主机的配置文件 重启之后,怎么访问都是出现以下的内容, do not permission to access / no this server ...
- JS简写
本文来源于多年的 JavaScript 编码技术经验,适合所有正在使用 JavaScript 编程的开发人员阅读. 本文的目的在于帮助大家更加熟练的运用 JavaScript 语言来进行开发工作. 文 ...
- Codeforces146D 概率DP
Bag of mice The dragon and the princess are arguing about what to do on the New Year's Eve. The drag ...