Vue原始碼解析五——資料響應系統
接下來重點來看Vue的資料響應系統。我看很多文章在講資料響應的時候先用一個簡單的例子介紹了資料雙向繫結的思路,然後再看原始碼。這裡也借鑑了這種方式,感覺這樣的確更有利於理解。
資料雙向繫結的思路
1. 物件
先來看元素是物件的情況。假設我們有一個物件和一個監測方法:
const data = { a: 1 }; /** * exp[String, Function]: 被觀測的欄位 * fn[Function]: 被觀測物件改變後執行的方法 */ function watch (exp, fn) { }
我們可以呼叫watch方法,當a
的值改變後列印一句話:
watch('a', () => { console.log('a 改變了') })
要實現這個功能,我們首先要能知道屬性a
被修改了。這時候就需要使用Object.defineProperty
函式把屬性a
變成訪問器屬性:
Object.defineProperty(data, 'a', { set () { console.log('設定了 a') }, get () { console.log('讀取了 a') } })
這樣當我們修改a
的值:data.a = 2
時,就會打印出設定了 a
, 當我們獲取a
的值時:data.a
, 就會打印出讀取了 a
.
在屬性的讀取和設定中我們已經能夠進行攔截並做一些操作了。可是在屬性修改時我們並不想總列印設定了 a
這句話,而是有一個監聽方法watch
,不同的屬性有不同的操作,對同一個屬性也可能監聽多次。
這就需要一個容器,把對同一個屬性的監聽依賴收集起來,在屬性改變時再取出依次觸發。既然是在屬性改變時觸發依賴,我們就可以放在setter
裡面,在getter
中收集依賴。這裡我們先不考慮依賴被重複收集等一些情況
const dep = []; Object.defineProperty(data, 'a', { set () { dep.forEach(fn => fn()); }, get () { dep.push(fn); } })
我們定義了容器dep
, 在讀取a
屬性時觸發get
函式把依賴存入dep中;在設定a
屬性時觸發set
函式把容器內的依賴挨個執行。
那fn
從何而來呢?再看一些我們的監測函式watch
watch('a', () => { console.log('a 改變了') })
該函式有兩個引數,第一個是被觀測的欄位,第二個是被觀測欄位的值改變後需要觸發的操作。其實第二個引數就是我們要收集的依賴fn
。
const data = { a: 1 }; const dep = []; Object.defineProperty(data, 'a', { set () { dep.forEach(fn => fn()); }, get () { // Target就是該變數的依賴函式 dep.push(Target); } }) let Target = null; function watch (exp, fn) { // 將fn賦值給Target Target = fn; // 讀取屬性,觸發get函式,收集依賴 data[exp]; }
現在僅能夠觀測a
一個屬性,為了能夠觀測物件data
上面的所有屬性,我們將定義訪問器屬性的那段程式碼封裝一下:
function walk () { for (let key in data) { const dep = []; const val = data[key]; Object.defineProperty(data, key, { set (newVal) { if (newVal === val) return; val = newVal; dep.forEach(fn => fn()); }, get () { // Target就是該變數的依賴函式 dep.push(Target); return val; } }) } }
用for迴圈遍歷data
上的所有屬性,對每一個屬性都用Object.defineProperty
改為訪問器屬性。
現在監測data
裡面基本型別值的屬性沒問題了,如果data
的屬性值又是一個物件呢:
data: { a: { aa: 1 } }
我們再來改一下我們的walk
函式,當val還是一個物件時,遞迴呼叫walk
:
function walk (data) { for (let key in data) { const dep = []; const val = data[key]; // 如果val是物件,遞迴呼叫walk,將其屬性轉為訪問器屬性 if (Object.prototype.toString.call(val) === '[object Object]') { walk(val); } Object.defineProperty(data, key, { set (newVal) { if (newVal === val) return; val = newVal; dep.forEach(fn => fn()); }, get () { // Target就是該變數的依賴函式 dep.push(Target); return val; } }) } }
添加了一段判斷邏輯,如果某個屬性的屬性值仍然是物件,就遞迴呼叫walk
函式。
雖然經過上面的改造,data.a.aa
是訪問器屬性了,但下面但程式碼仍然不能執行:
watch('a.aa', () => { console.log('修改了 a.b') })
這是為什麼呢?再看我們的watch
函式:
function watch (exp, fn) { // 將fn賦值給Target Target = fn; // 讀取屬性,觸發get函式,收集依賴 data[exp]; }
在讀取屬性的時候是data[exp]
,放到這裡就是data[a.aa]
,這自然是不對的。正確的讀取方式應該是data[a][aa]
. 我們需要對watch
函式做改造:
function watch (exp, fn) { // 將fn賦值給Target Target = fn; let obj = data; if (/\./.test(exp)) { const path = exp.split('.'); path.forEach(p => obj = obj[p]) return; } data[exp]; }
這裡增加了一個判斷邏輯,當監測的欄位中包含.
時,就執行if語句塊的內容。首先使用split函式將字串轉換為陣列:a.aa => [a, aa]
. 然後使用迴圈讀取到巢狀的屬性值,並且return結束。
Vue中提供了$watch
例項方法來觀測表示式,對複雜的表示式用函式取代:
// 函式 vm.$watch( function () { // 表示式 `this.a + this.b` 每次得出一個不同的結果時 // 處理函式都會被呼叫。 // 這就像監聽一個未被定義的計算屬性 return this.a + this.b }, function (newVal, oldVal) { // 做點什麼 } )
當第一個函式執行時,就會觸發this.a
、this.b
的get
攔截器,從而收集依賴。
我們的watch
函式第一個引數是函式時watch
函式要做些什麼改變呢?要想能夠收集依賴,就得讀取屬性觸發get
函式。當第一個引數是函式時怎麼讀取屬性呢?函式內是有讀取屬性的,所以只要執行一下函式就行了。
function watch (exp, fn) { // 將fn賦值給Target Target = fn; // 如果 exp 是函式,直接執行該函式 if (typeof exp === 'function') { exp() return } let obj = data; if (/\./.test(exp)) { const path = exp.split('.'); path.forEach(p => obj = obj[p]) return; } data[exp]; }
物件的處理暫且就到這裡,具體的我們在原始碼中去看。
2. 陣列
陣列有幾個變異方法會改變陣列本身:push
pop
shift
unshift
splice
sort
reverse
, 那怎麼才能知道何時呼叫了這些變異方法呢?我們可以在保證原來方法功能不變的前提下對方法進行擴充套件。可是如何擴充套件呢?
陣列例項的方法都來自於陣列建構函式的原型, 陣列例項的__proto__
屬性指向陣列建構函式的原型,即:arr.__proto__ === Array.prototype
, 我們可以定義一個物件,它的原型指向Array.prototype
,然後在這個物件中重新定義與變異方法重名的函式,然後讓例項的__proto__
指向該物件,這樣呼叫變異方法的時候,就會先呼叫重定義的方法。
const mutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; // 建立以Array.prototype為原型的物件 const arrayMethods = Object.create(Array.prototype); // 快取Array.prototype const originMethods = Array.prototype; mutationMethods.forEach(method => { arrayMethods[method] = function (...args) { // 呼叫原來的方法獲取結果 const result = originMethods[method].apply(this, args); console.log(`重定義了${method}方法`) return result; } })
我們來測試一下:
const arr = []; arr.__proto__ = arrayMethods; arr.push(1);
可以看到在控制檯打印出了重定義了push方法
這句話。
先大概有個印象,接下來我們來看原始碼吧。
例項物件代理訪問data
在initState
方法中,有這樣一段程式碼:
const opts = vm.$options ... if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) }
opts就是vm.$options
,如果opts.data
存在,就執行initData
方法,否則執行observe
方法,並給vm._data
賦值空物件。我們就從initData
方法開始,開啟探索資料響應系統之路。
initData
方法定義在core/instance/state.js
檔案中:
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 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 */) }
內容有點多我們從上往下依次來看,首先是這樣一段程式碼:
let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
我們知道在經過選項合併後,data
已經變成一個函數了。那為何這裡還有data
是否是一個函式的判斷呢?這是因為beforeCreate
生命週期是在mergeOptions
函式之後initState
函式之前呼叫的,mergeOptions
函式就是處理選項合併的。如果使用者在beforeCreate
中修改了vm.$options.data
的值呢?那它就可能不是一個函數了,畢竟使用者的操作是不可控的,所以這裡還是有必要判斷一下的。
正常情況下也就是data
是一個函式,就會呼叫getData
函式,並將data
和Vue例項vm
作為引數傳過去。該函式也定義在當前頁面中:
export function getData (data: Function, vm: Component): any { // #7573 disable dep collection when invoking data getters pushTarget() try { return data.call(vm, vm) } catch (e) { handleError(e, vm, `data()`) return {} } finally { popTarget() } }
其實該函式就是通過呼叫data
獲取到資料物件並返回:data.call(vm, vm)
. 用try...catch
包裹是為了捕獲可能出現的錯誤,如果出錯的話呼叫handleError
函式並返回一個空物件。
函式的開頭和結尾分別呼叫了pushTarget
和popTarget
, 這是為了防止使用 props 資料初始化 data 資料時收集冗餘的依賴。
再回到initData
函式中,所以現在data
和vm._data
就是最終的資料物件了。
接下來是一個if判斷:
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 ) }
isPlainObject
是判斷是否是一個純物件的,如果data
不是一個物件,在非生產環境下給出警告資訊。
繼續往下看:
// proxy data on instance // 獲取data物件的鍵 const keys = Object.keys(data) // 獲取props,是個物件 const props = vm.$options.props // 獲取methods,是個物件 const methods = vm.$options.methods let i = keys.length // 迴圈遍歷data的鍵 while (i--) { const key = keys[i] // 如果methods存在,並且methods中存在與data物件相同的鍵,發出警告。data優先 if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } // 如果props存在,並且props中存在與data物件相同的鍵,發出警告。 props優先 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)) { // isReserved 函式用來檢測一個字串是否以 $ 或者 _ 開頭,主要用來判斷一個欄位的鍵名是否是保留的 proxy(vm, `_data`, key) } } // observe data observe(data, true /* asRootData */)
while中的兩個if條件判斷了props
和methods
中是否有和data
物件相同的鍵,因為這三者中的屬性都可以通過例項物件代理訪問,如果相同就會出現衝突了。
const vm = new Vue({ props: { a: { default: 2 } } data: { a: 1 }, methods: { a () { console.log(3) } } })
當呼叫vm.a
的時候,就會產生覆蓋現象。為了防止這種情況出現,就在這裡做了判斷。
再看else if
中的內容,當!isReserved(key)
成立時,執行proxy(vm,
_data, key)
。isReserved
函式的作用是判斷一個字串是否以 $ 或者 _ 開頭
, 因為Vue內部的變數是以$
或_
開頭,防止衝突。如果 key 不是以$
或_
開頭,那麼將執行proxy
函式
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
函式通過Object.defineProperty
在例項物件vm
上定義了與data
資料欄位相同的訪問器屬性,代理的值是vm._data
上對應的屬性值。當訪問this.a
時,實際訪問的是this._data.a
的值。
最後一句程式碼是
// observe data observe(data, true /* asRootData */)
呼叫observe
將data
資料物件轉換成響應式的。
observe工廠函式
observe
函式定義在core/observer/index.js
檔案中, 我們找到該函式的定義,一點點來看
if (!isObject(value) || value instanceof VNode) { return }
首先判斷如果資料不是一個物件或者是一個VNode例項,直接返回。
let ob: Observer | void
接著定義了ob
變數,它是一個Observer例項,可以看到observe
函式的最後返回了ob
.
下面是一個if...else
分支:
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) }
首先是if分支,用hasOwn
判斷了資料物件是否包含__ob__
屬性,並且判斷屬性值是否是Observer
的例項。如果條件為真的話,就把value.__ob__
的值賦給ob
。
為什麼會有這個判斷呢?每個資料物件被觀測後都會在該物件上定義一個__ob__
屬性, 所以這個判斷是為了防止重複觀測一個物件。
接著是else if
分支,這個條件判斷有點多,我們一個個來看。
-
shouldObserve
必須為true該變數也定義在
core/observer/index.js
檔案內,
/** * In some cases we may want to disable observation inside a component's * update computation. */ export let shouldObserve: boolean = true export function toggleObserving (value: boolean) { shouldObserve = value }
這段程式碼定義了shouldObserve
變數,初始化為true。接著定義了toggleObserving
函式,該函式接收一個引數,這個引數用來更新shouldObserve
的值。shouldObserve
為true時可以進行觀測,為false時將不會進行觀測。
-
!isServerRendering()
必須為trueisServerRendering
函式用來判斷是否是服務端渲染,只有當不是服務端渲染的時候才會進行觀測 -
(Array.isArray(value) || isPlainObject(value))
必須為真只有當資料物件是陣列或者純物件時才進行觀測
-
Object.isExtensible(value)
必須為true被觀測的資料物件必須是可擴充套件的, 普通物件預設就是可擴充套件當。以下三個方法可以將物件變得不可擴充套件:
Object.preventExtensions()
、Object.freeze()
、Object.seal()
-
!value._isVue
必須為真Vue例項含有
_isVue
屬性,這個判斷是為了防止Vue例項被觀測
以上條件滿足之後,就會執行程式碼ob = new Observer(value)
,建立一個Observer
例項
Observer 建構函式
Observer
也定義在core/observer/index.js
檔案中,它是一個建構函式,用來將資料物件轉換成響應式的。
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 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(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]) } } }
以上是Observer
的全部程式碼,現在我們從constructor
開始,來看一下例項化Observer
都做了什麼。
__ob__
屬性
constructor
開始先初始化了幾個例項屬性
this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this)
value就是例項化Observer
時傳遞的引數,現在將它賦給了例項物件的value屬性。dep屬性指向例項化的Dep例項物件,它就是用來收集依賴的容器。vmCount屬性被初始化為0.
接著使用def函式為資料物件添加了__ob__
屬性,它的值就是當前Observer例項物件。def定義在core/util/lang.js
檔案中,是對Object.defineProperty
的封裝。
export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
用def來定義__ob__
屬性是要把它定義成不可列舉的,這樣遍歷物件就不會遍歷到它了。
假設我們的資料物件是
data = { a: 1 }
新增__ob__
屬性後變成
data = { a: 1, __ob__: { value: data, // data 資料物件本身 dep: new Dep(), // Dep例項 vmCount: 0 } }
處理純物件
接下來是一個if...else
判斷, 來區分陣列和物件,因為對陣列和物件的處理不同。
if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) }
我們先來看是物件的情況,也就是執行this.walk(value)
walk
函式就定義在constructor
的下面
walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } }
該方法就是用for迴圈遍歷了物件的屬性,並對每個屬性都呼叫了defineReactive
方法。
defineReactive 函式
defineReactive
也定義在core/observer/index.js
檔案中,找到它的定義:
/** * 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) 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) { val = obj[key] } let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { ... }, set: function reactiveSetter (newVal) { ... } }) }
因程式碼太長,省略了部分內容,之後我們再具體看。該函式的主要作用就是將資料物件的資料屬性轉換為訪問器屬性
函式體內首先定義了dep常量,它的值是Dep例項,用來收集對應欄位的依賴。
接下來是這樣一段程式碼:
const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return }
先通過Object.getOwnPropertyDescriptor
獲取欄位的屬性描述物件,再判斷該欄位是否是可配置的,如果不可配置,直接返回。因為不可配置的屬性是不能通過Object.defineProperty
改變其屬性定義的。
再往下接著看:
// 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] }
先儲存屬性描述物件裡面的get和set方法。如果這個屬性已經是訪問器屬性了,那它就存在get或set方法了,下面的操作會使用Object.defineProperty
重寫get和set方法,為了不影響原來的讀寫操作,就先快取setter/getter
。
接下來是一個if判斷,如果滿足條件的話,就讀取該屬性的值。
再下面是這樣一句程式碼:
let childOb = !shallow && observe(val)
因為屬性值val也可能是一個物件,所以呼叫observe
繼續觀測。但前面有一個條件,只有當shallow
為假時才會進行深度觀測。shallow
是defineReactive
的第五個引數,我們在walk中呼叫該函式時並沒有傳遞該引數,所以這裡它的值是undefined。!shallow
的是true,所以這裡會進行深度觀測。
不進行深度觀測的我們在initRender
函式中見過:
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => { !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm) }, true) defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => { !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm) }, true)
在Vue例項上定義屬性$attrs
和$listeners
時就是非深度觀測。
在get中收集依賴
接下來就是使用Object.defineProperty
設定訪問器屬性,先看一下get函式:
get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value },
get函式首先是要返回屬性值,還有就是在這裡收集依賴。
第一行程式碼就是獲取屬性值。先判斷了getter是否存在,getter就是屬性原有的get函式,如果存在的話呼叫該函式獲取屬性值,否則的話就用val作為屬性值。
接下來是收集依賴的程式碼:
if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } }
首先判斷Dep.target
是否存在,Dep.target
就是要收集的依賴,如果存在的話,執行if語句塊內的程式碼。
dep.depend()
dep物件的depend方法執行就是收集依賴。
然後判斷了childOb是否存在,存在的話執行childOb.dep.depend()
. 那麼childOb的值是誰呢?
如果我們有個資料物件:
data = { a: { b: 1 } }
經過observe觀測之後,新增__ob__
屬性,變成如下模樣:
data = { a: { b: 1, __ob__: { value, dep, vmCount } }, __ob__: { value, dep, vmCount } }
對於屬性a來說,childOb === data.a.__ob__
, 所以childOb.dep.depend()
就是data.a.__ob__.dep.depend()
在if語句裡面又一個if判斷:
if (Array.isArray(value)) { dependArray(value) }
如果屬性值是陣列,呼叫dependArray函式逐個觸發陣列元素的依賴收集
在set函式中觸發依賴
set: function reactiveSetter (newVal) { 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 if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() }
set函式主要是設定屬性值和觸發依賴 。
const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return }
首先也是獲取原來的屬性值。為什麼有這一步呢?因為要跟新值做比較,如果新舊值相等,就可以直接返回不用接下來的操作了。在if條件中,newVal === value
這個我們都明白,那後面這個(newVal !== newVal && value !== value)
條件是什麼意思呢?
這是因為一個特殊的值NaN
NaN === NaN // false
如果newVal !== newVal
,說明新值是NaN;如果value !== value
,那麼舊值也是NaN。那麼新舊值也是相等的,也不需要處理。
/* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() }
在非生產環境下,如果customSetter函式存在,將執行該函式。customSetter是defineReactive
的第四個引數,上面我們看initRender的時候有傳過這個引數:
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => { !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm) }, true)
第四個引數是一個箭頭函式,當修改vm.$attrs
時,會列印警告資訊$attrs是隻讀的
。所以customSetter的作用就是列印輔助資訊。
if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal }
如果存在getter不存在setter的話,直接返回。getter和setter就是屬性自身的get和set函式。
下面就是設定屬性值。如果setter存在的話,呼叫setter函式,保證原來的屬性設定操作不變。否則用新值替換舊值。
最後是這兩句程式碼:
childOb = !shallow && observe(newVal) dep.notify()
如果新值也是一個數組或純物件的話,這個新值是未觀測的。所以在需要深度觀測的情況下,要呼叫observe對新值進行觀測。最後呼叫dep.notify()
觸發依賴。
處理陣列
看完了純物件的處理,再來看一下陣列是怎麼轉換為響應式的。陣列有些方法會改變陣列本身,我們稱之為變異方法,這些方法有:push
pop
shift
unshift
reverse
sort
splice
,如何在呼叫這些方法的時候觸發依賴呢?看一下Vue的處理。
if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value)
首先是一個if...else 判斷,hasProto定義在core/util/env.js
檔案中。
// can we use __proto__? export const hasProto = '__proto__' in {}
判斷當前環境是否可以使用物件的__proto__
屬性, 該屬性在IE11及更高版本中才能使用。
如果條件為true的話,呼叫protoAugment方法, 傳遞了兩個引數,一個是陣列例項本身,一個是arrayMethods(代理原型)。
/** * Augment a target Object or Array by intercepting * the prototype chain using __proto__ */ function protoAugment (target, src: Object) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ }
該方法的作用就是將陣列例項的原型指向代理原型。這樣當陣列例項呼叫變異方法的時候就能先走代理原型重定義的方法。我們看一下arrayMethods
的實現,它定義在core/observer/array.js
檔案中:
import { def } from '../util/index' 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] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) })
這是這個檔案的全部內容,該檔案只做了一件事,就是匯出arrayMethods
物件。
const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto)
arrayMethods
是以陣列的原型為原型建立的物件。
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
這是定義了陣列的變異方法。
接著for迴圈遍歷變異方法,用def在代理原型上定義了與變異方法同名的方法。
methodsToPatch.forEach(function (method) { // cache original method const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) })
首先快取了陣列原本的變異方法
const original = arrayProto[method]
然後用def在arrayMethods物件上定義了與變異方法同名的函式。函式內首先呼叫了original原來的函式獲取結果
const result = original.apply(this, args)
並在函式末尾返回result。保證了攔截函式的功能與原來方法的功能是一致的。
const ob = this.__ob__ ... ob.dep.notify()
這兩句程式碼就是觸發依賴。當變異方法被呼叫時,陣列本身就被改變了,所以要觸發依賴。
再看其餘的程式碼:
let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted)
這段程式碼的作用就是收集新新增的元素,將其變成響應式資料 。
push和unshift方法的引數就是要新增的元素,所以inserted = args
。splice方法從第三個引數到最後一個引數都是要新增的新元素,所以inserted = args.slice(2)
。最後,如果存在新新增的元素,呼叫observeArray函式對其進行觀測。
以上是支援__proto__
屬性的時候,那不支援的時候呢?呼叫copyAugment方法,並傳遞了三個引數。前兩個跟protoAugment方法的引數一樣,一個是陣列例項本身,一個是arrayMethods代理原型,還有一個是arrayKeys,
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
它的值就是定義在arrayMethods物件上的所有的鍵,也就是所要攔截的變異方法的名稱。函式定義如下:
function copyAugment (target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }
這個方法的作用就是在陣列例項上定義與變異方法同名的函式,從而實現攔截。
if else程式碼之後,呼叫了observeArray方法this.observeArray(value)
, 並將陣列例項作為引數。
observeArray方法的定義如下:
/** * Observe a list of Array items. */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } }
迴圈遍歷陣列例項,並對陣列的每一項再進行觀測。這是因為如果陣列元素是陣列或純物件的話不進行這一步陣列元素就不是響應式的,這是為了實現深度觀測。比如:
const vm = new Vue({ data: { a: [[1,2]] } }) vm.a.push(1); // 能夠觸發響應 vm.a[1].push(1); // 不能觸發響應
所以需要遞迴觀測陣列元素。
Vue.set($set
) 和 Vue.delete($delete
) 的實現
我們知道,為物件或陣列直接新增或刪除元素Vue是攔截不到的。我們需要使用Vue.set、Vue.delete去解決,Vue還在例項物件上定義了$set
$delete
方便我們使用。其實不管是例項方法還是全域性方法它們的指向都是一樣的。我們來看以下它們的定義。
$set
$delete
定義在core/instance/state.js
檔案中的stateMixin方法中
export function stateMixin (Vue: Class<Component>) { ... Vue.prototype.$set = set Vue.prototype.$delete = del ... }
Vue.set和Vue.delete定義在core/global-api/index.js
檔案中的initGlobalAPI函式中:
export function initGlobalAPI (Vue: GlobalAPI) { ... Vue.set = set Vue.delete = del ... }
可以看到它們的函式值是相同的。 set和del定義在core/observer/index.js
檔案中。我們先來看一下set的定義
set
從上到下來看set的函式體,顯示這個if判斷:
if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) }
- isUndef
export function isUndef (v: any): boolean %checks { return v === undefined || v === null }
判斷變數是否是未定義,或者值為null。
- isPrimitive
export function isPrimitive (value: any): boolean %checks { return ( typeof value === 'string' || typeof value === 'number' || // $flow-disable-line typeof value === 'symbol' || typeof value === 'boolean' ) }
判斷變數是否是原始型別。
所以這個if語句的作用就是,如果target是undefined或者null或者它的型別是原始型別,在非生產環境下列印警告資訊 。
再看下一個if語句:
if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val }
- isValidArrayIndex
export function isValidArrayIndex (val: any): boolean { const n = parseFloat(String(val)) return n >= 0 && Math.floor(n) === n && isFinite(val) }
判斷變數是否是有效的陣列索引。
如果target是一個數組,並且key是一個有效的陣列索引,就執行if語句塊內的程式碼
我們知道splice變異方法是可以觸發響應的,target.splice(key, 1, val)
就利用了替換元素的能力,將指定位置元素的值替換為新值。所以陣列就是利用splice新增元素的。另外,當要設定的元素的索引大於陣列長度時 splice 無效,所以target的length取兩者中的最大值。
if (key in target && !(key in Object.prototype)) { target[key] = val return val }
這個if條件的意思是該屬性已經在target物件上有定義了,那麼只要重新設定它的值就行了。因為在純物件中,已經存在的屬性就是響應式的了。
const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val }
-
target._isVue
擁有_isVue
屬性說明這是一個Vue例項‘ -
(ob && ob.vmCount)
ob就是target.__ob__
,ob.vmCount也就是target.__ob__.vmCount
。來看一下這段程式碼:
export function observe (value: any, asRootData: ?boolean): Observer | void { if (asRootData && ob) { ob.vmCount++ } }
asRootData表示是否是根資料物件。什麼是根資料物件呢?看一下哪裡呼叫observe函式的時候傳遞了第二個引數:
function initData (vm: Component) { ... // observe data observe(data, true /* asRootData */) }
在initData中呼叫observe的時候傳遞了第二個引數為true,那根資料物件也就是data。也就是說當使用 Vue.set/$set 函式為根資料物件新增屬性時,是不被允許的。
所以當target是Vue例項或者是根資料物件時,在非生產環境會列印警告資訊。
if (!ob) { target[key] = val return val }
當!ob
為true時,說明不存在__ob__
屬性,那target也就不是響應式的,直接變更屬性值就行。
defineReactive(ob.value, key, val) ob.dep.notify()
這裡就是給物件新增新的屬性,並保證新新增的屬性是響應式的。ob.dep.notify()
觸發響應。
del
看完了set,再來看delete操作。
if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`) }
這個if判斷跟set函式的一樣。如果target是undefined、null或者原始型別值,在非生產環境下列印警告資訊。
if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1) return }
當target是陣列型別並且key是有效的陣列索引值時,也是使用splice來進行刪除操作,因為該變異方法可以觸發攔截操作。
const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ) return }
這一段if判斷也是一樣的,如果target是Vue例項或者是根資料物件,在非生產環境下列印警告資訊。也就是不能刪除Vue例項物件的屬性,也不能刪除根資料物件的屬性,因為data本身不是響應式的。
if (!hasOwn(target, key)) { return }
如果target物件上沒有key屬性,直接返回。
delete target[key]
進行到這裡就說明target是一個純物件,並且有key屬性,直接刪除該屬性。
if (!ob) { return }
如果ob物件不存在,說明target不是響應式的,直接返回。
ob.dep.notify()
如果ob物件存在,說明target是響應式的,觸發響應。