一步一步實現Vue資料繫結

Ace7523發表於2020-02-19

一步一步實現Vue資料繫結

嗯,直接進入正題吧。不知道大家是否有和我一樣的學習習慣,就是一步一步的從無到有的去實現一個原理。就好像升級打怪,裝備一點一點的升級才好玩。我把每次增加的程式碼,對應在git的提交中, 畢竟很多檔案,直接在一篇文章中很難描述清楚。

好難寫,有時候自己明白了是一回事,再寫出來就是另一回事了

曬兩張老照片

一步一步實現Vue資料繫結
一步一步實現Vue資料繫結

當時準備了大約一週多,在小組內分享,也算是記錄在上家公司的高光時刻吧,可惜當時很對想法都留在了紙上,於是這次記錄在掘金中吧。

0 目錄

  • 1 準備工作
  • 2 vue 物件資料劫持
  • 3 vue 陣列資料劫持
  • 4 模板編譯
  • 5 釋出訂閱模式
  • 6 依賴收集
  • ...
  • 最後 我的聯絡方式

1 準備工作

這階段程式碼git地址如下:github.com/Ace7523/vue… 因為我學習的習慣就是一步一步的從簡到繁增加功能,大家看時候對應的看提交版本的,這部分程式碼對應的是第一次commit。

webpack新建一個vue專案,相信這個大家都知道了,我就無須贅述。我會把每個階段的程式碼git地址提供出來。需要強調一點就是改了這裡:

一步一步實現Vue資料繫結

作用是 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資料繫結

2 利用 Object.defineProperty 對上圖中data方法返回的物件進行資料劫持,先來看一下劫持之前的資料表徵:

一步一步實現Vue資料繫結

寫資料劫持的方法

一步一步實現Vue資料繫結

這段程式碼也很容易理解,就是利用了一下Object.defineProperty,把物件的每個屬性的get和set重新寫了一遍。這麼做之後,我們再列印data資料看一下:

一步一步實現Vue資料繫結

可以看出,name age testObj,分別都擁有了get和set屬性,那麼這個是幹嘛用的呢?再來修改一點程式碼,如下

一步一步實現Vue資料繫結

執行一下下面的語句就知道了(解釋一下_data, data的值掛在的vm例項的_data上)

console.log( vm._data.name )
複製程式碼

列印結果如下:

一步一步實現Vue資料繫結

這就表明了,在想要讀取name屬性的值,之前,先列印的語句,也就是,實現資料劫持。同理,設定也是一樣。

3 完善上述的劫持,因為物件內部還包含物件的這種情況程式碼中沒有實現

一步一步實現Vue資料繫結

這次列印結果如下,testObj物件內部的物件,也擁有的set 和 get

一步一步實現Vue資料繫結

4 把vm._data 代理到 vm 上,一邊以後取值直接 vm.name 拿到的就是vm._data.name

就是通過這樣一個方法實現的,大家自己看下就好

一步一步實現Vue資料繫結

5 小結

這階段程式碼不難,其實分開一步步來看的話,每一階段都不難,難得是,後面說到依賴收集的時候,不記得這段的邏輯了,所以一定要好好消化。

3 vue 陣列資料劫持

這階段程式碼git地址如下:github.com/Ace7523/vue… 具體對應的是第二次commit。

為什麼要對陣列做一次特殊的劫持呢?因為就目前的物件劫持成程度,無法監聽到陣列push了一個元素,舉個例子:如下圖測試一下

一步一步實現Vue資料繫結

看看列印結果:

一步一步實現Vue資料繫結
從這個結果中可以看出,只觸發了一次get,沒有set。 get是在vm.arr , 這個過程觸發的,因為讀取arr的值就會觸發get。 列印出的4是陣列的push方法返回的值。(ps:陣列push返回變化後的陣列長度)

那這種情況肯定不是我們想要的,比如頁面根據陣列中元素個數來渲染按鈕,但是陣列中元素是介面動態返回的,雖然陣列已經是劫持過的,但是新增元素時候,我們不知道這個陣列新增了,那就不行了。

修改如下 在資料劫持的Observer 建構函式中 針對陣列重新封裝劫持方法

一步一步實現Vue資料繫結

arrayMethods observerArray這兩個方法寫在了另外一個array.js檔案中。稍微解釋一下,就是說在進行資料劫持的時候,發現是陣列的話,就先改變陣列的原型指向,然後再對這個陣列進行劫持。下面用程式碼來解釋

其實更通俗一點的說,就是要重寫陣列的push等方法,要求既要保留原來的push方法全部,又要有所增加。(限定只是被劫持的陣列元素的原型方法重新寫,而不是直接改變Array.prototype) 那要怎麼實現?就像下面那樣實現

一步一步實現Vue資料繫結

配合上邊圖的修改,首先,在劫持物件為陣列的時候,陣列的原型指向已經修改了

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資料繫結

也許一些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資料繫結

因為inserted是陣列,所以對他進行遍歷,要對新增加的每一項都要重新進行劫持。 好了,來增加一些程式碼來測試一下本部分增加的功能點吧。

一步一步實現Vue資料繫結

一步一步實現Vue資料繫結

執行結果如下

一步一步實現Vue資料繫結

小結 這一部分是增加了對於初始化vue例項中data引數中的陣列進行劫持。 其實也不難,但是卻很難用文章的形式把這部分講出來,不過我的這部分程式碼地址也貼出來了,大家down下來自己看看就會清楚其中的原理。

這裡提一下 vue 的資料劫持是有缺點的 1 不能對陣列的索引進行監控 2 arr.length = 0 這種情況也沒有監控到。 反正有個印象就行。

4 模板編譯

這小節的程式碼對應為第三次提交。

截止到目前為止,都是在歲new MyVue例項的資料做一些操作,並沒有把資料渲染到頁面中,所以這一節主要完成把data資料渲染到頁面中,編譯不是這邊文章的重點,後面會單獨一篇來專門寫編譯。

1 編譯是什麼,下面的圖就可以很直觀的表明了

一步一步實現Vue資料繫結
說白了就是把data中的資料和模板結合一下。如果不做一些特殊處理的話,{{name}} 是不會被翻譯的。

第一步,新增模板

一步一步實現Vue資料繫結

第二步,修改初始化邏輯,有模板的話,執行渲染邏輯

一步一步實現Vue資料繫結

第三步,vm._update()

一步一步實現Vue資料繫結
解釋一下這部分程式碼,先建立文件碎片,然後在文件碎片中執行編譯過程,最後再把文件碎片塞回dom中。 這麼繞一圈的意義,不直接操作dom,那很奢侈。

第四步,編譯。 也就是compiler(node,vm)的具體實現。

一步一步實現Vue資料繫結
解釋程式碼: 根據節點型別,做不同處理。 如果是元素節點,則繼續遞迴的編譯此節點。 如果是文字節點,呼叫文字節點的編譯方法。 (編譯過程遠比這複雜,只是本文重點不在這)

第五步,替換文字,也就是util.compilerText() 的具體實現。

一步一步實現Vue資料繫結
解釋程式碼,雖然看似比較繞,不過這裡也先不展開說明了,後面講到依賴收集的時候,這裡還會繼續修改。 總之,這部分程式碼,大家知道,就是利用正則,匹配到 {{name}} 然後在vm例項中讀取vm.name的值,賦值即可。 必須要值得注意的一點就是,這裡是name的第一次讀取。 再聯絡一下上面講到的資料劫持,,,這個第一次讀取,肯定是個很大的伏筆

5 釋出訂閱模式

到了這一部分要講釋出訂閱模式,先單獨講一下發布訂閱模式是什麼,然後再對之前程式碼做修改。對應程式碼為 第四次commit。

1 什麼是釋出訂閱模式?

一步一步實現Vue資料繫結
其實這幾行程式碼就實現了簡單釋出訂閱,Dep建構函式,他的例項上擁有subs屬性和id,id不用解釋。 subs用來存放watcher的,同時dep例項上還有一個notify方法,該方法執行,就會讓所有的watcher執行他自己的update方法。

換個通俗一點的解釋, 比如我有個微信公眾號,那麼假如有10個朋友關注了我,後面某一天我發了文章,我告訴了所有訂閱我的朋友,讓他們去回覆個1,回覆後給他們發紅包。 這就是釋出訂閱嘛,有訂閱者,被訂閱改變後的通知訂閱者。

new個例項來演示上述程式碼吧

一步一步實現Vue資料繫結
程式碼執行結果如下

一步一步實現Vue資料繫結
也,不難理解吧,就不多囉嗦了

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資料繫結
watcher.js 中 主要就是Watcher建構函式,在哪裡用這個建構函式呢?修改如下:

一步一步實現Vue資料繫結
也就是在頁面第一次渲染的時候,new Watcher,注意上一版程式碼,這裡是直接讓updateComponent() 這個方法執行。

回到Watcher建構函式中,這裡我們先記住,第二個引數,是個函式,這個函式,就是編譯模板的那個函式,把data中的資料去到,替換{{name}}中的值的那個函式。 那這個函式只要一執行,就會觸發到vm.name 的 get 攔截器,對吧。這裡很重要。

然後接著看, Watcher建構函式中, get() 方法內,有一個pushTarget(this), 這行程式碼的意思是 把 this 賦值 給Dep.target。 this是什麼? this是建構函式中的this,也就值new Watcher 這個例項。

接著看, 回到最開始的資料劫持那裡 ,做如下修改

一步一步實現Vue資料繫結
這段程式碼,就是對data中屬性做資料劫持的程式碼。 就是把data中的 name age 等,變成響應式的程式碼。 這段程式碼第一次被執行是什麼時候? 答 首次編譯的時候。 也就是剛才的watcher中 get() 方法第一次執行的時候。 在把name屬性變成響應式時, new 了一個 Dep, 同理 給age屬性變成響應式時,也new 了一個Dep 。

重點看這裡

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資料繫結

相關文章