Vue 实现响应式的机制简单来说就是 Object.defineProperty 实现的访问拦截和观察者模式. 其他关键词包括: Observer Dep Watcher 和依赖收集. 这篇文章将会分析 Vue.js 的源码以解释这些概念, 讲解响应式原理, 还会给出一个简单的例子以在 Chrome 开发工具中验证这篇文章的内容.
你可以在 lets-read-vue 中找到注释后的源码以及文末例子的源码.
Vue.js 项目的结构如下:
├── src
│ ├── compiler // template 编译
│ │ ├── codegen
│ │ ├── create-compiler.js
│ │ ├── directives
│ │ ├── error-detector.js
│ │ ├── helpers.js
│ │ ├── index.js
│ │ ├── optimizer.js
│ │ ├── parser
│ │ └── to-function.js
│ ├── core // 所有的核心代码, 重中之重
│ │ ├── components // 主要是 keep-alive 抽象组件
│ │ ├── config.js
│ │ ├── global-api
│ │ ├── index.js
│ │ ├── instance // 主要模块, 实现生命周期, 状态, 事件, 渲染等等
│ │ ├── observer // 响应式核心代码
│ │ ├── util
│ │ └── vdom // Virual DOM
│ ├── platforms
│ │ ├── web
│ │ └── weex
│ ├── server // 服务端渲染相关
│ │ ├── bundle-renderer
│ │ ├── create-basic-renderer.js
│ │ ├── create-renderer.js
│ │ ├── optimizing-compiler
│ │ ├── render-context.js
│ │ ├── render-stream.js
│ │ ├── render.js
│ │ ├── template-renderer
│ │ ├── util.js
│ │ ├── webpack-plugin
│ │ └── write.js
│ ├── sfc
│ │ └── parser.js
│ └── shared
│ ├── constants.js
│ └── util.js
这篇文章相关的代码都在 src/core 底下.
响应式模型
先给出一个 Vue.js 的响应式原理抽象成的模型.
接下来我们深入代码来讲解这个模型.
响应式初始化
当我们通过 new Vue({}) 创建 Vue 实例时, 构造函数会调用 Vue._init 方法, 其中会调用 initState, 而在这个方法会按序初始化 props methods data computed watch, 响应式初始化就发生在这里. 我们会着重讲解 data 和 computed 的初始化过程. computed 依赖 props 或者 data, 所以是订阅者, 想要知道某个被订阅者的变化, 正好构成一个响应式关系!
initData
该方法将 data 变为响应式的, 它做了以下这些事情:
- 从
data函数中获取返回值作为data, 这就是为什么在 Vue 中data应当是一个返回对象的函数 - 检查
data中的属性有没有和props重名的 - 将
data中的属性全部代理到 Vue 实例上以进行访问 - 观察
data对象
function initData(vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
// 遍历 data 中所有的属性
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
// 将 data 中的属性全部代理到 Vue 实例上以进行访问
proxy(vm, `_data`, key)
}
}
// observe data
// 使得 data 变为响应式的, 由于 asRootData 为 true, 可以想象有个 Observer 的 vmCount 会 + 1
observe(data, true /* asRootData */)
}
observe Observer defineReactive Dep
observe 尝试为一个对象创建 Observer, 或者返回已有的 Observer.
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 (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
// 如果作为根数据要加上 vmCount
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Observer 被附加到被观察的对象上, 一旦添加, 就会尝试将该对象的属性全部转化为 get/set 以实现依赖收集和触发更新.
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() // 创建 Dep, 这个 Dep 是对象自己而非它的属性的 Dep
this.vmCount = 0
def(value, '__ob__', this)
// 如果对象是一个数列, 用 Vue 更新后的数组方法实现响应式, 这就是为什么在 Vue 中用数组下标访问无法实现响应式的效果
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
// 如果是一个对象, 就转化 get/set
this.walk(value)
}
}
/**
* 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])
}
}
/**
* Observe a list of Array items.
*
* 如果对象是一个数组, 就 observe 数组中的每一个元素
*/
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Dep 对象是什么? 它是一个用来记录A 对 B 的变化感兴趣数据结构, 其中 B 是某个对象或对象的某个属性, 而 A 是一个 Watcher. 当 A 需要在 B 的数据的变化时收到通知, 就会在 B 的 Dep 中注册自己, 当 B 发现数据更新的时候, 就会通知所有感兴趣的 A. 这就是观察者模式.
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor() {
this.id = uid++
this.subs = []
}
// 将会被某个 Watcher 调用, 修改自己的订阅者数组
addSub(sub: Watcher) {
this.subs.push(sub)
}
removeSub(sub: Watcher) {
remove(this.subs, sub)
}
// 将会被某个 getter 调用, 收集 Dep.target 指向的 Watcher
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()
}
}
}
defineReactive 通过 Object.defineProperty 方法设置了某个属性的 get/set, 并在自己的作用域中创建了一个 Dep 对象. 它可以把某个属性变为响应式的, 原理就是 Object.definePropery 提供的 get 和 set. 当 Watcher 访问这个属性的时候, 首先会把自己标记为依赖收集的目标, 然后触发 get, get 会让自己闭包内保存的
Dep 进行依赖收集. 当这个属性被修改的时候, 会触发 set, set 会通知 Dep 让它去更新所有对它感兴趣的 Watcher.
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
}
// cater for pre-defined getter/setters
// 如果开发者定义的属性原本就有 setter/getter, 要对它们予以保留
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 如果不是浅观察, 而且被观察值是一个对象的话, 就会返回一个 Observer 对象
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) {
// 对该对象的属性也要进行依赖搜集, 因为这个 watcher 很可能就是对这些属性有依赖
// 问题在于: Vue 会为属性的属性的属性实现响应式吗?
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value) // 如果值是数组, 递归进行数组的依赖搜集
}
}
}
return value
},
set: function reactiveSetter(newVal) {
// 如果没有改变, 就不要 set
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
}
// 要观察一下对象, 因为这里的 setter 是整个对象被替换掉了
childOb = !shallow && observe(newVal)
// 通知该属性的 Dep 属性值已经改变, 对应的 Watcher 应该收到通知
dep.notify()
}
})
}
到这里, 被观察的一侧 (Dep) 需要做的工作就做好了.
initComputed
这个函数主要做了如下事情:
- 为每一个计算属性创建 Watcher 对象并添加到
_watchers数组中 - 在 Vue 实例上代理访问计算属性
const computedWatcherOptions = { computed: true }
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]
// computed 可以是一个有 get 和 set 两个函数的对象, 这里找到正确的 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
)
}
if (!isSSR) {
// create internal watcher for the computed property.
// 为计算属性创建 Watcher, 并且在创建的时候特别声明为计算属性而创建
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)) {
// 在 Vue 实例上代理访问计算属性
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)
}
}
}
}
相比于被观察者 Dep, 观察者 Watcher 要复杂得多! 所以我们就不把代码贴在这里了, 请去 lets-read-vue 中查看. 我们这里就讲对响应式来说很重要的几个方法.
// 这个方法用来对 computed 实际求值
get() {
pushTarget(this) // 先将自己设置为依赖搜集的对象
let value
const vm = this.vm
try {
// 这里调用了 getter 实现了依赖收集! 因为 getter 里面必然访问了某个对象的属性, 看 defineReactive
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)
}
popTarget() // 自己依赖搜集完毕, 让出位置
this.cleanupDeps()
}
return value
}
// 这个方法和 Dep 中的方法协作, 将会被一个 Dep 调用, Dep 会把自己传过来
// 更新 Dep 的过程, 是记录这一次更新过程中自己需要的依赖, 与上一次更新的依赖作比较
// 订阅新的依赖, 将不再需要的依赖剔除掉 (通过 cleanupDeps 方法)
addDep(dep: Dep) {
const id = dep.id
// 记录新的依赖
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
// 如果自己没有订阅过这个 Dep, 就订阅
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
// 当 setter 被触发的时候, 就会调用 Dep 的 update, Dep 再来调用 update 方法
update() {
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.
// 如果 Watcher 作为计算属性的 Watcher, 那么它会有两种模式, 当它没有订阅者的时候就是 lazy
// 模式, 仅仅将 Watcher 设置为 dirty, 然后当计算属性被访问的时候, 才会重新计算
// 如果有订阅者的时候, 就是 activated 模式, 立即计算新值, 但只有在值真的发生变化的时候
// 才去通知自己的订阅者
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) {
// 如果是渲染函数指令中的 Watcher 且有 .sync 修饰符, 就立即更新, 以后再讲
this.run()
} else {
// 否则进入更新队列, 之后在讲 Vue 的异步更新策略的时候会讲
queueWatcher(this)
}
}
例子
接下来我们根据一个非常简单的例子来串讲我们之前覆盖的内容, 代码可以在 lets-read-vue 的playground/responsive-demo.html 中找到.
<body>
<div id="app">
</div>
<script src="https://vuejs.org/js/vue.js"></script>
<script>
var vm = new Vue({
data: () => ({
message: 'Wendell'
}),
computed: {
helloMessage() {
return 'Hello ' + this.message
}
},
el: '#app'
})
</script>
</body>
在 Vue 实例初始化的时候, 先处理 data. initData 调用 observe 方法为 data 对象创建 Observer, 然后 Observer 调用自己的 walk 方法, walk 对每一个属性调用 defineReactive 把 message 变成响应式的. 现在 $data 和 message 都有自己的一个 Dep. 然后处理 computed, 为 computed 创建了一个 Watcher 并添加到 _watchers 数组中.
message属性的Dep保存在defineReactive函数调用时构成的闭包内, id 为 3.
helloMessage的Watcher被创建, 但它没有依赖, 因为我们还没对它求值, 因为它也没有触发message的get.
当我们在 console 访问 vm.$data.helloMessage 的时候, Watcher 的 get 将会被调用, 这时候就通过触发 message 的 get 实现依赖收集, message 的 Dep 的 subs 就有了 helloMessage 的
Watcher, 与之对应 helloMessage 的 Watcher 也会记录 message 的 Dep.
再次放上模型以供你温习.
当我们修改 message 的时候, 就会触发 message 的 set, 此时 message 的 Dep 就会去更新依赖, 调用 Watcher 的 update 方法. 而 Watcher 如果属于某个计算属性, 仅仅会把自己设置为脏值, 仅有计算属性重新被访问的时候才会去实际求值 (这一点之前没有讲).
其他
当然了, 实现响应式的方式并不只有 data 到 computed, 还有模板中的表达式, computed 相互的依赖和 watch 等等, 但原理都是如此, 就不再赘述了, 请自己阅读源码吧.





