watch
的初始化是在 stateMixin
中进行的,在 instance/state
中
export function initState (vm: Component) {
...
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
然后看一下 initWatch
的实现
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
这里不管其 handler
是一个 Array
或者 Fun
,如果是 Array
就遍历执行 createWatcher
函数,否则就直接执行 createWatcher
函数
function createWatcher (
vm: Component,
expOrFn: string | Function,
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)
}
这里因为 watch
可以是个对象,也可以是个函数。
拿到其最终的回调函数,最后调用 vm.$watch
函数,该函数定义在 Vue 的原型中,在 stateMixin
的时候定义。
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
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
核心在于最终会调用 const watcher = new Watcher(vm, expOrFn, cb, options)
,这样就可以监听到值的变化了,需要注意的是,这是一个 user watcher
,一旦我们 watch
的数据发生变化,就会执行 watcher
的 run
方法,从而执行我们的 cb
函数,如果设置了 immediate
,就会直接执行回调函数 cb
,最终返回一个 unwatchFn
,这个是移除这个 watcher
,这个我们在业务中使用的到。
watch
中常见的配置项有 immediate
,deep
和 sync
immediate
的实现我们已经清楚。deep
,我们知道之前我们实例化了一个wacher
,最终会执行get
方法
get () {
...
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
get
方法中,我们的是 this.deep
为 true
,最终会执行 traverse(value)
const seenObjects = new Set()
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || 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 {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
这里其值是 Array
或者 Object
都会递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher。
sync
的实现就比较简单了,因为我们知道,当响应式数据发生变化的时候,会触发watcher.update
方法,但只是把这个watcher
放入到一个队列中,在nextTick
之后才会执行该watcher
的回调函数,但我们通过设置sync
,就可以立即执行回调函数。
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
在小程序中实现 wacth
功能
新建一个 utils.js
function observe(obj, key, cb, deep, page) {
let val = obj[key]
if (val !== null && typeof val === 'object' && deep) {
Object.keys(val).forEach(item => {
observe(val, item, cb, deep, page)
})
}
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
return val
},
set(value) {
if (value === val) {
return
}
cb.call(page, value, val)
val = value
}
})
}
function setWatch(page) {
let { data, watch } = page
Object.keys(watch).forEach((item) => {
let cb = watch[item].handler || watch[item]
let deep = watch[item].deep
observe(data, item, cb, deep, page)
})
}
module.exports = {
setWatch
}
然后在页面中引入即可
import { setWatch } from '../../utils.js'
Page({
data: {
name: 123
},
onLoad() {
setWatch(this)
},
watch: {
name(newVal, oldVal) {
console.log(newVal)
}
}
})
方法是比较简单的,在 setWatch
函数中取到 handler
和 deep
,然后作为参数最终执行 observe
函数,然后利用 Object.defineProperty
来进行数据响应式。
着重说一下,我们的使用,可以看到我们在使用的时候还需要手动在 onLoad
中注册一下,这是不太好用的。再者我们可能很多个页面都需要使用该功能,那写起来就会很冗余,我们可以利用 mixin
的思路来改造一下 Page
。
在 utils
中增加一个方法
function customPage(options) {
const { onLoad } = options
options.onLoad = function() {
options.watch && setWatch(this)
onLoad && onLoad()
}
return Page(options)
}
module.exports = {
...
customPage
}
这样我们在页面中使用的时候只引用一个 customPage
即可,不用关心在 onLoad
中注册了。
import { customPage } from '../../utils/util.js'
customPage({
data: {
name: 123
},
watch: {
name(newVal) {
console.log(newVal)
}
}
})