Vue源碼解析02-數據響應式
開篇之前先了解幾個相關概念
MVC模式
模式簡介
MVC的全稱是Model(模型)-View(視圖)-Controller(控制器)
-
Model:這是數據層,存儲項目所需的數據。Model的作用是返回或者更新數據。在應用中常用於數據庫存儲數據。
-
View:視圖層,用於想用戶顯示數據。View本身不顯示任何數據,而是Controller或者Model讓View顯示數據。
-
Controller:控制層,MVC的核心部分。控制器相當於用戶和系統的鏈接,接收用戶的輸入,處理完成後,要求Model更新數據。
MVP模式
MVP模式將Controller更名爲Presenter,同時改變了通訊方向。全稱是Model(模型)-Presenter(呈現器)-View(視圖)。
-
MVP中的數據通信均是雙向的。
-
Model:數據層。
-
View:視圖層。
-
Presenter:Presenter層。Pressenter作爲View和Model的中間層起到了橋樑的作用。Presenter從Model層獲取數據通過接口發送給View層展示。View層將用戶操作發送給Presenter,藉由Presenter將數據發送給Model進行數據更新。
MVVM模式
MVVP模式將Presenter更名爲View-Model(對應MVC中的C-Controller),基本上於MVP模式一致。但是MVVM採用的是雙向數據綁定,View的變動自動反應到ViewModel上。
-
Model:數據層。
-
View:視圖層。
-
ViewModel:在vue中指的是vue的實例對象,是一個公開公共屬性和命令的抽象view;
-
View中的變化會自動更新到ViewModel,ViewModel的變化也會自動反應到View視圖中。這種自動更新是通過vue中的Observer觀察者實現的。
Vue的雙向數據綁定原理
數據雙向綁定的意思是view的變化能反應到ViewModel中,ViewModel中的變化能同步更新View視圖。
Vue雙向數據綁定的原理是數據劫持+訂閱發佈模式實現
數據劫持
數據劫持指的是在訪問或者修改某個屬性時,通過Object.defineProperty()或者Proxy對象攔截這個行爲,擴展額外的操作和行爲然後返回結果。
- Vue2中使用的是Object.defineProperty(),Vue3中使用的是Proxy對象的方式。
訂閱發佈模式
定義:對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有與該對象產生依賴關係的對象都會接收到通知。
-
優點:耦合性低,便於維護
-
缺點:創建訂閱者可能會消耗一定時間和內存,但是訂閱事件不一定會發生,訂閱者則會一直存在於內存中。
Vue的雙向數據綁定源碼解析
源碼分析入口點
在之前的源碼分析中我們知道,Vue的構造函數中實現了一個_init()方法,這個方法是用來初始化一些選項的:
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
//這裏初始化了Vue傳入的選項
this._init(options)
}
上面的_init(options)方法的實現在src/core/instance/init.js中的initMixin(Vue)中,分析其中的源碼,我們可以根據方法名稱做一下簡單的判斷:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化聲明週期,跟生命週期有關
initLifecycle(vm)
// 初始化事件,實現處理父組件傳遞的監聽事件的監聽器
initEvents(vm)
// 初始化渲染器$slots scopedSlots、_c、$createElement
initRender(vm)
// 調用生命週期鉤子函數
callHook(vm, 'beforeCreate')
// 獲取注入的數據
initInjections(vm) // resolve injections before data/props
// 初始化狀態props、methods、data、computed、watch
initState(vm)
// 提供數據
initProvide(vm) // resolve provide after data/props
// 調用生命週期鉤子函數
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果存在el則執行$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
通過分析initMixin(Vue)我們可以得到一個大概的結果,那就是在Vue初始化的時候我們完成了生命週期、事件、渲染器、狀態的初始化,同時還獲取了祖先組件注入的數據,同時爲後代組件的注入提供了數據。
其中initState(vm)方法則是對數據還有其他東西的一些初始化操作,跳入該方法中查看一下其內容src/core/instance/state.js
- 我們逐行分析一下該方法的實現把:
export function initState (vm: Component) {
// 在當前實例中創建了一個watcher的空數組
vm._watchers = []
// 保存了當前Vue實例的選項options
const opts = vm.$options
// 如果選項中的props存在的化則初始化props
if (opts.props) initProps(vm, opts.props)
// 如果選項中methods存在則初始化方法
if (opts.methods) initMethods(vm, opts.methods)
// 如果選項中的data存在則進行data的初始化操作
// data的處理,響應化處理
if (opts.data) {
// 一般情況下我們初始化Vue實例的時候都會傳入data,所以大部分情況是走這個方法的
// 初始化data
initData(vm)
} else {
// 數據響應化
observe(vm._data = {}, true /* asRootData */)
}
// 初始化computed
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
// 初始化watch
initWatch(vm, opts.watch)
}
}
初始化數據initData
分析上面這段代碼我們可以知道,在initState(vm)中是對數據進行了初始化的(先考慮傳入data的情況),那麼我們繼續順着代碼往下看:
function initData (vm: Component) {
// 取出data
let data = vm.$options.data
// 判斷data是否爲方法,如果是一個方法,則處理完畢後返回
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
//如果data不是一個object則警告
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的key,選項中的props、methods
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)) {
proxy(vm, `_data`, key)
}
}
// observe data
// 數據的響應化,遍歷開始
observe(data, true /* asRootData */)
}
- 上面代碼,我們發現了一個核心的方法:observe(),這個方法曾在initState()中出現過,所以,我們初始化數據的方法歸根結底會落在observe()上面代碼,我們發現了一個核心的方法:observe
observe:返回一個Observer
廢話不多說,直接上代碼:from src/core/observer/index.js
//該方法,接收了vue實例中的data數據,和一個boolean,返回了一個Observer(觀察者)實例
export function observe (value: any, asRootData: ?boolean): Observer | void {
//如果data數據不是一個對象或者是一個虛擬domVNode,直接結束
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 如果當前對象已經存在observer則返回
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 已經存在的Observer則會保存在value.__ob__中
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {// 否則,我們要新創建一個Observer將其返回
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
// 無論結果如何,最終都會返回一個observer
return ob
}
- 通過分析上述代碼我們可以得出一個結論**observe()**的作用就是返回一個Observer的實例。
Observer:判斷data類型做處理
所以接下來我們要看一下Observer的構造方法完成了什麼事情fromsrc/core/observer/index.js
//Observer的構造方法
// 接收傳入的Vue中的data數據
constructor (value: any) {
this.value = value
// 新建了一個Dep的實例,這個Dep是用來做依賴收集的,後面會用到
this.dep = new Dep()
//當前的vmCount數量
this.vmCount = 0
// 給當前data對象定義了一個__ob__屬性
def(value, '__ob__', this)
// 判斷當前對象是否爲數組,如果是數組單獨處理,
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {// 如果不是數組的話
//普通對象則用walk遍歷
this.walk(value)
}
}
walk (obj: Object) {
// 拿出obj(data對象)的key
const keys = Object.keys(obj)
// 對所有的key進行遍歷
for (let i = 0; i < keys.length; i++) {
// 實現數據響應式
// 傳入data對象和當前key,進行響應化處理
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
- 分析Observer的構造函數,我們知道了Observer的核心功能:判斷data對象是不是一個數組,根據判斷進行不同的響應化處理
當data爲對象時
- 當data爲對象時,walk()方法遍歷data的key,執行defineReactive()方法,下面看一下defineReactive方法的實現,在src/core/observer/index.js中
export function defineReactive (
obj: Object,// 接收一個object,就是vue實例的data
key: string,// data的key
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 這裏傳進來的是data對象,所以沒有一個data對象就有一個dep與之對應
const dep = new Dep()
// 查看對象上是否有該屬性,如果沒有則停止執行
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 獲取屬性的getter和setter
// cater for pre-defined getter/setters
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,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 依賴收集,這裏的Dep.target是watcher的實例
if (Dep.target) {
dep.depend()// 追加依賴關係,簡單來說就是將watcher加入到dep中,但是實際操作要複雜一點
// 如果childOb存在,說明該屬性是一個對象
if (childOb) {
// 繼續追加依賴
childOb.dep.depend()
// 如果是數組,繼續處理
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 如果getter存在則調用getter否則返回當前val
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
// 如果setter存在則執行setter執行更新,否則用新值覆蓋老值
if (setter) {
setter.call(obj, newVal)
} else {
// 更新本地的val
val = newVal
}
// 如果新傳入的val是一個數組的化,則遞歸進行響應話處理observe
childOb = !shallow && observe(newVal)
// 通知更新
dep.notify()
}
})
}
-
簡單總結一下defineReactive()
♣ 通過Object.defineProperty方法對data屬性的set和get方法進行數據劫持
♣ 創建Dep實例,每有一個data屬性則有一個dep與之對應
♣ 擴展了data屬性的get方法,將Dep.target靜態屬性中的watcher加入到dep實例中(依賴收集過程)
♣ 擴展了data屬性中的set方法,當數據被更新時,執行dep.notify()方法通知數據更新 -
所以我們順着代碼看一下依賴收集的過程和通知更新的方法
依賴收集
依賴收集的過程通過**dep.depend()**完成,我們來看一下它的實現from src\core\observer\dep.js,由於代碼較多,我們只粘貼關鍵代碼
export default class Dep{
static target: ?Watcher;// 靜態屬性中的watcher實例
id: number;
subs: Array<Watcher>;// 維護了一個watcher數組
depend () {
// 這裏的Dep.target是watcher的實例,
if (Dep.target) {
// 建立和watcher之間的關係,將當前Dep實例加入watcher中
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
// 通知watcher進行數據更新
subs[i].update()
}
}
}
// 將當前watcher實例賦值到Dep.target的靜態屬性上
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
// 將watcher實例賦值到Dep的靜態屬性上
Dep.target = target
}
- 總結dep的作用:維護了一個watcher數組,實現了watcher的增加刪除和通知更新
上面代碼中的Dep.target.addDep(this),是將當前的Dep實例加入到了watcher實例中,這裏有一個細節:理論上是每有一個Data則有一個Dep,當同一個Data多次被調用的時候,只需要創建多個watcher對其進行監聽,然後Dep進行依賴收集,通知watcher更新,所以理論上Dep和watcher是一對多的關係.
但是上面的代碼是將Dep實例添加到了Wtacher中,所以這就形成了多對多的關係.出現這種情況是因爲真正使用的時候,有的時候一個組件
Watcher的實現
這裏的Watcher主要是講Render Watcher,組件實例化的時候會產生一個Watcher的實例,在組件$mount過程中的mountComponent()方法中new Watcher:
這裏只粘貼部分核心代碼
// 定義組件更新函數
// _render()執行可以獲得虛擬dom,VNode
// _update()將虛擬DOM轉換爲真實DOM
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 創建Watcher實例
//vm:當前vue實例
//updateComponent:組件更新函數
// noop:
// {}:回調函數
//true:是否是瀏覽器的watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
上面的updateComponent()函數調用了Vue._render函數,最終會調用data屬性的get函數,最終完成依賴收集。
- Watcher對象的實現:
只粘貼部分核心代碼
constructor(){
// watcher創建的時候會執行當前watcher實例的get函數,這樣會出發依賴收集的過程
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
//將當前watcher實例賦值到Dep.target靜態屬性中
pushTarget(this)
let value
// 當前vue實例
const vm = this.vm
try {
// getter函數是上面的updateComponent()函數,會觸發依賴收集過程
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靜態屬性中的watcher實例
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
//依賴收集
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// 維護了一個depid映射關係
this.newDepIds.add(id)
this.newDeps.push(dep)
// 如果當前dep裏面沒有watcher,則將該watcher加入到dep中建立聯繫
if (!this.depIds.has(id)) {
// dep中維護了一個watcher的數組
// 將當前watcher加入到dep中的watcher數組中,實現dep對watcher的收集
dep.addSub(this)
}
}
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
// 實現數據更新
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 將當前watcher實例push到更新隊列中實現數據的更新
queueWatcher(this)
}
}
- 總結watcher的作用:解析傳入的updateComponent更新函數並進行依賴收集。每個組件都會有一個Watcher與之對應,數值變化會觸發更新函數進行重新渲染。
當data爲數組時,進行數組響應化處理
根據Observer的構造方法得知,當data爲數組時
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 給當前對象定義一個__ob__屬性
def(value, '__ob__', this)
// 判斷當前data對象是否爲數組
if (Array.isArray(value)) {
if (hasProto) {
// 覆蓋數組的原型方法
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 如果是數組則進行數組響應化的處理
this.observeArray(value)
} else {
//普通對象則用walk遍歷
this.walk(value)
}
}
// 數組響應化處理
observeArray (items: Array<any>) {
//遍歷數組
for (let i = 0, l = items.length; i < l; i++) {
// 取出數組的每一項進行響應化處理
observe(items[i])
}
}
上述代碼的核心功能主要是,protoAugment()方法擴展了當前data數組的原型方法,arrayMethods
直接上核心代碼
//src/core/observer/array.js:
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
// 取出數組的原型方法
const original = arrayProto[method]
// 攔截,添加額外行爲
// arrayMethods:數組的原型對象,定義特殊方法
def(arrayMethods, method, function mutator (...args) {
// 執行原先的任務
const result = original.apply(this, args)
// 額外任務:通知更新
// 從this.__ob__中取出觀察者
const ob = this.__ob__
let inserted
// 以下三個操作需要額外處理
// 如果是新添加的元素,還需要額外的做響應化的處理
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果inserted存在說明元素是新添加的,額外響應化的處理
if (inserted) ob.observeArray(inserted)
// notify change
// 核心:添加通知更新方法,每一個ob中都有一個dep和這個對象或者數組對應
ob.dep.notify()
return result
})
})
- 總結一下數組的響應化實現:通過擴展了數組原型的七個方法,實現了數組每一項的響應化,從而實現數組的響應化。
♣ 注意,通過上面的代碼我們可以看出,只有通過擴展的這七個方法才能實現數組的響應化:pop、push、shift、unshift、splice、sort、reverse
總結
因爲是自己邊分析源碼邊寫的一些東西,所以可能有點亂。爲了捋清思路做了張圖片,聊勝於無吧:
源碼加載運行流程
下面附上一張官方數據響應化的工作流程圖:
圖片來源:https://vuejs.org/v2/guide/reactivity.html
響應式的基本機制:
- 通過Object.defineProperty()進行數據劫持,擴展對象屬性的set和get方法
- watcher執行getter方法觸發對象屬性的get方法進行依賴收集
- 輸入寫入時觸發對象屬性的set方法,dep發佈通知,watcher進行數據更新
附上一張我理解的數據響應化流程圖:
以上,根據自己對源碼的理解,和網上一些大神的分析整理出來的,如有不對的地方,歡迎各位大神指正。