Vue的watch基本使用
在读watch的源码之前,我们先回顾一下watch的使用方法,可以看下面的这个demo。
<template>
<div>
<div>firstName: <input v-model="firstName"></div>
<div>lastName: <input v-model="lastName"></div>
<h2>fullName: {{ fullName }}</h2>
<button @click="changeColor">改变肤色</button>
<div>肤色:{{ detail.feature.skinColor }}</div>
<button @click="changeName">改变名字</button>
<div>名字:{{ detail.name }}</div>
</div>
</template>
<script>
export default {
data () {
return {
firstName: 'Jack',
lastName: 'Cheng',
fullName: '--',
detail: {
name: 'sss',
age: 22,
like: ['reading', 'exercise'],
feature: {
hair: 'long',
skinColor: 'black'
}
}
}
},
methods: {
changeColor () {
this.detail.feature.skinColor = 'yellow'
},
changeName () {
this.detail.name = 'hhhh'
}
},
watch: {
firstName (newVal, oldVal) {
console.log('firstName: ', newVal, oldVal)
this.fullName = newVal + ' ' + this.lastName
},
lastName: {
handler (newVal, oldVal) {
console.log('lastName: ', newVal, oldVal)
this.fullName = this.firstName + ' ' + newVal
},
immediate: true,
},
'detail.feature': {
handler (newVal, oldVal) {
console.log('我监听到了detail对象的改变', newVal, oldVal)
},
deep: true
},
'detail.name': [
function handler1 () {
console.log('改变了')
},
function handler2 () {
console.log('改变了')
}
]
}
}
</script>
第一次页面展示效果:
由于设置了
immediate: true,所以在第一次加载页面时,会执行监听的回调。
点击“改变肤色”,由于该feature属性是对象上的深层属性,所以我们需要加deep: true,可以实现深层监听。
其实,
wach的属性还可以设置数组,只是不经常这样用,但是在watch的实现源码中,对数组这种类型进行了兼容,所以这里加了一个数组的例子。
点击“改变名字”,可以看一下效果。
watch源码分析
先从入口出发,找到初始化函数initState
export function initState(vm: Component) {
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
// Composition API
initSetup(vm)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
const ob = observe((vm._data = {}))
ob && ob.vmCount++
}
if (opts.computed) initComputed(vm, opts.computed)
// 📢:在这里读取开发者设置的watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initWatch 函数
在这里,遍历了每一个侦听属性,并判断其类型,经过兼容处理,最后都会调用createWatcher这个方法。
function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
createWatcher 函数
由于侦听器的属性值可能为对象,也可能为函数,所以在这里进行了处理。
主要是将执行的回调函数存储在handler中,如果侦听属性为对象,则将这个对象存储在options中,最后将其作为参数,调用$watch方法。
function createWatcher(
vm: Component,
expOrFn: string | (() => any),
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
isPlainObject这个函数只是判断值是否为对象数据类型。
export function isPlainObject(obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
Vue.prototype.$watch
在这个方法中,主要是创建了watcher实例。
Vue.prototype.$watch = function (
expOrFn: string | (() => any),
cb: any,
options?: Record<string, any>
): Function {
const vm: Component = this
// 📢:这里确保cb是个函数
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 📢:用户侦听器标识
options.user = true
// 📢:每一个侦听属性都生成了一个watcher实例
const watcher = new Watcher(vm, expOrFn, cb, options)
// 判断是否为立即执行
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 函数
这里可以说明一下,在三个地方会创建watcher实例:渲染watcher、计算watcher、用户侦听watcher,这里我们主要分析用户侦听watcher。
由于Watcher这个类上面的代码比较多,这里精简化了一下,去掉不用关注的代码,更方便专注watch的实现分析。
export default class Watcher {
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function, // 要执行的回调函数
options?: WatcherOptions | null, // 配置的侦听属性值
isRenderWatcher?: boolean // 不用关心,渲染watcher用到的
) {
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user // 这里为true,标识用户侦听器
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.post = false
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = __DEV__ ? expOrFn.toString() : ''
// 📢:这里lazy为 false,所以直接调用get()
this.value = this.lazy ? undefined : this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
// 📢:这个方法和下面popTarget方法主要是建立依赖关系,实现了监听的数据上绑定我们侦听器的watcher,以及渲染watcher
pushTarget(this)
let value
const vm = this.vm
try {
// 📢:获取监听属性的值
value = this.getter.call(vm, vm)
} catch (e: any) {
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
// 📢:这里deep为true,可以深层监听,会调用traverse方法,对当前监听的对象或者数组进行递归处理,变为深度监听。
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: any = 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
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown() {
if (this.vm && !this.vm._isBeingDestroyed) {
remove(this.vm._scope.effects, this)
}
if (this.active) {
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
if (this.onStop) {
this.onStop()
}
}
}
}
traverse源码实现了一个递归调用,处理深层监听
export function traverse(val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
return val
}
function _traverse(val: any, seen: SimpleSet) {
let i, keys
const isA = isArray(val)
if (
(!isA && !isObject(val)) ||
val.__v_skip /* ReactiveFlags.SKIP */ ||
Object.isFrozen(val) ||
val instanceof VNode
) {
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 if (isRef(val)) {
_traverse(val.value, seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
pushTarget、popTarget 函数
这两个方法维护了一个栈,以及Dep.target的值。
在页面渲染的时候,targetStack = [ 渲染watcher ],Dep.target = 渲染watcher。
在读取用户的watch属性时,targetStack = [ 渲染watcher, 用户侦听watcher ],Dep.target = 用户侦听watcher。并将用户侦听watcher收集到监听数据上。
调用popTarget方法,targetStack = [ 渲染watcher ],Dep.target = 渲染watcher,将渲染watcher和数据之间的依赖进行收集。
监听的数据改变,就会触发set方法,同时触发依赖的watcher去执行回调。
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []
export function pushTarget(target?: DepTarget | null) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
可以总结一下,这两个方法其实就是进行了一个依赖收集,再结合发布订阅者模式,当数据变化了,就会通知相关以来的的watcher,从而进行一些操作。