vue早期源碼學習系列之二:如何監聽一個數組的變化

本系列更多的文章請移步我的博客:GitHub - youngwind/blog: 梁少峰的個人博客 或者掃描下方二維碼,關注我的公眾號「前端控」,歡迎交流討論!

前言

繼上一篇 #84 ,文末我們提到另一個問題如何監聽數組的變化?,今天我們就來解決這個問題。我們先來看一眼vue官方說明文檔?

Vue.js 包裝了被觀察數組的變異方法,故它們能觸發視圖更新。被包裝的方法有:

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

出處:vuejs.org.cn/guide/list

Vue.js 不能檢測到下面數組變化:

  • 直接用索引設置元素,如 vm.items[0] = {};
  • 修改數據的長度,如 vm.items.length = 0。

出處:https://vuejs.org.cn/guide/list.html#問題

為什麼說明文檔中提到只有某些特定方法才能觸發視圖更新呢?我們可以從vue的源碼中找到答案。

奇技淫巧

這次checkout的版本更上次一樣,都是這個位置。

相關的源碼是這兩個地方。

1. observe/array-augmentations.js

2. 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出來呢?

不能。原因有兩個:

  1. 如果我們return這個返回的數組,這個數組是由原生的Array構造出來的,所以它的push等方法依然是原生數組的方法,無法到達重寫的目的。
  2. 如果我們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框架比較?

TAG:Vuejs | 前端框架 | 前端开发 |