背景
??Vue作為目前前端三劍客來說,基本是人手必會的了有缆,并且越來越多的公司開始使用Vue框架進行前端業(yè)務(wù)的開發(fā)桌硫。但是更多的開發(fā)者都停留在組件的搬運和淺顯的Vue基礎(chǔ)使用,沒有深究Vue本身所蘊含的思想和實現(xiàn)原理链峭。這短時間看來對于業(yè)務(wù)開發(fā)并沒有什么幫助畦娄,但是長久上看又沾,要想成為一名高級前端工程師,深究框架實現(xiàn)原理是進階的資糧熙卡。
??另外杖刷,在之前部門內(nèi)部分享中,一名同事分享了如何手動實現(xiàn)一個簡易Vue框架驳癌。其中講了一些數(shù)據(jù)劫持滑燃、數(shù)據(jù)綁定等知識,讓我大開眼界的同時也深深懷疑自己颓鲜,自己實在是太菜了表窘。不過我這時候發(fā)現(xiàn)了,以前閱讀的《JavaScript高級程序設(shè)計》中很多枯燥的知識在這里得到了使用甜滨。以前讀這本書的時候就很懷疑乐严,這些基礎(chǔ)知識到底能被用在什么地方呢?在同事的分享中衣摩,找到了答案昂验,那就是運用在前端底層框架內(nèi)的開發(fā)上。我們平常工作書寫的業(yè)務(wù)代碼艾扮,都是講js作為實現(xiàn)邏輯的工具既琴,并沒有在js內(nèi)部去尋找一些東西。進入內(nèi)部去結(jié)合知識去理解泡嘴,才能更自然地理解一些復(fù)雜的問題甫恩。
??正好公司需要上繳四季度的績效考核文件,所以將Vue源碼閱讀作為個人成長的一部分酌予,并書寫博客文章記錄下來磺箕。
正文
??在這次閱讀源碼中纹腌,希望能夠摸索出適合自己的學(xué)習(xí)新知識的方式,首先說明一下源碼的版本是19年12月的最新版滞磺,版本號是2.6.11:
??對于Vue源碼閱讀升薯,我是一點思路都沒有的。同事的內(nèi)部分享中拋出了很多函數(shù)與代碼击困,勉強理解了其中一點涎劈,剩下的就不懂了。正好也趁此機會阅茶,歸納一下自己的學(xué)習(xí)方法蛛枚。首先去github下載了Vue的庫,準(zhǔn)備在本地一點點閱讀脸哀。打開文件一看蹦浦,有點懵逼了,這個文件結(jié)構(gòu)我沒見過白卜洹盲镶!
但是,不慌蝌诡!那么多的項目溉贿,結(jié)構(gòu)肯定千差萬別的,但是其中文件的角色是相同的浦旱,重點要放在文件的所起的作用上宇色。通過查資料和看代碼 基本確定了這些主要目錄的作用和屬性:
├── scripts ------------------------------- 包含與構(gòu)建相關(guān)的腳本和配置文件
│ ├── alias.js -------------------------- 源碼中使用到的模塊導(dǎo)入別名
│ ├── config.js ------------------------- 項目的構(gòu)建配置
├── build --------------------------------- 構(gòu)建相關(guān)的文件,一般情況下我們不需要動
├── dist ---------------------------------- 構(gòu)建后文件的輸出目錄
├── examples ------------------------------ 存放一些使用Vue開發(fā)的應(yīng)用案例
├── flow ---------------------------------- JS靜態(tài)類型檢查工具[Flow](https://flowtype.org/)的類型聲明
├── package.json
├── test ---------------------------------- 測試文件
├── src ----------------------------------- 源碼目錄
│ ├── compiler -------------------------- 編譯器代碼颁湖,用來將 template 編譯為 render 函數(shù)
│ │ ├── parser ------------------------ 存放將模板字符串轉(zhuǎn)換成元素抽象語法樹的代碼
│ │ ├── codegen ----------------------- 存放從抽象語法樹(AST)生成render函數(shù)的代碼
│ │ ├── optimizer.js ------------------ 分析靜態(tài)樹宣蠕,優(yōu)化vdom渲染
│ ├── core ------------------------------ 存放通用的,平臺無關(guān)的運行時代碼
│ │ ├── observer ---------------------- 響應(yīng)式實現(xiàn)甥捺,包含數(shù)據(jù)觀測的核心代碼
│ │ ├── vdom -------------------------- 虛擬DOM的 creation 和 patching 的代碼
│ │ ├── instance ---------------------- Vue構(gòu)造函數(shù)與原型相關(guān)代碼
│ │ ├── global-api -------------------- 給Vue構(gòu)造函數(shù)掛載全局方法(靜態(tài)方法)或?qū)傩缘拇a
│ │ ├── components -------------------- 包含抽象出來的通用組件抢蚀,目前只有keep-alive
│ ├── server ---------------------------- 服務(wù)端渲染(server-side rendering)的相關(guān)代碼
│ ├── platforms ------------------------- 不同平臺特有的相關(guān)代碼
│ │ ├── weex -------------------------- weex平臺支持
│ │ ├── web --------------------------- web平臺支持
│ │ │ ├── entry-runtime.js ---------------- 運行時構(gòu)建的入口
│ │ │ ├── entry-runtime-with-compiler.js -- 獨立構(gòu)建版本的入口
│ │ │ ├── entry-compiler.js --------------- vue-template-compiler 包的入口文件
│ │ │ ├── entry-server-renderer.js -------- vue-server-renderer 包的入口文件
│ ├── sfc ------------------------------- 包含單文件組件.vue文件的解析邏輯,用于vue-template-compiler包
│ ├── shared ---------------------------- 整個代碼庫通用的代碼
看到這么多的目錄 以及一大堆的專業(yè)術(shù)語 肯定是一臉懵逼的進來 一臉懵逼的出去 也就是說平時我們接觸的Vue的實例等等 都是表面最終生成的構(gòu)造函數(shù)或者方法涎永。這里先抓住主要的內(nèi)容思币,把重要的幾個目錄先拎出來看看:
- compiler:編譯器,用來將template轉(zhuǎn)化為render函數(shù)
- core: Vue的核心代碼羡微,包括響應(yīng)式實現(xiàn)谷饿、虛擬DOM、Vue實例方法的掛載妈倔、全局方法博投、抽象出來的通用組件等
- platform:不同平臺的入口文件,主要是 web 平臺和 weex 平臺的盯蝴,不同平臺有其特殊的構(gòu)建過程毅哗,當(dāng)然我們的重點是 web 平臺
- server:服務(wù)端渲染(SSR)的相關(guān)代碼听怕,SSR 主要把組件直接渲染為 HTML 并由 Server 端直接提供給 Client 端
- sfc:主要是 .vue 文件解析的邏輯
- shared:一些通用的工具方法,有一些是為了增加代碼可讀性而設(shè)置的
然后接下來要從哪個文件開始看起呢虑绵?這里看了一些同行的博文尿瞭,找到了一個好的方法。從package.json
文件往上回溯找到核心代碼翅睛。
博文中說任何前端項目都可以從 package.json
文件看起声搁,先來看看它的 script.dev
就是我們運行 npm run dev
的時候它的命令行:
"scripts": {
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
}
這里的 rollup 是一個類似于 webpack 的JS模塊打包器,事實上
Vue - v1.0.10
版本之前用的還是 webpack 捕发,其后改成了 rollup 疏旨,如果想知道為什么換成 rollup ,可以看看 尤雨溪本人的回答扎酷,總的來說就是為了打出來的包體積小一點檐涝,初始化速度快一點。
可以看到這里 rollup 去運行 scripts/config.js
文件法挨,并且給了個參數(shù) TARGET:web-full-dev
谁榜,那來看看 scripts/config.js
里面是什么:
// scripts/config.js
const builds = {
....
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
...
}
這里的 web-full-dev
就是對應(yīng)剛剛我們在命令行里傳入的命令,那么 rollup 就會按下面的 entry 入口文件開始去打包坷剧,還有其他很多命令和其他各種輸出方式和格式可以自行查看一下源碼惰爬。那么這里的重點是web/entry-runtime-with-compiler.js
文件,全局搜索一下惫企,找到了這個文件的位置:src/platforms/web/web/entry-runtime-with-compiler.js
。打開這個文件陵叽,發(fā)現(xiàn)導(dǎo)入了一個Vue:
順著這個引入路徑找到runtime/index
文件狞尔,發(fā)現(xiàn)這里的Vue也是導(dǎo)入的:
繼續(xù)回溯,找到src/core/index.js
文件巩掺。上面就說了這個文件里面是Vue的核心代碼偏序,一些Vue的關(guān)鍵特性就是在這個文件里面編寫的,這說明我們的回溯路徑是正確的胖替。
但遺憾的是研儒,這里的Vue還是引入的,不著急繼續(xù)找独令。打開instance/index
文件端朵,終于找到目標(biāo)了!燃箭!
??這里的Vue構(gòu)造函數(shù)就是我們追尋多久的冲呢,當(dāng)我們 new Vue( ) 的時候,實際上調(diào)用的就是這個構(gòu)造函數(shù)招狸,可以從這里開始看了敬拓。
??這里是構(gòu)造函數(shù)的核心文件,先是引入依賴邻薯,然后定義名字為Vue的構(gòu)造函數(shù)。然后調(diào)用五個方法乘凸,把構(gòu)造函數(shù)作為參數(shù)傳入進去厕诡。這五個方法就是在Vue構(gòu)造函數(shù)的原型Prototype上掛載方法或?qū)傩裕簿褪钦f這個五個方法所掛載的書寫構(gòu)成了Vue的構(gòu)造函數(shù)营勤。形象的說就像夾心雪糕一樣木人,一層層的包裹,最終成為了完整的構(gòu)造函數(shù)冀偶。
??內(nèi)部的包裝已經(jīng)完畢醒第,沿著路徑尋找到了下一步,到了core層下的index.js
:
在這一層又掛載和添加了什么東西进鸠?可以看到在這一層又給vue的構(gòu)造函數(shù)掛載initGlobalAPI 和 isServerRendering 以及版本信息稠曼, 我們先不去扣這一系列的掛載都起了什么作用,先走完這整體流程客年。(當(dāng)然命名的文件名基本上就是所掛載的東西霞幅、很直觀)
當(dāng)然,最主要的還是整體量瓜,避免一葉障目司恳。
// 引入了之前的構(gòu)造函數(shù)
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 將之前的構(gòu)造函數(shù)Vue作為參數(shù)傳進去
initGlobalAPI(Vue)
// 掛載isServer
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
// 掛載版本屬性
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue
到這里基本上vue上該掛載的都掛載上了,那么下一步的話就到了platforms這里绍傲,也就是平臺劃分扔傅,安裝不同平臺特有的方法,而且整體的劃分了web端以及weex端烫饼。
那么在這個platform里又干了什么猎塞? 以為web端為例:
- 覆蓋vue.config屬性 替換為平臺特有的屬性和方法
- extend 安裝相應(yīng)的指令和組
- 在vue.prototype 上定義patch 以及$mount
- 關(guān)于vue devtools的一些設(shè)置
接下來就到了最后一個處理Vue的地方 entry-runtime-with-compiler
:
最后一階段主要是重寫掛載以及添加編譯器,也就是將模板template編譯為render函數(shù) 杠纵。
到這里vue的構(gòu)造函數(shù)才算是真正的新鮮出爐荠耽。總結(jié)一下:
1. 在第一階段比藻,整體注入了五個部分铝量,vue構(gòu)造函數(shù)主體部分完成,包括各項初始化银亲,以及發(fā)布訂閱模式等等
- initMixin => created周期函數(shù)之前的操作慢叨,即各項初始化,期間調(diào)用 beforeCreate 鉤子
- stateMixin => 利用 definedProperty 進行靜態(tài)數(shù)據(jù)的訂閱發(fā)布群凶,并在其中實現(xiàn)幾項實例
api $set插爹、$delete、 $watch, - eventsMixin => 實例事件流的注入, 利用的是訂閱發(fā)布模式的事件流構(gòu)造
- lifecycleMixin => 注入幾個Vue原型函數(shù)
- renderMixin => 實現(xiàn)實例api $nextTick,后續(xù)詳解赠尾,實現(xiàn) _render 渲染虛擬dom
- Vue.prototype._update => 調(diào)用生命周期鉤子 beforeUpdate力穗,其后實現(xiàn) virtual dom 的更
新; - Vue.prototype.$forceUpdate => 實現(xiàn)實例 api forceUpdate 強制重新渲染實例,包括其下
的子組件(更新了 watcher 隊列); - Vue.prototype.$destroy => 調(diào)用生命周期鉤子 beforeDestroy , 其后移除各項實例子組件气嫁,
拆卸實例的watcher隊列及調(diào)用實例的 patch 方法將 virtual dom 置空(null)当窗,最后調(diào)用
鉤子 destroyed 并解除(實例api:$off)實例所有事件;
- Vue.prototype._update => 調(diào)用生命周期鉤子 beforeUpdate力穗,其后實現(xiàn) virtual dom 的更
2. 在第二階段掛載靜態(tài)的屬性和方法
- 第三階段 添加web平臺所需要的配置、組件和指令寸宵,以及編譯等崖面。