虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的 watcher 。这是为什么 Vue 提供一个更通用的方法通过watch 选项,来响应数据的变化。当你想要在数据变化响应时,执行异步操作或开销较大的操作,这是很有用的。本文主要介绍了vue 中的 watcher的相关资料,需要的朋友可以参考下,希望能帮助到大家。
大家对于 watch 应该不陌生,项目中都用过下面这种写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | watch: { someProp () { // do something } } // 或者 watch: { someProp: { deep: true, handler () { // do something } } } |
上面的写法告诉 vue,我需要监听 someProp 属性的变化,于是 vue 在内部就会为我们创建一个 watcher 对象。(限于篇幅,我们不聊 watcher 的具体实现,感兴趣的可以直接看源码 watcher)
然而在 vue 中,watcher 的功能并没有这么单一,先上段代码:
1 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 31 32 33 34 | <template> <p> <p>a: {{ a }}</p> <p>b: {{ b }}</p> <button @click="increment">+</button> </p> </template> <script> export default { data () { return { a: 1 } }, computed: { b () { return this.a * 2 } }, watch: { a () { console.log('a is changed') } }, methods: { increment () { this.a += 1 } }, created () { console.log(this._watchers) } } </script> |
在线demo
上面代码非常简单,我们现在主要关注 created 钩子中打印的 this._watchers,如下:
分别展开三个 watcher,观察每一个 expression,从上到下分别为:
1 2 3 | b() { return this.a * 2;↵ } "a" function () { vm._update(vm._render(), hydrating);↵ } |
上面三个 watcher 代表了三种不同功能的 watcher,我们将其按功能分为三类:
在 watch 中定义的,用于监听属性变化的 watcher (第二个)
用于 computed 属性的 watcher (第一个)
用于页面更新的 watcher (第三个)
normal-watcher
我们在 watch 中定义的,都属于这种类型,即只要监听的属性改变了,都会触发定义好的回调函数
computed-watcher
每一个 computed 属性,最后都会生成一个对应的 watcher 对象,但是这类 watcher 有个特点,我们拿上面的 b 举例:
属性 b 依赖 a,当 a 改变的时候,b 并不会立即重新计算,只有之后其他地方需要读取 b 的时候,它才会真正计算,即具备 lazy(懒计算)特性
render-watcher
每一个组件都会有一个 render-watcher, function () {↵ vm._update(vm._render(), hydrating);↵ }, 当 data/computed
中的属性改变的时候,会调用该 render-watcher 来更新组件的视图
三种 watcher 的执行顺序
除了功能上的区别,这三种 watcher 也有固定的执行顺序,分别是:
1 | computed-render -> normal-watcher -> render-watcher |
这样安排是有原因的,这样就能尽可能的保证,在更新组件视图的时候,computed 属性已经是最新值了,如果 render-watcher 排在 computed-render 前面,就会导致页面更新的时候 computed 值为旧数据。
下面从一段实例代码中看下vue中的watcher
在这个示例中,使用 watch 选项允许我们执行异步操作(访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这是计算属性无法做到的。
1 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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | <p id="watch-example"> <p> Ask a yes/no question: <input v-model="question"> </p> <p>{{ answer }}</p> </p> <!-- Since there is already a rich ecosystem of ajax libraries --> <!-- and collections of general-purpose utility methods, Vue core --> <!-- is able to remain small by not reinventing them. This also --> <!-- gives you the freedom to just use what you're familiar with. --> <script src="https://unpkg.com/axios@0.12.0/dist/axios.min.js"></script> <script src="https://unpkg.com/lodash@4.13.1/lodash.min.js"></script> <script> var watchExampleVM = new Vue({ el: '#watch-example', data: { question: '', answer: 'I cannot give you an answer until you ask a question!' }, watch: { // 如果 question 发生改变,这个函数就会运行 question: function (newQuestion) { this.answer = 'Waiting for you to stop typing...' this.getAnswer() } }, methods: { // _.debounce 是一个通过 lodash 限制操作频率的函数。 // 在这个例子中,我们希望限制访问yesno.wtf/api的频率 // ajax请求直到用户输入完毕才会发出 // 学习更多关于 _.debounce function (and its cousin // _.throttle), 参考: https://lodash.com/docs#debounce getAnswer: _.debounce( function () { var vm = this if (this.question.indexOf('?') === -1) { vm.answer = 'Questions usually contain a question mark. ;-)' return } vm.answer = 'Thinking...' axios.get('https://yesno.wtf/api') .then(function (response) { vm.answer = _.capitalize(response.data.answer) }) .catch(function (error) { vm.answer = 'Error! Could not reach the API. ' + error }) }, // 这是我们为用户停止输入等待的毫秒数 500 ) } }) </script> |
/* @flow */
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError
} from '../util/index'
let uid = 0
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
一个 watcher编译成 一个表达式 和 依赖集合, 还能出发回调当 v-model的值改变
这也能用到wathcer api中, 如果 vm.$watch('abc', fn);
*/
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: Set;
newDepIds: Set;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
//这个watcher所属的vm
this.vm = vm
// 一个vm 有多个watchers , vm.$watch(a,fn1) vm.$watch(b,fn2) {{b.UpperCase()}} 等等
vm._watchers.push(this)
// options
if (options) {
//是否深度检测依赖
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} 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 // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
//监控函数
this.getter = expOrFn
} else {
//监控表达式, 注意 这里也会返回一个路径的函数
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
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()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
//Dep.target = this
pushTarget(this)
let value
const vm = this.vm
if (this.user) {
try {
// 因为传入了 vm, 所以在methods中可以取得this.data.abc
// 比如 this.getter 是 fucntion(){ return this.price * this.nums; }; 因为执行这个函数, 也就会去获取 this.price 和 this.nums的值
// 就会分别触发这两个数据的get方法, 然后这个watcher就会加入这两个数据的dep, this.push(depq) this.push(dep2); 通过这一步就收集了这个函数的两个依赖
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else {
value = this.getter.call(vm, vm)
}
// "touch" every property so they are all tracked as
// dependencies for deep watching
//“触摸”每一个属性,这样它们都被跟踪为 深度观察依赖
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
/**
* Add a dependency to this directive.
添加一个依赖到这个指令
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
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.
* Will be called when a dependency changes.
*/
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()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
计算一个watcher的值, 这个只会被 延迟watchers调用
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
通过这个watcher, 收集所有的依赖?
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
从所有的依赖订阅清单中移除自己
*/
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.
// 移除依赖是一个昂贵的操作, 所有如果这个vm已经销毁le, 我们就跳一跳
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
递归追踪一个对象去唤起所有的已经转换的getters,
这样在一个对象的所以嵌套的熟悉都被收集成为一个深层的依赖
*/
const seenObjects = new Set()
function traverse (val: any) {
seenObjects.clear()
_traverse(val, seenObjects)
}
function _traverse (val: any, seen: Set) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
//for in 都不用了, 加快速度
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}如对本文有疑问,请提交到交流论坛,广大热心网友会为你解答!! 点击进入论坛