携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
前言
源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大
侦听属性的书写方式
- 函数形式
watch:{
sum(newVal, oldVal){
...
}
}
- 对象形式
watch:{
sum: {
handle(newVal, oldVal){
...
}
}
}
- 字符串形式
methods:{
add(){...}
},
watch:{
sum: 'add'
}
- 数组形式
watch:{
sum:[{
handle(newVal, oldVal){
...
}
}]
}
侦听属性的初始化
在初始化时,vue针对不同写法做了不同的处理,最后都调用vm.$watch去创建,在官方文档中侦听属性的写法只有函数形式一种,其余写发及参数时vm.$watch的写法,由于侦听属性也是通过vm.$watch初始化的,所以写法可以移植过来
// /src/core/instance/state.js
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)
}
}
}
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就是watcher的实例化,传入不同的属性配置
// /src/core/instance/state.js
Vue.prototype.$watch = function ( //$watch
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 // 是一个用户watcher
// watcher参数分别为vue实例,侦听属性的key,侦听属性对应的函数,自定义配置
const watcher = new Watcher(vm, expOrFn, cb, options)
// 立即执行 这里的pushTarget()是为保证将计算的值存在当前侦听属性的value中
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
// 立即执行handler函数,初始化顺序为props -> methods -> data -> computed -> watch,所以此时watch立即执行handler可以访问到data里的属性值
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}
watcher中侦听属性相关
// /src/core/observer/state.js
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// options
if (options) {
this.user = !!options.user// 是否为侦听属性
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 对类似于'a.b.c'的形式的侦听属性做处理,obj['a']['b']['c']
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
// 非计算属性实例化会默认调用get方法进行取值,计算属性的实例化时候不会去调用get
this.value = this.lazy
? undefined
: this.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 {
// deep深度监听执行
if (this.deep) {
traverse(value)
}
popTarget()
}
return value
}
...
run () {
if (this.active) {
//计算新值
const value = this.get()
// 判断旧值与新值不相同时调用handler函数
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 取旧值
const oldValue = this.value
// 赋新值,目的是下次的旧值是这次的新值
this.value = value
// 如果是用户watcher,则执行用户定义的侦听属性handler函数
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
}
深度监听
如果参数有deep: true会开启深度监听,让该对象或数组下的所有有__ob__属性(有__ob__属性说明已经被vue拦截过)的值记住当前的侦听属性watcher
// /src/core/observer/traverse.js
const seenObjects = new Set()
export function traverse (val: any) {
_traverse(val, seenObjects)
}
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
// 递归获取子值,触发getter,收集依赖,此时的watcher为侦听属性watcher
while (i--) _traverse(val[keys[i]], seen)
}
}
注意点
- 侦听属性有三种书写方式,对象形式、函数形式、字符串形式
- 由于初始化顺序为
prop、methods、data、computed、watch,所以watch的immediate属性,立即执行可以访问到data - 侦听属性依赖于对数据做过劫持,所以一般数据是监听不到的
总结
对于侦听属性,有三种书写方式,在初始化过程中会针对不同的形式处理成统一的形式,如果侦听属性为例如a.b.c的形式,就会对其进行拆分(split+循环访问)组合成可访问形式,初始化时为watcher传入一个user值表示是侦听属性,同时,在初始化过程中会将当前的侦听属性watcher被收集在在侦听对象的dep中,在初次渲染访问时,目标对象dep中就会存在侦听属性watcher和渲染watcher。由于在初始化data或者prop时对其做了劫持。所以当侦听的属性发生变化时,会再次进行计算值,当上次保存的值和本次值不一样时,会触发用户定义的handler函数。此外,侦听属性有两个属性值deep和immediate,表示深度监听和初始化时立即执行,immediate属性在初始化侦听属性时就会立即去执行用户定义的handler函数。deep属性是在初始化侦听属性时,就会递归获取它的子值,触发子值getter,收集当前的侦听属性watcher依赖。
系列链接
【Vue2.x原理剖析一】响应式原理
【Vue2.x原理剖析二】计算属性原理
【Vue2.x原理剖析三】侦听属性原理
【Vue2.x原理剖析四】模板编译原理
【Vue2.x原理剖析五】初始渲染及更新原理
【Vue2.x原理剖析六】diff算法原理
【Vue2.x原理剖析七】组件原理