Vue源碼解析之數組變異

力有不逮的對象

眾所周知,在 Vue 中,直接修改對象屬性的值無法觸發響應式。當你直接修改了對象屬性的值,你會發現,只有數據改了,但是頁面內容并沒有改變。

這是什么原因?

原因在于: Vue 的響應式系統是基于Object.defineProperty這個方法的,該方法可以監聽對象中某個元素的獲取或修改,經過了該方法處理的數據,我們稱其為響應式數據。但是,該方法有一個很大的缺點,新增屬性或者刪除屬性不會觸發監聽,舉個栗子:

var vm = new Vue({data () {return {obj: {a: 1}}}
})
// `vm.obj.a` 現在是響應式的vm.obj.b = 2
// `vm.obj.b` 不是響應式的
復制代碼

原因在于,在 Vue 初始化的時候, Vue 內部會對 data 方法的返回值進行深度響應式處理,使其變為響應式數據,所以, vm.obj.a 是響應式的。但是,之后設置的 vm.obj.b 并沒有經過 Vue 初始化時響應式的洗禮,所以,理所應當的不是響應式。

那么,vm.obj.b可以變成響應式嗎?當然可以,通過 vm.$set 方法就可以完美地實現要求,在此不再贅述相關原理了,之后應該會寫一篇文章講述 vm.$set 背后的原理。

更凄慘的數組

上面說了這么多,還沒有提到本篇文章的主角——數組,現在該主角出場了。

比起對象,數組的境遇更加凄慘一些,看看官方文檔:

由于 JavaScript 的限制, Vue 不能檢測以下變動的數組:

  1. 當你利用索引直接設置一個項時,例如:vm.items[indexOfItem] = newValue
  2. 當你修改數組的長度時,例如:vm.items.length = newLength

有可能官方文檔不是很清晰,那我們繼續舉個栗子:

var vm = new Vue({data () {return {items: ['a', 'b', 'c']}}
})
vm.items[1] = 'x' // 不是響應性的
vm.items.length = 2 // 不是響應性的
復制代碼

也就是說,數組連自身元素的修改也無法監聽,原因在于, Vuedata 方法返回的對象中的元素進行響應式處理時,如果元素是數組時,僅僅對數組本身進行響應式化,而不對數組內部元素進行響應式化。

這也就導致如官方文檔所寫的后果,無法直接修改數組內部元素來觸發響應式。

那么,有沒有破解方法呢?

當然有,官方規定了 7 個數組方法,通過這 7 個數組方法,可以很開心地觸發數組的響應式,這 7 個數組方法分別是:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

可以發現,這 7 個數組方法貌似就是原生的那些數組方法,為什么這 7 個數組方法可以觸發應式,觸發視圖更新呢?

你是不是心里想著:數組方法了不起呀,數組方法就可以為所欲為啊?

騷瑞啊,這 7 個數組方法是真的可以為所欲為的。

因為,它們是變異后的數組方法。

數組變異思路

什么是變異數組方法?

變異數組方法即保持數組方法原有功能不變的前提下對其進行功能拓展,在 Vue 中這個所謂的功能拓展就是添加響應式功能。

將普通的數組變為變異數組的方法分為兩步:

  1. 功能拓展
  2. 數組劫持

功能拓展

先來個思考題:

有這樣一個需求,要求在不改變原有函數功能以及調用方式的情況下,使得每次調用該函數都能在控制臺中打印出'HelloWorld'

其實思路很簡單,分為三步:

  1. 使用新的變量緩存原函數
  2. 重新定義原函數
  3. 在新定義的函數中調用原函數

看看具體的代碼實現:

function A () {console.log('調用了函數A')
}const nativeA = A
A = function () {console.log('HelloWorld')nativeA()
}
復制代碼

可以看到,通過這種方式,我們就保證了在不改變 A 函數行為的前提下對其進行了功能拓展。

接下來,我們使用這種方法對數組原本方法進行功能拓展:

// 變異方法名稱
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'
]const arrayProto = Array.prototype
// 繼承原有數組的方法
const arrayMethods = Object.create(arrayProto)mutationMethods.forEach(method => {// 緩存原生數組方法const original = arrayProto[method]arrayMethods[method] = function (...args) {const result = original.apply(this, args)console.log('執行響應式功能')return result}
})
復制代碼

從代碼中可以看出來,我們調用 arrayMethods 這個對象中的方法有兩種情況:

  1. 調用功能拓展方法:直接調用 arrayMethods 中的方法
  2. 調用原生方法:這種情況下,通過原型鏈查找定義在數組原型中的原生方法

通過上述方法,我們實現了對數組原生方法進行功能的拓展,但是,有一個巨大的問題擺在面前:我們該如何讓數組實例調用功能拓展后數組方法呢?

解決這一問題的方法就是:數組劫持。

數組劫持

數組劫持,顧名思義就是將原本數組實例要繼承的方法替換成我們功能拓展后的方法。

想一想,我們在前面實現了一個功能拓展后的數組 arrayMethods ,這個自定義的數組繼承自數組對象,我們只需要將其和普通數組實例連接起來,讓普通數組繼承于它即可。

而想實現上述操作,就是通過原型鏈。

實現方法如下代碼所示:

let arr = []
// 通過隱式原型繼承arrayMethods
arr.__proto__ = arrayMethods// 執行變異后方法
arr.push(1)
復制代碼

通過功能拓展和數組劫持,我們終于實現了變異數組,接下來讓我們看看 Vue 源碼是如何實現變異數組的。

源碼解析

我們來到 src/core/observer/index.js 中在 Observer 類中的 constructor 函數:

constructor (value: any) {this.value = valuethis.dep = new Dep()this.vmCount = 0def(value, '__ob__', this)// 檢測是否是數組if (Array.isArray(value)) {// 能力檢測const augment = hasProto? protoAugment: copyAugment// 通過能力檢測的結果選擇不同方式進行數組劫持augment(value, arrayMethods, arrayKeys)// 對數組的響應式處理this.observeArray(value)} else {this.walk(value)}
}
復制代碼

Observer 這個類是 Vue 響應式系統的核心組成部分,在初始化階段最主要的功能是將目標對象進行響應式化。在這里,我們主要關注其對數組的處理。

其對數組的處理主要是以下代碼

// 能力檢測
const augment = hasProto
? protoAugment
: copyAugment
// 通過能力檢測的結果選擇不同方式進行數組劫持
augment(value, arrayMethods, arrayKeys)
// 對數組的響應式處理,很本文關系不大,略過
this.observeArray(value)
復制代碼

首先定義了 augment 常量,這個常量的值由 hasProto 決定。

我們來看看 hasProto

export const hasProto = '__proto__' in {}
復制代碼

可以發現, hasProto 其實就是一個布爾值常量,用來表示瀏覽器是否支持直接使用 __proto__ (隱式原型) 。

所以,第一段代碼很好理解:根據根據能力檢測結果選擇不同的數組劫持方法,如果瀏覽器支持隱式原型,則調用 protoAugment 函數作為數組劫持的方法,反之則使用 copyAugment

不同的數組劫持方法

現在我們來看看 protoAugment 以及 copyAugment

function protoAugment (target, src: Object, keys: any) {/* eslint-disable no-proto */target.__proto__ = src/* eslint-enable no-proto */
}
復制代碼

可以看到, protoAugment 函數極其簡潔,和在數組變異思路中所說的方法一致:將數組實例直接通過隱式原型與變異數組連接起來,通過這種方式繼承變異數組中的方法。

接下來我們再看看 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])}
}
復制代碼

由于在這種情況下,瀏覽器不支持直接使用隱式原型,所以數組劫持方法要麻煩很多。我們知道該函數接收的第一個參數是數組實例,第二個參數是變異數組,那么第三個參數是什么?

// 獲取變異數組中所有自身屬性的屬性名
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
復制代碼

arrayKeys 在該文件的開頭就定義了,即變異數組中的所有自身屬性的屬性名,是一個數組。

回頭再看 copyAugment 函數就很清晰了,將所有變異數組中的方法,直接定義在數組實例本身,相當于變相的實現了數組的劫持。

實現了數組劫持后,我們再來看看 Vue 中是怎樣實現數組的功能拓展的。

功能拓展

數組功能拓展的代碼位于 src/core/observer/array.js ,代碼如下:

import { def } from '../util/index'// 緩存數組原型
const arrayProto = Array.prototype
// 實現 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// 緩存原生數組方法const original = arrayProto[method]// 在變異數組中定義功能拓展方法def(arrayMethods, method, function mutator (...args) {// 執行并緩存原生數組方法的執行結果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()// 返回原生數組方法的執行結果return result})
})
復制代碼

可以發現,源碼在實現的方式上,和我在數組變異思路中采用的方法一致,只不過在其中添加了響應式的處理。

總結

Vue 的變異數組從本質上是來說是一種裝飾器模式,通過學習它的原理,我們在實際工作中可以輕松處理這類保持原有功能不變的前提下對其進行功能拓展的需求。

轉載于:https://juejin.im/post/5c05465c518825158c53568e

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/281420.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/281420.shtml
英文地址,請注明出處:http://en.pswp.cn/news/281420.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

linux守護進程的編寫

linux監控一個進程進行 代碼如下: #!/bin/shcd /home/autoprocess/ autopgrep -f autoProcessNew.php | wc -l if [ "$auto" 0 ] then nohup php autoProcessNew.php & fi 監視autoProcessNew.php,使他一直監視轉載于:https://www.cnblogs.com/matengfei123/p/…

微軟2014編程之美初賽第一場——題目3 : 活動中心

【來源】 題目3 : 活動中心 【分析】 本題採用的是三分法。 輸入的一組點中找出左右邊界。作為起始邊界。 while(右邊界-左邊界<精度){將左右邊界構成的線段均勻分成3段&#xff0c;推斷切割點的距離關系&#xff0c;抹去距離大的一段。更新左右邊界。 } 輸出左(右)邊界 【…

windows10計算機里輸入法,win10電腦上輸入法不見了怎么辦

好的輸入法可以加快我們的工作效率&#xff0c;當電腦上輸入法不見時&#xff0c;你會調出來嗎?下面小編告訴你win10電腦上輸入法不見時弄出來的一些訣竅吧。win10電腦上輸入法不見了的解決方法win10電腦上輸入法不見了的解決方法&#xff1a;Win10系統輸入法圖標不見了的找回…

Java并發(二十一):線程池實現原理

一、總覽 線程池類ThreadPoolExecutor的相關類需要先了解&#xff1a; &#xff08;圖片來自&#xff1a;https://javadoop.com/post/java-thread-pool#%E6%80%BB%E8%A7%88&#xff09; Executor&#xff1a;位于最頂層&#xff0c;只有一個 execute(Runnable runnable) 方法&a…

進程池

轉自&#xff1a;https://www.cnblogs.com/kaituorensheng/p/4465768.html 在利用Python進行系統管理的時候&#xff0c;特別是同時操作多個文件目錄&#xff0c;或者遠程控制多臺主機&#xff0c;并行操作可以節約大量的時間。當被操作對象數目不大時&#xff0c;可以直接利用…

gulp版本號管理插件注意事項

2019獨角獸企業重金招聘Python工程師標準>>> 打開node_modules\gulp-rev\index.js 第144行 manifest[originalFile] revisionedFile; 更新為: manifest[originalFile] originalFile ?v file.revHash; 打開node_modules\rev-path\index.js 第10行 return filena…

bigfile.to服務器位置,Cloudera Manager 遷移服務器

Cloudera Manager還是比較耗資源的&#xff0c;想把Cloudera Manager&#xff0c;移動到比較好的機器上。在這篇文章中&#xff0c;Cloudera Manager安裝在bigserver1上面&#xff0c;bigserver1是奔騰雙核的CPU。1&#xff0c;Cloudera Manager占資源比較多cloudera manager占…

vue定時ajax獲取數據,vue 中使用 AJAX獲取數據的方法

在VUE開發時&#xff0c;數據可以使用jquery和vue-resource來獲取數據。在獲取數據時&#xff0c;一定需要給一個數據初始值。看下例&#xff1a;new Vue({el:#app,data:{data:""},created:function(){var url"json.jsp";var _selfthis;$.get(url,function…

轉:shell awk

簡單使用&#xff1a; awk &#xff1a;對于文件中一行行的獨處來執行操作 。 awk -F &#xff1a;{print $1,$4} :使用‘&#xff1a;’來分割這一行&#xff0c;把這一行的第一第四個域打印出來 。 詳細介紹&#xff1a; AWK命令介紹 awk語言的最基本功能是在文件或字符串中基…

Mac使用crontab來實現定時任務

crontab 定時執行 配置文件都在/etc/crontab下&#xff0c;如果沒有就創建 語法&#xff1a; crontab [-e [UserName]|-l [UserName]|-r [UserName]|-v [UserName]|File ] 說明&#xff1a; crontab 是用來讓使用者在固定時間或固定間隔執行程序之用&#xff0c;換句話說&#…

前端技術周刊 2018-12-03:DOM

前端快爆 Chrome 71 開始將試用 SXG 功能&#xff0c;它是由 IETF 提出&#xff0c;Web Package 協議規范下的 Signed HTTP Exchanges 功能的縮寫。該技術使得一個第三方服務器可以直接向用戶提供可靠資源&#xff0c;且不用與原站共享 HTTPS 證書密鑰。?點評&#xff1a;一項…

公司新來了一位阿里P9,在全員大會上講葷段子!還是上個世紀的老段子,太爛了!...

阿里P9在坊間的名聲一向不好&#xff0c;這幾年在業界出了不少令人無語的新聞&#xff0c;今天又來了一個&#xff1a;公司新來了一位阿里P9偽高管&#xff0c;全員大會上來先講了一個葷段子&#xff0c;這個破段子還是上個世紀的&#xff0c;太爛了&#xff01;關于這個段子&a…

【轉】博客美化(1)基本后臺設置與樣式設置

閱讀目錄 1.博客園后臺設置2.自定義樣式的設置博客園美化相關文章目錄&#xff1a;博客園博客美化相關文章目錄 一直都拜膜那些博客園的皮膚設計高手&#xff0c;由于本人對前端研究甚少&#xff0c;所以js,css這種東西只能看得懂最基本的&#xff0c;會簡單改改。然后一直對自…

Airdoc創始人:工智能可以在醫療領域多個環節發揮作用 但有局限性

7月1日&#xff0c;在由武漢國家生物產業基地建設管理辦公室主辦、火石創造承辦、光谷健康智慧園協辦的醫療大數據與醫學人工智能高峰論壇上&#xff0c;Airdoc創始人兼董事長張大磊做了題為《AI在醫療領域中應用的問題與局限》的演講。 Airdoc是醫療領域人工智能領軍企業&…

我的世界服務器抽獎系統怎么弄,我的世界自動識別貨幣抽獎機如何制作

我的世界是一款很經典的沙盒類游戲&#xff0c;在游戲中紅石和命令方塊是這部作品的核心&#xff0c;可以制作很多裝備和道具&#xff0c;下面給大家分享下我的世界自動識別貨幣抽獎機如何制作&#xff0c;希望對大家有所幫助。自動識別貨幣抽獎機制作方法廢話不多說,(貌似一句…

Java并發編程中volatile實現過程詳細解析

2019獨角獸企業重金招聘Python工程師標準>>> 首先并發編程有三大特性&#xff1a; 可見性&#xff0c;有序性&#xff0c;原子性。volatile關鍵字實現了前面兩個特性。那么它是如何實現這兩個特性的呢&#xff1f; 首先是可見性。可見性主要是讓緩存&#xff0c;直接…

《ASP.NET Core 6框架揭秘》實例演示[32]:錯誤頁面的N種呈現方式

由于ASP.NET是一個同時處理多個請求的Web應用框架&#xff0c;所以在處理某個請求過程中出現異常并不會導致整個應用的中止。出于安全方面的考量&#xff0c;為了避免敏感信息外泄&#xff0c;客戶端在默認情況下并不會得到詳細的出錯信息&#xff0c;這無疑會在開發過程中增加…

SpringMVC接受JSON參數詳解及常見錯誤總結我改

SpringMVC接受JSON參數詳解及常見錯誤總結 最近一段時間不想使用Session了&#xff0c;想感受一下Token這樣比較安全&#xff0c;穩健的方式&#xff0c;順便寫一個統一的接口給瀏覽器還有APP。所以把一個練手項目的前臺全部改成Ajax了&#xff0c;跳轉再使用SpringMVC控制轉發…

軟件定義存儲的定制化怎么走?

引言 當前&#xff0c;軟件定義存儲成為業內超高速增長的典型。有研究人員稱&#xff0c;從2014年到2019年&#xff0c;軟件定義存儲市場將從14億美元增長到62億美元以上&#xff0c;年復合增長率將達35%。軟件定義存儲所帶來的優勢顯而易見&#xff0c;但是對于企業來說&#…

Golang并發模型:合理退出并發協程

goroutine作為Golang并發的核心&#xff0c;我們不僅要關注它們的創建和管理&#xff0c;當然還要關注如何合理的退出這些協程&#xff0c;不&#xff08;合理&#xff09;退出不然可能會造成阻塞、panic、程序行為異常、數據結果不正確等問題。這篇文章介紹&#xff0c;如何合…