這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

一、概述
在我們開發中,經常要用到Vue.extend創建出Vue的子類來建構式,通過new 得到子類的實體,然后通過$mount掛載到節點,如代碼:
<div id="mount-point"></div>
<!-- 創建構造器 -->
var Profile = Vue.extend({
template:'<p>{{firstName}} {{lastName}} aka{{alias}}</p>',
data:function(){
return{
firstName:'Walter',
lastName:'White',
alias:'Heisenberg'
}
}
})
<!-- 創建Profile實體,并掛載到一個元素上 -->
new Profile().$mount('#mount-point');
$mount方法是怎么實作的,篇文章就來講一下
二、使用方式
vm.$mount( [elementOrSelector] )
(1)引數
{ Element | string } [elementOrSelector]
(2)回傳值
vm,即實體本身,
(3)用法
1、如果Vue.js實體在實體化時沒有收到el選項,則它處于“未掛載”狀態,沒有關聯的DOM元素, 2、可以使用vm.$mount手動掛載一個未掛載的實體, 3、如果沒有提供elementOrSelector引數,模板將被渲染為檔案之外的元素,并且必須使用原生DOM的API把它插入檔案中, 4、這個方法回傳實體自身,因而可以鏈式呼叫其他實體方法,
(4)例子
var MyComponent = Vue.extend({
template:'<div>Hello!</div>',
})
<!-- 創建并掛載到#app(會替換#app) -->
new MyComponent().$mount('#app');
<!-- 創建并掛載到#app(會替換#app) -->
new MyComponent().$mount({el:'#app'});
<!-- 創建并掛載到#app(會替換#app) -->
var component = new MyComponent().$mount();
document.getElementById('app').appendChild(component.$el);
1、在不同的構建版本中,vm.$mount的表現都不一樣,其差異主要體現在完整版(vue.js)和只包含運行時版本(vue.runtime.js)之間,
2、完整版和只包含運行時版本之間的差異在于是否有編譯器,而是否有編譯器的差異主要在于vm.$mount方法的表現形式,
3、在只包含運行時的構建版本中,vm.mount的作用會稍有不同,它首先會檢查template或el選項所提供的模板是否已經轉換成渲染函式(render函式),如果沒有,則立即進入編譯程序,將模板編譯成渲染函式,完成之后再進入掛載與渲染的流程中,
4、只包含運行時版本的vm.$mount沒有編譯步驟,它會默認實體上已經存在渲染函式,如果不存在,則會設定一個,并且,這個渲染函式在執行時會回傳一個空節點的VNode,以保證執行時不會因為函式不存在而報錯,同時如果是開發環境下運行,Vue.js會觸發警告,提示我們當前使用的是只包含運行時的版本,會讓我們提供渲染函式,或者去使用完整的構建版本,
5、從原理的角度來講,完整版和只包含運行時版本之間是包含關系,完整版包含只包含運行時版本,
三、完整版vm.$mount的實作原理
(1)實作代碼
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
<!-- 做些什么 -->
return mount.call(this,el);
}
1、將Vue原型上的$mount方法保存在mount中,以便后續使用,
2、然后Vue原型上的$mount方法被一個新的方法覆寫了,新方法中會呼叫原始的方法,這種做法通常被稱為函式劫持,(看原始碼的同學可能發現了,vue多處用了函式劫持的做法,例如:對陣列實作監聽的時候...)
3、通過函式劫持,可以在原始功能上新增一些其他功能,上面代碼中,vm.$mount的原始方法就是mount的核心功能,而在完整版中需要將編譯功能新增到核心功能上去,
(2)由于el引數支持元素型別或者字串型別的選擇器,所以第一步是通過el獲取DOM元素,
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
el = el && query(el);
return mount.call(this,el);
}
使用query獲取DOM元素
function query(el){
if(typeof el === 'string'){
const selected = document.querySelector(el);
if(!selected){
return document.createElement('div');
}
return selected;
}else{
return el;
}
}
1、如果el是字串,則使用doucment.querySelector獲取DOM元素,如果獲取不到,則創建一個空的div元素,
2、如果el不是字串,那么認為它是元素型別,直接回傳el(如果執行vm.$mount方法時沒有傳遞el引數,則回傳undefined)
(3)編譯器
1、首先判斷Vue.js實體中是否存在渲染函式,只有不存在時,才會將模板編譯成渲染函式,
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
el = el && query(el);
const options = this.$options;
if(!options.render){
<!-- 將模板編譯成渲染函式并賦值給options.render -->
}
return mount.call(this,el);
}
2、在實體化Vue.js時,會有一個初始化流程,其中會向Vue.js實體上新增一些方法,這里的this.$options就是其中之一,它可以訪問到實體化Vue.js時用戶設定的一些引數,例如tempalte和render,
3、如果在實體化Vue.js時給出了render選項,那么template其實是無效的,因為不會進入模板編譯的流程,而是直接使用render選項中提供的渲染函式,
4、Vue.js在官方檔案的template選項中也給出了相應的提示,如果沒有render選項,那么需要獲取模板并將模板編譯成渲染函式(render函式)賦值給render選項,
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
el = el && query(el);
const options = this.$options;
if(!options.render){
<!-- 新增獲取模板相關邏輯 -->
let template = options.template;
if(template){
}else if(el){
template = getOuterHTML(el);
}
}
return mount.call(this,el);
}
5、從選項中取出template選項,也就是取出用戶實體化Vue.js時設定的模板,如果沒有取到,說明用戶沒有設定tempalte選項,那么使用getOuterHTML方法從用戶提供的el選項中獲取模板,
function getOuterHTML(el){
if(el.outerHTML){
return el.outerHTML;
}else{
const container = document.createElement('div');
container.appendChild(el.cloneNode(true));
return container.innerHTML;
}
}
6、getOuterHTML方法會回傳引數中提供的DOM元素的HTML字串,
7、整體邏輯
如果用戶沒有通過template選項設定模板,那么會從el選項中獲取HTML字串當作模板,如果用戶提供了template選項,那么需要對它進一步決議,因為這個選項支持很多種使用方式,template選項可以直接設定成字串模板,也可以設定為以#開頭的選擇符,還可以設定成DOM元素,
8、從不同的格式中將模板決議出來
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
el = el && query(el);
const options = this.$options;
if(!options.render){
<!-- 新增獲取模板相關邏輯 -->
let template = options.template;
if(template){
if(typeof tempalte === 'string'){
if(tempalte.charAt(0) === "#"){
template = idToTemplate(tempalte);
}
}else if(tempalte.nodeType){
template = template.innerHTML;
}else{
if(process.env.NODE_ENV !== 'production'){
warn('invalid template option:'+tempalte,this);
}
return this;
}
}else if(el){
template = getOuterHTML(el);
}
}
return mount.call(this,el);
}
9、如果tempalte是字串并且以#開頭,則它將被用作選擇符,通過選擇符獲取DOM元素后,會使用innerHTML作為模板,
10、使用idToTemplate方法從選擇符中獲取模板,idToTemplate使用選擇符獲取DOM元素之后,將它的innerHTML作為模板,
function idToTemplate(id){
const el = query(id);
return el && el.innerHTML;
}
11、如果template是字串,但不是以#開頭,就說明template是用戶設定的模板,不需要進行任何處理,直接使用即可,
12、如果template選項的型別不是字串,則判斷它是否是一個DOM元素,如果是,則使用DOM元素的innerHTML作為模板,如果不是,只需要判斷它是否具備nodeType屬性即可,
13、如果tempalte選項既不是字串,也不是DOM元素,那么Vue.js會觸發警告,提示用戶template選項是無效的,
14、獲取模板之后,下一步是將模板編譯成渲染函式,通過執行compileToFunctions函式可以將模板編譯成渲染函式并設定到this.options上,
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
el = el && query(el);
const options = this.$options;
if(!options.render){
<!-- 新增獲取模板相關邏輯 -->
let template = options.template;
if(template){
if(typeof tempalte === 'string'){
if(tempalte.charAt(0) === "#"){
template = idToTemplate(tempalte);
}
}else if(tempalte.nodeType){
template = template.innerHTML;
}else{
if(process.env.NODE_ENV !== 'production'){
warn('invalid template option:'+tempalte,this);
}
return this;
}
}else if(el){
template = getOuterHTML(el);
}
<!-- 新增編譯相關邏輯 -->
if(tempalte){
const { render } = compileToFunctions(
template,
{...},
this
)
options.render = render;
}
}
return mount.call(this,el);
}
15、將模板編譯成代碼字串并將代碼字串轉換成渲染函式的程序是在compileToFunctions函式中完成的,其內部實作如下
function compileToFunctions(template,options,vm){
options = extend({},options);
<!-- 檢查快取 -->
const key = options.delimiters
? String(options.delimiters)+tempalte
:template;
if(cache[key]){
return cache[key];
}
<!-- 編譯 -->
const compiled = compile(template,options);
<!-- 將代碼字串轉換為函式 -->
const res = {};
res.render = createFunction(compiled.render);
return (cache[key] = res)
}
function createFunction(code){
return new Function(code);
}
1)首先,將options屬性混合到空物件中,其目的是讓options稱為可選引數,
2)檢查快取中是否已經存在編譯后的模板,如果模板已經被編譯,就會直接回傳快取中的結果,不會重復編譯,保證不做無用功來提升性能,
3)呼叫compile函式來編譯模板,將模板編譯成代碼字串并存盤在compiled中的render屬性中,
4)呼叫createFunction函式將代碼字串轉換成函式,其實作原理箱單簡單,使用new Function(code)就可以完成,
5)在代碼字串被new Function(code)轉換成函式之后,當呼叫函式時,代碼字串會被執行,例如
const code = 'console.log("Hello Berwin")';
const render = new Function(code);
render();//Hello Berwin
6)最后,將渲染函式回傳給呼叫方,
16、當通過compileToFunctions函式得到渲染函式之后,將渲染函式設定到this.$options上,
四、只包含運行時版本的vm.$mount的實作原理
(1)只包含運行時版本的vm.
mount方法的核心功能,實作如下
Vue.prototype.$mount = function(el){
el = el && inBrower ? query(el) : undefined;
return mountComponent(this,el);
}
1、$mount方法將ID轉換為DOM元素后,使用mountComponent函式將Vue.js實體掛載到DOM元素上,
2、將實體掛載到DOM元素上指的是將模板渲染到指定的DOM元素中,而且是持續性的,以后當資料(狀態)發生變化時,依然可以渲染到指定的DOM元素中,
3、實作這個功能需要開啟watcher,
watcher將持續觀察模板中用到的所有資料(狀態),當這些資料(狀態)被修改時它將得到通知,從而進行渲染操作,這個程序回持續到實體被銷毀,
export function mountComponent(vm,el){
if(!vm.$options.render){
vm.$options.render = createEmptyVNode;
if(process.env.NODE_ENV !== 'production'){
<!-- 在開發環境發出警告 -->
}
}
}
4、mountComponent方法會判斷實體上是否存在渲染函式,如果不存在,則設定一個默認的渲染函式createEmptyVNode,該渲染函式執行后,會回傳一個注釋型別的VNode節點,
5、事實上,如果在mountComponent方法中發現實體上沒有渲染函式,則會將el引數指定頁面中的元素節點替換成一個注釋節點,并且在開發環境下在瀏覽器的控制臺中給出警告,
(2)Vue.js實體在不同的階段會觸發不同的生命周期鉤子,在掛載實體之前會觸發beforeMount鉤子函式,
export function mountComponent(vm,el){
if(!vm.$options.render){
vm.$options.render = createEmptyVNode;
if(process.env.NODE_ENV !== 'production'){
<!-- 在開發環境發出警告 -->
}
callHook(vm,'beforeMount')
}
}
1、鉤子函式觸發后,將執行真正的掛載操作,掛載操作與渲染類似,不同的是渲染指的是渲染一次,而掛載指的是持續性渲染,掛載之后,每當狀態發生變化時,都會進行渲染操作,
(3)mountComponent具體實作
export function mountComponent(vm,el){
if(!vm.$options.render){
vm.$options.render = createEmptyVNode;
if(process.env.NODE_ENV !== 'production'){
<!-- 在開發環境發出警告 -->
}
<!-- 觸發生命周期鉤子 -->
callHook(vm,'beforeMount');
<!-- 掛載 -->
vm._watcher = new Watcher(vm,()=>{
vm._update(vm._render())
},noop);
<!-- 觸發生命周期鉤子 -->
callHook(vm,'mounted');
return vm;
}
}
1、vm._update作用:呼叫虛擬DOM中的patch方法來執行節點的比對與渲染操作,
2、vm._render作用:執行渲染函式,得到一份新的VNode節點樹,
3、vm._update(vm._render())作用:先呼叫渲染函式得到一份最新的VNode節點樹,然后通過vm._update方法對最新的VNode和上一次渲染用到的舊VNode進行對比并更新DOM節點,簡單來說,就是執行了渲染操作,
(4)掛載是持續性的,而持續性的關鍵就在于new Watcher這行代碼,
1、Watcher的第二個引數支持函式,并且當它是函式時,會同時觀察函式中所讀取的所有Vue.js實體上的回應式資料,
2、當watcher執行函式時,函式中所讀取的資料都將會觸發getter去全域找到watcher并將其收集到函式的依賴串列中,即,函式中讀取的所有資料都將被watcher觀察,這些資料中的任何一個發生變化時,watcher都將得到通知,
3、當資料發生變化時,watcher會一次又一次地執行函式進入渲染流程,如此反復,這個程序會持續到實體被銷毀,
4、掛載完畢后,會觸發mounted鉤子函式,
如果不懂watcher,其實可以去掉看,就簡單很多
export function mountComponent(vm,el){
if(!vm.$options.render){
vm.$options.render = createEmptyVNode;
if(process.env.NODE_ENV !== 'production'){
<!-- 在開發環境發出警告 -->
}
<!-- 觸發生命周期鉤子 -->
callHook(vm,'beforeMount');
<!-- 掛載 -->
vm._update(vm._render())
<!-- 觸發生命周期鉤子 -->
callHook(vm,'mounted');
return vm;
}
}
這樣,是不是很容易理解了,整個mountComponent,一句關鍵代碼:vm._update(vm._render()),表示通過執行vm._render()得到VNode,再把VNode傳入vm._update() ,vm._update()得功能是 將傳入的VNode 變成 真實Dom渲染到頁面,
簡單地總結一下:
$mount()的思路就是, 判斷 用戶傳入的option有沒有render函式,
1.有的話就走運行時版本,
2.沒有的話就自動生成render函式,然后在執行運行時版本(其實這就是編譯時版本,比運行時版本多了異步生成render函式的步驟),
執行運行時版本的時候,
- 通過render()獲得Vnode
- 把Vnode傳入_update() 實作渲染
本文轉載于:
https://juejin.cn/post/6844904181438889991
如果對您有所幫助,歡迎您點個關注,我會定時更新技術檔案,大家一起討論學習,一起進步,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/526907.html
標籤:其他
上一篇:開箱即用 yyg-cli(腳手架工具):快速創建 vue3 組件庫和vue3 全家桶專案
下一篇:JS防抖與節流
