Watch 用法
1. 常见用法
<template>
<div class="home">
<h3>Watch 用法1:常见用法</h3>
<input v-model="message" />
<p>{{ copyMessage }}</p>
</div>
</template>
<script>
export default {
name: "home",
data() {
return {
message: "Hello Vue",
copyMessage: "",
};
},
watch: {
message(value) {
this.copyMessage = value;
},
},
};
</script>
一开始,页面上面显示空白,在 input 里面输入内容的时候,message 的值发生了改变,watch 监听到 message 值的变化,将 value 赋值给 copymessage ,此时 copymessage 有了值,在界面上面显示出来。
2. 绑定方法
<template>
<div class="home">
<h3>Watch 用法2:绑定方法</h3>
<input v-model="message" />
<p>{{ copyMessage }}</p>
</div>
</template>
<script>
export default {
name: "home",
data() {
return {
message: "Hello Vue",
copyMessage: "",
};
},
watch: {
message: "handleMessage",
},
methods: {
handleMessage(value) {
this.copyMessage = value;
},
},
};
</script>
一开始,页面上面显示空白,在 input 里面输入内容的时候,message 的值发生了改变,watch 监听到 message 值的变化,触发 handleMessage 方法, handleMessage 将 value 赋值给 copymessage ,此时 copymessage 有了值,在界面上面显示出来。
3. deep + handler
<template>
<div class="home">
<h3>Watch 用法3:deep + handler</h3>
<input v-model="deepMessage.a.b" />
<p>{{ copyMessage }}</p>
</div>
</template>
<script>
export default {
name: "home",
data() {
return {
deepMessage: {
a: {
b: "Deep Message",
},
},
copyMessage: "",
};
},
watch: {
deepMessage: {
handler: "handleDeepMessage",
// 开启深度监听
deep: true,
},
},
methods: {
handleDeepMessage(value) {
this.copyMessage = value.a.b;
},
},
};
</script>
一开始,页面上面显示空白,在 input 里面输入内容的时候,deepMessage.a.b 的值发生了改变,watch 监听到 deepMessage.a.b 值的变化,触发 handleDeepMessage方法, handleDeepMessage将 value 赋值给 copymessage ,此时 copymessage 有了值,在界面上面显示出来
4. immediate
<template>
<div class="home">
<h3>Watch 用法4:immediate</h3>
<input v-model="message" />
<p>{{ copyMessage }}</p>
</div>
</template>
<script>
export default {
name: "home",
data() {
return {
message: "Hello Vue",
copyMessage: "",
};
},
watch: {
message: {
handler: "handleMessage",
// 提前把 message 的值渲染到界面上
immediate: true,
},
},
methods: {
handleMessage(value) {
this.copyMessage = value;
},
},
};
</script>
页面第一次渲染的时,message 没有变化,copyMessage 也就没有被赋值。想要显示出来,可以设置 immediate,提前把 message 的值渲染到界面上。
5. 绑定多个 handler
<template>
<div class="home">
<h3>Watch 用法5:绑定多个 handler</h3>
<input v-model="message" />
<p>{{ copyMessage }}</p>
</div>
</template>
<script>
export default {
name: "home",
data() {
return {
message: "Hello Vue",
copyMessage: "",
};
},
watch: {
message: [
{
handler: "handleMessage",
},
"handleMessage2",
function (value) {
this.copyMessage = this.copyMessage + "...";
},
],
},
methods: {
handleMessage(value) {
this.copyMessage = value;
},
handleMessage2(value) {
this.copyMessage = this.copyMessage + "*";
},
},
};
</script>
message 发生变化时,依次调用handleMessage,handleMessage2,function 函数。对 copyMessage 去做出相对应的赋值。
6. 监听对象属性
<template>
<div class="home">
<h3>Watch 用法6:监听对象属性</h3>
<input v-model="deepMessage.a.b" />
<p>{{ copyMessage }}</p>
</div>
</template>
<script>
export default {
name: "home",
data() {
return {
deepMessage: {
a: {
b: "Hello Vue",
},
},
copyMessage: "",
};
},
watch: {
"deepMessage.a.b": "handleMessage",
},
methods: {
handleMessage(value) {
this.copyMessage = value;
},
},
};
</script>
一开始,页面上面显示空白,在 input 里面输入内容的时候,deepMessage.a.b 的值发生了改变,watch 监听到 deepMessage.a.b 值的变化,触发 handleMessage方法, handleMessage 将 value 赋值给 copymessage ,此时 copymessage 有了值,在界面上面显示出来。
watch能监听computed的属性?
答案是:可以的
<template>
<div class="home">
<p>{{ total }}</p>
<button @click="addGoodsNum">添加商品</button>
</div>
</template>
<script>
export default {
data: function () {
return {
goodsNum: 0,
price: 100,
};
},
mounted: function () {
this.goodsNum = 1;
},
methods: {
addGoodsNum: function () {
this.goodsNum = this.goodsNum + 1;
},
},
computed: {
total: function () {
var totalPrice = this.goodsNum * this.price;
return totalPrice;
},
},
watch: {
total: function (newValue, oldValue) {
console.log('总价在变化', newValue)
},
},
};
</script>
上面这段代码中,total
是由 this.goodsNum * this.price
计算而来的,同时也可以被 watch
监听,在 total
发生变化的时候,可以获取最新的值。
Watcher 原理
vm.$watch 其实是对 Watcher 的一种封装
或许你听说过收集依赖,那依赖是什么?也就是说收集谁?
当数据发生变化的时候,需要通知用到这些数据的地方,而使用这个数据的地方有很多,类型也很杂。这时需要抽象出一个能集中处理这些情况的类(Watcher)。收集的时候,只收集这个类的实例。那么数据变化的时候,也只通知它,在由它负责通知其他地方。
相当于一个中介的角色,数据变化时通知它,然后它再通知其他地方。
(这坨代码有点长,可以先大致浏览一下)
/* @flow */
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError,
invokeWithErrorHandling,
noop
} 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
/**
*观察者解析表达式,收集依赖关系,
*并在表达式值更改时触发回调。
*这用于$ watch()api和指令。
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
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) {
// 如果用户传入了 deep 参数
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 // uid for batching
this.active = true
this.dirty = this.lazy // 结合 Watcher 的 dirty 属性来分辨计算属性的返回值是否发生了改变
this.deps = [] // 记录自己都订阅了哪些 Dep
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// expOrFn 参数是支持函数的,如果是函数直接将它赋值给 getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 使用 parsePath 读取 keypath 中的数据。keyPath => deepMessage.a.b
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* 实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,然后会执行它的 this.get() 方法,进入 get 函数
*/
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
// 递归去访问 value,触发它所有子项的 getter
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* 记录自己都订阅了哪些 Dep
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// 记录当前的 Watcher 已经订阅了这个 Dep,避免重复的添加
this.newDepIds.add(id)
// 记录自己都订阅了哪些 Dep
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。
dep.addSub(this)
}
}
}
/**
* 清理依赖项收集。
* 每次添加完新的订阅,会移除掉旧的订阅
*/
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.
* 依赖项更改时将被调用
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
// 需要重新计算 "计算属性" 的返回值
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
// 获取当前的值
const value = this.get()
// 满足新旧值不等、新值是对象类型、deep 模式任何一个条件
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 设置新值
const oldValue = this.value
this.value = value
//
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
// 执行 watcher 的回调,注意回调函数执行的时候会把第二个和第三个参数传入新值 value 和旧值 oldValue
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
// 重新获取值,用于 computed
this.value = this.get()
// 计算属性的值并没有变,不需要重新计算
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* 从所有依赖项的 Dep 列表中将自己移除
*/
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.
// 取消观察数据,本质上是把 watcher 实例从当前正在观察的状态的依赖列表中移除
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
// 从所有依赖项的 Dep 列表中将自己移除
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// 先执行 new Watcher 来实现 vm.$watch 的基本功能
const watcher = new Watcher(vm, expOrFn, cb, options)
// 如果用户使用了 immediate 参数,则立即执行一次 cb
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
// 取消观察函数,用来停止触发回调
return function unwatchFn () {
watcher.teardown()
}
}
当用户执行这个函数时,实际上是执行了 watcher.teardown() 来取消观察数据,其本质是把 watcher 实例从当前正在观察的状态依赖列表中移除。
首先是在 Watcher 中记录自己都订阅过哪些 Dep(收集依赖存储的地方)
addDep
/**
* 记录自己都订阅了哪些 Dep
*/
addDep(dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// 记录当前的 Watcher 已经订阅了这个 Dep,避免重复的添加
this.newDepIds.add(id)
// 记录自己都订阅了哪些 Dep
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。
dep.addSub(this)
}
}
}
从所有依赖项的 Dep 列表中将自己移除
/**
* 从所有依赖项的 Dep 列表中将自己移除
*/
teardown() {
if (this.active) {
// 取消观察数据,本质上是把 watcher 实例从当前正在观察的状态的依赖列表中移除
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
// 从所有依赖项的 Dep 列表中将自己移除
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
Watcher 想监听某个数据,就会触发某个数据收集依赖的逻辑,将自己收集进去,然后当它发生变化时,就会通知 Watcher。
deep 参数的实现原理
除了触发当前这个被监听数据的收集依赖的逻辑之外,还要把当前监听的这个值在内的所有子值都触发一遍收集依赖逻辑。这就可以实现当前这个依赖的所有子数据发生变化时,通知当前 Watcher。
/**
* 实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,然后会执行它的 this.get() 方法,进入 get 函数
*/
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 {
// 递归去访问 value,触发它所有子项的 getter
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
const seenObjects = new Set()
/**
* 递归 value 的所有子值来触发它们收集依赖的能力
* @param {*} val
*/
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
// 如果它不是 Array 和 Object,或者已经被冻结了,那么直接返回
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
// 拿到 val 的 dep.id,用这个 id来保证不会重复收集依赖
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
// 如果是数组,则循环数组,将数组中的每一项递归调用 _traverse
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
// 如果 Object 类型的数据,则循环 Object 中的所有 key,然后执行一次读取操作,再递归子值。
keys = Object.keys(val)
i = keys.length
// val[keys[i]] => 读取 key 的时候,会触发收集依赖的操作,把当前的 Watcher 收集进去。
while (i--) _traverse(val[keys[i]], seen)
}
}