Vue2 源碼分析

源碼版本:v2.1.10

分析目標(biāo)

通過(guò)閱讀源碼,對(duì) Vue2 的基礎(chǔ)運(yùn)行機(jī)制有所了解赢底,主要是:

  • Vue2 中數(shù)據(jù)綁定的實(shí)現(xiàn)方式
  • Vue2 中對(duì) Virtual DOM 機(jī)制的使用方式

源碼初見(jiàn)

項(xiàng)目構(gòu)建配置文件為 build/config.js砌溺,定位 vue.js 對(duì)應(yīng)的入口文件為 src/entries/web-runtime-with-compiler.js,基于 rollup 進(jìn)行模塊打包。

代碼中使用 flow 進(jìn)行接口類型標(biāo)記和檢查闷旧,在打包過(guò)程中移除這些標(biāo)記蓉冈。為了閱讀代碼方便城舞,在 VS Code 中安裝了插件 Flow Language Support轩触,然后關(guān)閉工作區(qū) JS 代碼檢查,這樣界面就清爽很多了家夺。

Vue 應(yīng)用啟動(dòng)一般是通過(guò) new Vue({...})脱柱,所以,先從該構(gòu)造函數(shù)著手拉馋。

注:本文只關(guān)注 Vue 在瀏覽器端的應(yīng)用榨为,不涉及服務(wù)器端代碼。

Vue 構(gòu)造函數(shù)

文件:src/core/instance/index.js

該文件只是構(gòu)造函數(shù)煌茴,Vue 原型對(duì)象的聲明分散在當(dāng)前目錄的多個(gè)文件中:

  • init.js:._init()
  • state.js:.$data .$set() .$delete() .$watch()
  • render.js:._render() ...
  • events.js:.$on() .$once() .$off() .$emit()
  • lifecycle.js:._mount() ._update() .$forceUpdate() .$destroy()

構(gòu)造函數(shù)接收參數(shù) options 随闺,然后調(diào)用 this._init(options)

._init() 中進(jìn)行初始化蔓腐,其中會(huì)依次調(diào)用 lifecycle矩乐、events、render回论、state 模塊中的初始化函數(shù)散罕。

Vue2 中應(yīng)該是為了代碼更易管理,Vue 類的定義分散到了上面的多個(gè)文件中透葛。

其中笨使,對(duì)于 Vue.prototype 對(duì)象的定義,通過(guò) mixin 的方式在入口文件 core/index.js 中依次調(diào)用僚害。對(duì)于實(shí)例對(duì)象(代碼中通常稱為 vm)則通過(guò) init 函數(shù)在 vm._init() 中依次調(diào)用硫椰。

Vue 公共接口

文件:src/core/index.js

這里調(diào)用了 initGlobalAPI() 來(lái)初始化 Vue 的公共接口,包括:

  • Vue.util
  • Vue.set
  • Vue.delete
  • Vue.nextTick
  • Vue.options
  • Vue.use
  • Vue.mixin
  • Vue.extend
  • asset相關(guān)接口:配置在 src/core/config.js

Vue 啟動(dòng)過(guò)程

調(diào)用 new Vue({...}) 后萨蚕,在內(nèi)部的 ._init() 的最后靶草,是調(diào)用 .$mount() 方法來(lái)“啟動(dòng)”。

web-runtime-with-compiler.jsweb-runtime.js 中岳遥,定義了 Vue.prototype.$mount()奕翔。不過(guò)兩個(gè)文件中的 $mount() 最終調(diào)用的是 ._mount() 內(nèi)部方法,定義在文件 src/core/instance/lifecycle.js 中浩蓉。

Vue.prototype._mount(el, hydrating)

簡(jiǎn)化邏輯后的偽代碼:

vm = this
vm._watcher = new Watcher(vm, updateComponent)

接下來(lái)看 Watcher派继。

Watcher

文件:src/core/observer/watcher.js

先看構(gòu)造函數(shù)的簡(jiǎn)化邏輯:

// 參數(shù):vm, expOrFn, cb, options
this.vm = vm
vm._watchers.push(this)
// 解析 options,略....
// 屬性初始化捻艳,略....
this.getter = expOrFn // if `function`
this.value = this.lazy ? undefined : this.get()

由于缺省的 lazy 屬性值為 false驾窟,接著看 .get() 的邏輯:

pushTarget(this) // !
value = this.getter.call(this.vm, this.vm)
popTarget()
this.cleanupDeps()
return value

先看這里對(duì) getter 的調(diào)用,返回到 ._mount() 中认轨,可以看到绅络,是調(diào)用了 vm._update(vm._render(), hydrating),涉及兩個(gè)方法:

  • vm._render():返回虛擬節(jié)點(diǎn)(VNode)
  • vm._update()

來(lái)看 _update() 的邏輯,這里應(yīng)該是進(jìn)行 Virtual DOM 的更新:

// 參數(shù):vnode, hydrating
vm = this
prevEl = vm.$el
prevVnode = vm._vnode
prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
if (!prevVnode) {
  // 初次加載
  vm.$el = vm.__patch__(vm.$el, vnode, ...)
} else {
  // 更新
  vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// 后續(xù)屬性配置恩急,略....

參考 Virtual DOM 的一般邏輯杉畜,這里是差不多的處理過(guò)程,不再贅述衷恭。

綜上此叠,這里的 watcher 主要作用應(yīng)該是在數(shù)據(jù)發(fā)生變更時(shí),觸發(fā)重新渲染和更新視圖的處理:vm._update(vm._render())匾荆。

接下來(lái)拌蜘,我們看下 watcher 是如何發(fā)揮作用的杆烁,參考 Vue 1.0 的經(jīng)驗(yàn)牙丽,下面應(yīng)該是關(guān)于依賴收集、數(shù)據(jù)綁定方面的細(xì)節(jié)了兔魂,而這一部分烤芦,和 Vue 1.0 差別不大。

數(shù)據(jù)綁定

watcher.get() 中調(diào)用的 pushTarget()popTarget() 來(lái)自文件:src/core/observer/dep.js析校。

pushTarget()popTarget() 兩個(gè)方法构罗,用于處理 Dep.target,顯然 Dep.targetwather.getter 的調(diào)用過(guò)程中會(huì)用到智玻,調(diào)用時(shí)會(huì)涉及到依賴收集遂唧,從而建立起數(shù)據(jù)綁定的關(guān)系。

Dep 類的 .dep() 方法中用到了 Dep.target吊奢,調(diào)用方式為:

Dep.target.addDep(this)

可以想見(jiàn)盖彭,在使用數(shù)據(jù)進(jìn)行渲染的過(guò)程中,會(huì)對(duì)數(shù)據(jù)屬性進(jìn)行“讀”操作页滚,從而觸發(fā) dep.depend()召边,進(jìn)而收集到這個(gè)依賴關(guān)系。下面來(lái)找一下這樣的調(diào)用的位置裹驰。

state.js 中找到一處隧熙,makeComputedGetter() 函數(shù)中通過(guò) watcher.depend() 間接調(diào)用了 dep.depend()。不過(guò) computedGetter 應(yīng)該不是最主要的地方幻林,根據(jù) Vue 1.0 的經(jīng)驗(yàn)贞盯,還是要找對(duì)數(shù)據(jù)進(jìn)行“數(shù)據(jù)劫持”的地方,應(yīng)該是defineReactive()沪饺。

defineReactive() 定義在文件 src/core/observer/index.js躏敢。

// 參數(shù):obj, key, val, customSetter?
dep = new Dep()
childOb = observe(val)
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function () {
    // 略,調(diào)用了 dep.depend()
  },
  set: function () {
    // 略随闽,調(diào)用 dep.notify()
  }
})

結(jié)合 Vue 1.0 經(jīng)驗(yàn)父丰,這里應(yīng)該就是數(shù)據(jù)劫持的關(guān)鍵了。數(shù)據(jù)原有的屬性被重新定義,屬性的 get() 被調(diào)用時(shí)蛾扇,會(huì)通過(guò) dep.depend() 收集依賴關(guān)系攘烛,記錄到 vm 中;而在 set() 被調(diào)用時(shí)镀首,則會(huì)判斷屬性值是否發(fā)生變更坟漱,如果發(fā)生變更,則通過(guò) dep.notify() 來(lái)通知 vm更哄,從而觸發(fā) vm 的更新操作芋齿,實(shí)現(xiàn) UI 與數(shù)據(jù)的同步,這也就是數(shù)據(jù)綁定后的效果了成翩。

回過(guò)頭來(lái)看 state.js觅捆,是在 initProps() 中調(diào)用了 defineReactive()。而 initProps()initState() 中調(diào)用麻敌,后者則是在 Vue.prototype._init() 中被調(diào)用栅炒。

不過(guò)最常用的其實(shí)是在 initData() 中,對(duì)初始傳入的 data 進(jìn)行劫持术羔,不過(guò)里面的過(guò)程稍微繞一些赢赊,是將這里的 data 賦值到 vm._data 并且代理到了 vm 上,進(jìn)一步的處理還涉及 observe()Observer 類级历。這里不展開(kāi)了释移。

綜上,數(shù)據(jù)綁定的實(shí)現(xiàn)過(guò)程為:

  • 初始化:new Vue() -> vm._init()
  • 數(shù)據(jù)劫持:initState(vm) -> initProps(), initData() -> dep.depend()
  • 依賴收集:vm.$mount() -> vm._mount() -> new Watcher() -> vm._render()

渲染

首先來(lái)看 initRender()寥殖,這里在 vm 上初始化了兩個(gè)與創(chuàng)建虛擬元素相關(guān)的方法:

  • vm._c()
  • vm.$createElement()

其內(nèi)部實(shí)現(xiàn)都是調(diào)用 createElement()玩讳,來(lái)自文件:src/core/vdom/create-element.js

而在 renderMixin() 中初始化了 Vue.prototype._render() 方法扛禽,其中創(chuàng)建 vnode 的邏輯為:

render = vm.$options.render
try {
  vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
  // ...
}

這里傳入 render() 是一個(gè)會(huì)返回 vnode 的函數(shù)锋边。

接下來(lái)看 vm._update() 的邏輯,這部分在前面有介紹编曼,初次渲染時(shí)是通過(guò)調(diào)用 vm.__patch__() 來(lái)實(shí)現(xiàn)豆巨。那么 vm.__patch__() 是在哪里實(shí)現(xiàn)的呢?在 _update() 代碼中有句注釋掐场,提到:

    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.

在文件 web-runtime.js 中往扔,找到了:

Vue.prototype.__patch__ = inBrowser ? patch : noop

顯然示在瀏覽器環(huán)境下使用 patch(),來(lái)自:src/platforms/web/runtime/patch.js熊户,其實(shí)現(xiàn)是通過(guò) createPatchFunction()萍膛,來(lái)自文件 src/core/vdom/patch

OK嚷堡,以上線索都指向了 vdom 相關(guān)的模塊蝗罗,也就是說(shuō)艇棕,顯然是 vdom 也就是 Virtual DOM 參與了渲染和更新。

不過(guò)還有個(gè)問(wèn)題沒(méi)有解決串塑,那就是原始的字符串模塊沼琉,是如何轉(zhuǎn)成用于 Virtual DOM 創(chuàng)建的函數(shù)調(diào)用的呢?這里會(huì)有一個(gè)解析的過(guò)程桩匪。

回到入口文件 web-runtime-with-compiler.js打瘪,在 Vue.prototype.$mount() 中,有一個(gè)關(guān)鍵的調(diào)用:compileToFunctions(template, ...)傻昙,template 變量值為傳入的參數(shù)解析得到的模板內(nèi)容闺骚。

模板解析

文件:src/platforms/web/compiler/index.js

函數(shù) compileToFunctions() 的基本邏輯:

// 參數(shù):template, options?, vm?
res = {}
compiled = compile(template, options)
res.render = makeFunction(compiled.render)
// 拷貝數(shù)組元素:
// res.staticRenderFns <= compiled.staticRenderFns
return res

這里對(duì)模板進(jìn)行了編譯(compile()),最終返回了根據(jù)編譯結(jié)果得到的 render()妆档、staticRenderFns僻爽。再看 web-runtime-with-compiler.jsVue.prototype.$mount() 的邏輯,則是將這里得到的結(jié)果寫入了 vm.$options 中过吻,也就是說(shuō)进泼,后面 vm._render() 中會(huì)使用這里的 render()

再來(lái)看 compile() 函數(shù)纤虽,這里是實(shí)現(xiàn)模板解析的核心,來(lái)做文件 src/compiler/index.js绞惦,基本邏輯為:

// 參數(shù):template, options
ast = parse(template.trim(), options)
optimize(ast, options)
code = generate(ast, options)
return {
  ast,
  render: code.render,
  staticRenderFns: code.staticRenderFns
}

邏輯很清晰逼纸,首先從模板進(jìn)行解析得到抽象語(yǔ)法樹(shù)(ast),進(jìn)行優(yōu)化济蝉,最后生成結(jié)果代碼杰刽。整個(gè)過(guò)程中肯定會(huì)涉及到 Vue 的語(yǔ)法,包括指令王滤、組件嵌套等等贺嫂,不僅僅是得到構(gòu)建 Virtual DOM 的代碼。

需要注意的是雁乡,編譯得到 render 其實(shí)是代碼文本第喳,通過(guò) new Function(code) 的方式轉(zhuǎn)為函數(shù)。

總結(jié)

Vue2 相比 Vue1 一個(gè)主要的區(qū)別在于引入了 Virtual DOM踱稍,但其 MVVM 的特性還在曲饱,也就是說(shuō)仍有一套數(shù)據(jù)綁定的機(jī)制。

此外珠月,Virtual DOM 的存在扩淀,使得原有的視圖模板需要轉(zhuǎn)變?yōu)楹瘮?shù)調(diào)用的模式,從而在每次有更新時(shí)可以重新調(diào)用得到新的 vnode啤挎,從而應(yīng)用 Virtual DOM 的更新機(jī)制驻谆。為此,Vue2 實(shí)現(xiàn)了編譯器(compiler),這也意味著 Vue2 的模板可以是純文本胜臊,而不必是 DOM 元素氛谜。

Vue2 基本運(yùn)行機(jī)制總結(jié)為:

  • 文本模板,編譯得到生成 vnode 的函數(shù)(render)区端,該過(guò)程中會(huì)識(shí)別并記錄 Vue 的指令和其他語(yǔ)法
  • new Vue() 得到 vm 對(duì)象值漫,其中傳入的數(shù)據(jù)會(huì)進(jìn)行數(shù)據(jù)劫持處理,從而可以收集依賴织盼,實(shí)現(xiàn)數(shù)據(jù)綁定
  • 渲染過(guò)程是將所有數(shù)據(jù)交由渲染函數(shù)(render)進(jìn)行調(diào)用得到 vnode杨何,應(yīng)該 Virtual DOM 的機(jī)制實(shí)現(xiàn)初始渲染和更新

寫在最后

對(duì) Vue2 的源碼分析,是基于我之前對(duì) Vue1 的分析和對(duì) Virtual DOM 的了解沥邻,見(jiàn)【鏈接】中之前的文章危虱。

水平有限,錯(cuò)漏難免,歡迎指正。

感謝閱讀荒辕!

鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末烦磁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鹦筹,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剪勿,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡方庭,警方通過(guò)查閱死者的電腦和手機(jī)厕吉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)械念,“玉大人头朱,你說(shuō)我怎么就攤上這事×浼酰” “怎么了项钮?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)欺殿。 經(jīng)常有香客問(wèn)我寄纵,道長(zhǎng),這世上最難降的妖魔是什么脖苏? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任程拭,我火速辦了婚禮,結(jié)果婚禮上棍潘,老公的妹妹穿的比我還像新娘恃鞋。我一直安慰自己崖媚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布恤浪。 她就那樣靜靜地躺著畅哑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪水由。 梳的紋絲不亂的頭發(fā)上荠呐,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音砂客,去河邊找鬼泥张。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鞠值,可吹牛的內(nèi)容都是我干的媚创。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼彤恶,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼钞钙!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起声离,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤芒炼,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后抵恋,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體焕议,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年弧关,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唤锉。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡世囊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出窿祥,到底是詐尸還是另有隱情株憾,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布晒衩,位于F島的核電站嗤瞎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏听系。R本人自食惡果不足惜贝奇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望靠胜。 院中可真熱鬧掉瞳,春花似錦毕源、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至该镣,卻和暖如春冻璃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背损合。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工省艳, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人塌忽。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓拍埠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親土居。 傳聞我的和親對(duì)象是個(gè)殘疾皇子枣购,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 我曾經(jīng)和一個(gè)人約定好了,若他死了擦耀,我便同他離去棉圈,在另一個(gè)世界繼續(xù)做他的密探。 但是后來(lái)我違約了眷蜓。 魔種的生命是要比...
    帥氣的濟(jì)南蜀黍閱讀 2,929評(píng)論 0 8
  • 千風(fēng) 秋分到吁系,雷公居然要失業(yè)了德召?河神沒(méi)糧餉居然難過(guò)得哭出來(lái)?小蟲(chóng)見(jiàn)到電母的第一面竟然感嘆“甄妃再現(xiàn)”汽纤? 雷公上岗、電母...
    阿鐮閱讀 857評(píng)論 1 2