前言
Computed和Watch区别是什么?
这可能是Vue技术面试最常问到的面试问题之一,我们都知道计算数据是监听数据变化并返回计算函数返回值的,watch就是单纯的数据监听处理。那么二者在源码中究竟又是如何实现的呢?
下面我们就从源码实现层面为同学们详细说一说二者的区别,加深各位同学对计算属性和watch的理解。
读完本篇文章各位同学将会掌握如下几点知识:
- 从源码理解计算属性和watch的实现原理;
- Watcher的调度逻辑;
- Vue源码中的依赖收集过程;
- 渲染Watcher的理解
- Vue实例的初始化流程
阅读本文前需要对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代理方法,因为计算属性watcher默认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的源码分析,码字不易,如果各位同学觉得还不错,有所收获,还望不吝点赞+收藏+关注。
本文章收录在本人的专栏中,我会在专栏Vue的学习乐园中持续输出Vue相关文章,包括但不限于使用技巧、源码解读、面试技巧等,感兴趣的同学可以关注一下。如果有同学有Vue相关的问题,也可以在评论区留言,我会尽快答复。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。