Vue源碼解析之?dāng)?shù)組變異的實(shí)現(xiàn)
力有不逮的對(duì)象
眾所周知,在 Vue 中,直接修改對(duì)象屬性的值無(wú)法觸發(fā)響應(yīng)式。當(dāng)你直接修改了對(duì)象屬性的值,你會(huì)發(fā)現(xiàn),只有數(shù)據(jù)改了,但是頁(yè)面內(nèi)容并沒(méi)有改變。
這是什么原因?
原因在于: Vue 的響應(yīng)式系統(tǒng)是基于Object.defineProperty
這個(gè)方法的,該方法可以監(jiān)聽(tīng)對(duì)象中某個(gè)元素的獲取或修改,經(jīng)過(guò)了該方法處理的數(shù)據(jù),我們稱(chēng)其為響應(yīng)式數(shù)據(jù)。但是,該方法有一個(gè)很大的缺點(diǎn),新增屬性或者刪除屬性不會(huì)觸發(fā)監(jiān)聽(tīng),舉個(gè)栗子:
var vm = new Vue({ data () { return { obj: { a: 1 } } } }) // `vm.obj.a` 現(xiàn)在是響應(yīng)式的 vm.obj.b = 2 // `vm.obj.b` 不是響應(yīng)式的
原因在于,在 Vue
初始化的時(shí)候, Vue
內(nèi)部會(huì)對(duì) data
方法的返回值進(jìn)行深度響應(yīng)式處理,使其變?yōu)轫憫?yīng)式數(shù)據(jù),所以, vm.obj.a
是響應(yīng)式的。但是,之后設(shè)置的 vm.obj.b
并沒(méi)有經(jīng)過(guò) Vue
初始化時(shí)響應(yīng)式的洗禮,所以,理所應(yīng)當(dāng)?shù)牟皇琼憫?yīng)式。
那么,vm.obj.b
可以變成響應(yīng)式嗎?當(dāng)然可以,通過(guò) vm.$set
方法就可以完美地實(shí)現(xiàn)要求,在此不再贅述相關(guān)原理了,之后應(yīng)該會(huì)寫(xiě)一篇文章講述 vm.$set
背后的原理。
更凄慘的數(shù)組
上面說(shuō)了這么多,還沒(méi)有提到本篇文章的主角——數(shù)組,現(xiàn)在該主角出場(chǎng)了。
比起對(duì)象,數(shù)組的境遇更加凄慘一些,看看官方文檔:
由于 JavaScript 的限制, Vue 不能檢測(cè)以下變動(dòng)的數(shù)組:
- 當(dāng)你利用索引直接設(shè)置一個(gè)項(xiàng)時(shí),例如:
vm.items[indexOfItem] = newValue
- 當(dāng)你修改數(shù)組的長(zhǎng)度時(shí),例如:
vm.items.length = newLength
有可能官方文檔不是很清晰,那我們繼續(xù)舉個(gè)栗子:
var vm = new Vue({ data () { return { items: ['a', 'b', 'c'] } } }) vm.items[1] = 'x' // 不是響應(yīng)性的 vm.items.length = 2 // 不是響應(yīng)性的
也就是說(shuō),數(shù)組連自身元素的修改也無(wú)法監(jiān)聽(tīng),原因在于, Vue 對(duì) data 方法返回的對(duì)象中的元素進(jìn)行響應(yīng)式處理時(shí),如果元素是數(shù)組時(shí),僅僅對(duì)數(shù)組本身進(jìn)行響應(yīng)式化,而不對(duì)數(shù)組內(nèi)部元素進(jìn)行響應(yīng)式化。
這也就導(dǎo)致如官方文檔所寫(xiě)的后果,無(wú)法直接修改數(shù)組內(nèi)部元素來(lái)觸發(fā)響應(yīng)式。
那么,有沒(méi)有破解方法呢?
當(dāng)然有,官方規(guī)定了 7 個(gè)數(shù)組方法,通過(guò)這 7 個(gè)數(shù)組方法,可以很開(kāi)心地觸發(fā)數(shù)組的響應(yīng)式,這 7 個(gè)數(shù)組方法分別是:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
可以發(fā)現(xiàn),這 7 個(gè)數(shù)組方法貌似就是原生的那些數(shù)組方法,為什么這 7 個(gè)數(shù)組方法可以觸發(fā)應(yīng)式,觸發(fā)視圖更新呢?
你是不是心里想著:數(shù)組方法了不起呀,數(shù)組方法就可以為所欲為啊?
騷瑞啊,這 7 個(gè)數(shù)組方法是真的可以為所欲為的。
因?yàn)?,它們是變異后的?shù)組方法。
數(shù)組變異思路
什么是變異數(shù)組方法?
變異數(shù)組方法即保持?jǐn)?shù)組方法原有功能不變的前提下對(duì)其進(jìn)行功能拓展,在 Vue 中這個(gè)所謂的功能拓展就是添加響應(yīng)式功能。
將普通的數(shù)組變?yōu)樽儺悢?shù)組的方法分為兩步:
- 功能拓展
- 數(shù)組劫持
功能拓展
先來(lái)個(gè)思考題:
有這樣一個(gè)需求,要求在不改變?cè)泻瘮?shù)功能以及調(diào)用方式的情況下,使得每次調(diào)用該函數(shù)都能在控制臺(tái)中打印出'HelloWorld'
其實(shí)思路很簡(jiǎn)單,分為三步:
- 使用新的變量緩存原函數(shù)
- 重新定義原函數(shù)
- 在新定義的函數(shù)中調(diào)用原函數(shù)
看看具體的代碼實(shí)現(xiàn):
function A () { console.log('調(diào)用了函數(shù)A') } const nativeA = A A = function () { console.log('HelloWorld') nativeA() }
可以看到,通過(guò)這種方式,我們就保證了在不改變 A 函數(shù)行為的前提下對(duì)其進(jìn)行了功能拓展。
接下來(lái),我們使用這種方法對(duì)數(shù)組原本方法進(jìn)行功能拓展:
// 變異方法名稱(chēng) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] const arrayProto = Array.prototype // 繼承原有數(shù)組的方法 const arrayMethods = Object.create(arrayProto) mutationMethods.forEach(method => { // 緩存原生數(shù)組方法 const original = arrayProto[method] arrayMethods[method] = function (...args) { const result = original.apply(this, args) console.log('執(zhí)行響應(yīng)式功能') return result } })
從代碼中可以看出來(lái),我們調(diào)用 arrayMethods 這個(gè)對(duì)象中的方法有兩種情況:
- 調(diào)用功能拓展方法:直接調(diào)用 arrayMethods 中的方法
- 調(diào)用原生方法:這種情況下,通過(guò)原型鏈查找定義在數(shù)組原型中的原生方法
通過(guò)上述方法,我們實(shí)現(xiàn)了對(duì)數(shù)組原生方法進(jìn)行功能的拓展,但是,有一個(gè)巨大的問(wèn)題擺在面前:我們?cè)撊绾巫寯?shù)組實(shí)例調(diào)用功能拓展后數(shù)組方法呢?
解決這一問(wèn)題的方法就是:數(shù)組劫持。
數(shù)組劫持
數(shù)組劫持,顧名思義就是將原本數(shù)組實(shí)例要繼承的方法替換成我們功能拓展后的方法。
想一想,我們?cè)谇懊鎸?shí)現(xiàn)了一個(gè)功能拓展后的數(shù)組 arrayMethods
,這個(gè)自定義的數(shù)組繼承自數(shù)組對(duì)象,我們只需要將其和普通數(shù)組實(shí)例連接起來(lái),讓普通數(shù)組繼承于它即可。
而想實(shí)現(xiàn)上述操作,就是通過(guò)原型鏈。
實(shí)現(xiàn)方法如下代碼所示:
let arr = [] // 通過(guò)隱式原型繼承arrayMethods arr.__proto__ = arrayMethods // 執(zhí)行變異后方法 arr.push(1)
通過(guò)功能拓展和數(shù)組劫持,我們終于實(shí)現(xiàn)了變異數(shù)組,接下來(lái)讓我們看看 Vue
源碼是如何實(shí)現(xiàn)變異數(shù)組的。
源碼解析
我們來(lái)到 src/core/observer/index.js
中在 Observer
類(lèi)中的 constructor
函數(shù):
constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) // 檢測(cè)是否是數(shù)組 if (Array.isArray(value)) { // 能力檢測(cè) const augment = hasProto ? protoAugment : copyAugment // 通過(guò)能力檢測(cè)的結(jié)果選擇不同方式進(jìn)行數(shù)組劫持 augment(value, arrayMethods, arrayKeys) // 對(duì)數(shù)組的響應(yīng)式處理 this.observeArray(value) } else { this.walk(value) } }
Observer
這個(gè)類(lèi)是 Vue
響應(yīng)式系統(tǒng)的核心組成部分,在初始化階段最主要的功能是將目標(biāo)對(duì)象進(jìn)行響應(yīng)式化。在這里,我們主要關(guān)注其對(duì)數(shù)組的處理。
其對(duì)數(shù)組的處理主要是以下代碼
// 能力檢測(cè) const augment = hasProto ? protoAugment : copyAugment // 通過(guò)能力檢測(cè)的結(jié)果選擇不同方式進(jìn)行數(shù)組劫持 augment(value, arrayMethods, arrayKeys) // 對(duì)數(shù)組的響應(yīng)式處理,很本文關(guān)系不大,略過(guò) this.observeArray(value)
首先定義了 augment
常量,這個(gè)常量的值由 hasProto
決定。
我們來(lái)看看 hasProto
:
export const hasProto = '__proto__' in {}
可以發(fā)現(xiàn), hasProto
其實(shí)就是一個(gè)布爾值常量,用來(lái)表示瀏覽器是否支持直接使用 __proto__
(隱式原型) 。
所以,第一段代碼很好理解:根據(jù)根據(jù)能力檢測(cè)結(jié)果選擇不同的數(shù)組劫持方法,如果瀏覽器支持隱式原型,則調(diào)用 protoAugment
函數(shù)作為數(shù)組劫持的方法,反之則使用 copyAugment
。
不同的數(shù)組劫持方法
現(xiàn)在我們來(lái)看看 protoAugment
以及 copyAugment
。
function protoAugment (target, src: Object, keys: any) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ }
可以看到, protoAugment
函數(shù)極其簡(jiǎn)潔,和在數(shù)組變異思路中所說(shuō)的方法一致:將數(shù)組實(shí)例直接通過(guò)隱式原型與變異數(shù)組連接起來(lái),通過(guò)這種方式繼承變異數(shù)組中的方法。
接下來(lái)我們?cè)倏纯?copyAugment
:
function copyAugment (target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] // Object.defineProperty的封裝 def(target, key, src[key]) } }
由于在這種情況下,瀏覽器不支持直接使用隱式原型,所以數(shù)組劫持方法要麻煩很多。我們知道該函數(shù)接收的第一個(gè)參數(shù)是數(shù)組實(shí)例,第二個(gè)參數(shù)是變異數(shù)組,那么第三個(gè)參數(shù)是什么?
// 獲取變異數(shù)組中所有自身屬性的屬性名 const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
arrayKeys
在該文件的開(kāi)頭就定義了,即變異數(shù)組中的所有自身屬性的屬性名,是一個(gè)數(shù)組。
回頭再看 copyAugment
函數(shù)就很清晰了,將所有變異數(shù)組中的方法,直接定義在數(shù)組實(shí)例本身,相當(dāng)于變相的實(shí)現(xiàn)了數(shù)組的劫持。
實(shí)現(xiàn)了數(shù)組劫持后,我們?cè)賮?lái)看看 Vue
中是怎樣實(shí)現(xiàn)數(shù)組的功能拓展的。
功能拓展
數(shù)組功能拓展的代碼位于 src/core/observer/array.js
,代碼如下:
import { def } from '../util/index' // 緩存數(shù)組原型 const arrayProto = Array.prototype // 實(shí)現(xiàn) arrayMethods.__proto__ === Array.prototype export const arrayMethods = Object.create(arrayProto) // 需要進(jìn)行功能拓展的方法 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache original method // 緩存原生數(shù)組方法 const original = arrayProto[method] // 在變異數(shù)組中定義功能拓展方法 def(arrayMethods, method, function mutator (...args) { // 執(zhí)行并緩存原生數(shù)組方法的執(zhí)行結(jié)果 const result = original.apply(this, args) // 響應(yīng)式處理 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() // 返回原生數(shù)組方法的執(zhí)行結(jié)果 return result }) })
可以發(fā)現(xiàn),源碼在實(shí)現(xiàn)的方式上,和我在數(shù)組變異思路中采用的方法一致,只不過(guò)在其中添加了響應(yīng)式的處理。
總結(jié)
Vue 的變異數(shù)組從本質(zhì)上是來(lái)說(shuō)是一種裝飾器模式,通過(guò)學(xué)習(xí)它的原理,我們?cè)趯?shí)際工作中可以輕松處理這類(lèi)保持原有功能不變的前提下對(duì)其進(jìn)行功能拓展的需求。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
vue-cli監(jiān)聽(tīng)組件加載完成的方法
今天小編就為大家分享一篇vue-cli監(jiān)聽(tīng)組件加載完成的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09如何在vue里面優(yōu)雅的解決跨域(路由沖突問(wèn)題)
這篇文章主要介紹了如何在vue里面優(yōu)雅的解決跨域(路由沖突問(wèn)題),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-01-01淺談vue-props的default寫(xiě)不寫(xiě)有什么區(qū)別
這篇文章主要介紹了淺談vue-props的default寫(xiě)不寫(xiě)有什么區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08vue項(xiàng)目使用高德地圖時(shí)報(bào)錯(cuò):AMap?is?not?defined解決辦法
這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目使用高德地圖時(shí)報(bào)錯(cuò):AMap?is?not?defined的解決辦法,"AMap is not defined"是一個(gè)錯(cuò)誤提示,意思是在代碼中沒(méi)有找到定義的AMap,需要的朋友可以參考下2023-12-12淺談angular4.0中路由傳遞參數(shù)、獲取參數(shù)最nice的寫(xiě)法
下面小編就為大家分享一篇淺談angular4.0中路由傳遞參數(shù)、獲取參數(shù)最nice的寫(xiě)法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03el-input無(wú)法輸入的問(wèn)題和表單驗(yàn)證失敗問(wèn)題解決
在做項(xiàng)目的時(shí)候發(fā)現(xiàn)一個(gè)情況,輸入框無(wú)法輸入值并且表單校驗(yàn)失靈,所以下面這篇文章主要給大家介紹了關(guān)于el-input無(wú)法輸入的問(wèn)題和表單驗(yàn)證失敗問(wèn)題解決的相關(guān)資料,需要的朋友可以參考下2023-02-02