Vue源码解析之数组变异
力有不逮的對象
眾所周知,在 Vue 中,直接修改對象屬性的值無法觸發(fā)響應式。當你直接修改了對象屬性的值,你會發(fā)現(xiàn),只有數(shù)據(jù)改了,但是頁面內(nèi)容并沒有改變。
這是什么原因?
原因在于: Vue 的響應式系統(tǒng)是基于Object.defineProperty這個方法的,該方法可以監(jiān)聽對象中某個元素的獲取或修改,經(jīng)過了該方法處理的數(shù)據(jù),我們稱其為響應式數(shù)據(jù)。但是,該方法有一個很大的缺點,新增屬性或者刪除屬性不會觸發(fā)監(jiān)聽,舉個栗子:
var vm = new Vue({data () {return {obj: {a: 1}}} }) // `vm.obj.a` 現(xiàn)在是響應式的vm.obj.b = 2 // `vm.obj.b` 不是響應式的 復制代碼原因在于,在 Vue 初始化的時候, Vue 內(nèi)部會對 data 方法的返回值進行深度響應式處理,使其變?yōu)轫憫綌?shù)據(jù),所以, vm.obj.a 是響應式的。但是,之后設置的 vm.obj.b 并沒有經(jīng)過 Vue 初始化時響應式的洗禮,所以,理所應當?shù)牟皇琼憫健?/p>
那么,vm.obj.b可以變成響應式嗎?當然可以,通過 vm.$set 方法就可以完美地實現(xiàn)要求,在此不再贅述相關原理了,之后應該會寫一篇文章講述 vm.$set 背后的原理。
更凄慘的數(shù)組
上面說了這么多,還沒有提到本篇文章的主角——數(shù)組,現(xiàn)在該主角出場了。
比起對象,數(shù)組的境遇更加凄慘一些,看看官方文檔:
由于 JavaScript 的限制, Vue 不能檢測以下變動的數(shù)組:
有可能官方文檔不是很清晰,那我們繼續(xù)舉個栗子:
var vm = new Vue({data () {return {items: ['a', 'b', 'c']}} }) vm.items[1] = 'x' // 不是響應性的 vm.items.length = 2 // 不是響應性的 復制代碼也就是說,數(shù)組連自身元素的修改也無法監(jiān)聽,原因在于, Vue 對 data 方法返回的對象中的元素進行響應式處理時,如果元素是數(shù)組時,僅僅對數(shù)組本身進行響應式化,而不對數(shù)組內(nèi)部元素進行響應式化。
這也就導致如官方文檔所寫的后果,無法直接修改數(shù)組內(nèi)部元素來觸發(fā)響應式。
那么,有沒有破解方法呢?
當然有,官方規(guī)定了 7 個數(shù)組方法,通過這 7 個數(shù)組方法,可以很開心地觸發(fā)數(shù)組的響應式,這 7 個數(shù)組方法分別是:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
可以發(fā)現(xiàn),這 7 個數(shù)組方法貌似就是原生的那些數(shù)組方法,為什么這 7 個數(shù)組方法可以觸發(fā)應式,觸發(fā)視圖更新呢?
你是不是心里想著:數(shù)組方法了不起呀,數(shù)組方法就可以為所欲為啊?
騷瑞啊,這 7 個數(shù)組方法是真的可以為所欲為的。
因為,它們是變異后的數(shù)組方法。
數(shù)組變異思路
什么是變異數(shù)組方法?
變異數(shù)組方法即保持數(shù)組方法原有功能不變的前提下對其進行功能拓展,在 Vue 中這個所謂的功能拓展就是添加響應式功能。
將普通的數(shù)組變?yōu)樽儺悢?shù)組的方法分為兩步:
功能拓展
先來個思考題:
有這樣一個需求,要求在不改變原有函數(shù)功能以及調(diào)用方式的情況下,使得每次調(diào)用該函數(shù)都能在控制臺中打印出'HelloWorld'
其實思路很簡單,分為三步:
看看具體的代碼實現(xiàn):
function A () {console.log('調(diào)用了函數(shù)A') }const nativeA = A A = function () {console.log('HelloWorld')nativeA() } 復制代碼可以看到,通過這種方式,我們就保證了在不改變 A 函數(shù)行為的前提下對其進行了功能拓展。
接下來,我們使用這種方法對數(shù)組原本方法進行功能拓展:
// 變異方法名稱 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í)行響應式功能')return result} }) 復制代碼從代碼中可以看出來,我們調(diào)用 arrayMethods 這個對象中的方法有兩種情況:
通過上述方法,我們實現(xiàn)了對數(shù)組原生方法進行功能的拓展,但是,有一個巨大的問題擺在面前:我們該如何讓數(shù)組實例調(diào)用功能拓展后數(shù)組方法呢?
解決這一問題的方法就是:數(shù)組劫持。
數(shù)組劫持
數(shù)組劫持,顧名思義就是將原本數(shù)組實例要繼承的方法替換成我們功能拓展后的方法。
想一想,我們在前面實現(xiàn)了一個功能拓展后的數(shù)組 arrayMethods ,這個自定義的數(shù)組繼承自數(shù)組對象,我們只需要將其和普通數(shù)組實例連接起來,讓普通數(shù)組繼承于它即可。
而想實現(xiàn)上述操作,就是通過原型鏈。
實現(xiàn)方法如下代碼所示:
let arr = [] // 通過隱式原型繼承arrayMethods arr.__proto__ = arrayMethods// 執(zhí)行變異后方法 arr.push(1) 復制代碼通過功能拓展和數(shù)組劫持,我們終于實現(xiàn)了變異數(shù)組,接下來讓我們看看 Vue 源碼是如何實現(xiàn)變異數(shù)組的。
源碼解析
我們來到 src/core/observer/index.js 中在 Observer 類中的 constructor 函數(shù):
constructor (value: any) {this.value = valuethis.dep = new Dep()this.vmCount = 0def(value, '__ob__', this)// 檢測是否是數(shù)組if (Array.isArray(value)) {// 能力檢測const augment = hasProto? protoAugment: copyAugment// 通過能力檢測的結果選擇不同方式進行數(shù)組劫持augment(value, arrayMethods, arrayKeys)// 對數(shù)組的響應式處理this.observeArray(value)} else {this.walk(value)} } 復制代碼Observer 這個類是 Vue 響應式系統(tǒng)的核心組成部分,在初始化階段最主要的功能是將目標對象進行響應式化。在這里,我們主要關注其對數(shù)組的處理。
其對數(shù)組的處理主要是以下代碼
// 能力檢測 const augment = hasProto ? protoAugment : copyAugment // 通過能力檢測的結果選擇不同方式進行數(shù)組劫持 augment(value, arrayMethods, arrayKeys) // 對數(shù)組的響應式處理,很本文關系不大,略過 this.observeArray(value) 復制代碼首先定義了 augment 常量,這個常量的值由 hasProto 決定。
我們來看看 hasProto:
export const hasProto = '__proto__' in {} 復制代碼可以發(fā)現(xiàn), hasProto 其實就是一個布爾值常量,用來表示瀏覽器是否支持直接使用 __proto__ (隱式原型) 。
所以,第一段代碼很好理解:根據(jù)根據(jù)能力檢測結果選擇不同的數(shù)組劫持方法,如果瀏覽器支持隱式原型,則調(diào)用 protoAugment 函數(shù)作為數(shù)組劫持的方法,反之則使用 copyAugment 。
不同的數(shù)組劫持方法
現(xiàn)在我們來看看 protoAugment 以及 copyAugment 。
function protoAugment (target, src: Object, keys: any) {/* eslint-disable no-proto */target.__proto__ = src/* eslint-enable no-proto */ } 復制代碼可以看到, protoAugment 函數(shù)極其簡潔,和在數(shù)組變異思路中所說的方法一致:將數(shù)組實例直接通過隱式原型與變異數(shù)組連接起來,通過這種方式繼承變異數(shù)組中的方法。
接下來我們再看看 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ù)接收的第一個參數(shù)是數(shù)組實例,第二個參數(shù)是變異數(shù)組,那么第三個參數(shù)是什么?
// 獲取變異數(shù)組中所有自身屬性的屬性名 const arrayKeys = Object.getOwnPropertyNames(arrayMethods) 復制代碼arrayKeys 在該文件的開頭就定義了,即變異數(shù)組中的所有自身屬性的屬性名,是一個數(shù)組。
回頭再看 copyAugment 函數(shù)就很清晰了,將所有變異數(shù)組中的方法,直接定義在數(shù)組實例本身,相當于變相的實現(xiàn)了數(shù)組的劫持。
實現(xiàn)了數(shù)組劫持后,我們再來看看 Vue 中是怎樣實現(xiàn)數(shù)組的功能拓展的。
功能拓展
數(shù)組功能拓展的代碼位于 src/core/observer/array.js ,代碼如下:
import { def } from '../util/index'// 緩存數(shù)組原型 const arrayProto = Array.prototype // 實現(xiàn) arrayMethods.__proto__ === 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// 緩存原生數(shù)組方法const original = arrayProto[method]// 在變異數(shù)組中定義功能拓展方法def(arrayMethods, method, function mutator (...args) {// 執(zhí)行并緩存原生數(shù)組方法的執(zhí)行結果const result = original.apply(this, args)// 響應式處理const ob = this.__ob__let insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':inserted = args.slice(2)break}if (inserted) ob.observeArray(inserted)// notify changeob.dep.notify()// 返回原生數(shù)組方法的執(zhí)行結果return result}) }) 復制代碼可以發(fā)現(xiàn),源碼在實現(xiàn)的方式上,和我在數(shù)組變異思路中采用的方法一致,只不過在其中添加了響應式的處理。
總結
Vue 的變異數(shù)組從本質(zhì)上是來說是一種裝飾器模式,通過學習它的原理,我們在實際工作中可以輕松處理這類保持原有功能不變的前提下對其進行功能拓展的需求。
轉(zhuǎn)載于:https://juejin.im/post/5c05465c518825158c53568e
總結
以上是生活随笔為你收集整理的Vue源码解析之数组变异的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2018-2019-1 20165211
- 下一篇: Vue 组件实例属性的使用