活着,最有意义的事情,就是不遗余力地提升自己的认知,拓展自己的认知边界。
引言
在Vue中,会遇到三种类型的watcher实例,他们分别为
render watcher:用于vue组件渲染watch watcher:被监听的属性值变化后,执行对应的回调函数computed watcher:用于控制计算属性是否需要重新计算
vue组件渲染,计算属性和属性监听都会创建watcher实例,那么
watcher是如何初始化的?- 用户编写的
computed选项,watch选项是如何初始化的? - 不同的
watcher实例有何区别? - 不同的
watcher是如何收集依赖的? - 不同的
watcher是如何实现更新的? - 源码对于我们编写代码有哪些启示?
希望下面的解读,能帮你解读上面的疑惑,旅途愉快!!!
测试案例
<!DOCTYPE html>
<html>
<head>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="demo">
<h1>鞋袜搭配</h1>
<div>{{sock}}</div>
<div @click="change">change</div>
</div>
<script>
// 创建实例
const app = new Vue({
el: '#demo',
data: {
sock: '袜子',
shoes: '帆布鞋',
stillLove: true,
feelHurt: true,
life: {
light: 'can not bear',
chinese: '不能承受的生命之轻',
author: ''
}
},
computed: {
missing: {
get: {
feelLive(){
return this.stillLove || this.feelHurt;
},
cache: false
}
},
dressup(){
return this.sock + ',' + this.shoes
}
},
methods:{
change(){
this.sock.name = '蓝色图案的袜子'
},
changeAuthor(){
this.life.author = '米兰昆德拉'
}
},
watch: {
life: [
{
handler: function(newVal, oldVal){
console.log(newVal)
},
deep: true,
sync: true,
immediate: true,
},
{
handler: function(newVal, oldVal){
console.log(newVal)
}
},
function(newVal, oldVal){
console.log(newVal)
}
]
}
});
</script>
</body>
</html>
源码解析
watcher实例化
构造函数参数
vm: Component,//vue实例
expOrFn: string | Function,//字面意思:表达式或函数,在不同的watcher中有不同的处理逻辑
cb: Function,//回调函数
options?: ?Object,//控制选项
isRenderWatcher?: boolean
isRenderWatcher:是否是render watcher,也就是渲染watcher(用于触发组件渲染,在响应式机制中有详细描述)
处理options
不同类型的watcher的options是不同的(见具体watcher的实例化分析)
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
}
deep:被监听属性的回调函数,响应内部属性的变化。user:标识用户代码,值为true时,出现异常时会有错误提示。lazy:用于computed watcher,值为true时,不会执行run方法。sync:用于watch watcher,值为true时,同步执行run方法,执行更新。before:用于触发beforeUpdate钩子
挂载回调函数
this.cb = cb //回调函数
挂载特定场景下使用的属性
this.id = ++uid // uid for batching 在batch过程中区分不同的watcher
this.active = true //定义watcher的激活状态
this.dirty = this.lazy // for lazy watchers
依赖收集相关属性
在响应式机制章节中,依赖收集阶段的addDep方法中使用这些属性。
this.deps = [] //执行cleanupDeps后,将newDeps赋值给deps
this.newDeps = [] //用于收集更新后当前组件实例依赖的deps
this.depIds = new Set() //执行cleanupDeps后,将newDepIds赋值给depIds
this.newDepIds = new Set() //防止重复收集依赖
处理expOrFn参数
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) //解析path字符串:例如,obj.key.getter
}
this.getter会在后面的get方法中调用。
给watcher的value属性赋值
this.value = this.lazy
? undefined
: this.get()
在响应式机制章节中,对render watcher的get方法触发渲染的过程有详细介绍。
get方法的源码如下:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)//建立watcher实例与dep实例的关联,对于三种watcher都适用
} catch (e) {
//略
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {//仅对watch watcher实例适用
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
pushTarget:将当前的watcher实例赋值给Dep.target;popTarget:将Dep.target的值还原为之前的watcer实例;cleanupDeps:执行到此时,表明当前watcher的依赖收集完毕,需要将newDepIds和newDeps的值分别赋给depIds和deps,并且将newDepIds和newDeps的值清空,等待下一次页面更新时,重新收集依赖;
如何理解watch watcher的deep属性 当watch watcher的options中deep属性为true时,会执行traverse函数,源码如下:
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)
}
}
- 如果
val既不是数组,也不是对象,则直接返回; - 如果
val是冻结的对象,直接返回; - 如果
val是虚拟dom对象,直接返回; - 如果
val被observe了,可以通过对应dep的id属性,防止重复建立watcher与dep之间的关联; - 如果
val是数组,其的元素是对象或数组,则递归地执行_traverse,那么通过val[i]访问时,会触发依赖收集,建立当前watch watcher实例和对应dep实例的关联;如果val的元素是简单值,则不需要建立关联,通过指定的7中方法即可触发val的reactiveGetter; - 如果
val是对象,访问val[keys[i]]时,会建立watch watcher实例与对应dep实例的关联;
上面的描述很抽象,举个例子吧:
data(){
return {
foot: {
haveScar: true,
touched: ''
}
}
},
methods: {
touch(){
this.foot.touched = '疼'
}
},
watch: {
'foot': {
handler(newVal, oldVal){
console.log(newVal)
},
deep: true,
}
}
- 首先,在
initWatch时,会为foot创建一个watch watcher实例,由于deep的值为true,所以会为foot的touched属性对应的dep实例和watch watcher实例建立关联; - 当
touched属性的值变化后,会执行dep.notify,然后通知所有关联的watcher实例去更新; - 因此,当
foot的touched属性变化后,foot的handler也会执行,deep的意义也正在于此;
watcher实例执行更新
通常一个属性的值变化后,会触发对应的reactiveSetter函数,执行dep.notify函数,通知dep.subs中的所有watcher实例执行update。
watcher的update方法
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
- 场景一:
lazy属性为true,将dirty的值恢复为true,表示watcher监听的值变化了,但并不会立即执行,也不会加入到异步更新队列。 - 场景二:
sync属性为true,立即执行watcher实例的run方法。 - 场景三:大部分情况下,会将该
watcher实例加入到异步更新队列中,然后依次执行nextTick,timerFunc,flushCallbacks,flushSchedulerQueue(这些方法将会在后续的异步更新机制章节中详细说明),然后执行watcher的run方法。
watcher的run方法
run () {
if (this.active) {
const value = this.get()//获取watcher的value修改后的值
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) {
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)
}
}
}
}
active:表示watcher实例是否处于激活状态;- 当
watcher的类型是render watcher时,get方法中的this.getter(vm,vm)本质上就是执行updateComponent,返回值永远是undefined,所以不会执行后面的逻辑; - 当
watcher的类型是watch watcher时,根据user属性的设置,有分别的执行回调函数; computed watcher实例不会执行run方法;
computed watchers
computed选项的初始化
computed选项的初始化,发生在beforeCreate钩子之后,created钩子之前。
那么Vue是如何初始化用户编写的coputed代码,请看下面的源码:
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key] //获取用户定义的代码
// 用户编写代码的两种形式:1、函数 2、定义取值器
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {//非服务端渲染
// create internal watcher for the computed property.
// 为每个computed选项创建一个内部watcher,内部watcher是相对于vue实例而言的
// 一个vue实例对应一个render watcher,所以也可以理解为是相对于render watcher而言的
//创建watcher实例,并将key和watchers(computedWatchers)建立映射
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions //对所有的computed watcher是相同的(见第一行代码)
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
computed选项是一个对象,会为每一个key创建一个watcher实例.- 用户编写在
computed中的代码(userDef),就是watcher实例化过程中的expOrFn defineComputed:定义计算属性,详细逻辑(见下文)- 对于所有的
computed watcher,options中仅传入了lazy:true,不会立即调用get方法,value的默认值是undefined
defineComputed:定义计算属性
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering() //客户端渲染:缓存watcher实例
if (typeof userDef === 'function') { //userDef是函数
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else { //userDef是对象,且拥有取值器get方法
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)//定义属性
}
- 通常情况下,用户编写的
userDef是函数,服务端渲染不缓存watcher实例,客户端渲染缓存watcher实例。 - 通过对源码的分析,若想自行控制一个计算属性是否缓存时,将
userDef写成对象形式,并定义cache属性,通过cache的值来决定一个计算属性是否缓存对应的watcher实例。 - 最后,将计算属性挂载到
vm上(可以通过this访问)
createComputedGetter:创建计算属性的getter(缓存)
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {//计算属性变化后,重新计算
watcher.evaluate()
}
if (Dep.target) {//依赖收集
watcher.depend()
}
return watcher.value //返回计算属性的值
}
}
}
_computedWatchers属性上保存了所有计算属性的computed watcher实例。- 初始化阶段,只是将
computedGetter函数赋值给对应key的取值器。computedGetter函数的执行细节,见下文。
createGetterInvoker:创建getter触发器
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
}
}
fn:是用户编写的userDefthis:computedGetter被赋值给计算属性key的取值器,并被挂载到vm实例上,所以此处的this是vm- 通过源码可知,此
computedGetter中并没有使用缓存的computed watcher实例,而是每次访问时都将userDef重新执行一遍,无论计算属性依赖的变量是否变化。
computed watcher的值是何时计算的?
在userDef代码中,打一个断点,下面我们看看执行userDef时的调用堆栈:
- 从userDef开始往前回溯,一直到
Vue._render(可参考响应式机制章节的依赖收集) (匿名):渲染函数,执行过程中访问计算属性computedGetter:计算属性的取值器,从vm的_computedWatchers中取出对应计算属性的watcher实例,初次渲染时,由于watcher实例化时,dirty和lazy属性被赋值为true,所以一定会执行watcher.evaluate()方法。evaluate:在此方法中,做了两件事,执行watcher实例的get方法,将dirty属性赋值为false。get:主要执行了getter方法,这个方法是由构造函数的expOrFn参数决定的,也就是用户编写的userDef。dressup:要执行的userDef,除了会返回计算属性的值,在访问其他属性的时候,还会将computed watcher实例与data中的关联属性对应的dep实例建立关联。
computed watcher的依赖收集
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
- 在执行
watcher.evaluate()方法后,对计算属性进行了重新计算; - 建立
render watcher实例与data中关联属性对应的dep实例之间的关系。(如果模板template中有直接访问data中的属性,那么在data初始化的过程中,将会建立render watcher实例与dep实例之间的关联;如果模板template中没有直接访问,而是通过计算属性间接访问,那么在此补充建立render watcher实例与dep实例之间的关联。建立关联时,会通过dep实例的id属性,防止重复) - 最后,返回计算属性的值;
计算属性值的更新
当计算属性依赖的属性key发生变化后,key对应的dep会通知关联的watcher实例执行update,其中包括computed watcher实例,render watcher实例等。
由于computed watcher实例的lazy属性值为true,因此执行实例方法update时,会将watcher实例的dirty属性置为true,不会调用run方法。
当render watcher实例执行update,run方法后,会重新渲染页面,在执行渲染函数时,会访问计算属性。
由于createComputedGetter方法中为计算属性定义了取值器(computedGetter),所以访问计算属性时,如果computed watcher实例的dirty属性为true,将会调用evaluate方法,重新计算计算属性的值。
watch watcher
在初始化用户编写的watch选项时,依次执行如下函数:
Vue._initinitStateinitWatch
initWatch:初始化watch选项
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key] //从watch选项中取出key对应的处理函数
if (Array.isArray(handler)) {//handler可以是数组
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
- 在初始化阶段,
Vue会为watch选项的每一个key的每一个处理函数执行createWatcher; - 源码告诉我们:可以为
key定义一个或多个handler;
createWatcher:调用$watch
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)
}
- 如果
handler是一个普通对象,意味着我们在写代码时要将处理函数挂载到handler属性上; - 如果
handler是字符串,意味着我们可以将options写在data,methods等选项中; - 最后调用
vue的实例方法$watch;
$watch的逻辑
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {//将handler从cb中解析出来
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true //标识:用户编写
//创建watch watcher实例,会挂载回调函数,并挂载value属性,即expOrFn属性对应的值
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()
}
}
- 用户可以在代码中动态的调用
$watch,也可以通过编写watch选项间接调用$watch; - 只要
cb是普通对象,就需要执行createWatcher函数,从中去解析handler,也就是真正的处理函数; - 在
watcher实例化时,computed watcher和render watcher传入的cb都是noop,也就是什么都不执行,只有watch watcher传入的cb是用户编写的处理函数;由于lazy的值是false,所以会执行watcher的get方法,调用this.getter(vm, vm),其实就是获取被watch的属性expOrFn的值,在此过程中会收集依赖,建立watch watcher实例与dep实例的关联(下面有详细说明)。 - 如果用户编写的
options中的immediate属性是true,则在expOrFn的值第一次变化时,就执行回调函数,回调函数中的oldValue是undefined。 - 执行
invokeWithErrorHandling,出现异常时,在控制台打印info。 - 最后,返回
unwatchFn,用于销毁watcher实例,本质上是将watcher实例从vm._watchers中删除,从dep的subs中删除,解除对watcher实例的引用。(适用场景:动态调用$watch)
依赖收集
在dep.js文件中的depend方法中打一个断点,下面看一下调用堆栈:
Vue.$watch:调用Vue的原型方法$watchWatcher:创建watcher实例get:在watcher实例化过程中,lazy属性为false时,执行get方法(匿名):(注意)此处不是渲染函数,而是在实例化过程中,赋值给getter属性的parsePath函数(下面会展示parsePath的源码)proxyGetter:调用parsePath函数中访问了data选项中的属性,data中的属性通过proxyGetter被代理并挂载到vm上reactiveGetter:data选项中的属性,在initData阶段定义的数据劫持depend:dep实例的方法
parsePath的逻辑
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
}
}
- 在实例化过程中,调用
parsePath,传入了expOrFn(对于watch watcher来说,就是watch选项中被监听的属性),并将返回的函数赋值给watcher实例的getter属性。 - 在
watcher的get方法中,调用了this.getter(vm, vm),第一个vm是绑定函数上下文中的this,第二个vm就是函数的实参,形参是obj,通过segments数组,将递归转换成了循环,最终返回的就是被监听属性对应的值。 - 在执行
obj = obj[segments[i]]代码时,由于访问了data选项中的属性,所以触发了相应属性的取值器,建立watch watcher实例与dep实例的关联。
触发watch wacher的handler
watch watcher实例为什么不能设置lazy为true
如果给watch的options设置lazy为true,那么watch watcher实例的handler将不会执行,被监听的key和handler之间并没有建立关联。
computed watcher之所以可以设置lazy为true,是因为在初始化时,为计算属性定义了computedGetter函数,当页面重新渲染时,执行渲染函数,访问计算属性,computedGetter会执行,因此会获取计算属性的最新结果。
sync为false时的调用堆栈如下:
当sync为false时,会将watch watcher实例添加到异步更新队列中,依次执行了queueWatcher,nextTick,timerFunc,flushCallbacks,flushSchedulerQueue,run等方法,异步更新机制将在后续的文章中详细说明。
sync为true时的调用堆栈如下:
当sync属性为true时,在update方法中,会直接调用run方法,进而执行watch watcher实例的回调函数,也就是用户编写的handler。
render watcher
Initial Render:初次渲染
关于render watcher的详细内容,在响应式机制章节的初始化render watcher阶段中已做详细说明,
Rerender:重新渲染
render watcher与watch watcher的异步更新流程大致相似,区别在于:
watch watcher可以通过sync属性控制同步或异步执行run方法,而render watcher只能是异步执行的;watch watcher是通过执行watcher.get方法获取修改的值,通过回调函数执行用户逻辑,而render watcher是通过watcher.get触发渲染的。
编码小贴士
computed选项
userDef(计算属性的值)是函数:
computed: {
timeToSleep(){
return this.workIsDone && this.messageIsSend
}
}
userDef是对象:
computed: {
missing: {
get(){
return this.stillLove || this.feelHurt;
},
cache: false
},
}
当设置cache属性为false时,computed watcher实例将不会保存在vm._watchers中,只要访问计算属性都将执行一遍userDef。
watch 选项
示例:
data(){
return {
life: {
light: 'can not bear',
chinese: '不能承受的生命之轻',
author: ''
}
}
},
methods: {
changeAuthor(){
this.life.author = '米兰昆德拉'
}
},
watch: {
life: [
{
handler: function(newVal, oldVal){
console.log(newVal)
},
deep: true,
sync: true,
immediate: true,
},
{
handler: function(newVal, oldVal){
console.log(newVal)
}
},
function(newVal, oldVal){
console.log(newVal)
}
]
}
options的三种形式:数组,对象,函数- 当
options是数组时,每一个handler可以是对象,也可以是函数,当handler是对象时,内部需要显示定义handler属性,用来标识回调函数。 - 当配置
deep为true时,被监听的属性的回调函数可以响应内部属性的变化。 - 当配置
immediate为true时,在初始化时就会执行一次回调函数。 - 当配置
sync为true时,watcher实例执行update方法时,会同步执行run方法,进而执行回调函数,而不会进入异步更新队列。
结束语
千山万水何惧怕,拨开云雾见红霞
—— 祝君好梦 ——