vue早期源碼學習系列之二:如何監聽一個數組的變化
本系列更多的文章請移步我的博客:GitHub - youngwind/blog: 梁少峰的個人博客 或者掃描下方二維碼,關注我的公眾號「前端控」,歡迎交流討論!
前言
繼上一篇 #84 ,文末我們提到另一個問題如何監聽數組的變化?,今天我們就來解決這個問題。我們先來看一眼vue官方說明文檔?
Vue.js 包裝了被觀察數組的變異方法,故它們能觸發視圖更新。被包裝的方法有:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
出處:https://vuejs.org.cn/guide/list.html#u53D8_u5F02_u65B9_u6CD5
Vue.js 不能檢測到下面數組變化:
- 直接用索引設置元素,如 vm.items[0] = {};
- 修改數據的長度,如 vm.items.length = 0。
出處:https://vuejs.org.cn/guide/list.html#問題
為什麼說明文檔中提到只有某些特定方法才能觸發視圖更新呢?我們可以從vue的源碼中找到答案。
奇技淫巧
這次checkout的版本更上次一樣,都是這個位置。
相關的源碼是這兩個地方。1. observe/array-augmentations.js2. observe/observer.js // line 38
整體思路是什麼呢? → 通過重新包裝數據中數組的push、pop等常用方法。注意,這裡重新包裝的只是數據數組(也就是我們要監聽的數組,也就是vue實例中擁有的data數據)的方法,而不是改變了js原生Array中的原型方法。
為什麼不能修改原生Array的原型方法呢?這道理很顯然,因為我們是在寫一個框架,而非一個應用,我們不應該過多地影響全局。如果你真得採取了這種糟糕的方法,想像以下場景:」你在一個應用中使用了vue,但是你在vue實例以外定義了一些數組,你改變這些與vue無關的數組的時候,居然觸發了vue的方法!!「這能忍??
代碼實現
const aryMethods = [push, pop, shift, unshift, splice, sort, reverse];nconst arrayAugmentations = [];nnaryMethods.forEach((method)=> {nn // 這裡是原生Array的原型方法n let original = Array.prototype[method];nn // 將push, pop等封裝好的方法定義在對象arrayAugmentations的屬性上n // 注意:是屬性而非原型屬性n arrayAugmentations[method] = function () {n console.log(我被改變啦!);nn // 調用對應的原生方法並返回結果n return original.apply(this, arguments);n };nn});nnlet list = [a, b, c];n// 將我們要監聽的數組的原型指針指向上面定義的空數組對象n// 別忘了這個空數組的屬性上定義了我們封裝好的push等方法nlist.__proto__ = arrayAugmentations;nlist.push(d); // 我被改變啦! 4nn// 這裡的list2沒有被重新定義原型指針,所以就正常輸出nlet list2 = [a, b, c];nlist2.push(d); // 4n
PS:如果不能理解這裡的proto,請翻看《Javascript的高級程序設計》第148頁,以及參看這個答案,多看幾遍你就懂了。(吐槽:每次碰到js原型都不好描述.....)
作者寫得有問題?
ok,目前為止我們已經實現了如何監聽數組的變化了。
但是,我們仔細回想一下,難道只能通過作者那樣的方法來實現嗎?不覺得直接重新定義proto指針有點奇怪嗎?有其他實現的方法嗎?
我們回到最開始的目標:對於某些特定的數組(數據數組),他們的push等方法與原生Array的push方法不一樣,但是其他的又都一樣。這不就是經典的繼承問題嗎? 子類和父類很像,但是呢,子類有點地方又跟父類不同我們只需要繼承父類,然後重寫子類的prototype中的push方法不就可以了嗎?紅寶書告訴我們組合繼承才是最常用的繼承方法啊!(請參考紅寶書第168頁)難道是作者糊塗了?(想到這兒,我心裡一陣竊喜,拜讀了作者的代碼這麼久,終於讓我發現一個bug了,不過好像也算不上是bug)
廢話不多說,我趕緊自己用組合繼承實現了一下。function FakeArray() {n Array.call(this,arguments);n}nnFakeArray.prototype = [];nFakeArray.prototype.constructor = FakeArray;nnFakeArray.prototype.push = function () {n console.log(我被改變啦);n return Array.prototype.push.call(this,arguments);n};nnlet list = [a,b,c];nnlet fakeList = new FakeArray(list);n
結果如下圖所示
雖然我成功地重新定義push方法,但是為什麼fakeList是一個空對象呢?
原因是:構造函數默認返回的本來就是this對象,這是一個對象,而非數組。Array.call(this,arguments);這個語句返回的才是數組。那麼我們能不能將Array.call(this,arguments);直接return出來呢?
不能。原因有兩個:- 如果我們return這個返回的數組,這個數組是由原生的Array構造出來的,所以它的push等方法依然是原生數組的方法,無法到達重寫的目的。
- 如果我們return這個返回的數組,其實最後fakeList === [[[a,b,c]]],它變成了一個數組的數組的數組,因為list本身是一個數組,arguments用封裝了一層數組,new Array本身接收數組作為參數的時候本來就會返回包裹這個數組的數組,new Array([a, b]) === [[a, b]],所以就變成三層數組了。
shit.....太麻煩了!看來還是沒有辦法通過組合繼承的模式來實現一開始的目標。(寫到這兒,我心裡默念:還是老司機厲害啊!我還是太年輕了......)
後話
目前為止,我們已經知道如何監聽對象和數組的變化了,下一步應該做什麼呢?
答案是:實現一個watch庫什麼是watch庫?你看一下這個就知道了。推薦閱讀:
※為什麼越來越多的大型網站開始放棄原來自己開發的論壇,開始啟用discuz呢?
※前端每周清單第3期:Instant App將至,WebAssembly將獲默認支持,PWA實踐漸增
※「每日一題」Fetch API 是什麼?能代替 AJAX 嗎?
※怎麼看待一個阿里工作四年出來的,但卻連children()這樣的方法都不知道是什麼意思的前端?
※支持ie8的mvvm框架比較?