昨天有朋友問我vue在頁面第一次加載時到底做了些什么,看來這個問題在很多朋友心中可能還比較模糊,今天我們一起來詳細的看看vue的首次渲染程序
vue原始碼下載地址:vue原始碼
了解vue首次渲染全程序,我們應該從哪說起呢,很明顯,是不是應該從入口檔案說起啊,即main.js
1、vue初始化
首先,我們看main.js中,第一個最關鍵的肯定是引入vue吧
import vue from 'vue'
其實,vue被打包后,dist檔案夾中存在多個版本,分別是
通用版本(UMD):中的完整版 vue.js 和運行時版本 vue.runtime.js
CommonJs版本:中的完整版vue.common.js 和 運行時版本vue.runtime.common.js
ES Module版本:中的完整版vue.esm.js 和 運行時版本vue.runtime.esm.js
一般在vue2.6以后,我們用vue/cli創建的專案用的都是vue.runtime.esm.js運行時版本
即,引入vue時會引入vue.esm.js這個版本
那么,vue引入以后,是不是vue中的相關代碼會被執行啊,那最新執行vue原始碼中的哪塊代碼呢(引入的vue就是vue原始碼中被打包后的vue),我們先得知道入口檔案在哪
vue入口檔案
vue的入口檔案主要在vue原始碼結構的src/platforms/web下

vue打包時,可以選擇不同的vue入口檔案來進行打包,不同的入口檔案打包出來的vue版本不同,
這里我們主要來說完整版entry-runtime-with-compiler.js
下面我們先來了解下完整版和運行時版本的區別
完整版和運行時版本的區別
完整版是運行時版本 + 編譯器的組合
運行時版本不帶編譯器compiler,即沒有模板編譯功能,主要用來創建vue實體,渲染虛擬dom,體積小,更輕量(compiler編譯器有3000多行代碼)
什么意思呢,即
<body>
<div id="app">
<p>我是index.html中的內容</p>
</div>
</body>
new Vue({
template: '<div>我是template模板渲染出來的內容</div>'
}).$mount('#app')
上面的情況,
如果是完整版vue,存在compiler編譯器,會將new Vue時傳入的template編譯成render函式,并賦值給options的render屬性,然后$mount后,會渲染render函式成虛擬dom,再將虛擬dom轉話為真實dom,所以最終頁面會出現 我是template模板渲染出來的內容 這句話,原本的那句話會被覆寫
如果是運行時版本,沒有編譯器,不會編譯template中的內容,則頁面只會存在原來的dom
下面我們來繼續往下看
找到入口檔案后,我們開始看看會執行哪些東西

可以看出,入口檔案先匯入了vue,然后經過了一些處理,最終又匯出了vue
我們先通過匯入vue的路徑一步一步找到vue建構式的創建在哪創建的,如上圖,從runtime/index中匯入了vue,那么我們去看runtime/index

這個檔案也是一樣,import了vue 經過了一些處理,然后又匯出了vue,我們繼續往上找,找core/index

這個檔案也是一樣,我們繼續往上找,找./instance/index

在這里,我們找到了我們的vue建構式的創建,是在原始碼的src/core/instance/index.js檔案中,
那么,我們從剛剛上面的參考關系,就能發現,vue被我們引入到專案中后,首先會執行的檔案的順序是
src/core/instace/index.js ===> 1
src/core/index.js ===> 2
src/platforms/web/runtime/index.js ===> 3
src/platforms/web/entry-runtime-with-compiler.js 4
那么,我們再來看,每個檔案都執行了些什么,
首先 src/core/instace/index.js
1.1、src/core/instace/index.js
首先,此檔案定義了vue建構式,并初始化了一些vue的實體屬性和實體方法,即,在vue.prototype原型下新增了各種方法和屬性

下面,我們具體來看下,每一個方法具體初始化了vue的哪些實體屬性或方法
1.1.1、initMixin(Vue)

1.1.2、stateMixin(Vue)

1.1.3、eventsMixin(Vue)

1.1.4、lifecycleMixin(Vue)

1.1.5、renderMixin(Vue)

src/core/instace/index.js執行完后,會繼續執行下一個檔案
src/core/instace/index.js ===> 1
src/core/index.js ===> 2
src/platforms/web/runtime/index.js ===> 3
src/platforms/web/entry-runtime-with-compiler.js 4
1.2、src/core/index.js

可以看出,這個檔案,主要是給vue新增了很多靜態實體方法和屬性,具體新增了哪些,
我們繼續看被執行的那個方法initGlobalAPI(Vue)
1.2.1 initGlobalAPI(Vue)
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
// 新增了一個config屬性
Object.defineProperty(Vue, 'config', configDef)
// 新增了一個靜態成員 util
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
// 新增了3個靜態成員set delete nextTick
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 新增了一個靜態成員 observable
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
// 初始化了options 此時options是空物件</T>
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
// 注冊了一個全域組件keep-alive builtInComponents內部就是keep-alive的組件匯出
extend(Vue.options.components, builtInComponents)
// 下面是分別初始化了Vue.use() Vue.mixin() Vue.extend()
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
// 初始化Vue.directive(), Vue.component(), vue.filter()
initAssetRegisters(Vue)
}
1.3、src/platforms/web/runtime/index.js

1.4、src/platforms/web/entry-runtime-with-compiler.js

此檔案,最主要的作用就重寫了vue原型下的$mount方法,具體$mount方法中做了些什么,我們后面會講
1.5、vue初始化總結
上面寫的整個程序,都是用戶在使用vue時,引入vue檔案后,立刻會執行的一些東西
這些執行完后,是不是會繼續去執行我們專案中的main.js檔案啊,
此時會執行到
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
這個時候,會開始呼叫,我們的vue建構式
2、vue建構式執行
此時,會先執行vue建構式,

可以看出,主要是執行了_init方法,從這里開始,vue的生命周期開始執行了

上面只是_init()方法中最主要的一部分代碼,(代碼太多,我就不全部截圖了,你們自己到原始碼中看),可以看出:
2.1、beforeCreate鉤子
在生命周期beforeCreate鉤子之前,vue主要做的事情就是給vue原型新增各種屬性和方法,給vue新增各種靜態屬性和方法,以及給vm實體新增各種屬性和方法
2.2、created鉤子
上圖可以看出,beforeCreate鉤子執行結束后,主要執行了3個方法:initInjections, initState, initProvide
// 把inject注入到vm實體
callHook(vm, 'beforeCreate')
// 把inject注入到vm實體
initInjections(vm)
// 初始化vm的$props,$methods,$data,computed,watch
initState(vm)
// 把provide注入vm實體
initProvide(vm)
// 執行created生命周期
callHook(vm, 'created')
其實,重點是initState(vm)方法,該方法中,初始化了vm實體的$props, $data, $methods, computed, watch等,同時,在里面呼叫了一個initData()方法,該方法內會呼叫observer() 方法,將data中的資料都轉化為回應式資料,即添加資料攔截器,
所以可以看出,在created生命周期之前,vm的$props, $data, $methods, computed, watch屬性都會初始化完成,
故,這也就是為什么,我們可以在created中呼叫我們data中的各種資料以及呼叫props或者methods等下面的各種方法了,
created生命周期走完以后,繼續往下看

可以看出,這里判斷了vm.$options.el是否存在,vm.$options.el是什么啊,
new Vue({})時,傳入的那個物件的所有屬性,都會被掛載options下,
new Vue({
el: '#app'
router,
store,
render: h => h(App)
})
故,vm.$options.el就是上面傳入的el,
這里判斷el是否存在,如果存在,才會繼續往下執行$mount
那大家可能會好奇了,如果不存在,那是不是就卡死了,后面都不會走了,是的,如果沒有,就不會繼續走了,要想代碼繼續往下走,必然要執行$mount方法,
此時,我們再看一直vue常用的情況
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
這里沒有傳入el,所以原始碼中的

肯定是不會走的,但是,用戶在new Vue的時候可以自己用new 出來的vue實體去呼叫$mount,這么一來,大家看我們官網的生命周期圖,可能就更容易看懂了

好了,下面我們繼續往下,下一步是執行 $mount,我們來看 $mount方法
2.3、$mount函式
我們之前初始化的時候,重寫過$mount還記得嗎,所以,此時我們執行$mount時,執行的是重寫后的mount

這里在重寫前先存盤了重寫前的mount方法,然后在最后呼叫了重寫前的mount方法,
重寫后,最關鍵的代碼是判斷是否有render函式

這一步的主要作用就是判斷是否有render函式,
如果有,直接往下執行重寫前的$mount方法前渲染render函式,
如果沒有,就會前判斷是否存在template模板(options.template是否存在,options.template可能是id選擇器,可能是dom),如果存在模板,就會獲取到模板中的內容,并賦值給template,options.template不存在,那么會直接以el指定的dom為模板(即#app),獲取到el下的dom,賦值給template
template取到dom后,然后繼續往下,將此template編譯成render函式,并將編譯出來的render函式掛載options.render屬性下

然后會繼續執行重寫前的$mount,理解了這,我們就能理解生命周期圖中的另一部分了

2.4、beforeMount
下面,我們繼續來看重寫前的$mount函式的執行

可以看出\ $mount中主要是執行了函式mountComponent,我們繼續看mountComponent函式

可以看出,此函式,主要做了以下4件事
我們一件一件來看
1、執行了beforeMount鉤子,所以可以得出結論,再beforeMount之前,我們主要是初始化和得到render函式,而beforeMount之后,才是開始將render函式渲染成虛擬dom,然后更新真實dom
render函式得到的途徑有3種
第一:用戶自己傳入render

第二:.vue檔案編譯成render

這種方式,就是自己傳入了一個render函式,函式內用h函式前執行了App.vue檔案,
.vue檔案最終轉化為render函式需要借助vue-loader來完成
第三、將template模板編譯成render函式

2、定義了一個updateComponent函式,此函式內呼叫了vm的_update方法,同時執行了vm._render()方法,并將執行后的結果當做引數傳給_update方法,_render方法我們前面說過,他內部渲染了render函式成為虛擬dom,故_render()的回傳值是一個vnode,
我們先來看下_render()函式內部如何將render函式轉化為虛擬dom的

然后我們再看_update函式內部做了啥

可以看出,_update函式中,執行了__patch__方法去對比兩個新舊dom,從而找出差異,更新真實dom,如果是首次渲染,則直接將當前的vnode,生成真實的dom,
故得出結論,整個updateComponent方法的主要作用就是渲染render函式,更新dom
而什么時候更新dom的關鍵,就在于什么時候去呼叫這個updateComponent函式了
3、new 了一個watcher實體

可以看出,new一個watcher實體的同時,傳入了updateComponent函式作為引數,
此時,我們看new Watcher時,會執行Watcher建構式,我們看Watcher建構式內做了啥

watcher分為3種,渲染watcher,$watch函式的watcher,computed的watcher,我們這里渲染頁面的是渲染watcher
上面將我們傳入的函式傳給了getter

繼續往下走,呼叫了get()

可以看出,get()中呼叫了我們傳入的函式,而我們傳入的函式就是渲染render函式,并觸發虛擬dom更新真實dom,而回傳的值,就是渲染后的真實dom,最后賦值給了this.value,而this.value最后會用于更新依賴者,而我們當前這個wather實體,是主vue實體的watcher,故可以理解為整個頁面的watcher,當我們呼叫this.$fouceUpdate()時,就是呼叫這個實體的update方法,去更新整個頁面,
所以說,new Wacher的時候 updateComponent會自動呼叫一次,這就是我們的首次渲染,
此時,我們繼續往下看

這內部,還做了個判斷,如果vm._isMounted為true(即Mounted鉤子已經執行過了),而vm._isDestroyed為fase時(即當前組件還未銷毀),此時,如果產生更新,則說明并非首次渲染,那么執行beforeUpdate鉤子,后續肯定還會走updated,這里我們就不說updated的事了
new Watcher后,代碼繼續往下走

判斷了當前vnode如果null,說明之前沒有生成過虛擬dom,也就說明這次肯定是首次渲染,此時,vm._isMounted置為true,并執行mounted鉤子函式,此時,首次渲染完成,
2.5、mounted
可以看出,整個beforeMount 到 mounted程序中,主要做的作業就是
1、渲染render函式成為虛擬dom vnode
2、執行vm._update函式,將虛擬dom轉化為真實dom
如果是beforeUpdate 到 updated鉤子之間,說明不是首次渲染,那么虛擬dom會有新舊兩個,此時vm._update函式的作用就是對比新舊兩個vnode,得出差異,更新需要更新的地方
首次渲染整個程序就是這樣,有問題歡迎下方評論或者私信我
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/277692.html
標籤:其他
上一篇:js動態添加,jq,ajax
