詳解Vue資料驅動原理
前言
Vue區別於傳統的JS庫,例如JQuery,其中一個最大的特點就是不用手動去操作DOM,只需要對資料進行變更之後,檢視也會隨之更新。 比如你想修改div#app裡的內容:
/// JQuery <div id="app"></div> <script> $('#app').text('lxb') </script>
<template> <div id="app">{{ message }}</div> <button @click="change">點選修改message</button> </template> <script> export default { data () { return { message: 'lxb' } },methods: { change () { this.message = 'lxb1' // 觸發檢視更新 } } } </script>
在程式碼層面上的最大區別就是,JQuery直接對DOM進行了操作,而Vue則對資料進行了操作,接下來我們通過分析原始碼來進一步分析,Vue是如何做到資料驅動的,而資料驅動主要分成兩個部分依賴收集和派發更新。
資料驅動
// _init方法中 initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm,'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) // 重點分析 initProvide(vm) // resolve provide after data/props callHook(vm,'created')
在Vue初始化會執行_init方法,並呼叫initState方法. initState相關程式碼在src/core/instance/state.js下
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm,opts.props) // 初始化Props if (opts.methods) initMethods(vm,opts.methods) // 初始化方法 if (opts.data) { initData(vm) // 初始化data } else { observe(vm._data = {},true /* asRootData */) } if (opts.computed) initComputed(vm,opts.computed) // 初始化computed if (opts.watch && opts.watch !== nativeWatch) { // 初始化watch initWatch(vm,opts.watch) } }
我們具體看看initData是如何定義的。
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' // 把data掛載到了vm._data上 ? getData(data,vm) // 執行 data.call(vm) : data || {} if (!isPlainObject(data)) { data = {} // 這也是為什麼 data函式需要返回一個object不然就會報這個警告 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 const keys = Object.keys(data) // 取到data中所有的key值所組成的陣列 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)) { // 避免方法名與data的key重複 warn( `Method "${key}" has already been defined as a data property.`,vm ) } } if (props && hasOwn(props,key)) { // 避免props的key與data的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 */) // 響應式處理 }
其中有兩個重要的函式分別是proxy跟observe,在往下閱讀之前,如果還有不明白Object.defineProperty作用的同學,可以點選這裡進行了解,依賴收集跟派發更新都需要依靠這個函式進行實現。
proxy
proxy分別傳入vm,'_data',data中的key值,定義如下:
const sharedPropertyDefinition = { enumerable: true,configurable: true,get: noop,set: noop } export function proxy (target: Object,sourceKey: string,key: string) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target,key,sharedPropertyDefinition) }
proxy函式的邏輯很簡單,就是對vm._data上的資料進行代理,vm._data上儲存的就是data資料。通過代理的之後我們就可以直接通過this.xxx訪問到data上的資料,實際上訪問的就是this._data.xxx。
observe
oberse定義在src/core/oberse/index.js下,關於資料驅動的檔案都存放在src/core/observe這個目錄中:
export function observe (value: any,asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { // 判斷是否是物件或者是VNode return } let ob: Observer | void // 是否擁有__ob__屬性 有的話證明已經監聽過了,直接返回該屬性 if (hasOwn(value,'__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && // 能否被觀察 !isServerRendering() && // 是否是服務端渲染 (Array.isArray(value) || isPlainObject(value)) && // 是否是陣列、物件、能否被擴充套件、是否是Vue函式 Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) // 對value進行觀察 } if (asRootData && ob) { ob.vmCount++ } return ob }
observe函式會對傳入的value進行判斷,在我們初始化過程會走到new Observer(value),其他情況可以看上面的註釋。
Observer類
export class Observer { value: any; // 觀察的資料 dep: 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 // 把__ob__變成不可列舉的,因為沒有必要改變watcher本身 def(value,'__ob__',this) 會執行 value._ob_ = this(watcher例項)操作 if (Array.isArray(value)) { // 當value是陣列 if (hasProto) { protoAugment(value,arrayMethods) // 重寫Array.prototype的相關方法 } else { copyAugment(value,arrayMethods,arrayKeys) // 重寫Array.prototype的相關方法 } this.observeArray(value) } else { this.walk(value) // 當value為物件 } } /** * Walk through all properties 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. */ observeArray (items: Array<any>) { for (let i = 0,l = items.length; i < l; i++) { observe(items[i]) // 遍歷value陣列的每一項並呼叫observe函式,進行響應式處理 } } }
Observe類要做的事情通過檢視原始碼也是清晰明瞭,對資料進行響應式處理,並對陣列的原型方法進行重寫!defineReactive函式就是實現依賴收集和派發更新的核心函數了,實現程式碼如下。
依賴收集
defineReactive
export function defineReactive ( obj: Object,// data資料 key: string,// data中對應的key值 val: any,// 給data[key] 賦值 可選 customSetter?: ?Function,// 自定義setter 可選 shallow?: boolean // 是否對data[key]為物件的值進行observe遞迴 可選 ) { const dep = new Dep() // Dep例項 **每一個key對應一個Dep例項** 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 if ((!getter || setter) && arguments.length === 2) { // 沒有getter或者有setter,並且傳入的引數有兩個 val = obj[key] } let childOb = !shallow && observe(val) // 根據shallow,遞迴遍歷val物件,相當於val當做data傳入 Object.defineProperty(obj,{ enumerable: true,get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { // 當前的全部的Watcher例項 dep.depend() // 把當前的Dep.target加入到dep.subs陣列中 if (childOb) { // 如果val是物件, childOb.dep.depend() // 會在value._ob_的dep.subs陣列中加入Dep.target,忘記ob例項屬性的同學可往回翻一番 if (Array.isArray(value)) { dependArray(value) // 定義如下,邏輯也比較簡單 } } } return value },set: function reactiveSetter (newVal) { // .... } }) } function dependArray (value: Array<any>) { for (let e,i = 0,l = value.length; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.dep.depend() // 如果e是響應式資料,則往e._ob_.dep.subs陣列中加入Dep.target if (Array.isArray(e)) { dependArray(e) // 遞迴遍歷 } } }
程式碼中多次用到了Dep類和Dep.target,理解清楚了它們的作用,我們就離Vue資料驅動的原理更近一步了,相關的程式碼如下:
Dep
let uid = 0 /** * A dep is an observable that can have multiple * directives subscribing to it. */ export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ // 每一個dep都有一個唯一的ID this.subs = [] // 存放watcher例項的陣列 } addSub (sub: Watcher) { this.subs.push(sub) // 往this.subs加入watcher } removeSub (sub: Watcher) { remove(this.subs,sub) // 刪除this.subs對應的watcher } depend () { if (Dep.target) { // watcher.addDep(this) actually Dep.target.addDep(this) // 在watcher類中檢視 } } 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) // 根據watcher的id進行排序 } for (let i = 0,l = subs.length; i < l; i++) { subs[i].update() // 遍歷subs陣列中的每一個watcher執行update方法 } } } // The current target watcher being evaluated. // This is globally unique because only one watcher // can be evaluated at a time. Dep.target = null // Dep.target 代表當前全域性的watcher const targetStack = [] export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target // 賦值 } export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] // 賦值 }
Dep的定義還是非常清晰的,程式碼註釋如上,很明顯Dep跟Watcher就跟捆綁銷售一樣,互相依賴。我們在分析denfineReactive的時候,在對資料進行響應式操作的時候,通過Object.defineProperty重寫了getter函式。
Object.defineProperty(obj,get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { // 當前的全部的Watcher例項 dep.depend() // 把當前的Dep.target加入到dep.subs陣列中 // .. } return value },
其中的dep.depend()實際上就是執行了Dep.target.addDep(this),this指向Dep例項,而Dep.target是一個Watcher例項,即執行watcher.addDep(this)函式。我們接下來在看看這個函式做了什麼:
class Watcher { 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) // 會把watcher插入到dep.subs陣列中 } } } }
可以通過下圖以便理解data、Dep、Watcher的關係:
回到程式碼中,其中dep.addSub(this)就是會把當前的wathcer例項插入到dep.subs的陣列中,為之後的派發更新做好準備,這樣依賴收集就完成了。但是到現在為止,我們只分析了依賴收集是怎麼實現的,但是依賴收集的時機又是在什麼時候呢?什麼時候會觸發getter函式進而實現依賴收集的?在進行依賴收集的時候,Dep.tagrget對應wathcer又是什麼呢?
Watcher大致可以分為三類: * 渲染Watcher: 每一個例項對應唯一的一個(有且只有一個) * computed Watcher: 每一個例項可以有多個,由computed屬性生成的(computed有多少個keyy,例項就有多少個computedWatcher) * user Watcher: 每一個例項可以有多個,由watch屬性生成的(同computed一樣,userWatcher的數量由key數量決定) 為避免混淆,我們接下來說的Watcher都是渲染Watcher。我們知道在Vue初始化的過程中,在執行mountComponent函式的時候,會執行new Watcher(vm,updateComponent,{},true),這裡的Watcher就是渲染Watcher
class Wachter { get () { pushTarget(this) // Dep.target = this let value const vm = this.vm try { 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 } }
new Watcher對於渲染watcher而言,會直接執行this.get()方法,然後執行pushTarget(this),所以當前的Dep.target為渲染watcher(用於更新檢視)。 而在我們執行this.getter的時候,會呼叫render函式,此時會讀取vm例項上的data資料,這個時候就觸發了getter函數了,從而進行了依賴收集,這就是依賴收集的時機,比如
{{ message }} // 會讀取vm._data.message,觸發getters函式
派發更新
我們繼續來看defineReactive函式裡
export function defineReactive ( obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean ) { const dep = new Dep() // .. Object.defineProperty(obj,get: function reactiveGetter () { // .. },set: function reactiveSetter (newVal) { /* 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 if (setter) { setter.call(obj,newVal)https://cn.vuejs.org//images/data.png } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() // 遍歷dep.subs陣列,取出所有的wathcer執行update操作 } }) }
當我們修改資料的時候,會觸發setter函式,這個時候會執行dep.notify,dep.subs中所有的watcher都會執行update方法,對於渲染Watcher而言,就是執行this.get()方法,及更新檢視。這樣一來,就實現了資料驅動。 到這裡,Vue的資料驅動原理我們就分析完了,如果還對這個流程不大清楚的,可以結合參考官方給的圖解:
總結
- 通過Object.defineProperty函式改寫了資料的getter和setter函式,來實現依賴收集和派發更新。
- 一個key值對應一個Dep例項,一個Dep例項可以包含多個Watcher,一個Wathcer也可以包含多個Dep。
- Dep用於依賴的收集與管理,並通知對應的Watcher執行相應的操作。
- 依賴收集的時機是在執行render方法的時候,讀取vm上的資料,觸發getter函式。而派發更新即在變更資料的時候,觸發setter函式,通過dep.notify(),通知到所收集的watcher,執行相應操作。
以上就是詳解Vue資料驅動原理的詳細內容,更多關於Vue資料驅動原理的資料請關注我們其它相關文章!