Vue2仓库链接:https://github.com/vuejs/vue
本文使用的是 2.6.14 版本的源代码
问题引入我们来看一个具体的例子:
你可以 点击这里 来在线运行这个例子。
1 2 3 4 <div id ="app" > <p > {{sum}}</p > <button @click ="change" > +1</button > </div >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 new Vue ({ el : "#app" , data ( ) { return { count : 1 , } }, methods : { change ( ) { this .count ++ }, }, computed : { sum ( ) { return this .count + 1 }, }, })
我们现在要研究的是:当 this.count
发生了变化,Vue
是如何实现同时变化 sum
的。
看源码之前,我们最先想到的办法就是直接解析计算属性的函数体代码,但是 js 代码的灵活性注定了这样比较麻烦,而且这么做会产生很多的限制。
如果有听过 CMD 模块规范,可以发现里面有一个要求:不能为 require
定义别名,因为 CMD 模块规范主要靠静态解析代码来获得模块的依赖关系的,如果把 require 改名了,就检测不到了。
Vue2 采用了一种独特的依赖收集的方式,具体内容我们慢慢谈。
初始化计算属性我们翻到源码的 src/core/instance/state.js
,找到 initComputed
这个函数。
这个函数是相当长啊。但要注意,Vue2 的源码是非常庞大的,但实际上 80% 以上的部分都是在处理一些边界性的情况,我们在阅读源码的时候,可以忽略这些细枝末节,只看主要的部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 function initComputed (vm : Component , computed : Object ) { const watchers = vm._computedWatchers = Object .create (null ) 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) { watchers[key] = new Watcher ( vm, getter || noop, noop, computedWatcherOptions ) } 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) } } } }
我们先看到 for (const key in computed)
这一段,很容易看出这是遍历所有的计算属性。
中间一个 if
语句里面弹出一个警告,先跳过。
接下来定义了一个 Watcher
:
1 2 3 4 5 6 watchers[key] = new Watcher ( vm, getter || noop, noop, computedWatcherOptions )
后面还有一句 defineComputed
我们也注意一下。然后下面的代码也是用来报错的,暂时不管。服务端SSR
这一块我们也不考虑。
整的来说,这一部分代码我们可以简化一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function initComputed (vm : Component , computed : Object ) { const watchers = vm._computedWatchers = Object .create (null ) for (const key in computed) { const userDef = computed[key] const getter = userDef watchers[key] = new Watcher ( vm, getter || noop, noop, computedWatcherOptions ) defineComputed (vm, key, userDef) } }
现在就很清晰了。所以接下来的核心便是 Watcher
和 defineComputed
。
定义计算属性还是 src/core/instance/state.js
这个文件,我们往下翻到下面这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 export function defineComputed ( target : any, key : string, userDef : Object | Function ) { const shouldCache = !isServerRendering () if (typeof userDef === 'function' ) { sharedPropertyDefinition.get = shouldCache ? createComputedGetter (key) : createGetterInvoker (userDef) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter (key) : createGetterInvoker (userDef.get ) : 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 ) } } Object .defineProperty (target, key, sharedPropertyDefinition) }
首先我们就说了,不考虑服务端渲染,所以默认 shouldCache
为 true
,然后我们假设用户定义就是一个函数(定义是对象的话也是同理的)。
下面又是判断环境的,直接不考虑。所以简化后代码是这样的:
1 2 3 4 5 6 7 8 9 export function defineComputed ( target : any, key : string, userDef : Object | Function ) { sharedPropertyDefinition.get = createComputedGetter (key) sharedPropertyDefinition.set = noop Object .defineProperty (target, key, sharedPropertyDefinition) }
对,就三句话!
接下来我们来看 createComputedGetter
这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 } } }
我们继续删代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 function createComputedGetter (key) { return function computedGetter () { const watcher = this ._computedWatchers [key] if (watcher.dirty ) { watcher.evaluate() } if (Dep .target ) { watcher.depend () } return watcher.value } }
根据函数调用的逻辑,这里的 this
应该是 vm
。
这里的 watch
就是 initComputed
函数里创建的 watcher
对象,而且每个计算属性都有一个属于自己的 Watcher
对象。
所以我们需要关注几个东西:
Watcher
、watcher.dirty
、watcher.evaluate
和 watcher.depend
Dep.target
接下来我们先康康数据是如何定义的。
数据的 getter 和 setter我们翻到源码的 src/core/observer/index.js
,找到 defineReactive
这个函数(约第135行):
注:data 属性的解析流程可以参考 src/core/instance/state.js
里面的 initData
相关代码,最终可发现它的调用到了 defineReactive 这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import Dep from './dep' export function defineReactive ( obj : Object , key : string, val : any, customSetter?: ?Function , shallow?: boolean ) { const dep = new Dep () const property = Object .getOwnPropertyDescriptor (obj, key) if (property && property.configurable === false ) { return } const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments .length === 2 ) { val = obj[key] } let childOb = !shallow && observe (val) Object .defineProperty (obj, key, { enumerable : true , configurable : true , get : function reactiveGetter () { const value = getter ? getter.call (obj) : val if (Dep .target ) { dep.depend () if (childOb) { childOb.dep .depend () if (Array .isArray (value)) { dependArray (value) } } } return value }, set : function reactiveSetter (newVal) { const value = getter ? getter.call (obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env .NODE_ENV !== 'production' && customSetter) { customSetter () } if (getter && !setter) return if (setter) { setter.call (obj, newVal) } else { val = newVal } childOb = !shallow && observe (newVal) dep.notify () } }) }
可能大家并不知道这个函数是干嘛的,我们先来看注释,可以大概看出这是用来在对象上定义一个响应式的数据的。
第一行代码定义了一个 dep
实例,这又是之前的 Dep
对象
中间一堆代码看上去是做一些判断,先跳过。
接下来是 Object.defineProperty
,熟悉响应式原理的朋友应该清楚这是Vue2的响应式原理的核心。我们就从这里开始看吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 Object .defineProperty (obj, key, { enumerable : true , configurable : true , get : function reactiveGetter () { const value = getter ? getter.call (obj) : val if (Dep .target ) { dep.depend () if (childOb) { childOb.dep .depend () if (Array .isArray (value)) { dependArray (value) } } } return value }, set : function reactiveSetter (newVal) { const value = getter ? getter.call (obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env .NODE_ENV !== 'production' && customSetter) { customSetter () } if (getter && !setter) return if (setter) { setter.call (obj, newVal) } else { val = newVal } childOb = !shallow && observe (newVal) dep.notify () } })
getter 那里可能看起来简单一些,大概是先调用了 getter
,然后调用了 dep.depend
。childOb
可能看起来是一个监听子对象的,暂时也忽略吧。
setter
这里又出现了一个 dep.notify
,看来 dep
对象很重要啊。
深入 Watcher在 createComputedGetter
这个函数里面我们看到里面一个函数返回的是 watcher.value
,然后找到当前目录下的 watcher.js
,发现它的构造函数里面有一句:
1 2 3 this .value = this .lazy ? undefined : this .get ()
这说明,watcher.value
这个值大概率是 get
函数的返回值。
但具体是怎么调用和刷新的,咱们先不深究。我们看到 get
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 get () { 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 { if (this .deep ) { traverse (value) } popTarget () this .cleanupDeps () } return value }
我们还是把几行重要的代码留着:
1 2 3 4 5 6 7 8 9 get () { pushTarget (this ) let value const vm = this .vm value = this .getter .call (vm, vm) popTarget () this .cleanupDeps () return value }
可以看到它和 dep
这个库有着很强的交互。
深究 dep我们来找到当前目录下的 dep.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import type Watcher from './watcher' import { remove } from '../util/index' import config from '../config' let uid = 0 export default class Dep { static target : ?Watcher ; id : number; subs : Array <Watcher >; constructor () { this .id = uid++ this .subs = [] } addSub (sub : Watcher ) { this .subs .push (sub) } removeSub (sub : Watcher ) { remove (this .subs , sub) } depend () { if (Dep .target ) { Dep .target .addDep (this ) } } notify () { const subs = this .subs .slice () if (process.env .NODE_ENV !== 'production' && !config.async ) { subs.sort ((a, b ) => a.id - b.id ) } for (let i = 0 , l = subs.length ; i < l; i++) { subs[i].update () } } } 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 ] }
这段代码看起来很简洁,但缺失这套机制的核心。
首先看 pushTarget
和 popTarget
。很明显,这使用了一个栈,表示把当前的 watcher
推入和弹出 Dep.target
。
再来看上面的get
的代码,我们不妨可以看出其整体流程了:
graph LR
PUSH[将当前 watcher 入栈]-->CALL[调用 getter]-->POP[将当前 watcher 出栈] 这也可以看出,Dep.target
表示了当前将要执行的 getter
所在的 watcher
对象。对于计算属性来说,就是在执行计算属性定义的那个函数。在执行前入栈,执行后出栈。
接下来我们回顾 defineReactive
函数,它先创建了一个 Dep
(每个 data 都有一个唯一的 Dep
对象),然后在 getter
里面调用 dep.depend
函数;在 setter
里面则调用 dep.notify
。
先看 depend
函数,就是在目标 watcher 上注册自己。再来看watcher
中的 addDep
这个函数:
1 2 3 4 5 6 7 8 9 10 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)) { dep.addSub (this ) } } }
我们大概知道这个函数主要是在Dep
和Watcher
双方各保留一份对方的引用。
这意味着,在计算属性函数调用的过程中,被访问到的所有 data
都会在内部的 getter
执行 depend
函数,使得计算属性所在的 watcher
知道它依赖了哪些数据属性。
我们再看 notify
代码:
1 2 3 4 5 6 7 notify () { const subs = this .subs .slice () for (let i = 0 , l = subs.length ; i < l; i++) { subs[i].update () } }
这很明朗了,就是通知 watcher
去更新数据。而且 notify
是通过数据的 set
调用的,这意味着当 data
中的数据修改时,就会触发与之相关联的计算属性进行更新。
计算属性的缓存在 defineComputed
函数中我们可以看到这一行:
1 2 3 4 5 6 watchers[key] = new Watcher ( vm, getter || noop, noop, computedWatcherOptions )
我们找到 computedWatcherOptions
,发现:
1 const computedWatcherOptions = { lazy : true }
然后我们来看 watcher
的 update
函数:
1 2 3 4 5 6 7 8 9 10 update () { if (this .lazy ) { this .dirty = true } else if (this .sync ) { this .run () } else { queueWatcher (this ) } }
由于这里 lazy
是 true
,所以,它只做了一件事情:把 this.dirty
设置为 true
我们再来看 createComputedGetter
这个函数的这段代码:
1 2 3 4 5 6 7 8 9 10 if (watcher) { if (watcher.dirty ) { watcher.evaluate() } if (Dep .target ) { watcher.depend () } return watcher.value }
结果发现,如果 watcher.dirty
是 true
,则执行 evaluate
函数,我们来看一下:
1 2 3 4 evaluate () { this .value = this .get () this .dirty = false }
这个函数就是重新执行 get
函数,然后把 dirty
设置为 false
。
这里的逻辑就清晰了。如果 watcher.dirty
是 true
,那么重新执行 getter
,生成新的值;否则,使用原来的值。
小结看完了这么多的代码,我们来小结一下。
计算属性 getter 调用把计算属性对应的 watcher
加入依赖目标栈中,同时设置 Dep.target
执行计算属性的 getter
执行过程中,任何被访问到的 data
,其对应的 dep
和 计算属性 watcher
都会形成响应的依赖 这整个过程称为 依赖收集 。而 Dep.target
表示了被访问的 data
的访问源。
被依赖数据的更新这个就很简单了,它会通过自己的 dep
对象通知所有的 watcher
进行更新。但只有下次访问这个计算属性的时候,才会执行计算属性里面定义的函数。
这个过程称为 依赖更新 。
设计技巧栈的使用:收集了计算属性调用过程中所有 watcher
观察者模式:每一个 Dep
对象通过 notify
函数对所有的 watcher
调用 update
。其中被观察者是被依赖数据,观察者是计算属性。 依赖收集技术:如果一个函数调用内没有异步代码,那么它调用的任何外部的东西都是可以跟踪的。 实现一个简单的依赖收集这里面实现了一个简单的依赖收集,在函数调用过程中,收集其访问到的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 class Watcher { deps = [] addDep (dep) { this .deps .push (dep) dep.addSub (this ) } } class Dep { static target = null static targets = [] subs = [] addSub (watcher) { this .subs .push (watcher) } removeSub (watcher) { this .subs = this .subs .filter (watcher) } depend ( ) { if (Dep .target ) { Dep .target .addDep (this ) } } } Dep .push = (watcher ) => { Dep .targets .push (watcher) Dep .target = watcher } Dep .pop = (watcher ) => { Dep .targets .pop (watcher) Dep .target = Dep .targets [Dep .targets .length - 1 ] } function initData (origin ) { let inited = { __data : origin } for (const key in origin) { let dep = new Dep (); dep.__key = key Object .defineProperty (inited, key, { get () { dep.depend () return inited.__data [key] }, set (val) { inited.__data [key] = val } }) } return inited } function run (fn, ...args ){ if (!fn.watcher ) fn.watcher = new Watcher () Dep .push (fn.watcher ) const result = fn (...args) Dep .pop (fn.watcher ) console .log (fn.watcher .deps ) return result } let data = initData ({ firstName : "123456" , lastName : "hello" , id : 1 , }) function test () { return data.firstName + data.lastName } run (test)