注:本篇文章需要您先了解vue的响应式原理,入不了解,可以点击此处查看作者的vue的响应式原理相关文章
注:本文代码一律为简略版,只保留当前思路下的主要流程
watch
简单来讲是通过创建一个非组件的watcher
(本文称为watch watcher
)并传入一个回调函数cb
,将watch watcher
与要监听的属性对应的Dep
进行绑定而实现的,当Dep
通知watcher
更新时,就会执行cb
中的内容
下边用一个简单的例子来看下Vue
中到底都走了什么逻辑:
new Vue({
data(){
return {
aaa:1
}
},
watch:{
aaa(){
console.log('我有改动啦~')
}
}
})
无论是上边例子中的调用方式还是Vue.$watch
的方式,watch
的处理最终都会走$watcher
函数。
对该函数将创建watcher
所需的options
中标添加user:true
属性,用来标识是用户创建的watcher
;
如果用户传入了immediate:true
,在$watch
函数中new watcher
后会调用一遍用户传入的函数cb.call(vm, watcher.value)
然后在$watcher中调用new Watcher(vm, expOrFn, cb, options)创建watch watcher
vm: vm
expOrFn: ‘aaa’
cb: fun(){console.log(‘我有改动啦~’)}
options: {user:true,deep:boolean}
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// 如果第二个参数传的是一个对象而不是函数的话,执行createWatcher去处理对象格式,下方有简略版演示
if (isPlainObject(cb)) {
// options中传入的watch在执行$watch前一定会执行过createWatcher函数
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true // 对该函数将创建`watcher`所需的`options`中标添加`user:true`属性
// 核心:创建watch watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
// immediate处理,直接执行函数function cb(){console.log('我有改动啦~')}
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () { // 返回移除watcher方法
watcher.teardown()
}
}
function createWatcher(){
...
// $watch('aaa', fun(){console.log('我有改动啦~')}, {/*deep:true,immediate:true*/})
return vm.$watch(expOrFn, handler, options)
}
在watcher
中,有一步如果是组件watcher
会走getter = expOrFn
,在这里如果检测到expOrFn
是字符串,就会将getter
创建为一个取值函数(下边解释)
然后和正常组件watcher
一样,走get()
在调用get()时,执行顺序为
调用
pushTarget
: 将Dep.target
存入watcher
调用getter
:getter
的作用是返回this[expOrFn]
属性(会找到并返回aaa
属性的值)(因为初始化watch
是在初始化data/computed
之后执行的,所以这时候的this[expOrFn]
已经可以取到data/computed
中的内容了)
调用了getter
,getter会返回"aaa"属性的值,因为"aaa"已经是响应式的了,所以会执行Object.defineProperty -> get
,使this[expOrFn]
的dep
和当前watch watcher
相互收集依赖
调用popTarget
:清除Dep.target
即可完成属性的dep
与监听watcher
的相互绑定,watch
方法就实现了。
在更新时,会调用watcher update
,执行cb
函数
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
// 跳过初始化参数和非必要部分的代码
this.deep = !!options.deep // 深度监听
this.user = !!options.user // 是否是用户传入
this.cb = cb // 回调函数
// if创建组件watcher的流程
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else { // else用于watch中执行的流程
this.getter = parsePath(expOrFn) // 返回一个函数,函数调用后会返回expOrFn字符串对应的属性(走defineProperty的get)
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
)
}
}
this.value = this.get() // get执行结束后,value的值为"aaa"的值
}
get () {
pushTarget(this) // 将该watcher(this)push到dep.target上
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // getter会返回"aaa"属性的值,因为"aaa"已经是响应式的了,所以会执行`Object.defineProperty -> get`,使`this[expOrFn]`的`dep`和当前`watch watcher`相互收集依赖
} catch (e) {
throw e
} finally {
if (this.deep) { // 如果是深度监听
// 对val进行遍历,(因为遍历就会访问其属性 -> get)使得里边所有的响应式属性都收集到该watch watcher
traverse(value)
}
popTarget() // 将watcher pop出dep.target
this.cleanupDeps()
}
return value
}
}
计算属性是创建一个计算属性watcher
,然后让计算属性中用到的属性的dep
同时和组件watcher + 计算属性watcher
绑定,当属性被修改时,将计算属性watcher
标记为脏值,在再次使用计算属性时,重新调用函数获取新的computed
值
还是用一个例子来解释下代码逻辑
new Vue({
data() {
return {
aaa: '123',
}
},
computed:{
bbb:{ // 以对象格式可以更好的说明源码
get () {
return 'bbb的' + this.aaa
},
set(e) {
console.log('set',e)
}
}
}
})
初始化时,计算属性处理的入口函数是initComputed
,在内部首先创建了计算属性watcher
来实现计算属性的功能并将watcher
放入到vm._computedWatchers
对象中(vm._computedWatchers.bbb = 当前watcher实例
),然后调用defineComputed(vm, key, userDef)
将get/set
方法代理到vm
上
function initComputed (vm: Component, computed: Object) {
// 在vm上创建vm._computedWatchers空对象
const watchers = vm._computedWatchers = Object.create(null)
//为每个computed的属性都创建一个内部watcher,并存在vm._computedWatchers里
for (const key in computed) {
// 获取opttion.computed.bbb(可能是函数也可能是对象{get:fun,set:fun})
const userDef = computed[key]
// 获取get函数(fun () { return 'bbb的' + this.aaa })
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 为计算属性创建一个内部watcher,与普通watcher不同的是,options里的lazy为true(computedWatcherOptions里有lazy属性)
watchers[key] = new Watcher(
vm,getter || (() => {}),
() => {},
{ lazy: true }
)
// 处理get/set函数,并将其Object.defineProperty代理到vm上
defineComputed(vm, key, userDef)
}
}
创建计算属性watcher
: new Watcher(vm,getter||noop,noop,computedWatcherOptions)
vm:vm
getter:fun () { return 'bbb的' + this.aaa }
noop:noop = function () {}
computedWatcherOptions:{ lazy: true }
计算属性watcher
中有一个dirty:boolean
属性,用于标识该watcher
的value
是否需要更新,默认为true
(需要更新)
watcher
的get
方会返回value
(getter
方法的返回值,也就是return 'bbb的'+this.aaa
),计算属性不会每次获取时都执行内部的函数,只有在内部使用的属性改变时才会调用获取新的返回值;因此,和其他watcher
不同,计算属性watcher
创建时不会立即执行getter
函数去求值,求值的时机是defineComputed
函数代理的defineProperty -> get
方法(组件中使用this.bbb
)时调用watcher.evaluate()
来执行get
方法获取新的value
(defineProperty -> get
下边会解释)
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
// options:配置项
this.lazy = !!options.lazy // 表示为计算属性
this.cb = cb // 回调函数(计算属性中为空函数【() => {}】)
this.dirty = this.lazy // 该值是否为脏值(用于计算属性)
// 用于存储dep
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.getter = expOrFn
// 注:计算属性中这块不会调用get方法
// 如果是计算属性,则不需要在这里就求值,在外界get这个计算属性时再求值
// 如果非计算属性,调用get方法收集依赖
this.value = this.lazy
? undefined
: this.get()
}
// get在watch中已经说过,此处就不细说了,反正主要目的就是收集依赖+返回value
get() {
pushTarget(this) // 将该watcher(this)push到dep.target上
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 组件watcher在执行getter方法时,会走组件中所有用到的属性的defineProperty.get
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
popTarget() // 将watcher pop出dep.target
}
return value
}
// evaluate: 作用是调用get,绑定依赖并更新value
// 在计算属性被响应式代理get时才会执行该函数
// 执行完get后,会把dirty设置false,表明已经求过值了。
// 当这个watcher的依赖有变化后,会通过执行watcher的update方法(在observer/dep.js里的notify方法里,subs[i].update(),这里subs[i]其实就是watcher),再次把dirty变为true
evaluate() {
this.value = this.get()
this.dirty = false
}
}
初始化时,创建计算属性watcher
,虽然计算属性watcher
不会立即执行getter
方法获取value
,但只要页面中用到了该计算属性(bbb
),那么就会走计算属性的defineProperty -> get
,这时getter
就一定会执行
class Watcher {
constructor () {
...
// 计算属性watcher的lazy肯定为true,不会走this.get()方法
this.value = this.lazy ? undefined : this.get()
}
get() {
...
}
}
<template>
这是页面,我有一个参数是{{bbb}} ←在这里调用了,走了bbb的getter,getter会收集依赖
</template>
defineComputed
用Object.defineProperty
将computed
的方法代理到vm
上,Object.defineProperty -> get
方法中会通过watcher
中的dirty
属性判断computed
返回值是否需要更新,如果需要,就调用watcher.evaluate
更新,最后返回value
值,不需要更新就直接返回value
(此处跳过收集依赖部分内容,下边单独说)
const noop = function () { } // noop是个空函数(在下方会常用到)
const sharedPropertyDefinition = { // Object.defineProperty用到的对象
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
// 为sharedPropertyDefinition赋值get、set
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get ? createComputedGetter(key) : noop
sharedPropertyDefinition.set = userDef.set || noop
}
// 代理
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter(key) {
// 这个函数才是我们在this.bbb时调用执行的getter
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) { // if能在_computedWatchers中找到bbb
// dirty:该值是否为脏值,如果是脏值就需要重新计算,计算是通过watcher.evaluate函数,计算后会把dirty改为false
if (watcher.dirty) {
watcher.evaluate() // evaluate的功能就是调用getter然后dirty=true
}
// 收集依赖 在下边会说
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
上边也说过,计算属性watcher
中有一个dirty
属性,这个值是用来判断该计算属性是否是“脏值”,创建后默认为true
,但是,dirty
属性除了初始化时,还在什么时候变成false
的呢?
在开始之前,我们要先重新认识下处理Dep.target
的两个方法:
Vue
为target
做了一个堆栈的存储:在pushTarget
时,会同时将当前target
放入targetStack
数组中,如果连续两次pushTarget
,那么当第一次调用popTarget
,会将Dep.target
置为 第一次pushTarget
时的target
Dep.target = null
const targetStack = []
function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
/*
* ************ 举例说明 ************
*/
pushTarget(A) // target:A targetStack:[A]
pushTarget(B) // target:B targetStack:[A,B]
popTarget() // target:A targetStack:[A]
让我们按照时间顺序来看一下:
初始化时候生成的计算属性watcher
不会执行get
,也就不会进行依赖收集,
渲染时,首先会创建一个组件watcher
,并放入Dep.target
中,然后调用update、render
(this.getter.call(vm, vm)
)函数,在这个过程中会get
计算属性aaa
,
class Watcher {
constructor () {
...
// 计算属性watcher:此处没有走get,就不会收集依赖
// 组件watcher,走了get并收集依赖
this.value = this.lazy ? undefined : this.get()
}
get() { // 收集依赖并return value
pushTarget(this) // 放入Dep.target中
...
value = this.getter.call(vm, vm) // 渲染函数
// 渲染时目前执行到此处-----------
...
popTarget() // 移除Dep.target中
return value
}
}
get
计算属性aaa
会执行computedGetter
(2.中代理的函数),在内部会调动watcher.evaluate()
,也就是会执行计算属性watcher
的get
class Watcher {
constructor () {...}
evaluate() {
this.value = this.get() // 此处调用了get
this.dirty = false // 将dirty设为false
}
...
}
计算属性watcher
的在get
中,因为执行了pushTarget(this)
,会将计算属性watcher
放入Dep.target
中来顶替组件watcher
,然后下边走了this.getter.call(vm, vm)
(当前getter
为fun() {return 'bbb的' + this.aaa}
)将getter
中用到的属性(this.aaa
)和当前计算属性watcher
相互收集依赖,最后走popTarget()
后,Dep.target
又变成了组件watcher
// 当前执行的为计算属性watcher
get() {
pushTarget(this) // Dep.target:计算属性watcher targetStack:[组件watcher, 计算属性watcher]
...
value = this.getter.call(vm, vm) // fun() {return 'bbb的' + this.aaa}
// this.aaa本身就是响应式属性,他的get中会执行dep.depend方法,将dep和Dep.target中的watcher相互绑定
...
popTarget() // Dep.target:组件watcher targetStack:[组件watcher]
return value
}
// 备注 dep的depend方法
depend () {
if (Dep.target) {
Dep.target.addDep(this) // 调用Dep.target这个watcher中的addDep将当前dep和watcher相互绑定
}
}
到了这里,我们刚刚抛出的疑问已经解决了,计算属性中用到的参数(aaa
)的dep
已经和计算属性watcher
相互收集,当属性aaa
更新时,就会调用watcher
的update
将dirty
置位true
但是还有一个问题,就是计算属性本身是个watcher
,当他内部使用的属性(如当前例子中的aaa
)没有在页面中使用时,aaa
的dep
只会和当前计算属性watcher
进行了绑定,并没有何组件watcher
进行绑定,这时,当aaa
改变导致计算属性bbb
改变,虽然将dirty
置位true
,但组件并没有更新,这要怎么办呢?(答:因为computedGetter
还没有走完…),让我们继续往下看~
让我们再来看一下computedGetter
方法(获取计算属性时执行的Object.defineProperty -> get
方法)
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) { // if能在_computedWatchers中找到bbb
// dirty:该值是否为脏值,如果是脏值就需要重新计算,计算是通过watcher.evaluate函数,计算后会把dirty改为false
if (watcher.dirty) {
watcher.evaluate() // evaluate的功能就是调用getter然后dirty=true
}
// *********************重点***************************
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
还记不记得,这个函数才刚刚执行到watcher.evaluate()
这块,还没有完全执行完毕
当watcher.evaluate()
执行完毕后,代码走到了上边写的"重点"的位置,下一步是判断Dep.target
有没有值
计算属性中执行完popTarget()
后,将计算属性watcher
给pop
出去了,所以当前Dep.target
的值为组件watcher
,所以肯定会进入if
中
// 当前执行的所有和该流程有关的pushTarget/popTarget:
// 创建组件时
popTarget(组件watcher)
// Dep.target:组件watcher targetStack:[组件watcher]
----------------
// 计算属性watcher中 evaluate() -> get()
pushTarget(计算属性watcher)
// Dep.target:计算属性watcher targetStack:[组件watcher, 计算属性watcher]
...
popTarget()
// Dep.target:组件watcher targetStack:[组件watcher]
如果有值,继续执行watcher.depend()
收集依赖,watcher.depend()
中的watcher
指的是计算属性watcher
,因为他在上一步时刚刚执行完getter
,与dep
相互收集完依赖,所以该计算属性watcher
内部包含当前计算属性watcher
所用到的所有dep
,那我们只需要将这些dep
和组件watcher
进行依赖收集,就可以当这些dep
对应的属性改变时,调用组件watcher
进行组件的更新操作啦~
class Watcher {
constructor () {...}
depend () { // 循环执行内部所有dep的depend方法,使得这些dep与Dep.target这个watcher相互收集依赖
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}