前言
Computed和Watch区别是什么?
这可能是Vue技术面试最常问到的面试问题之一,我们都知道计算数据是监听数据变化并返回计算函数返回值的,watch就是单纯的数据监听处理。那么二者在源码中究竟又是如何实现的呢?
下面我们就从源码实现层面为同学们详细说一说二者的区别,加深各位同学对计算属性和watch的理解。
阅读本文前需要对Vue
的响应式原理有一定的了解,响应式原理的内容不在这篇进行讲解,感兴趣的可以看这篇学透Vue源码~nextTick原理的响应式原理部分。
注意:
- 本文是基于Vue2.6的源码解读
- 本文旨在解析computed和watch的源码实现,同时剧情需要也对Vue源码中watcher调度、依赖收集、vue实例初始化等有介绍,篇幅较长,可能需要各位同学30分钟左右时间详细阅读并思考+理解,相信各位坚持看到最后一定会有所收获。
准备开始
首先我们从Vue的初始化流程看起,我们首先找到Vue2.6
源码中的src/core/instance/index.js
,去查看计算属性的初始化过程,来理解其原理。
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
//为Vue的原型对象挂在一些全局方法,如_init()等方法
initMixin(Vue)
//为Vue的原型对象挂一些处理状态的方法
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
我们看到在index.js
中找到这样一段代码,此处的Vue函数就是我们通过new Vue()
创建Vue实例的构造函数,内部调用了this._init()
。我们全局搜索发现_init()
这个内部方法是在initMixin(Vue)
中添加的,我们直接查看这个函数。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
/* 省略无关代码... */
//对生命周期做初始化操作
initLifecycle(vm)
//事件初始化
initEvents(vm)
initRender(vm)
//调用beforeCreate生命周期函数
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
//初始化处理data/props/computed/watch等
initState(vm)
initProvide(vm) // resolve provide after data/props
//调用created生命周期函数
callHook(vm, 'created')
/* 省略无关代码... */
}
}
我们省略无关代码,发现在initMixin
函数中做了很多的初始化操作,包括生命周期、事件、渲染、状态等。这次我们只关注initSate()
函数中,这个函数中做了对data
、computed
和watch
初始化操作,我们看一下下面的代码。
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
//处组件属性
if (opts.props) initProps(vm, opts.props)
//处理method
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
//处理data选项
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
//我们要看的computed
if (opts.computed) initComputed(vm, opts.computed)
//我们后面要看的watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
因为这次主要是理解计算属性computed
和watch
,因此我们就只看上面代码中的initComputed
和initWatch
函数的实现。因为initWatch
的实现比较简单容易理解,我们把它放到最后🐶,我们就来先看一下computed
的初始化过程。
Computed
创建计算属性Watcher
不多说,直接进入正题,我们一起来看initComputed()
函数都做了啥事。
const computedWatcherOptions = { lazy: true }
//计算属性的初始化函数
function initComputed (vm: Component, computed: Object) {
//在vm实例上创建一个_compiutedWatchers的空对象
const watchers = vm._computedWatchers = Object.create(null)
//遍历computed选项
for (const key in computed) {
const userDef = computed[key]
//获取计算属性对应的getter函数,
const getter = typeof userDef === 'function' ? userDef : userDef.get
//错误处理
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
//为计算属性创建一个Watcher,并保存到组件实例上的计算属性集合watchers中
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
//判断实例上是不是已经又了与计算属性同名的key
if (!(key in vm)) {
//在vm实例上定义计算属性,即定义计算属性对应的key(date或者props中)
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)
}
}
}
}
虽然在上面的代码中我们给出了一些注释,为了加深理解,我们下面详细的说一下initComputed
都做了那些事。
首先此函数在传入的vm
实例还是那个创建一个_computedWatchers
用于保存每个计算属性的Watcher
实例,之后遍历computed
选项中的key
检查我们的定义是否合法,即是否为计算属性的每个key
定义了一个函数作为getter
,或者一个带有get
函数的对象;
然后就是比较关键的一步,也是我们要着重关注一下的,就是我们为每个计算属性创建了一个Watcher
实例,这里我们关注一下已给创建Watcher
实例时的传参。
new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
这里我们忽略无关的细节,不去关心第三个参数,分别说一下第1,2,4个参数的含义,这里说明一下noop
是Vue
的一个内部表示什么都不做的一个函数,即不做操作的意思。首先第一个参数就是当前的vm实例,第二个参数是计算属性对应的getter
函数,最后一个参数就是我们在代码的第一行定义的计算属性的配置,只有一个属性lazy
为true
。
代码的最后我们对每个计算属性做了一个判断,是不是已经在vm
上定义过同名的data
或者props
,定义过的话不做处理,直接执行warn
提醒开发者错误信息,否则执行defineComputed(vm,key,userDef)
函数,这个函数传入的3个参数分别的,vm
实例,计算属性对应的key
,以及开发者定义的计算属性上的值,可能是一个带有get
的对象或者直接是一个函数。那么接下来我们来看defineComputed
函数的实现。
计算属性定义到组件实例
闲话少说,我们先进入代码中去看一下。
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
if (typeof userDef === 'function') {//如果是计算属性对应的key定义为一个函数
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop
} else {//如果是一个计算属性对应的key定义为一个对象
sharedPropertyDefinition.get = userDef.get
? createComputedGetter(key): noop
sharedPropertyDefinition.set = userDef.set || noop
}
//错误提示
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
//在vm上定义一个同名的key,并且为它设置属性定义配置
Object.defineProperty(target, key, sharedPropertyDefinition)
}
上面这段代码的核心就是最后一行,即为计算属性在vm
上定义一个同名的属性,这就是为什么我们能够通过this
的点语法直接在vm
上访问到计算属性的的原因。
这里的关键就是我们在之前定义的sharedPropertyDefinition
,这个结构我们很熟悉,就是定义属性时的一些配置,这里我们关心的是它的get
和set
配置,即获取属性和设置属性的代理方法。
同时我们注意到,我们在此函数中也对sharedPropertyDefinition
的get
和set
分别进行了赋值,这里分了两种情况进行处理,即userDef
是函数和对象这两种情况。
不管是那种情况我们注意到都是通过createComputedGetter
封装返回一个闭包函数,那么我们就看一下这个函数的处理过程。
function createComputedGetter (key) {
//返回了一个函数,作为计算属性在vm上的get
return function computedGetter () {
//获取到当前计算属性对应的Watcher实例
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
//检查数据是否是脏数据,需要更新
if (watcher.dirty) {
watcher.evaluate()//触发函数内响应式数据的依赖收集
}
//渲染Watcher是否存在
if (Dep.target) {
watcher.depend()//会遍历渲染Watcher的this.deps并执行每个dep的depend方法,把渲染Watcher也加入到计算属性内部调用的响应式数据对应的key的依赖中,这样响应式数据更新,不仅会触发计算属性的更新,也会触发渲染函数的更新
}
//最后返回watcher的值
return watcher.value
}
}
}
在第5行获取之前为计算属性创建的Watcher
对象,Watcher
默认dirty
是true
的,会执行watcher.evaluate()
方法。
上面的代码主要是对watcher
的操作,需要各位同学熟系Watcher
的实现,下面给出精简版的Watcher
实现:
class Watcher{
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
){
// options
if (options) {
this.lazy = !!options.lazy
this.before = options.before
} else {
this.lazy = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
this.value = this.lazy
? undefined
: this.get()
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {//排重,不重复添加
//添加当前Watcher到依赖集合中
dep.addSub(this)
}
}
}
cleanupDeps() {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp: any = 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
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
evaluate () {
this.value = this.get()
this.dirty = false
}
get () {
pushTarget(this)//是当前vue实例的Watcher
let value
const vm = this.vm
value = this.getter.call(vm, vm)//这里getter是render函数
popTarget()
//这一步是设置当前计算属性Watcher的this.deps为内部调用的响应式数据的Watcher
this.cleanupDeps()
return value
}
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
const value = this.get()
//watch的情况变更之后直接
this.cb.call(this.vm, value, oldValue)
}
}
下面我们看watcher
的evaluate
都做了什么;
evaluate () {
this.value = this.get()
this.dirty = false
}
我们发现evaluate
方法实际上是调用了watcher
的get
方法。
为了更好的理解上面的get
方法的实现,我们接下来需要了解两方面的知识:
watcher
是如何调度的- 响应式数据的依赖收集是如何进行的
watcher是如何调度的
在watcher
的get
方法中我们遇到了两个全局函数,pushTarget
和popTarget
,要了解这块儿内容我们就需要去了解vue
源码中对watcher
是如何调度的。我们来看一下下面的代码实现。
Dep.target = null
const targetStack = []
//放入栈
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
//退出栈
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
这部分代码Vue
使用一个全局唯一的栈targetStack
来做Watcher
的调度,分别定义了出栈和入栈的函数pushTarget
和popTarget
,我们看到入栈会直接把Dep.target
设置为入栈的Watcher
,出栈函数会把栈顶元素出栈,然后设置新的栈顶元素(Watcher
)为Dep.target
。
总结一句话就是,Dep.taget
永远是targetStack
栈顶的Watcher
对象。
响应式数据的依赖收集是如何进行的
我们都知道vue
的响应式数据处理是使用 Object.defineProperty
实现的,源码中我们通过添加get
代理方法来做依赖收集,代码实现如下。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
//get代理将Dep.target即Watcher对象添加到依赖集合中
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {//Dep.target是栈顶Watcher
dep.depend();//这一步会把栈顶Watcher添加到对应响应式数据key的deps中。
}
return value
}
}
为了方便阅读和理解我们省略了无关代码,如果Dep.target
存在我们就执行dep.depend()
,我们去Dep.js
中看一下Dep
类的这个方法的实现。
//Dep.js
depend () {
if (Dep.target) {
//这一步把当前dep放入到对应的watcher对象中,进行记录,不重复添加;并且在调用dep.addSub添加新的watcher到dep中。
Dep.target.addDep(this)
}
}
我们看到这个方法直接调用了Dep.target
也就是当前栈顶Watcher
的addDep
方法,
看一下Watcher
的addDep
的实现
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
dep.addSub(this)
}
}
在watcher
的addDep
中,把dep
的id
保存到newDepIds
集合中,它是一个Set
集合,把dep
对象保存到newDeps
集合中,它是一个数组,最后调用传入的dep
对象的addSub
方法把当前watcher
保存到dep
中。并且我们会通过对集合中是否已存在对应的dep.id
来避免重复添加当前watcher
到dep
中。
这里可能有的同学会感觉有点绕,可以这样简单理解依赖收集的过程:当代码中调用响应式数据对应的key
时,会触发依赖收集操作,这个操作会把targetStack
栈顶的watcher
作为依赖收集到当前key
的dep
中,同时也会把dep
的id
和对象引用保存到watcher
中,做排重和后续计算属性的用途。
理解watcher.get
下面我们来看get
的实现
get () {
//当前计算属性Watcher入栈
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 {
//完成计算属性Watcher的依赖收集过程后需要将计算属性Watcher出栈
popTarget()
//这一步是设置当前计算属性Watcher的this.deps为内部调用的响应式数据的Watcher
this.cleanupDeps()
}
return value
}
首先调用pusTaget
入栈,然后会调用getter
方法,这个getter
方法就是我们传入的计算属性的计算函数,执行和这个方法会使用响应式数据,因此会触发依赖收集过程,会把当前计算数据watcher
作为依赖收集到响应式数据的dep
中,同时会把响应式数据的dep
保存到内部集合newDepsId
和newDeps
中。
最后在finally
中将当前计算属性出栈,执行cleanupDeps
方法,cleanupDeps
方法是把newDeps
和newDepsId
赋值给watcher
的depsId
和deps
集合属性中。
至此就完成了computedGetter
函数中watcher.evaluate()
的执行。
初始化流程和渲染Watcher
上面我们完成了对evaluate
的理解,我们继续分析computedGetter
的代码。
if (watcher) {
//检查数据是否是脏数据,需要更新
if (watcher.dirty) {
watcher.evaluate()//触发函数内响应式数据的依赖收集
}
//渲染Watcher是否存在
if (Dep.target) {
watcher.depend()//会遍历渲染Watcher的this.deps并执行每个dep的depend方法,把渲染Watcher也加入到计算属性内部调用的响应式数据对应的key的依赖中,这样响应式数据更新,不仅会触发计算属性的更新,也会触发渲染函数的更新
}
//最后返回watcher的值
return watcher.value
}
我们发现这段代码接下来是回去判断Dep.target
是否存在,上面我们说过Dep.target
是什么,它是targetStack
栈顶的watcher
对象。那么什么时候Dep.target
存在,什么时候他不存在呢?
两点知识:
- 渲染
watcher
- 初始化流程
首先我们要了解一个新概念,即渲染watcher
对象,它是vue
实例mounted
阶段创建的Watcher
对象,用于监听模板视图中所有使用到的响应式数据的变,更新视图,调用代码如下:
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
这个操作在一个updateComponent
函数中执行,他的实现是这样的。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
render
函数时通过compile(vm.tempalte)
或者开发者直接在组件实例上定义的render
函数,我们把updateComponent
函数作为watcher
的第二个函数传入到watcher
中,他会被赋值到渲染watcher
的getter
,执行getter
就是执行updateComponent
,进而执行render
函数,执行render
函数就会调用到template
模板中使用到的响应式数据,进而触发依赖收集过程,把当前Dep.target
也就是渲染watcher
收集到依赖集合中,之后我们每次修改响应属性就会出发渲染watcher
的run
方法去执行视图变更了。
以上就是对渲染Watcher
的简单讲解,下面我们还需要知道在Vue
的源代码中计算属性初始化和渲染函数创建的先后顺序,我们通过Vue
源码分析Vue
实例初始化流程来分析。
//src\core\instance\init.ts 重点关注initState和$mount的执行顺序
Vue.prototype._init = function (options?: Record<string, any>) {
//省略无关代码...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // resolve injections before data/props
//状态初始化,包括data、computed、watch
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
//组件模板编译、渲染和挂载等操作
vm.$mount(vm.$options.el)
}
}
//src\platforms\web\runtime\index.ts 这里是$mount的定义
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
//src\core\instance\init.ts 关注渲染函数的创建过程
export function mountComponent(
vm: Component,
el: Element | null | undefined,
hydrating?: boolean
): Component {
//省略无关代码...
vm.$el = el
callHook(vm, 'beforeMount')
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true /* isRenderWatcher */
)
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
通过代码我们知道,源码中会先执行initState()
,后执行vm.$mount()
,前者是vue
实例中data
、computed
、watch
等选项的初始化,后者是会做组件模板编译、渲染Watcher
创建、挂载dom
等操作,因此在创建渲染watcher
时,已经完成了数据的响应式处理和计算属性的处理。
如果我们现在模板中使用了计算属性,即在渲染函数中调用了计算属性的key
,就会触发计算属性的get
方法即上面定义的computeGetter
闭包函数,这时会先根据dirty
情况判断会有两种情况:
- 是脏数据,执行
evaluate()
,就会计算属性watcher
会先入targetStack
栈,执行getter
来得到计算属性watcher
的value
,这时栈顶元素是计算属性watcher
,计算属性函数中所有调用到的响应式数据都会将Dep.target
也就是当前计算属性watcher
作为依赖收集起来,最后会先将当前计算属性watcher
出栈,此时栈顶watcher
即Dep.target
是渲染watcher
; - 不是脏数据,此时栈顶
watcher
即Dep.target
是渲染watcher
。
然后代码会判断Dep.target
的存在,当然它是存在的并且现在它就是渲染watcher
对象,因此接下来会执行计算属性watcher.depend()
,我们来看下depend
方法实现:
//Watcher.js
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
方法实现很简单,遍历watcher
中的deps
集合,然后调用每个dep
的depend
方法。这里的deps
集合我们在上面的依赖收集部分说过,它是当前收集了当前watcher
的响应式数据对应的dep
对象集合,而dep
的depend
方法就是把Dep.target
即targetStack
栈顶元素收集到当前响应式数据的依赖中。
理解了depend
的实现,在这里就是把渲染watcher
作为依赖收集到计算属性watcher
的deps
集合中每个dep
中,即计算属性所依赖的响应式数据的dep
中,这样,我们更新了计算属性的计算函数中调用的响应式数据的时候,不仅会触发计算属性的变更,也会触发渲染watcher
的update
进而更新视图。
优化
计算属性Watcher
是借助lazy
和dirty
属性实现的缓存优化。
首先我们定义Watcher
是默认传入的lazy
参数是true
,即懒加载,使用缓存;而dirt
则是标识当前value
是否过期,dirt
为true
则表示数据已过期,是脏数据,需要更新。
因此对于新创建的watcher
,如果lazy
是true
标识是懒加载的,不会再创建时调用get
获取value
,同时会使得dirty
继承lazy
的值;
如果是懒加载的情况即lazy
为true
,这时dirty
也是true
,即数据不是最新的,需要更新,那么会在调用代理的get
方法是执行watcher.evaluate
方法重新获取数据并赋值给value
,然后设置dirty
为false
;
evaluate () {
this.value = this.get()
this.dirty = false
}
dirty
会在计算属性依赖的key
更新时触发当前计算属性对应的watcher
的update
方法把dirty
设置true
,表示数据不再是最新的了,再次获取的时候需要执行evaluate()
方法更新。
update () {
//懒加载,只标记为脏数据,不立即执行run获取最新的value
if (this.lazy) {
this.dirty = true
} else if (this.sync) {//同步更新watcher直接执行run
this.run()
} else {//异步更新需要加入到异步更新队列
queueWatcher(this)
}
}
我们看update
中的处理,如果是懒加载,设置this.dirty
为true
,即把计算属性标记为脏数据, 不立即执行run
方法做更新操作。这里还有对this.sync
的处理,这是是否为异步更新的处理。关于异步更新的内容不熟悉的同学可以看一下学透Vue源码~nextTick原理中对Vue异步更新的讲解。
举个例子
上面根据vue的源码详细的介绍了计算属性的实现原理,但是可能有的同学可能还是觉得好像有点晦涩难懂,没关系,下面我们通过一个一个例子,分几种情况,详细说一下计算属性整个的工作流程,相信能帮助各位同学更好的理解。
<template>
<div>{{myComputedProp}}</div>
</tempalte>
<script>
export default{
data:{
num1:1,
num2:2
},
computed:{
myComputedProp(){
return this.num1+this.num2;
}
},
mounted(){
console.log(this.myComputedProp)
}
}
</script>
当我们执行上面的代码时,会发生什么呢?
- 计算属性初始化:创建计算属性
watcher
,将计算属性的key的同名属性添加到组件实例上,并定义get
代理方法; $mount
渲染,创建渲染watcher
,执行渲染函数出触发依赖收集:我们注意到我们在模板中使用了计算属性myComputedProp
,即我们在执行渲染函数时会调用此计算属性myComputedProp
,因为此时($mounted
执行时)计算属性已经完成了初始化处理,我们调用此计算属性会触发get
代理方法,因为计算属性watche
r默认dirty
为true
,因此会执行evaluate
方法,evaluate
中会先执行get
然后设置dirty
为false
,get
方法会计算得到计算函数返回的值赋值给watcher.value
,这里watcher.value=3
(num1+num2
),并且收集计算属性watcher
到num1
和num2
的dep
中,同时把num1
和num2
的dep
保存到计算属性Watcher
的deps
中;之后会判断Dep.target
的是否存在,这时它是存在的并且是渲染函数,进而执行计算属性watcher
的depend
方法,这个方法会把渲染watcher
同时添加到num1
和num2
的dep
中;mounted
中打印this.myComputedProp
,此时因为myComputedProp
对应的watcher
的dirty
是false
,并且渲染完成后渲染函数也出栈了,这时Dep.target
为null
了,因此不会执行watcher
的evaluate
和depend
方法,直接返回watcher
的value
值
接下来我们修改一下代码,修改mounted
代码如下:
mounted(){
this.num1=3;
console.log(this.myComputedProp)
}
代码会如何执行呢:
- 同上
- 同上
- 在
mounted
中我们修改了num1
的值,他会触发所有依赖的update
方法,这里就会触发计算属性watcher
和渲染watcher
的update
方法,对于计算属性watcher
,会设置dirty
为true
,我们在下一行代码中打印this.myComputedProp
时,就会执行watcher.evalueate
执行计算属性函数,得到最新的计算属性的值赋值给watcher.value=5
(num1+num2
),并再次设置计算属性watcher
的dirty
为false
;渲染watcher
执行渲染函数更新视图(注意:上面提到过,这里不一定是立即执行视图更新,因为vue
组件视图更新是异步更新的,因此这里会涉及到$nextTick
的实现,不是本章的探讨内容,感兴趣的同学可以擦参考学透Vue源码~nextTick原理)。
再次修改一下代码,修改mounted代码如下:
created(){
this.num1=3;
console.log(this.myComputedProp)
}
mounted(){
console.log(this.myComputedProp)
}
代码会如何执行呢:
- 同上
- 我们在前面数据
vue
初始化流程时了解到,created
的调用是在initState
和$mount
之间进行的,因此这是计算属性完成了初始化,我们修改num1
会触发所有依赖的update
方法,这里就只会触发计算属性watcher
的update
方法,因为这是在$mount
之前,还未创建渲染watcher
。计算属性watcher
会设置dirty
为true
,我们在下一行代码中打印this.myComputedProp
时,就会执行watcher.evalueate
执行计算属性函数,得到最新的计算属性的值赋值给watcher.value=5
(num1+num2
),并再次设置计算属性watcher
的dirty
为false
。 - 同上2
- 同上3
Watch
watch
的实现就简单一些,只需要为要监听的key
新创建一个watcher
,把处理函数作为cb
参数传入即可。
初始化
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
//handler是数组的处理
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
initWatch
函数入参是vm
和选项中的watch
,因为一个watch
监听的key
下可以有多个handler
,所以需要对handler
做是否为数组的判断。
最后就是调用createWatcher
函数处理,下面我们找到Watcher
函数看一下。
function createWatcher (
vm: Component,
expOrFn: string | Function,//传入的监听的key
handler: any,
options?: Object
) {
//handler是对象的处理
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
//handler是字符串的处理
if (typeof handler === 'string') {
handler = vm[handler]
}
//直接调用$watch监听数据
return vm.$watch(expOrFn, handler, options)
}
$watch
$watch
是怎么定义的呢,我们来找一下。
/src/core/instance/index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
//为Vue的原型对象挂在一些全局方法,如_init()等方法
initMixin(Vue)
//$set,$selete,$watch等状态修改原型方法的定义
stateMixin(Vue)
//事件相关
eventsMixin(Vue)
//生命周期相关
lifecycleMixin(Vue)
//渲染
renderMixin(Vue)
export default Vue
在下面的函数中我们找到了$watch
的定义。
/src/core/instance/state.js
export function stateMixin (Vue: Class<Component>) {
//忽略无关代码...
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
//最终是调用new Watcher()创建了一个vm的data中key的监听对象
const watcher = new Watcher(vm, expOrFn, cb, options)
//立即执行一次监听处理函数
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
}
最终是调用new Watcher()
创建了一个vm
的data
中key
的监听对象,这里传入的参数,vm
即watch
监听属性所在的组件对象,expOrFn
为要监听的属性。
最后
通过上面的👆源码分析我们可以知道,计算属性和watch实际上都有监听属性变化的能力。只不过计算属性可以当做Vue实例上的响应式数据使用(通过Object.defineProperty
定义了同名属性),会自动监听计算属性函数调用到的响应式数据的变更,并且会返回计算属性函数的返回值;watcher是显式的为要监听的数据创建一个Watcher监听数据变更,不能作为vue实例上的响应式数据值使用。
以上就是本人对于计算属性Computed
和Watch
的源码分析,码字不易,如果各位同学觉得还不错,有所收获,还望不吝点赞+收藏+关注。