持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 33 天,点击查看活动详情
学习 Vue2 中 computed
1. start
- 纸上得来终觉浅,绝知此事要躬行。
- 我阅读了
Vue2
中computed
相关源码,终于理解了它的运行逻辑,写一篇文章记录一下自己的收获。 - 阅读本文建议对
Vue2
中的Dep、Watcher
有一定了解。
源码版本 : Vue@2.6.14
2. computed 的使用
首先,先介绍一下 computed
是如何使用的。
2.1 函数形式
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>lazy_tomato</title>
</head>
<body>
<div id="app">
<div>{{ tomato }}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
name: '番茄',
say: '好吃',
}
},
computed: {
tomato() {
return this.name + '---' + this.say
},
},
})
</script>
</body>
</html>
2.2 自定义计算属性的 get set
<body>
<div id="app">
<div>firstName{{ firstName }}</div>
<div>lastName{{ lastName }}</div>
<div>fullName{{ fullName }}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
firstName: '番茄',
lastName: '好吃',
}
},
computed: {
fullName: {
// getter
get: function () {
return this.firstName + '-' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split('-')
this.firstName = names[0]
this.lastName = names[names.length - 1]
},
},
},
mounted() {
setTimeout(() => {
this.fullName = '西红柿-酸'
}, 3000)
},
})
</script>
</body>
2.3 总结
Vue2 中的 计算属性 computed
本身是一个对象。
- 属性值可以为一个函数,直接当做 getter 的形式使用。
- 属性值可以为一个对象,可以自定义 getter , setter。
3. 源码阅读
3.1 计算属性的初始化
3.1.1 initComputed
computed
的初始化是在 _init
中的 initState
=> initComputed
。
\src\core\instance\state.js
var computedWatcherOptions = { lazy: true }
function initComputed(vm, computed) {
// 1. 在组件上定义一个 `_computedWatchers` 对象,存储这个组件中计算属性的 `watcher实例`;
var watchers = (vm._computedWatchers = Object.create(null))
var isSSR = isServerRendering() // 是服务端渲染
// 2. 遍历传入的 computed, (选项 computed 是一个对象类型)
for (var key in computed) {
var userDef = computed[key]
// 3. 定义一个变量 getter , 如果 computed 中属性的属性值是函数,则 getter = 属性值,否则存储属性值的 get;
var getter = typeof userDef === 'function' ? userDef : userDef.get
if (getter == null) {
warn('Getter is missing for computed property "' + key + '".', vm)
}
if (!isSSR) {
// 4. 每一个计算属性,都会创 `new Watcher()`,这里注意两点:1.传入了配置` { lazy: true }`; 2.传入了自定义的 getter;
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// 5. 排除 key 重复的情况,执行`defineComputed`。
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else {
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
)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(
'The computed property "' + key + '" is already defined as a method.',
vm
)
}
}
}
}
3.1.2 源码逻辑讲解
-
在组件实例上定义一个属性
_computedWatchers
,存储这个组件中计算属性的watcher实例
; -
遍历传入的
computed
选项;-
定义一个变量 getter , 如果 computed 中属性的属性值是函数,则 getter = 属性值,否则存储属性值的 get;
这行代码,就实现了计算属性的两种写法。
- 计算属性直接是一个函数的写法
- 计算属性是对象,可以定义
get
,set
属性;
-
每一个计算属性,都会
new Watcher()
,这里注意两点:1.传入了配置{ lazy: true }
; 2.传入了自定义的 getter; -
在排除
key
重复的情况,执行defineComputed
。
-
3.1.3 小节
梳理一下 computed
初始化的整体逻辑
Vue实例
上绑定一个_computedWatchers
属性,存储这个组件中计算属性的watcher实例
;for in
的方式遍历computed
,给每一个属性值都创建一个watcher实例
,并且把computed
中的每一个属性绑定到Vue实例
上。
- 选项
computed
是一个对象,以后在编写代码的时候,不要再纠结computed
是对象还是函数,傻傻分不清楚。Vue实例
上的_computedWatchers
对象,存储这个组件中计算属性的watcher实例
。以后如果打印this
看到_computedWatchers
这个属性不会觉得陌生了;computed
中的属性,会生成一个对应的watcher实例
,这个watcher实例
的属性有一些特殊。(特殊在哪里?后续会细说)computed
中的属性名,会绑定到Vue实例
上,并且给其赋值我们处理好的函数。 这也就是为什么可以直接使用this.xxx
访问computed
中的属性名。
3.2 new Watcher
计算属性的 new Watcher()
有哪些特殊操作?我结合一个真实的使用案例来分析。
3.2.1 使用案例:
<script src="./vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
name: 'lazy',
}
},
computed: {
tomato() {
return this.name.toUpperCase()
},
},
})
</script>
3.2.2 computed 初始化的时候:
// 在 initComputed 函数中初始化的源码如下:
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
// 案例代码可以转换为如下逻辑
// watchers['tomato'] = new Watcher(
// vm,
// tomato() => {
// return this.name.toUpperCase()
// },
// noop,
// { lazy: true }
// )
3.2.3 Watcher 类:
var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid$1 // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new _Set()
this.newDepIds = new _Set()
this.expression = expOrFn.toString()
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
warn(
'Failed watching path: "' +
expOrFn +
'" ' +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy ? undefined : this.get()
}
3.2.4 最终得到的 watcher 实例:
// 下方实例经过简化
var w1 = {
active: true,
dirty: true,
expression: "tommto () {\n return '132'\n }",
getter: function tommto() {
return this.name.toUpperCase()
},
id: 1,
lazy: true,
sync: false,
user: false,
value: undefined,
}
3.2.5 小节:
解释一下上述的逻辑,总的来说就是实例化了一个 watcher实例
。
但是这个 watcher实例
的属性有点特殊:
- 属性:
getter
是用户定义的函数(computed 选项定义的函数); - 属性:
dirty
是true
(标示需不需要重新求值); - 属性:
lazy
是true
(标示自己是计算属性生成的 watcher); - 属性:
value
是undefined
;
注意
value
默认是空的。
3.3 defineComputed
3.3.1 defineComputed 源码
在 initComputed
中,除了实例化了一个 watcher实例
,还执行了 defineComputed
,我们来看看它的源码。
源码:
function noop(a, b, c) {}
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
}
function createComputedGetter(key) {
return function computedGetter() {
var watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 需要重新计算
if (watcher.dirty) {
watcher.evaluate()
}
// 收集依赖
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
// 参数,新增this
function createGetterInvoker(fn) {
return function computedGetter() {
return fn.call(this, this)
}
}
function defineComputed(target, key, userDef) {
// 不是服务端渲染,则需要缓存。
var 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 (sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
'Computed property "' + key + '" was assigned to but it has no setter.',
this
)
}
}
// 5. 在组件实例上存储这个计算属性。
Object.defineProperty(target, key, sharedPropertyDefinition)
}
3.3.2 整体逻辑
上述代码的整体逻辑:
-
将
computed
选项中的每个属性,绑定到我们的 Vue 实例上。 -
绑定的时候重写了他们的
getter,setter
。-
源码精简
// 1. 我们演示案例不是服务端渲染,所以源码中`shouldCache` 为 `true`; // 2. `sharedPropertyDefinition` 就是一个普通对象; // 源码可以精简为如下代码: const obj = {} obj.get = createComputedGetter(key) obj.set = () => {} Object.defineProperty(vm, key, obj)
-
重写
getter
,即createComputedGetter
-
3.3.3 createComputedGetter
createComputedGetter
,会返回一个名为 createComputedGetter
的函数,当做这个计算属性的 getter。
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
}
}
}
// 重新调用 get 方法。
Watcher.prototype.evaluate = function evaluate() {
this.value = this.get()
this.dirty = false
}
// 收集依赖
Watcher.prototype.depend = function depend() {
var i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
3.3.4 解释上述代码:
-
当我使用这个计算属性的时候,就会调用
computedGetter
函数。computed: { tomato() { return this.name + '---好吃' }, }, // 这种方式使用计算属性,就会调用 `computedGette` this.tomato
-
computedGetter
函数中的逻辑;- 拿到
_computedWatchers
中存储的计算属性的 watcher实例
; evaluate
(评估)表示重新触发get
,重新求值;dirty
(脏的)标示需要重新求值的状态变量;watcher.dirty
存在,重新求值。Dep.target
存在,收集依赖。
- 拿到
-
watcher.evaluate()
, 它会执行computed
配置中用户定义的函数,更新我们计算属性的值,然后设置dirty
为 false; -
Dep.target
即收集依赖。
4. 页面渲染的流程
4.1 案例:
<body>
<div id="app">
<div>{{ tomato }}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
name: '番茄',
}
},
computed: {
tomato() {
return this.name + '---好吃'
},
},
})
</script>
</body>
4.2 渲染页面前:
// 案例执行逻辑如下:
1`new Vue()`
2`_init()`
3`initState()`
4`initDate()`
5`initComputed()`
此时内存中存在以下数据:
- 配置项
data
中name
属性的 dep(d1
); - 计算属性
tomato
生成的watcher实例
(w1
);
w1
可以理解为:{ dirty: true, lazy: true,value: undefined,}
4.3 开始渲染页面:
-
渲染页面会创建一个渲染 watcher (
w2
) -
渲染页面的时候,因为模板中使用到了
tomato
,会执行对应computedGetter
的逻辑 -
计算属性首次加载,
w1.dirty
默认为true
,会触发watcher.evaluate()
; -
watcher.evaluate()
,会触发computed
选项中用户自定义的函数,然后设置w1.dirty
为false
; -
用户自定义函数又读取了
this.name
,所以会触发this.name
的getter
。例如执行:
return this.name + '---好吃'
,会触发this.name
的getter
。 -
Dep.target
的相关逻辑-
执行用户自定义的函数的时候:
`d1` 收集 `w1`; w1 中的 deps 收集 `d1`;
-
computedGetter
中的Dep.target
`w1 中的 deps 中所有的 dep` 都收集 `w2` // 上述逻辑其实就是 `d1 收集 w2`
-
4.4 页面改变
假如 name
值发生改变,调用 d1
中存储的所有 watcher实例
的 update
方法。
执行顺序从先到后。先通知 w1,再通知 w2;
Watcher.prototype.update = function update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
计算watcher 的update w1.update
//
if (this.lazy) {
this.dirty = true
}
由于 w1
是计算属性定义的,则lazy 为 true
,所以设置 w1.dirty = true
然后结束。
渲染watcher的 updataw2.update
// 渲染 watcher
queueWatcher(this)
queueWatcher()
,会执行 渲染 watcher
的 getter
。因为模板中使用了 tomato
这个计算属性, 随后会触发 computedGetter
。
如果需要更新
w1.dirty 为true
,会触发watcher.evaluate()
,更新计算属性的值。
如果没有更新
w1.dirty 为false
,不会触发watcher.evaluate()
,计算属性的旧值。
总结
计算Watcher
和普通 Watcher 的区别:
看到别人总结的非常好,这里借鉴过来。
- 用 lazy 为 true 标示为它是一个
计算Watcher
; - 计算 Watcher 的 get 和 set 是在初始化(initComputed)时经过 defineComputed() 方法重写了的;
- 当它所依赖的属性发生改变时虽然也会调用
计算Watcher.update()
,但是因为它的 lazy 属性为 true,所以只执行把 dirty 设置为 true 这一个操作,并不会像其它的 Watcher 一样执行 queueWatcher() 或者 run(); - 当有用到这个
计算Watcher
的时候,例如视图渲染时调用了它时,才会触发计算Watcher
的 get,但又由于这个 get 在初始化时被重写了,其内部会判断 dirty 的值是否为 true 来决定是否需要执行 evaluate()重新计算; - 因此才有了这么一句话:当计算属性所依赖的属性发生变化时并不会马上重新计算(只是将 dirty 设置为了 true 而已),而是要等到其它地方读取这个计算属性的时候(会触发重写的 get)时才重新计算,因此它具备懒计算特性;
其他需要注意的事项:
computed
中不支持异步,watch
中支持;computed
中注重结果,watch
中注重过程;computed
中存在缓存,methods
中不存在缓存。;
个人总结
-
computed
本质上也是通过new Wathcer
来实现的; -
用 lazy 为 true 标示为它是一个
计算Watcher
; -
用 dirty 为 true 标示它是否需要重新计算;
-
Watcher类
的形参expression
,可以接收字符串或者函数。方便自定义watcher中的getter
; -
依赖收集和通知更新的执行顺序很有意思,先
dirty 为 true
,后重新求值。 -
计算Watcher
更注重结果。
end
- 加油!