![一步一步實現Vue資料繫結](https://i.iter01.com/images/abee872a8f1fb0360a3158c970f3a63bb03de5f70e2cdd953985048e53246e21.jpg)
嗯,直接進入正題吧。不知道大家是否有和我一樣的學習習慣,就是一步一步的從無到有的去實現一個原理。就好像升級打怪,裝備一點一點的升級才好玩。我把每次增加的程式碼,對應在git的提交中, 畢竟很多檔案,直接在一篇文章中很難描述清楚。
好難寫,有時候自己明白了是一回事,再寫出來就是另一回事了
曬兩張老照片
![一步一步實現Vue資料繫結](https://i.iter01.com/images/3588cd32aa2b1bc1da0267ec96e13686fa5b5713a0884dea351f8cdf2ab4fbb4.jpg)
![一步一步實現Vue資料繫結](https://i.iter01.com/images/83adbd96830c6449eff431a0e0f5df41cde6d2e1de33ad8af2a194a744747f05.jpg)
當時準備了大約一週多,在小組內分享,也算是記錄在上家公司的高光時刻吧,可惜當時很對想法都留在了紙上,於是這次記錄在掘金中吧。
0 目錄
- 1 準備工作
- 2 vue 物件資料劫持
- 3 vue 陣列資料劫持
- 4 模板編譯
- 5 釋出訂閱模式
- 6 依賴收集
- ...
- 最後 我的聯絡方式
1 準備工作
這階段程式碼git地址如下:github.com/Ace7523/vue… 因為我學習的習慣就是一步一步的從簡到繁增加功能,大家看時候對應的看提交版本的,這部分程式碼對應的是第一次commit。
webpack新建一個vue專案,相信這個大家都知道了,我就無須贅述。我會把每個階段的程式碼git地址提供出來。需要強調一點就是改了這裡:
![一步一步實現Vue資料繫結](https://i.iter01.com/images/f537941352f28e94778cf7461a8cd7e6711b03feeca8746e1db89154bfe31ba6.png)
作用是 import 引用依賴時候優先在指定目錄下尋找,找不到的話再去node_modules中尋找。
再順帶提一點,es6的export 和 export default 的區別是什麼,因為程式碼中有兩種寫法,不要弄混哦
答: export default 一個檔案只能有一個, 引用時候 import XXX from ...
export 一個檔案中可以有多個,引用時候用 import { XXX } from ...
2 vue 物件資料劫持
這階段程式碼git地址如下:github.com/Ace7523/vue… 對應第一次commit
先來提兩個問題,1,vue是資料驅動的這句話怎麼理解? 2,如何實現資料驅動?
答: 資料驅動我個人理解是,資料優先,只要資料變了,頁面就跟著變化,而不用再去想著這個資料應該和頁面中哪個dom位置相關,再手動去改。
答:對資料進行劫持,比如一個var a = 10 ,當讀取a的值時候,知道在讀取,當給a賦值操作時候,又知道在賦值。這就是對資料的劫持。
因為完整程式碼地址我都有提供,所以就截圖來看關鍵的點
1 建立自己的vue例項
![一步一步實現Vue資料繫結](https://i.iter01.com/images/4c4a9a1bd759835ac31e071d5be7c369c0f7be2ccd582c1449988b18f3b49d68.png)
2 利用 Object.defineProperty 對上圖中data方法返回的物件進行資料劫持,先來看一下劫持之前的資料表徵:
![一步一步實現Vue資料繫結](https://i.iter01.com/images/4417bc6dbfef5822f3ca7053860e6f8a8b8fafb317de592243476644ddbc11fb.png)
寫資料劫持的方法
![一步一步實現Vue資料繫結](https://i.iter01.com/images/ed8aa37924588553a87e34f01396094958230dbef76f9c409b51fd7b7912fbc2.png)
這段程式碼也很容易理解,就是利用了一下Object.defineProperty,把物件的每個屬性的get和set重新寫了一遍。這麼做之後,我們再列印data資料看一下:
![一步一步實現Vue資料繫結](https://i.iter01.com/images/47c8b141af18675c021dc0bb94bd2b6c37bbfef2f0f81120e78ad23c93602e9c.png)
可以看出,name age testObj,分別都擁有了get和set屬性,那麼這個是幹嘛用的呢?再來修改一點程式碼,如下
![一步一步實現Vue資料繫結](https://i.iter01.com/images/f34fa022c48bf3d9694a942f7caa113174ce876231551cf91a50140be93f6bc2.png)
執行一下下面的語句就知道了(解釋一下_data, data的值掛在的vm例項的_data上)
console.log( vm._data.name )
複製程式碼
列印結果如下:
![一步一步實現Vue資料繫結](https://i.iter01.com/images/6d26c53e6f12e71ef512cb3344d372e69b46e8357c1884e2c72978e2e5b50bb9.png)
這就表明了,在想要讀取name屬性的值,之前,先列印的語句,也就是,實現資料劫持。同理,設定也是一樣。
3 完善上述的劫持,因為物件內部還包含物件的這種情況程式碼中沒有實現
![一步一步實現Vue資料繫結](https://i.iter01.com/images/c537366678ecacdb77284203529196bf82d894dd9d60509083868549bd02d531.png)
這次列印結果如下,testObj物件內部的物件,也擁有的set 和 get
![一步一步實現Vue資料繫結](https://i.iter01.com/images/6b110d9e5196f61d48fcd0e6075d5f54ce086ea2e76f8d367a2a692c162996f6.png)
4 把vm._data 代理到 vm 上,一邊以後取值直接 vm.name 拿到的就是vm._data.name
就是通過這樣一個方法實現的,大家自己看下就好
![一步一步實現Vue資料繫結](https://i.iter01.com/images/599fa06ae6b9c8ab9111a9d1e801da619c51af5d78dfcbaa858494e8605f3dd2.png)
5 小結
這階段程式碼不難,其實分開一步步來看的話,每一階段都不難,難得是,後面說到依賴收集的時候,不記得這段的邏輯了,所以一定要好好消化。
3 vue 陣列資料劫持
這階段程式碼git地址如下:github.com/Ace7523/vue… 具體對應的是第二次commit。
為什麼要對陣列做一次特殊的劫持呢?因為就目前的物件劫持成程度,無法監聽到陣列push了一個元素,舉個例子:如下圖測試一下
![一步一步實現Vue資料繫結](https://i.iter01.com/images/7f08f2e88e268063f75b74a9f7b9663acca266e5fceb0210db2da3ab70bd5b73.png)
看看列印結果:
![一步一步實現Vue資料繫結](https://i.iter01.com/images/27b644d2e5ec07e7acfc0445c81a723d0e9264b3666d5242d939cb636a805e78.png)
那這種情況肯定不是我們想要的,比如頁面根據陣列中元素個數來渲染按鈕,但是陣列中元素是介面動態返回的,雖然陣列已經是劫持過的,但是新增元素時候,我們不知道這個陣列新增了,那就不行了。
修改如下 在資料劫持的Observer 建構函式中 針對陣列重新封裝劫持方法
![一步一步實現Vue資料繫結](https://i.iter01.com/images/85ed0dbb23366321b385cd16eb188e9730293ef6cb50da626b9f421db58a07fa.png)
arrayMethods observerArray這兩個方法寫在了另外一個array.js檔案中。稍微解釋一下,就是說在進行資料劫持的時候,發現是陣列的話,就先改變陣列的原型指向,然後再對這個陣列進行劫持。下面用程式碼來解釋
其實更通俗一點的說,就是要重寫陣列的push等方法,要求既要保留原來的push方法全部,又要有所增加。(限定只是被劫持的陣列元素的原型方法重新寫,而不是直接改變Array.prototype) 那要怎麼實現?就像下面那樣實現
![一步一步實現Vue資料繫結](https://i.iter01.com/images/7a8d29f03dd70520dc6ced52d62ea258afee92814e71f140e5ad1d77a2c6f6a9.png)
配合上邊圖的修改,首先,在劫持物件為陣列的時候,陣列的原型指向已經修改了
data.__proto__= arrayMethods
複製程式碼
而這個arrayMethods是繼承了原生陣列的原型,所以,該有的方法都會有,此時我們只需把需要改變的方法修改即可。圖中的7個方法會改變原陣列,所以,要對這7個方法重新加工一下。就上圖來解釋,其實上圖對於那7個方法,可以還一點都沒有修改,但是留了位置,可以在那圖中17行位置新增一些操作。 其實這也就是所謂的切片程式設計,即保留原有功能的基礎之上,新增新的功能。
再囉嗦幾句,因為怕這裡會有朋友還不理解。 從劫持那裡說起,當判斷到要劫持的資料型別是陣列的時候,就先改變了這個被劫持的陣列的原型,arr為例,當劫持後,再次執行arr.push的時候,這個push方法不是在原生陣列的原型鏈中獲取的,而是在我們寫的這個array.js中獲取的,因為此時是這樣執行的
arr.push() 就是 arrayMethods.push()
arrayMethods.push = function(...args){
let r = oldArrayProtoMethods[push].apply(this,args);
//
// 這裡就是要增加的功能部分
//
return r
}
複製程式碼
估計這麼寫一下就明朗多了吧,apply不明白的話,建議補一補js基礎。 然後我們接著完善這個方法,注意一下apply的第二個引數是陣列。看程式碼
![一步一步實現Vue資料繫結](https://i.iter01.com/images/addc996bcb77512c538ef188e915bc8e2e05efcd8913768007f4663990fbdcd0.png)
也許一些js基礎不牢固的小夥伴又要問了,...args是什麼,apply中的args又是什麼。 答:... 用在函式引數中,就是剩餘運算子,用在不知道引數具體有幾個的時候,如
fun(...args){
console.log(args)
}
fun(1,2,3,4)
// 列印結果是 [1,2,3,4]
複製程式碼
所以還是以push為例子,push(4),那麼此時 inserted = [4], 就表示對原來的那個[1,2,3]的陣列arr,改變arr,讓arr變成了[1,2,3,4] , 那麼我們肯定要對這個新增加的元素 進行 劫持,這才能夠是陣列一直都是響應式的。也就是增加如下程式碼
![一步一步實現Vue資料繫結](https://i.iter01.com/images/9f663634ccdc87484811dfa6a8bf371440a925604bd91ee5dfeff4a485accd6c.png)
因為inserted是陣列,所以對他進行遍歷,要對新增加的每一項都要重新進行劫持。 好了,來增加一些程式碼來測試一下本部分增加的功能點吧。
![一步一步實現Vue資料繫結](https://i.iter01.com/images/0baea90d5324c6dc0d91893d0fab871f4052d2d91fb77dace59419c6ef163b8d.png)
![一步一步實現Vue資料繫結](https://i.iter01.com/images/078c9911c6b968f7cf953d84041301a805977b6e910f3c86618f2dca636f085c.png)
執行結果如下
![一步一步實現Vue資料繫結](https://i.iter01.com/images/a99a96e3faeab9b65821c72f588e550306aa00d64d5a7c071499cd5275ec9527.png)
小結 這一部分是增加了對於初始化vue例項中data引數中的陣列進行劫持。 其實也不難,但是卻很難用文章的形式把這部分講出來,不過我的這部分程式碼地址也貼出來了,大家down下來自己看看就會清楚其中的原理。
這裡提一下 vue 的資料劫持是有缺點的 1 不能對陣列的索引進行監控 2 arr.length = 0 這種情況也沒有監控到。 反正有個印象就行。
4 模板編譯
這小節的程式碼對應為第三次提交。
截止到目前為止,都是在歲new MyVue例項的資料做一些操作,並沒有把資料渲染到頁面中,所以這一節主要完成把data資料渲染到頁面中,編譯不是這邊文章的重點,後面會單獨一篇來專門寫編譯。
1 編譯是什麼,下面的圖就可以很直觀的表明了
![一步一步實現Vue資料繫結](https://i.iter01.com/images/ace337058dc5483fce7891d33494905cda796247c59202f4ee940e5102a62297.png)
第一步,新增模板
![一步一步實現Vue資料繫結](https://i.iter01.com/images/79cacbf2543b94659aaa43cd2401cee9373b8a71998486c0b4b475a86a586031.png)
第二步,修改初始化邏輯,有模板的話,執行渲染邏輯
![一步一步實現Vue資料繫結](https://i.iter01.com/images/c2fa57f78f4f795ec6d6d167930e5f58b8eb3e66c8d342f23390a637af057b27.png)
第三步,vm._update()
![一步一步實現Vue資料繫結](https://i.iter01.com/images/b3f9958a62db883b770c438ba8745f56da331be74aa17a9b81dd6586d03949b7.png)
第四步,編譯。 也就是compiler(node,vm)的具體實現。
![一步一步實現Vue資料繫結](https://i.iter01.com/images/44deb831f2ab22a1de3412dba499639ec54ec4d85b682f6561585fdb734429eb.png)
第五步,替換文字,也就是util.compilerText() 的具體實現。
![一步一步實現Vue資料繫結](https://i.iter01.com/images/30789ea91906f037f29353dc7870b3ce52b4970a3d28b4432b8f9f3299ea8f43.png)
5 釋出訂閱模式
到了這一部分要講釋出訂閱模式,先單獨講一下發布訂閱模式是什麼,然後再對之前程式碼做修改。對應程式碼為 第四次commit。
1 什麼是釋出訂閱模式?
![一步一步實現Vue資料繫結](https://i.iter01.com/images/00241d5a930629f2c69845300b2cd3642a86157e41b41f8cbd4d2d24f010aae5.png)
換個通俗一點的解釋, 比如我有個微信公眾號,那麼假如有10個朋友關注了我,後面某一天我發了文章,我告訴了所有訂閱我的朋友,讓他們去回覆個1,回覆後給他們發紅包。 這就是釋出訂閱嘛,有訂閱者,被訂閱改變後的通知訂閱者。
new個例項來演示上述程式碼吧
![一步一步實現Vue資料繫結](https://i.iter01.com/images/a15b806a8115e87ad5585283ba2ec1e4ec9a13b6fdf36019354a8339a3129342.png)
![一步一步實現Vue資料繫結](https://i.iter01.com/images/5b2dec8c19ab4e4d73f3285516c4d6b716996efb2efb7f679276133079b1bff2.png)
2 釋出訂閱模式的使用
先不上程式碼,要先理清一下這個釋出訂閱模式怎麼使用。 上面的程式碼理解了之後,我再來理一下他們之間的關係。
-
1 dep是什麼? dep是一個new出來例項, 這個例項可以存放watcher。
-
2 watcher是什麼? watcher就是一個物件吧,這個物件上擁有一個update方法。
-
3 watcher是觀察者沒錯,那誰是被觀察者? 答,上個例子中其實沒有被觀察者,因為dep只是一個用來存放watcher的例項,只有當一個物件和dep繫結了關係後,那麼這個物件才是被觀察者。 是這個道理吧,因為假如有一個物件 { num: 9} , 我們想做的是,當物件中的num數值發生變化時候,就做一些操作,所以我們需要一箇中間介質來存放所有的觀察者,如下
{ num:9, dep: { subs: [watcher1, watcher2, ...], notify: ()={...} } }
-
4 如何響應? 因為一些場景,我們改變了物件的num的值情況的話,再去觸發這個物件的dep屬性上的notify方法,就可以做到這個物件的所有觀察者給予響應。
3 vue 中是如何使用的?
答: vue是資料驅動的,例項中的data屬性,上邊掛在了很多資料,如本例子中name, age等,他們分別都是被觀察者,name擁有一個自己的dep,age也擁有一個dep,兩個他們是不同的例項。也就是說,當age的值改變後,就會讓age擁有的dep上的所有watcher觸發update,想必大家肯定知道這個update方法內部要做什麼了吧,沒錯,就是更新檢視。
6 依賴收集
即將到達最難理解的地方,也越發覺得這些很難用文字來描述了。 這部分對應的是程式碼的第5次提交。
如果看到這裡,我還是希望上面的內容已經全都好好吸收了,否則接下來的依賴收集是很容易暈的!!!
1 建立watcher.js
![一步一步實現Vue資料繫結](https://i.iter01.com/images/5e3a004709766f839a92f4f538b4211319ac4183be2ef315a04fb9575de67552.png)
![一步一步實現Vue資料繫結](https://i.iter01.com/images/47febc775a78f8c7180f6c449f685c8ef899b94aac1ff15cd81d8db01cde4ce1.png)
回到Watcher建構函式中,這裡我們先記住,第二個引數,是個函式,這個函式,就是編譯模板的那個函式,把data中的資料去到,替換{{name}}中的值的那個函式。 那這個函式只要一執行,就會觸發到vm.name 的 get 攔截器,對吧。這裡很重要。
然後接著看, Watcher建構函式中, get() 方法內,有一個pushTarget(this), 這行程式碼的意思是 把 this 賦值 給Dep.target。 this是什麼? this是建構函式中的this,也就值new Watcher 這個例項。
接著看, 回到最開始的資料劫持那裡 ,做如下修改
![一步一步實現Vue資料繫結](https://i.iter01.com/images/11d9bb77239bd2fc6425c97ba2e6e59e8374f47dbdca020548b9fc8a9fc7e659.png)
重點看這裡
if (Dep.target) {
dep.addSub(Dep.target)
}
複製程式碼
Dep.target是什麼 ? 就是那個new 的 watcher 吧 。 這個watcher被存在了dep中,這個dep又是和name屬性繫結的。當name的值發生變化後,也就是走到了這個name的set攔截方法中時,執行這個dep的notify,也就是讓那個watcher的update執行了,update中存放的是那個渲染模板的方法,所以,完成了頁面的更新。
這裡,,,很複雜,只看文字描述應該很晦澀,但是我又不知道怎麼來畫圖描述這一段流程。程式碼對應的是第五次提交,大家直接看程式碼吧。
我再 小結一下這部分的流程
- 1 建立vue例項,例項中有data屬性,如 data.name。 同時又有模板,{{name}}, 這毋庸置疑吧。
- 2 vue會把data.name 和 {{name}}進行編譯, 變成Ace7523
- 3 那麼在編譯的過程中,就會對data.name進行值的讀取。
- 4 data.name 在獲取值之前,已經寫好了資料攔截,並new了一個dep例項,這個例項用來存放watcher。
- 5 watcher中有重新渲染頁面的方法
- 6 data.name 只要被重新賦值,就會走到set攔截器中, 執行dep.notify()
- 7 notify就會讓watcher中的重新渲染頁面中的方法執行,也就完成了頁面的更新。
最後 我的聯絡方式
文字功底有限,有些地方感覺很難直接描述清晰,大家看的有不理解的地方,可以直接加我問我,我們一起探討,加我時註明是掘金就好。
![一步一步實現Vue資料繫結](https://i.iter01.com/images/4ddc936c8fc81f13d05dbfc9f62600f53219610ac8a7f69e924cf760290579fd.jpg)