重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
正文
计算属性
计算属性的初始化是在Vue初始化的 initState
方法中:
// ... initState(vm) // ...
具体实现是在 src/core/instance/state.js
里面:
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
可以看到在里面初始化了 props
、methods
、data
,另外也初始化了 computed
和 watch
,这里就看看下 initComputed
的具体实现:
function initComputed (vm: Component, computed: Object) { // $flow-disable-line const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const isSSR = isServerRendering() for (const key in computed) { const userDef = computed[key] 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 ) } if (!isSSR) { // create internal watcher for the computed property. watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } // 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) } } } }
先定义了一个 watchers
和 _computedWatchers
为空对象,然后判断是不是服务端渲染,这里肯定是false,后面遍历 computed
,这个 computed
其实就是我们自定义写的 computed
属性,然后拿到每一个定义的值,它可以是函数也可以是对象,一般我们都会写一个函数,如果定义成对象的话,就必须定义一个 get
属性,相当于拿到 getter
,接着判断 getter
有没有,如果没有就报错,接着实例化一个 Watcher
,对应的值就是 watchers[key]
,这里就可以看出来 computed
里面的定义其实就是通过 watcher
来实现的,传入 watcher
的值分别是vm实例、getter(也就是定义的get)、noop回调函数和一个computedWatcherOptions(定义了一个对象: {computed: true})。接着判断 key
在不在 vm
中,如果不在就走一个 defineComputed
方法,如果在,那肯定在 data
或者 props
定义过,就报错,所以在 computed
里面定义的键值不可以在 data
或者 props
里出现。
来看下这个 defineComputed
方法:
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : userDef sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : userDef.get : noop sharedPropertyDefinition.set = userDef.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 ) } } Object.defineProperty(target, key, sharedPropertyDefinition) }
如果定义的每个计算属性是一个函数,就把对象的 get
和 set
赋值,也就是说当访问我们自定义的 computed
属性的时候,就会执行对应的重新赋值的 get
方法,这里 shouldCache
是true,所以会走 createComputedGetter
方法,如果我们写的是一个对象,就走下面的 else
逻辑,也会走到 createComputedGetter
,因为这个 computed
属性一般是计算而来的,所以一般我们不会用对象的方式去写。
计算属性的依赖收集
接着看下这个 createComputedGetter
做了什么:
function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { watcher.depend() return watcher.evaluate() } } }
单从名字来看,就是定义了在我们访问 computed
属性的时候的 getter
,可以看到每次访问的时候就会执行 computedGetter
,拿到刚才存起来的 _computedWatchers
,走一个 depend
方法,返回执行 watcher.evaluate()
方法。
到这里计算属性的初始化流程就结束了,回到上面说到的访问 computed
的时候会 new 一个 watcher
,它和之前说过的创建渲染watcher有哪些不一样,我们来重新看下 watcher
的构造函数,在 src/core/observer/watcher.js
里,这里只看关键点:
if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } 复制代码
前面传进来的对象 {computed: true}
在这里用到了,作为 computed watcher
,会在当前实例定义一个 value
,并且定义一个 dep
为 new Dep()
,然后就结束了,并不会像以前一样去求值。举个例子:
computed: { sum(){ return this.a + this.b } }
当 render
函数执行访问到 this.sum
的时候,就触发了计算属性的 getter
,它就会拿到计算属性的 watcher
,执行上面提到的 watcher.depend()
:
/** * Depend on this watcher. Only for computed property watchers. */ depend(){ if(this.dep && Dep.target){ this.dep.depend(); } }
这个时候的 Dep.target
不是当前的 computed watcher
,而是 渲染watcher,所以this.dep.depend()
就相当于 渲染 watcher 订阅了这个 computed watcher
的变化(有关的订阅可以看这篇),接着按照上面 computedGetter
的逻辑就会执行 watcher.evaluate()
去求值:
/** * Evaluate and return the value of the watcher. * This only gets called for computed property watchers. */ evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value }
evaluate
里判断 this.dirty
(dirty 就是传入的 computed 布尔值),如果是true,就通过 this.get()
求值,然后把 dirty
设置为false,而在执行 get
的时候,里面有一个 value = this.getter.call(vm, vm)
,也就是执行了计算属性定义的 getter
函数,在上面的例子中就是执行了我们自定义的 return this.a + this.b
,由于 a
和 b
都是响应式对象,所以在执行它们的 getter
的时候,会将自身的 dep
添加到当前正在计算的 watcher
中,此时的 Dep.target
就是 computed watcher
了,到此求值结束。
计算属性的派发更新
一旦对计算属性依赖的数据做了修改,就会触发 setter
过程,然后通知所有订阅它变化的 watcher
进行派发更新,最终执行 watcher.update()
方法(有关派发更新可以看这篇):
/** * Subscriber interface. * Will be called when a dependency changes. */ update () { /* istanbul ignore else */ if (this.computed) { // A computed property watcher has two modes: lazy and activated. // It initializes as lazy by default, and only becomes activated when // it is depended on by at least one subscriber, which is typically // another computed property or a component's render function. if (this.dep.subs.length === 0) { // In lazy mode, we don't want to perform computations until necessary, // so we simply mark the watcher as dirty. The actual computation is // performed just-in-time in this.evaluate() when the computed property // is accessed. this.dirty = true } else { // In activated mode, we want to proactively perform the computation // but only notify our subscribers when the value has indeed changed. this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } }
通过注释可以看出来 computed watcher
有两种模式:lazy
和 activated
。如果 this.dep.subs.length === 0
为true,就说明没有人订阅这个 computed watcher
的变化,就只把 this.dirty
改成true,然后在下次再访问这个计算属性的时候才会去重新求值;否则如果订阅了,就执行:
this.getAndInvoke(() => { this.dep.notify() }) getAndInvoke (cb: Function) { const value = this.get() 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 this.dirty = false if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) } } }
这个在派发更新也分析过:
getAndInvoke
函数也比较简单,先通过get
方法得到一个新值value
,然后让新值和之前的值做对比,如果值不相等,或者新值value
是一个对象,或者它是一个 deep watcher 的话,就执行回调(user watcher 比 渲染watcher 多了一个报错提示而已),注意回调函数cb
执行的时候会把第一个和第二个参数传入新值value
和旧值oldValue
,这就是当我们自定义watcher
的时候可以拿到新值和旧值的原因。
getAndInvoke
会重新计算,对比新旧值,如果变化了就执行回调函数,这里的回调函数就是 this.dep.notify()
,在求值 sum
的情况下,就是触发了 watcher
的重新渲染,对于这个 getAndInvoke
里面的 value !== this.value
其实算是做了一个优化,如果新值和旧值相等的话,就不做什么处理,也不会触发回调,这样就少走了一层渲染。
计算属性小总结
计算属性本质上就是一个 computed watcher
,它在创建的过程中会实例一个 watcher
,访问它的时候走定义的 computedGetter
进行依赖收集,在计算的时候触发依赖的更新,并且当计算属性最终计算的值更新发生变化才会触发 渲染 watcher 的重新渲染。
监听属性
最上面提到,在Vue实例初始化的时候会执行监听属性的初始化,它是在 computed
初始化之后的:
if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) }
看下 initWatcher
的具体实现,它定义在 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) } } }
可以看到对 watcher
做了遍历,拿到每一个 handler
,因为Vue是支持 watcher
的同一个 key
有多个 handler
的,所以如果 handler
是一个数组,就遍历数组并调用 createWatcher
方法,否则直接调用 createWatcher
:
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) }
在 createWatcher
中,对 handler
的类型做了判断,然后拿到它的回调函数,也就是我们自定义的函数,最后调用 vm.$watch
函数,这个 vm.$watch
是一个Vue原型上扩展的方法,它是在执行 stateMixin
的时候被加进去的:
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 const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() } }
换句话说,监听属性最终执行的其实是 $watch
,首先判断了 cb
如果是一个对象,就直接调用 createWatcher
,因为 $watch
是可以直接外部调用的,它可以传一个对象,也可以传一个函数。接着实例化一个 watcher
,注意这个 watcher
是一个 user watcher
,因为是用户直接调用的,而且 options.user = true
。这样通过实例化一个 watcher
的方式,一旦我们自定义的 watcher
发生了变化,最终会执行 watcher
的 run
方法(watcher 里的 update 调用的 run),从而调用我们的回调函数,也就是 cb
,如果 options
上的 immediate
也是true,就直接执行回调函数,最终返回了一个 unwatchFn
方法,调用一个 teardown
来移除这个 watcher
:
/** * Scheduler job interface. * Will be called by the scheduler. */ run () { if (this.active) { this.getAndInvoke(this.cb) } } /** * Remove self from all dependencies' subscriber list. */ teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } }
到底有几种 watcher
在 Watcher
的构造函数里对 options
做了处理,代码如下:
if (options) { this.deep = !!options.deep this.user = !!options.user this.computed = !!options.computed this.sync = !!options.sync // ... } else { this.deep = this.user = this.computed = this.sync = false }
可以看出来一共有四种 watcher
。
deep watcher
这个一般在深度监听的时候会用到,我们一般会这样写:
var vm = new Vue({ data() { a: { b: 1 } }, watch: { a: { handler(newVal) { console.log(newVal) } } } }) vm.a.b = 2
这种情况下 handler
是不会监听到b的变化的,因为此时 watch 的是 a
对象,只触发了 a
的 getter
,并没有触发 a.b
的 getter
,所以没有订阅它,所以在修改 a.b
的时候,虽然触发了 setter
, 但是没有可通知的对象,所以也不会触发 watch 的回调函数。
此时如果想监听到 a.b
,就得这样使用:
watch: { a: { deep: true, handler(newVal) { console.log(newVal) } } }
这样就创建了一个 deep watcher
,在 watcher
执行 get
求值的过程中有这么一段:
try { value = this.getter.call(vm, vm) } catch (e) { //... } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } //... }
在对 value
求值后,如果 this.deep
为true,就执行 traverse(value)
方法,它的定义在 src/core/observer/traverse.js
:
import { _Set as Set, isObject } from '../util/index' import type { SimpleSet } from '../util/index' import VNode from '../vdom/vnode' const seenObjects = new Set() /** * Recursively traverse an object to evoke all converted * getters, so that every nested property inside the object * is collected as a "deep" dependency. */ 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) } }
traverse
其实就是对一个对象做了深度递归遍历,遍历过程其实就是对一个子对象进行 getter
访问,这样就可以收集到依赖,也就是订阅它们的 watcher
,在遍历过程中还会把子响应式对象通过它们的 dep id
记录到 seenObjects
,避免以后重复访问。
在执行 traverse
之后,再对 watch 对象的内部任何一个属性进行监听就都可以调用 watcher
的回调函数。
user watcher
这个在上面有分析到,通过 vm.$watch
创建的 watcher
是一个 user watcher
,其实源码涉及到它的地方就是 get
求值方法和 getAndInvoke
调用回调函数了:
get() { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } }, getAndInvoke() { // ... if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } }
handleError
在 Vue 中是一个错误捕获并且暴露给用户的一个利器。
computed watcher
computed watcher
几乎就是为计算属性量身定制的,上面已经对它做了详细的分析,这里就不再赘述了。
sync watcher
这个在 watch
的 update
方法里:
update () { if (this.computed) { // ... } else if (this.sync) { this.run() } else { queueWatcher(this) } }
当响应式数据发生变化之后,就会触发 watch.update()
,然后把这个 watcher
推到一个队列中,等下一个tick再进行 watcher
的回调函数,如果设置了 sync
,就直接在当前tick中同步执行 watcher
回调函数了。
只有当我们需要 watch 的值的变化到执行 watcher
的回调函数是一个同步过程的时候才会去设置该属性为 true。