Vue.js 源碼實現


Vue.js 代碼實現

檢驗學習效果的最好方法就是自己造輪子。最近在學習Vue源碼,寫了一個迷你版vue,實現數據響應式。從step1到step3.2,是開發步驟和實現思路,每一步都可以獨立運行。

代碼地址:https://github.com/dora-zc/miniature-vue

目錄結構

├── README.md
├── step0
│ └── defineProperty_test.html
├── step1
│ ├── XVue.js
│ └── index.html
├── step2
│ ├── XVue.js
│ └── index.html
├── step3.1
│ ├── XVue.js
│ ├── compile.js
│ └── index.html
└── step3.2
​ ├── XVue.js
​ ├── compile.js
​ └── index.html

以上每個step文件夾對應下面的每一步驟,代表了代碼實現的順序,每個文件夾下的代碼都可以獨立運行。

1. 步驟一

創建XVue.js。

創建Vue類,通過Observer劫持監聽所有屬性。

observe函數的作用:遞歸遍歷data選項,它當中的defineReactive函數為data中每一個key定義getter和setter,達到數據劫持的目的。

步驟一對應代碼目錄:step1

2. 步驟二

處理頁面上的<div>{{msg}}</div>,也就是收集依賴,當msg的值發生變化時,視圖需要做出相應的變化。因此需要創建依賴管理器,把所有依賴保存起來,當數據發生變化的時候再去更新對應的依賴。

2.1 創建Dep類

Dep負責將視圖中的所有依賴收集管理,包括依賴添加和派發通知

1- 在Dep類中創建數組deps=[],用來存放Watcher的實例

2-創建addDep方法,添加Watcher

3-創建notify方法,通知所有的Wather執行更新。遍歷deps數組,調用每個Wather的更新方法

2.2 創建監聽器Watcher類

Watcher是具體的更新執行者。

1-將當前Watcher實例添加到Dep.target上。

Dep.target = this

之后在get時,就能通過Dep.target拿到當前Watcher的實例。

2-創建update方法

3-set方法中,調用dep.notify,讓依賴管理器通知更新,則所有的Watcher會執行update方法

那么問題來了:Watcher在什么時候收集最合適?

在defineReactive函數的get方法中,get方法觸發時,把Watcher放進Dep.target中。

那么問題又來了:為什么是在get方法中呢?

因為在掃描視圖中的依賴時,如果掃描到<div>{{msg}}</div>,此時一定會去訪問msg的值,就會觸發get。一旦get被觸發,就能將Watcher放進dep中,實現依賴收集的目的。所以get是一個合適的時間點。

代碼測試:在get中輸出dep.deps,如果Watcher已經放進去了,並且控制台打印出Watcher中的update方法中的log,說明這一步操作成功了。

至此,已經完成的工作如下:

步驟二對應代碼目錄:step2

現在,Watcher發生變化時,視圖還沒有更新,下面我們將要完成視圖更新的操作。

首先,需要Compile對界面模板解析指令,進行編譯,編譯的階段實際是創建Watcher的階段。Watcher是由編譯器創建的。編譯器在做依賴收集的時候,順便把Watcher創建了。Watcher在創建的時候,立刻就能知道它將來要更新的是誰,它應該被誰管理,它發生變化以后值應該是什么。於是Watcher就知道調誰(Updater去做更新了)。

3.步驟三

創建compile.js,用於掃描模板中所有依賴(指令、插值、綁定、事件…),創建更新函數和Watcher

3.1 掃描模板

1-創建編譯器Compile類,接收兩個參數,el(宿主元素或選擇器)和vm(當前vue實例)

2-創建node2Fragment函數,將dom節點( $el )截成代碼塊( 轉換為Fragment )來處理,而不是直接做dom操作,提高執行效率

3-創建compile函數,執行編譯( 將模板中的動態值替換為真實的值 ),傳入代碼塊

4-將生成的結果追加至宿主元素

3.1.1 node2Fragment函數

創建一個新的fragment,將原生節點移動至fragment

返回fragment,傳給編譯函數進行編譯

3.1.2 compile函數

獲取所有的孩子節點,進行遍歷,判斷節點類型,並作出相應的判斷

處理元素節點

處理文本節點( 只處理{{msg}} 這種情況,其他的全部不處理)

...其他的節點類型暫時不判斷了

遍歷可能存在的子節點,往下遞歸

下面是compile函數中的兩個核心方法

1-compileElement方法:編譯元素節點

<div v-text="test" @click="onClick">{{msg}}</div>

拿到所有屬性名稱,進行遍歷

2-compileText方法:編譯文本節點

代碼測試:

在XVue constructor中,創建編譯器實例,將宿主元素el和當前vue實例作為參數傳入。

如果compileElement和compileText兩個函數能觸發,控制台打印出"開始編譯元素節點"和"開始編譯文本節點",則說明功能正常,可以繼續讓下走了。

對應代碼:step3.1

3.2 編譯元素節點和文本節點,並創建更新函數

3.2.1 編譯元素節點compileElement方法實現

獲取節點所有屬性,進行遍歷。判斷指令和事件,已經相應的處理方法。

指令只試着處理v-text,v-html,v-model三個,其他的暫不處理

v-model:雙向綁定還需要處理視圖對模型的更新

3.2.2 創建更新器函數

更新器函數:接收四個參數,node,vm,exp,dir(指令)

針對指令的更新器主要是在做dom操作

在更新器函數中創建Watcher實例,當Watcher監聽到變化的時候,就能觸發視圖的更新。

至此,全部代碼已經完成,雙向數據綁定順利實現!

對應代碼:step3.2

Vue.js 工作機制

初始化

在new Vue()之后,Vue會調用初始化函數,會初始化聲明周期、事件、props、methods、data、computed和watcher等。其中最重要的是通過Object.defineProperty設置setter和getter,用來實現響應式和依賴收集。

初始化之后會調用$.mount掛載組件。

編譯

編譯模塊分為三個階段:

1-parse

使用正則解析模板中的vue的指令、變量等等,形成抽象語法樹AST

2-optimize

標記一些靜態節點,用作后面的性能優化,在diff的時候直接略過

3-generate

把第一步生成的AST轉化為渲染函數 render function

響應式

這一塊是vue最核心的內容。初始化的時候通過defineProperty進行綁定,設置通知的機制,當編譯生成的渲染函數被實際渲染的時候,會觸發getter進行依賴收集,在數據變化的時候,觸發setter進行更新。

虛擬dom

虛擬dom是由react首創,Vue2開始支持,就是用JavaScript對象來描述dom結構,數據修改的時候,我們先修改虛擬dom中的數據,然后數組做diff算法,最后再匯總所有的diff,力求做最少的dom操作,畢竟js里對比很快,而真實的dom操作太慢了。

<div name="小菠蘿" style="color:red" @click="xx">
   <a>click me</a>
</div>
// vdom
{
   tag:'div',
   props:{
      name:'小菠蘿',
      style: {color:red},
      onClick:xx
   },
   children:[
      {
        tag:'a',
        text:'click me'
      }
   ]
}

更新視圖

數據修改觸發setter,然后監聽器會通知進行修改,通過對比兩個dom樹,得到改變的地方,就是patch,然后只需要把這些差異修改即可。

編譯

compile的核心邏輯是獲取dom,遍歷dom,獲取{{}}格式的變量,以及每個dom的屬性,截取v-和@開頭的部分來設置響應式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM