computed和watch
# computed 和 watch 浅析
# 一、computed
在 Vue 的 template 模板内是可以写一些简单的 js 表达式的很便利,如上直接计算 this.firstName + ' ' + this.lastName,因为在模版中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响,而 computed 的设计初衷也正是用于解决此类问题。
# 1.特点
计算属性是基于它们的响应式依赖进行缓存的
computed 具有
缓存机制
,依赖值不变的情况下其会直接读取缓存进行复用computed 是依赖已有的变量来计算一个目标变量,大多数情况都是多个变量凑在一起计算出一个变量
computed 不能进行异步操作
Ps: 计算属性内的值须是响应式数据才能触发重新计算。
# 二、watch
watcher 更像是一个 data 的数据监听回调,当依赖的 data 的数据变化,执行回调,在方法中会传入 newVal 和 oldVal。可以提供输入值无效,提供中间值 特场景。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。如果你需要在某个数据变化时做一些事情,使用 watch。
特点:
- watch 是监听某一个变量的变化,并执行相应的回调函数,通常是一个变量的变化决定多个变量的变化
- watch 可以进行异步操作
watch 的原理就是为需要观察的数据创建并收集user-watcher
当数据改变时通知到user-watcher
将新值和旧值传递给用户自己定义的回调函数
参数:
personInfo = {
name:'lili',
age:12,
Hobbies:['singing', 'dance']
}
@Watch('personInfo', { immediate: true, deep: true })
onPersonInfoChange(newValue,oldValue) {
code...
}
2
3
4
5
6
7
8
9
10
以上参数的含义是初始化立即执行一次 onPersonInfoChange 函数,在 personInfo 里的任何字段变化时,执行 onPersonInfoChange
immediate、deep 实现原理是:
immediate 初始化时立即执行一次回调函数
deep 是递归的对它的子值进行依赖收集,任何依赖发生变化就执行回调
# 三、源码浅析
一个简单的例子
<div id="app">
<h2>{{ this.text }}</h2>
<h2>{{ this.count }}</h2>
<button @click="changeName">Change name</button>
<button @click="add">Add</button>
</div>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
name: 'xiaoming',
count: 0
}
},
computed: {
text() {
return `Hello, ${this.name}!`
}
},
methods: {
changeName() {
this.name = 'onlyil'
},
add() {
this.count += 1
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
还是从 vue 初始化看起,从 new Vue() 开始,构造函数会执行 this._init,在 _init 中会进行合并配置、初始化生命周期、事件、渲染等,最后执行 vm.$mount 进行挂载。
# 1.初始化 computed
// src/core/instance/index.js
function Vue(options) {
// ...
this._init(options)
}
// src/core/instance/init.js
Vue.prototype._init = function(options?: Object) {
// 合并选项
// ...
// 一系列初始化
// ...
initState(vm)
// ...
// 挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
计算属性的初始化就在 initState 中:
// src/core/instance/state.js
export function initState(vm: Component) {
const opts = vm.$options
// ...
// 初始化 computed
if (opts.computed) initComputed(vm, opts.computed)
// ...
}
2
3
4
5
6
7
8
看一下 initComputed 做了什么
function initComputed(vm, computed) {
const watchers = (vm._computedWatchers = Object.create(null))
// 遍历 computed 选项,依次进行定义
for (const key in computed) {
const getter = computed[key]
// 为计算属性创建内部 watcher
watchers[key] = new Watcher(
vm,
getter || noop, // 计算属性 text 函数
noop,
computedWatcherOptions // { lazy: true } ,指定 lazy 属性,表示要实例化 computedWatcher
)
// 为计算属性定义 getter
defineComputed(vm, key, userDef)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
首先定义一个 watchers 空对象,同时挂在 vm._computedWatchers 上,用来存放该 vm 实例的所有 computedWatcher。
接下来看实例化 computedWatcher :
class Watcher {
constructor(vm, expOrFn, cb, options) {
// options 为 { lazy: true }
if (options) {
// ...
this.lazy = !!options.lazy
// ...
}
this.dirty = this.lazy // for lazy watchers, 初始 dirty 为 true
this.getter = expOrFn
// lazy 为 true,不进行求值,直接返回 undefined
this.value = this.lazy ? undefined : this.get()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
回到上边,为计算属性创建内部 watcher 之后的 watchers 对象是这样的:
{
text: Watcher {
lazy: true,
dirty: true,
deps: [],
getter: function () {
return `Hello, ${this.name}!`
},
value: undefined, // 直接赋值为 undefined ,
}
}
2
3
4
5
6
7
8
9
10
11
接下来看定义计算属性的 getter
function defineComputed(target, key, userDef) {
Object.defineProperty(target, key, {
get: function() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
到这里 computed 就初始化好了
再回顾一下流程
- 定义 vm._computedWatchers 用来存放该 vm 实例的所有 computedWatcher
- 遍历 computed 选项并实例化 watcher,不求值,直接将 watcher.value = undefined
- 通过 defineComputed 定义计算属性的 getter ,等待后边读取时触发
# 2.读取 computed
初始化完成后,会进入 mount 阶段,在执行 render 生成 vnode 时会读取到计算属性 text ,上例的 render 函数是这样:
function render() {
var h = arguments[0]
return h('div', [
h('h2', [this.text]), // 这里读取了计算属性 text
h(
'button',
{
on: {
click: this.changeName
}
},
['changeName']
)
])
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
render 执行时会触发计算属性的 getter ,也就是上边定义的访问器属性:
get: function () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 此时 dirty 为 true ,进行求值
if (watcher.dirty) {
// 求值,对 data 进行依赖收集,使 computedWatcher 订阅 data
// 这里的 data 就是 "this.name"
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
求值 watcher.evaluate()
取出 vm._computedWatchers 中对应的 watcher ,此时 watcher.dirty 为 true,执行 watcher.evaluate() 。
// watcher.evaluate
evaluate() {
this.value = this.get()
this.dirty = false
}
2
3
4
5
后面的步骤就是响应式数据进行依赖收集,也就是对示例中的 name 进行依赖收集,收集的是谁呢?上面提到此时的 Dep.target 是渲染 watcher ,那么总结下来,这一步做的是:
让 computedWatcher 订阅的响应式数据收集渲染 watcher
有关响应式数据原理可以在另外的文章了解
# 3.computed 触发更新
当点击按钮时,执行 this.name = 'onlyil' ,会触发 name 的访问器属性 set ,执行 dep.notify() ,依次触发它所收集的 watcher 的更新逻辑,也就是 [ computedWatcher, 渲染 watcher ] 的 update 。
触发 computedWatcher 更新
// watcher.update
update() {
// computedWatcher 的 lazy 为 true
if (this.lazy) {
this.dirty = true
}
// ...
}
2
3
4
5
6
7
8
将 dirty 置为 true,表示该计算属性“脏”了,需要重新计算
接下来触发渲染 watcher 更新
// watcher.update
update() {
// ...
//
queueWatcher(this)
}
2
3
4
5
6
这里就是加入异步更新队列,最终又会执行到 render 函数来生成 vnode ,同首次渲染一样,在 render 过程中又会读取到计算属性 text ,再次触发它的 getter :
get: function () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 此时 dirty 为 true ,"脏"了
if (watcher.dirty) {
// 重新求值
watcher.evaluate()
}
// ...
return watcher.value
}
}
2
3
4
5
6
7
8
9
10
11
12
13
这里触发重新求值我们在 computed 选项定义的函数,页面就展示了新值 Hello, onlyil!
computed 更新结束。
# 4.总结
通过上边的过程分析,可以做出如下总结:
- 首次渲染时实例化 computedWatcher 并定义属性 dirty: false ,在 render 过程中求值并进行依赖收集;
- 当 computedWatcher 订阅的响应式数据也就是 name 改变时,触发 computedWatcher 的更新,修改 dirty: true ;
- render 函数执行时读取计算属性 text ,发现 dirty 为 true ,重新求值,页面视图更新。
- 可以发现一个关键点,computedWatcher 的更新只做了一件事:修改 dirty: true ,求值操作始终都在 render 过程中。
# 5.问题
点击 Add 按钮 count 会发生改变,那么在重渲染时 computedWatcher 会重新求值吗?
答案是不会,计算属性 text 的 getter 函数并没有读取 count ,所以它的 computedWatcher 不会订阅 count 的变化,即 count 的 dep 也不会收集该 computedWatcher 。
所以细品官方文档里的描述
计算属性是基于它们的响应式依赖进行缓存的
# 四、应用场景
computed:
场景:模板中的某个值需要通过一个或多个数据计算得到并且不用重复数据计算
重点计算得到的新值
**
watch:
场景:监听属性主要是监听某个值发生变化后,对新值去进行逻辑处理。
重点: 某一个特定值发生了变化,需要根据它的变化做一些事情
简单记就是:一般情况下computed 是多对一,watch 是一对多