# 源码分析vue watch侦听器
在阅读侦听器源码之前,建议先看一下响应式相关的源码源码分析vue响应式原理 (opens new window),有助理解。
# 侦听器的几种用法
<div id="app">
<input v-model="text1">
<input v-model="text2">
<input v-model="text3.val">
<input v-model="text4.val">
</div>
var vm = new Vue({
el: '#app',
data: {
text1: 'Hello 1',
text2: 'Hello 2',
text3: {
val: 'Hello 3',
},
text4: {
val: 'Hello 4',
},
},
watch: {
text1: 'fun',
text2: function(newVal, oldVal){
console.log('newVal:', newVal)
console.log('oldVal:', oldVal)
},
'text3.val': {
handler: function(newVal, oldVal){
console.log('newVal:', newVal)
console.log('oldVal:', oldVal)
},
deep: true,
immediate: true,
},
text4: [
'fun',
function(newVal, oldVal){
console.log('newVal:', newVal)
console.log('oldVal:', oldVal)
},
{
handler: function(newVal, oldVal){
console.log('newVal:', newVal)
console.log('oldVal:', oldVal)
},
deep: true,
immediate: true,
},
]
},
methods: {
fun (newVal, oldVal) {
console.log('newVal:', newVal)
console.log('oldVal:', oldVal)
}
}
})
# vm.$watch API
vm.$watch( expOrFn, callback, [options] ) (opens new window)
参数:
{string | Function} expOrFn
{Function | Object} callback
{Object} [options]
{boolean} deep 为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做
{boolean} immediate 在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调
返回值:{Function} unwatch
# 源码分析
# vue实例化入口
在vue源码入口文件vue/src/core/index.js (opens new window)中,可以看到import Vue from './instance/index'
,导入了Vue这个对象。
在vue/src/core/instance/index.js (opens new window)中,
import { initMixin } from './init'
import { stateMixin } from './state'
//...
function Vue (options) {
//...
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
// ...
export default Vue
可以看到Vue是一个函数方法,调用该方法时会调用一个叫_init的初始化方法,并传入options参数,同时这个文件还执行了 initMixin 和 stateMixin 方法。
# initMixin 和 _init
在vue/src/core/instance/init.js (opens new window)中,
// ...
import { initState } from './state'
import { extend, mergeOptions, formatComponentName } from '../util/index'
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ...
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...
initState(vm)
// ...
}
}
看到_init方法就是在initMixin方法中定义的,在_init方法中,声明了常量vm并赋值当前实例,接受了options并做了处理,还调用了initState方法。
# initState
在vue/src/core/instance/state.js (opens new window)中,
import {
set,
del,
observe,
defineReactive,
toggleObserving
} from '../observer/index'
export function initState (vm: Component) {
// ...
const opts = vm.$options
// ...
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
vue/src/core/util/env.js (opens new window)
// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch
如果watch存在,并排除火狐浏览器Object对象原生watch方法,则调用initWatch方法。
# 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)
}
}
}
initWatch方法根据传入的watch对象的每一个属性,调用了createWatcher,传入当前实例,属性名,属性值。
回忆一下最开始提到的这种写法:
//...
text4: [
'fun',
function(newVal, oldVal){
console.log('newVal:', newVal)
console.log('oldVal:', oldVal)
},
{
handler: function(newVal, oldVal){
console.log('newVal:', newVal)
console.log('oldVal:', oldVal)
},
deep: true,
immediate: true,
},
]
//...
于是可以理解,如果属性值是数组,则进行遍历,以同一个key不同handler多次调用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)
}
vue/src/shared/util.js (opens new window)
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
回忆一下最开始提到的这种写法:
//...
text1: 'fun',
'text3.val': {
handler: function(newVal, oldVal){
console.log('newVal:', newVal)
console.log('oldVal:', oldVal)
},
deep: true,
immediate: true,
}
//...
如果handler是否为一个Object实例,将handler对象的handler属性的值,作为vm.$watch的第二个参数,原入参handler对象作为第三个参数。
如果handler是一个字符串,则从实例对象上获取这个属性,作为第二个参数。
# stateMixin
export function stateMixin (Vue: Class<Component>) {
//...
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// 如果传进来的第二个参数还是对象,则回到createWatcher
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// 实例化Watcher观察者实例
const watcher = new Watcher(vm, expOrFn, cb, options)
// 如果设置了immediate,直接调用一次回调
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 返回unwatchFn方法,可通过闭包伪删除当前观察者实例
return function unwatchFn () {
watcher.teardown()
}
}
}
# Watcher 构造函数
vue/src/core/observer/watcher.js (opens new window)
回忆一下源码分析vue响应式原理 (opens new window)中, 我们的data对象,有一个__ob__属性,对应一个Observer实例,Observer实例会重写data上的每一个属性,并通过闭包保存每个属性各自的dep数组, 而每一个dep数组,收集了这个属性的所有Watcher观察者实例,而每一个观察者实例各自有一个deps依赖集,反向收集闭包的dep。
理解这个之后,我们再来稍微看一下Watcher
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
}
// _watcher存放观察者实例
vm._watchers.push(this)
// options
if (options) {
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 // 注意:Watcher实例的id是递增的
this.active = true
this.dirty = this.lazy
this.deps = [] // 已添加的依赖数组
this.newDeps = [] // 一个缓存数组,用于保存即将要添加的依赖
this.depIds = new Set() // 已添加的依赖id数组
this.newDepIds = new Set() // 一个缓存数组,用于保存即将要添加的依赖id
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// expOrFn 可能是一个函数,也可能是字符串表达对象上的属性,如”a.b“,需要通过parsePath解析,返回也是一个函数
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
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
)
}
}
// lazy为false执行get初始化
this.value = this.lazy
? undefined
: this.get()
}
// 获取监听属性的值,收集dep依赖
get () {
// 修改Dep.target指向当前Watcher
pushTarget(this)
let value
const vm = this.vm
try {
// 执行getter获取监听的data属性,同时触发该属性对应的dep依赖的depend()方法,通过该方法调用addDep
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// 如果设置了深度监听
if (this.deep) {
traverse(value) // 调用traverse对数组和对象类型进行递归遍历,触发每一个getter
}
// 修改Dep.target为null
popTarget()
this.cleanupDeps()
}
return value
}
// 增加依赖,在dep依赖的depend()方法中调用,
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// 将依赖增加到缓存里
this.newDepIds.add(id)
this.newDeps.push(dep)
// 调用dep依赖的addSub收集当前观察者
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
// 清空缓存依赖
cleanupDeps () {
// 遍历依赖数组
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
// 不在缓存里的dep需要调用dep依赖的removeSub删除当前观察者
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
// 把newDepIds和newDeps设置给depIds和deps并清空缓存
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
}
// 更新
update () {
// lazy为true,设置dirty为true
if (this.lazy) {
this.dirty = true
}
// 同步则执行run
else if (this.sync) {
this.run()
}
// 异步执行queueWatcher推送到观察者队列,最终会通过nextTick调用到run方法
else {
queueWatcher(this)
}
}
// 更新值并执行回调
run () {
// active默认为true
if (this.active) {
const value = this.get()
// 值不等时,或值是数组或对象时,或深度监听时
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 给this.value赋最新的值
const oldValue = this.value
this.value = value
// 执行Watcher的回调
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)
}
}
}
}
// 触发get,将dirty设置为false
evaluate () {
this.value = this.get()
this.dirty = false
}
// 遍历deps执行每个dep依赖的depend方法
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// 伪删除当前Watcher实例
teardown () {
if (this.active) {
// 当_isBeingDestroyed为false,从当前vue实例的观察者实例数组中移除自身
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
// 遍历deps执行每个dep依赖的removeSub,从而移除当前watch
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
// 设置active为false
this.active = false
}
}
}
关于expOrFn的理解,API文档上有这样一个例子可以帮助理解:
// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做点什么
})
// 函数
vm.$watch(
function () {
// 表达式 `this.a + this.b` 每次得出一个不同的结果时
// 处理函数都会被调用。
// 这就像监听一个未被定义的计算属性
return this.a + this.b
},
function (newVal, oldVal) {
// 做点什么
}
)
vue/src/core/util/lang.js (opens new window)
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
vue/src/core/observer/traverse.js (opens new window):
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)
}
}
# queueWatcher
vue/src/core/observer/scheduler.js (opens new window):
const queue: Array<Watcher> = [] // 观察者队列
let has: { [key: number]: ?true } = {} // 用于保存观察者id的对象
let waiting = false // 用于判断上一轮nextTick的清空观察者任务是否执行完毕
let flushing = false // 用于判断是否正在清空观察者队列
let index = 0 // 正在清空的观察者在队列中的下标
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
// 如果不是正在清空队列,直接将新传入的观察者推到队尾
if (!flushing) {
queue.push(watcher)
}
// 如果正在清空队列
else {
let i = queue.length - 1
/** 正在清空的观察者不是队列最后一个,并且最后一个观察者id大于传入的id,
* (注意,在Watcher构造函数部分我们知道观察者实例的id是递增的数字,所以我们可以进行上述比较)
* 那么我们就需要将观察者插入到队列的中间。
* 如果正在清空最后一个观察者,那么效果其实和上面的if是一样的,插入队尾,
* 在下一轮nextTick进行清空。*/
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 上一轮清空已经完毕
if (!waiting) {
waiting = true
// 设置同步则直接调用
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
// 异步则将清空方法传给nextTick
nextTick(flushSchedulerQueue)
}
}
}
flushSchedulerQueue 清空观察者队列
export const MAX_UPDATE_COUNT = 100
const activatedChildren: Array<Component> = []
// 重置状态
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
// 执行清空队列
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
/** 清空观察者队列前做好排序以确保:
* 1. 父组件比子组件先更新,因为父组件比子组件先创建
* 2. user watchers 比 render watcher 先更新
* 3. 如果一个父组件的watcher正在清空,而子组件被销毁,那么子组件的watcher略过
*/
queue.sort((a, b) => a.id - b.id)
// 因为在清空过程中队列仍然可以改变,所有每轮循环动态取队列长度
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
// 在这里从has中清除当前watcher的id
has[id] = null
// 执行watcher的run方法,执行回调
watcher.run()
// in dev build, check and stop circular updates.
// 非生成环境,如果此处has中又有这个watcher id
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
// 记录本次清空队列已对这个watcher执行清空操作的次数
circular[id] = (circular[id] || 0) + 1
// 如果超过设置的最大限制100次,要报警告,提示可能有死循环
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
// 重置状态前保存副本
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// 调用组件的 updated/activated 钩子
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
关于生命周期的部分就不展开了