Vue源碼學習記錄之手寫vm.$mount方法

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

一、概述

在我們開發中,經常要用到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() 實現渲染

到此這篇關於Vue源碼學習記錄之手寫vm.$mount方法 的文章就介紹到這瞭,更多相關vue vm.$mount方法 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: