Vue源碼解讀(一):響應式數據

NO IMAGE

此文章是基於 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入口文件

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實例的初始化包括一系列的數據初始化,它的目錄結構如下:

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函數中賦值的。

根據代碼,我們也可以得到選項對象初始化的順序:

Vue源碼解讀(一):響應式數據

我們先來說說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中自定義getset函數,並在get中進行依賴收集,在set中派發更新。

其中,depDep的實例,那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
}

從代碼中,我們可以發現:

  1. Dep中分別定義了getset中需要用的dependnotify函數,並且可以發現depend是添加一個訂閱者,notify是用來更新訂閱者的
  2. Dep通過靜態屬性target來控制在同一時間內只有一個觀察者,並且通過pushTarget來給target屬性賦值。
  3. 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的回調函數並且渲染組件。

總結

  1. Vue中數據響應原理主要涉及到下面幾個類:
Vue源碼解讀(一):響應式數據

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

Vue源碼解讀(一):響應式數據

  1. 下圖是幾個類的關聯:
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方法,最後會重新渲染。

參考文獻

Vue 源碼

相關文章

基於React的滾動條方案

基於React實現高度簡潔的Form表單方案

移動端中跳轉支付寶、微信

【stepbystep】使用Vue封裝一個表單校驗