本文根据 Vue 源码 v2.x 进行分析这里只梳理最源码中最主要的部分, 略过非核心的一些部分响应式更新主要涉及到 Watcher,Dep,Observer 这几个主要类
本文主要弄清楚以下几个容易搞混的问题:
Watcher,Dep,Observer 这几个类之间的关系?
Dep 中的 subs 存储的是什么?
Watcher 中的 deps 存储的是什么?
Dep.target 是什么, 该值是何处赋值的?
本文直接从新建 Vue 实例入手, 一步一步揭开 Vue 的响应式原理, 假设有以下简单的 Vue 代码:
var vue = new Vue({
el: "#app",
data: {
counter: 1
},
watch: {
counter: function(val, oldVal) {
console.log('counter changed...')
}
}
})
1. Vue 实例初始化
从 Vue 的生命周期可知, 首先进行 init 初始化操作, 这部分代码在 instance/init.js 中
src / core / instance / init.js 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
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) // 对 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)
}
}
2.initData
Vue 实例为它的每一个 data 都实现了 getter/setter 方法, 这是实现响应式的基础关于 getter/setter 可查看 MDN web docs 简单来说, 就是在取值 this.counter 的时候, 可以自定义一些操作, 再返回 counter 的值; 在修改值 this.counter = 10 的时候, 也可以在设置值的时候自定义一些操作 initData(vm) 的实现在源码中的 instance/state.js
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 的实现
src / core / observer / index.js
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 类
3. Observer
首先看看该类的构造方法,
new Observer(value)
首先执行的是该构造方法作者的注释说了, Observer Class 将每个目标对象的键值 (即 data 中的数据) 转换成 getter/setter 形式, 用于进行依赖收集和通过依赖通知更新
src / core / observer / index.js
/**
* 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 方法, 该方法最重要的有两处
为每个 data 声明一个 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 的初始化:
src / core / instance / state.js 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)
}
}
4.initWatch
这里
initWatch(vm, opts.watch)
对应到我们的例子中如下所示:
initWatch 源码如下:
src / core / instance / state.js
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 原型链上的方法, 源码如下:
core / instance / state.js
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 类
5. Watcher
根据我们的例子, new Watcher(...) 如下图所示:
首先执行 Watcher 类的构造方法, 源码如下所示, 省略了部分代码:
core / observer / watcher.js
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) 的具体实现方法如下:
core / util / lang.js
/**
* 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 类之间的关联关系, 图中还是以我们的例子进行描述:
红色箭头: Watcher 类实例化, 调用 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 类的源码
6. Dep
上图中关联 Observer 和 Watcher 类的是 Dep, 那么 Dep 是什么呢?
Dep 可以比喻为出版社, Watcher 好比读者, Observer 好比东野圭吾相关书籍比如读者 w1 对东野圭吾的白夜行 (我们例子中的 counter) 感兴趣, 读者 w1 一旦买了东野圭吾的书, 那么就会自动在这本书的出版社 (Dep 实例) 里面注册填 w1 信息, 一旦该出版社有了东野圭吾这本书最新消息 (比如优惠折扣) 就会通知 w1
现在看下 Dep 的源码:
core / observer / dep.js 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
7. 总结
到这里, 主要的三个类都研究完了, 现在基本可以回答文章开头的几个问题了
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 实例
8. 扩展
这里看一个计算属性的例子:
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 实例的创建:
core / instance / state.js
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) 更详细信息请查看源码
9. 参考
vue 官方文档
vue 源码
vue 源码解析
来源: https://juejin.im/post/5a734b6cf265da4e70719386