文章首发于github Blog。javascript
本文根据Vue源码v2.x进行分析。这里只梳理最源码中最主要的部分,略过非核心的一些部分。响应式更新主要涉及到Watcher
,Dep
,Observer
这几个主要类。html
本文主要弄清楚如下几个容易搞混的问题:vue
Watcher
,Dep
,Observer
这几个类之间的关系?Dep
中的 subs
存储的是什么?Watcher
中的 deps
存储的是什么?Dep.target
是什么,该值是何处赋值的?本文直接重新建Vue实例入手,一步一步揭开Vue的响应式原理,假设有如下简单的Vue代码:java
var vue = new Vue({
el: "#app",
data: {
counter: 1
},
watch: {
counter: function(val, oldVal) {
console.log('counter changed...')
}
}
})
复制代码
从Vue的生命周期可知,首先进行init
初始化操做,这部分代码在instance/init.js
中。react
src/core/instance/init.js
git
initLifecycle(vm) // vm生命周期相关变量初始化操做
initEvents(vm) // vm事件相关初始化
initRender(vm) // 模板解析相关初始化
callHook(vm, 'beforeCreate') // 调用beforeCreate钩子函数
initInjections(vm) // resolve injections before data/props
initState(vm) // vm状态初始化(重点在这里)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 调用created钩子函数
复制代码
上述源码中的initState(vm)
是要研究的重点,里面实现了props
,methods
,data
,computed
,watch
的初始化操做。这里根据上述例子,重点看data
和watch
,源码位置在instance/state.js
github
src/core/instance/state.js
web
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) // 对vm的data进行初始化,主要是经过Observer设置对应getter/setter方法
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
// 对添加的watch进行初始化
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
复制代码
Vue实例为它的每个data都实现了getter/setter
方法,这是实现响应式的基础。关于getter/setter
可查看MDN web docs。 简单来讲,就是在取值this.counter
的时候,能够自定义一些操做,再返回counter的值;在修改值this.counter = 10
的时候,也能够在设置值的时候自定义一些操做。initData(vm)
的实如今源码中的instance/state.js
。express
src/core/instance/state.js
数组
while (i--) {
...
// 这里将data,props,methods上的数据所有代理到vue实例上
// 使得vm.counter能够直接访问
}
// 这里略过上面的代码,直接看最核心的observe方法
// observe data
observe(data, true /* asRootData */)
复制代码
这里observe()
方法将data变成可观察的,为何说是可观察的?主要是实现了getter/setter
方法,让Watcher
能够观察到该数据的变化。下面看看observe
的实现。
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value) // 重点在这里,响应式的核心所在
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
复制代码
这里只关注new Observer(value)
,这是该方法的核心所在,经过Observer
类将vue的data
变成响应式。 根据咱们的例子,此时入参value
的值是{ counter: 1 }
。 下面就具体看看Observer
类。
首先看看该类的构造方法,new Observer(value)
首先执行的是该构造方法。做者的注释说了,Observer Class将每一个目标对象的键值(即data中的数据)转换成getter/setter
形式,用于进行依赖收集和经过依赖通知更新。
/** * Observer class that are attached to each observed * object. Once attached, the observer converts target * object's property keys into getter/setters that * collect dependencies and dispatches updates. */
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value) // 遍历data对象中{counter : 1, ..} 中的每一个键值(如counter),设置其setter/getter方法。
}
}
...
}
复制代码
这里最核心的就是this.walk(value)
方法,this.observeArray(value)
是对数组数据的处理,实现对应的变异方法,这里先不考虑。
继续看walk()
方法,注释中已说明walk()
作的是遍历data对象中的每一设置的数据,将其转为setter/getter
。
/** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
复制代码
那么最终将对应数据转为getter/setter
的方法就是defineReactive()
方法。从方法命名上也容易知道该方法是定义为可响应的,结合最开始的例子,这里调用就是defineReactive(...)
如图所示:
源码以下:
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
// dep 为当前数据的依赖实例
// dep 维护着一个subs列表,保存依赖与当前数据(此时是当前数据是counter)的观察者(或者叫订阅者)。观察者便是Watcher实例。
const dep = new Dep() ---------------(1)
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
let childOb = !shallow && observe(val)
// 定义getter与setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 这里在获取值以前先进行依赖收集,若是Dep.target有值的话。
if (Dep.target) { -----------------(2)
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// 依赖收集完后返回值
return value
},
...
}
复制代码
先看getter
方法,该方法最重要的有两处。
dep
实例对象,随后dep
就被对应的data给闭包引用了。举例来讲就是每次对counter
取值或修改时,它的dep实例均可以访问到,不会消失。Dep.target
来判断是否收集依赖,仍是普通取值。这里Dep.target
的赋值后面再将,这里先知道有这么一回事。而后再看下setter
方法,源码以下:
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// 这里对数据的值进行修改
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 最重要的是这一步,即经过dep实例通知观察者个人数据更新了
dep.notify()
}
复制代码
到这里基本上Vue实例data的初始化就基本结束,经过下图回顾下initData
的过程:
随后要进行的是watch
的初始化:
export function initState (vm: Component) {
...
if (opts.data) {
initData(vm) // 对vm的data进行初始化,主要是经过Observer设置对应getter/setter方法
}
// initData(vm) 完成后进行 initWatch(..)
...
// 对添加的watch进行初始化
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
复制代码
这里initWatch(vm, opts.watch)
对应到咱们的例子中以下所示:
initWatch
源码以下:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
// handler 是观察对象的回调函数
// 如例子中counter的回调函数
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)
}
}
}
复制代码
createWatcher(vm, key, handler)
是根据入参构建Watcher
实例信息,源码以下:
function createWatcher ( vm: Component, keyOrFn: string | Function, handler: any, options?: Object ) {
// 判断是不是对象,是的话提取对象里面的handler方法
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 判断handler是不是字符串,是的话说明是vm实例上的一个方法
// 经过vm[handler]获取该方法
// 如 handler='sayHello', 那么handler = vm.sayHello
if (typeof handler === 'string') {
handler = vm[handler]
}
// 最后调用vm原型链上的$watch(...)方法建立Watcher实例
return vm.$watch(keyOrFn, handler, options)
}
复制代码
$watch
是定义在Vue原型链上的方法,源码以下:
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
// 建立Watcher实例对象
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
// 该方法返回一个函数的引用,直接调用该函数就会调用watcher对象的teardown()方法,从它注册的列表中(subs)删除本身。
return function unwatchFn () {
watcher.teardown()
}
}
复制代码
通过一系列的封装,这里终于看到了建立Watcher实例对象了。下面将详细讲解Watcher
类。
根据咱们的例子,new Watcher(...)
以下图所示:
首先执行Watcher
类的构造方法,源码以下所示,省略了部分代码:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
this.cb = cb // 保存传入的回调函数
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = [] // 保存观察数据当前的dep实例对象
this.newDeps = [] // 保存观察数据最新的dep实例对象
this.depIds = new Set()
this.newDepIds = new Set()
// parse expression for getter
// 获取观察对象的get方法
// 对于计算属性, expOrFn为函数
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 经过parsePath方法获取观察对象expOrFn的get方法
this.getter = parsePath(expOrFn)
...
}
// 最后经过调用watcher实例的get()方法,
// 该方法是watcher实例关联观察对象的关键之处
this.value = this.lazy
? undefined
: this.get()
}
复制代码
parsePath(expOrFn)
的具体实现方法以下:
/** * Parse simple path. */
const bailRE = /[^\w.$]/ // 匹配不符合包含下划线的任意单词数字组合的字符串
export function parsePath (path: string): any {
// 非法字符串直接返回
if (bailRE.test(path)) {
return
}
// 举例子如 'counter'.split('.') --> ['counter']
const segments = path.split('.')
// 这里返回一个函数给this.getter
// 那么this.getter.call(vm, vm),这里vm就是返回函数的入参obj
// 实际上就是调用vm实例的数据,如 vm.counter,这样就触发了counter的getter方法。
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
复制代码
这里很巧妙的返回了一个方法给this.getter
, 即:
this.getter = function(obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
复制代码
this.getter
将在this.get()
方法内调用,用来获取观察对象的值,并触发它的依赖收集,这里便是获取counter
的值。
Watcher构造方法的最后一步,调用了this.get()
方法,该方法源码以下:
/** * Evaluate the getter, and re-collect dependencies. */
get () {
// 该方法其实是设置Dep.target = this
// 把Dep.target设置为该Watcher实例
// Dep.target是个全局变量,一旦设置了在观察数据中的getter方法就可以使用了
pushTarget(this)
let value
const vm = this.vm
try {
// 调用观察数据的getter方法
// 进行依赖收集和取得观察数据的值
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
// 此时观察数据的依赖已经收集完
// 重置Dep.target=null
popTarget()
// 清除旧的deps
this.cleanupDeps()
}
return value
}
复制代码
关键步骤已经在上面代码中注释了,下面给出一个Observer,Watcher类之间的关联关系,图中仍是以咱们的例子进行描述:
get()
方法,并设置Dep.target
为当前watcher实例,触发观察对象的getter
方法。counter
对象的getter
方法被触发,调用dep.depend()
进行依赖收集并返回counter
的值。依赖收集的结果:1.counter
闭包的dep实例的subs
添加观察它的watcher实例w1;2. w1的deps
中添加观察对象counter
的闭包dep。counter
的值变化后,触发subs
中观察它的w1执行update()
方法,最后其实是调用w1的回调函数cb。Watcher类中的其余相关方法都比较直观这里就直接略过了,详细请看Watcher类的源码。
上图中关联Observer和Watcher类的是Dep,那么Dep是什么呢?
Dep能够比喻为出版社,Watcher比如读者,Observer比如东野圭吾相关书籍。好比读者w1对东野圭吾的白夜行(咱们例子中的counter)感兴趣,读者w1一旦买了东野圭吾的书,那么就会自动在这本书的出版社(Dep实例)里面注册填w1信息,一旦该出版社有了东野圭吾这本书最新消息(好比优惠折扣)就会通知w1。
如今看下Dep的源码:
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
// 保存观察者watcher实例的数组
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 () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
复制代码
Dep类比较简单,对应方法也很是直观,这里最主要的就是维护了保存有观察者实例watcher的一个数组subs
。
到这里,主要的三个类都研究完了,如今基本能够回答文章开头的几个问题了。
Q1:Watcher
,Dep
,Observer
这几个类之间的关系?
A1:Watcher
是观察者观察通过Observer
封装过的数据,Dep
是Watcher
和观察数据间的纽带,主要起到依赖收集和通知更新的做用。
Q2:Dep
中的subs
存储的是什么?
A2: subs
存储的是观察者Watcher实例。
Q3:Watcher
中的deps
存储的是什么?
A3:deps
存储的是观察数据闭包中的dep
实例。
Q4:Dep.target
是什么, 该值是何处赋值的?
A4:Dep.target
是全局变量,保存当前的watcher实例,在new Watcher()
的时候进行赋值,赋值为当前Watcher实例。
这里看一个计算属性的例子:
var vue = new Vue({
el: "#app",
data: {
counter: 1
},
computed: {
result: function() {
return 'The result is :' + this.counter + 1;
}
}
})
复制代码
这里的result
的值是依赖与counter
的值,经过result
更能体现出Vue的响应式计算。计算属性是经过initComputed(vm, opts.computed)
初始化的,跟随源码追踪下去会发现,这里也有Watcher实例的建立:
watchers[key] = new Watcher(
vm, // 当前vue实例
getter || noop, // result对应的方法 function(){ return 'The result is :' + this.counter + 1;}
noop, // noop是定义的一个空方法,这里没有回调函数用noop代替
computedWatcherOptions // { lazy: true }
)
复制代码
示意图以下所示:
这里计算属性result
由于依赖于this.counter
,所以设置一个watcher用来观察result
的值。随后经过definedComputed(vm, key, userDef)
来定义计算属性。在计算获取result
的时候,又会触发this.counter
的getter
方法,这样使得result
的值依赖于this.counter
的值。
最后会为计算属性result
定义它的setter/getter
属性:Object.defineProperty(target, key, sharedPropertyDefinition)
。更详细信息请查看源码。