此文章是基於 Vue 2 版本
先看個例子:
var vm = new Vue({
props: {
rootProp: Boolean
},
data {
a: '',
b: ''
},
computed: {
rootCompute () {
return ''
}
},
watch: {
rootWatcher (newVal, oldVal) {}
},
method: {}
})
在上面的代碼中,我們創建了一個Vue
實例,並且我們給了它一個選項對象來進行初始化。
下面我們將根據Vue
源碼來說明Vue
是如何實現響應式的。
Vue
項目結構
在github
上將Vue
的源碼clone
到本地,我們可以看到,Vue
的源碼目錄結構主要如下:

Vue
入口文件
Vue
在入口文件是src/core/index.js
,其中簡化的代碼如下:
// src/core/index.js
import Vue from './instance/index'
// ...
export default Vue
從代碼中,我們可以發現,在這個文件中,我們導出了一個Vue
實例。那麼初始化Vue
實例則是在src/core/instance/index.js
文件中完成的。
Vue
實例的初始化
Vue
實例的初始化包括一系列的數據初始化,它的目錄結構如下:

來到index.js
中
// src/core/instance/index.js
import { initMixin } from './init'
function Vue (options) {
// ...
this._init(options)
}
initMixin(Vue)
// ...
export default Vue
從代碼中,我們可以看出Vue
是一個構造函數,創建Vue
實例時,我們會執行_init
函數,那麼 _init
函數到底做了什麼?又定義在哪呢?在說_init
函數之前,我們先來看看initMixin
函數。
initMixin
函數是在創建實例之前就執行多的,我們來看看initMixin
函數。
// src/core/instance/init.js
import { initState } from './state'
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
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 鉤子函數
}
}
從這裡我們可以看出,Vue
構造函數中的_init
函數在這裡有定義, 所以運行new Vue()
時, _init
函數就會進行一系列的初始化,包括Vue
實例的生命週期,事件、數據等
我們的重點在initState(vm)
,裡面實現了props
, methods
,data
,computed
,watch
的初始化操作。我們來看看源碼
// src/core/instance/state.js
import Watcher from '../observer/watcher'
import { pushTarget, popTarget } from '../observer/dep'
import { observe } from '../observer/index'
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)
}
}
代碼中的vm.$options
就是選項對象,它也是在_init
函數中賦值的。
根據代碼,我們也可以得到選項對象初始化的順序:

我們先來說說data
的初始化
初始化Data
熟悉Vue
的朋友都知道,在Vue
組件中,data
被要求是一個函數,所以對於data
的初始化是執行initData(vm)
函數。
// src/core/instance/state.js
import { observe } from '../observer/index'
function initData(vm: Component) {
//...
observe(data, true /* asRootData */)
}
代碼中observe
是在其他地方定義的,它的參數data
是組件中data
函數的返回值,比如上面的例子,此時參數data
就是對象{ a: '', b: '' }
那我們接著來看observe
函數
Observer
(觀察者)
// src/core/observe/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
let ob: Observer | void
ob = new Observer(value)
return ob
}
其中,observe
實例化了Observer
對象,參數為對象{ a: '', b: '' }
。
現在我們來看看Observer
做了什麼
// src/core/observe/index.js
import Dep from './dep'
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
if (Array.isArray(value)) {
// ...
} else {
this.walk(value)
}
}
// 遍歷對象,通過 defineProperty 函數實現雙向綁定
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// ...
}
類Observer
在初始化時保存了傳進來的data
,並且實例化了一個Dep
。
當data
為對象時,調用了walk
函數, 遍歷了對象的每個屬性,並且調用了defineReactive
函數,對每個屬性實現雙向綁定。
下面我們來看看defineReactive
的具體實現
// Define a reactive property on an Object.
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
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,
// 攔截 getter,當取值時會觸發該函數
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
// ...
}
return value
},
// 攔截 setter,當賦值時會觸發該函數
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// ...
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// ...
dep.notify()
}
})
}
defineReactive
函數利用了Object.defineProperty()
對對象屬性進行了重寫,並且每個屬性都有一個Dep
實例
在Object.defineProperty
中自定義get
和set
函數,並在get
中進行依賴收集,在set
中派發更新。
其中,dep
是Dep
的實例,那Dep
到底是什麼呢?
—— Dep
其實是一個訂閱者的管理中心,管理著所有的訂閱者
下面我們來看看Dep
Dep
(訂閱者管理中心)
給data
屬性添加訂閱者有一個前提條件 —— Dep.target
存在,那Dep.target
是什麼呢?
我們來看src/core/observe/dep.js
中關於Dep
的代碼:
// src/core/observe/dep.js
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) {
// 調用 Watcher 的 addDep 函數
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.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
從代碼中,我們可以發現:
- 在
Dep
中分別定義了get
和set
中需要用的depend
和notify
函數,並且可以發現depend
是添加一個訂閱者,notify
是用來更新訂閱者的 Dep
通過靜態屬性target
來控制在同一時間內只有一個觀察者,並且通過pushTarget
來給target
屬性賦值。Dep
中方法主要是對Watcher
隊列進行增加、移除,所以Dep
其實是Watcher
管理中心,管理著所有的Watcher
。
下面我們來看看Watcher
:
Watcher
(訂閱者)
// src/core/observe/watcher.js
import Dep, { pushTarget, popTarget } from './dep'
export default class Watcher {
// ...
id: number
constructor (options) {
// ...
this.id = ++uid
}
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)
}
}
}
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 一般會進入這裡
queueWatcher(this)
}
}
}
// src/core/observe/scheduler.js
let flushing = false
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 判斷 Watcher 是否 push 過
// 因為存在改變了多個數據,多個數據的 Watch 是同一個
if (has[id] == null) {
has[id] = true
if (!flushing) {
// 最初會進入這個條件
queue.push(watcher)
} else {
// 在執行 flushSchedulerQueue 函數時,如果有新的派發更新會進入這裡
// 插入新的 watcher
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 最初會進入這個條件
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
// 將所有 Watcher 統一放入 nextTick 調用
// 因為每次派發更新都會引發渲染
nextTick(flushSchedulerQueue)
}
}
}
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// 根據 id 排序 watch,確保:
// 1. 組件更新從父到子
// 2. 用戶寫的 Watch 先於渲染 Watch
// 3. 如果在父組件 watch run 的時候有組件銷燬了,這個 Watch 可以被跳過
queue.sort((a, b) => a.id - b.id)
// 不緩存隊列長度,因為在遍歷的過程中可能隊列的長度發生變化
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// 執行 beforeUpdate 鉤子函數
watcher.before()
}
id = watcher.id
has[id] = null
// 在這裡執行用戶寫的 Watch 的回調函數並且渲染組件
watcher.run()
// 判斷無限循環
// 比如在 watch 中又重新給對象賦值了,就會出現這個情況
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
// ...
break
}
}
}
在派發更新的全過程中,核心流程就是給對象賦值,觸發set
中的派發更新函數。將所有Watcher
都放入nextTick
中進行更新,nextTick
回調中執行用戶Watch
的回調函數並且渲染組件。
總結
Vue
中數據響應原理主要涉及到下面幾個類:

下圖是類中的一些屬性和方法:

- 下圖是幾個類的關聯:

- 圖中的紅色箭頭是
Watcher
的實例化。在實例化的過程中,會調用方法get
來設置Dep.target
為當前Watcher
實例,並且觸發觀察對象的getter
方法,進行依賴收集。 - 圖中的藍色箭頭是依賴收集的過程。觀察對象的
getter
方法被觸發,經過dep.depend()
、Dep.target.addDep()
和dep.addSub()
等方法,會將當前觀察對象的dep
實例添加到Watcher
實例的deps
中,並且將當前Watcher
實例添加到Dep
的屬性subs
中進行統一管理。 - 圖中的黃色箭頭是派發更新過程。當觀察對象改變後,會調用
dep.notify()
方法,觸發subs
中當前的watcher1
實例的update
方法,最後會重新渲染。