Vue源碼學習記錄之手寫vm.$mount方法
這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
一、概述
在我們開發(fā)中,經常要用到Vue.extend創(chuàng)建出Vue的子類來構造函數(shù),通過new 得到子類的實例,然后通過$mount掛載到節(jié)點,如代碼:
<div id="mount-point"></div> <!-- 創(chuàng)建構造器 --> var Profile = Vue.extend({ template:'<p>{{firstName}} {{lastName}} aka{{alias}}</p>', data:function(){ return{ firstName:'Walter', lastName:'White', alias:'Heisenberg' } } }) <!-- 創(chuàng)建Profile實例,并掛載到一個元素上 --> new Profile().$mount('#mount-point');
$mount方法是怎么實現(xiàn)的,篇文章就來講一下
二、使用方式
vm.$mount( [elementOrSelector] )
(1)參數(shù)
{ Element | string } [elementOrSelector]
(2)返回值
vm,即實例本身。
(3)用法
1、如果Vue.js實例在實例化時沒有收到el選項,則它處于“未掛載”狀態(tài),沒有關聯(lián)的DOM元素。
2、可以使用vm.$mount手動掛載一個未掛載的實例。
3、如果沒有提供elementOrSelector參數(shù),模板將被渲染為文檔之外的元素,并且必須使用原生DOM的API把它插入文檔中。
4、這個方法返回實例自身,因而可以鏈式調用其他實例方法。
(4)例子
var MyComponent = Vue.extend({ template:'<div>Hello!</div>', }) <!-- 創(chuàng)建并掛載到#app(會替換#app) --> new MyComponent().$mount('#app'); <!-- 創(chuàng)建并掛載到#app(會替換#app) --> new MyComponent().$mount({el:'#app'}); <!-- 創(chuàng)建并掛載到#app(會替換#app) --> var component = new MyComponent().$mount(); document.getElementById('app').appendChild(component.$el);
1、在不同的構建版本中,vm.$mount的表現(xiàn)都不一樣。其差異主要體現(xiàn)在完整版(vue.js)和只包含運行時版本(vue.runtime.js)之間。
2、完整版和只包含運行時版本之間的差異在于是否有編譯器,而是否有編譯器的差異主要在于vm.$mount方法的表現(xiàn)形式。
3、在只包含運行時的構建版本中,vm.mount的作用會稍有不同,它首先會檢查template或el選項所提供的模板是否已經轉換成渲染函數(shù)(render函數(shù))。如果沒有,則立即進入編譯過程,將模板編譯成渲染函數(shù),完成之后再進入掛載與渲染的流程中。
4、只包含運行時版本的vm.$mount沒有編譯步驟,它會默認實例上已經存在渲染函數(shù),如果不存在,則會設置一個。并且,這個渲染函數(shù)在執(zhí)行時會返回一個空節(jié)點的VNode,以保證執(zhí)行時不會因為函數(shù)不存在而報錯。同時如果是開發(fā)環(huán)境下運行,Vue.js會觸發(fā)警告,提示我們當前使用的是只包含運行時的版本,會讓我們提供渲染函數(shù),或者去使用完整的構建版本。
5、從原理的角度來講,完整版和只包含運行時版本之間是包含關系,完整版包含只包含運行時版本。
三、完整版vm.$mount的實現(xiàn)原理
(1)實現(xiàn)代碼
const mount = Vue.prototype.$mount; Vue.prototype.$mount = function(el){ <!-- 做些什么 --> return mount.call(this,el); }
1、將Vue原型上的$mount方法保存在mount中,以便后續(xù)使用。
2、然后Vue原型上的$mount方法被一個新的方法覆蓋了。新方法中會調用原始的方法,這種做法通常被稱為函數(shù)劫持。(看源碼的同學可能發(fā)現(xiàn)了,vue多處用了函數(shù)劫持的做法,例如:對數(shù)組實現(xiàn)監(jiān)聽的時候...)
3、通過函數(shù)劫持,可以在原始功能上新增一些其他功能。上面代碼中,vm.$mount的原始方法就是mount的核心功能,而在完整版中需要將編譯功能新增到核心功能上去。
(2)由于el參數(shù)支持元素類型或者字符串類型的選擇器,所以第一步是通過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元素,如果獲取不到,則創(chuàng)建一個空的div元素。
2、如果el不是字符串,那么認為它是元素類型,直接返回el(如果執(zhí)行vm.$mount方法時沒有傳遞el參數(shù),則返回undefined)
(3)編譯器
1、首先判斷Vue.js實例中是否存在渲染函數(shù),只有不存在時,才會將模板編譯成渲染函數(shù)。
const mount = Vue.prototype.$mount; Vue.prototype.$mount = function(el){ el = el && query(el); const options = this.$options; if(!options.render){ <!-- 將模板編譯成渲染函數(shù)并賦值給options.render --> } return mount.call(this,el); }
2、在實例化Vue.js時,會有一個初始化流程,其中會向Vue.js實例上新增一些方法,這里的this.$options就是其中之一,它可以訪問到實例化Vue.js時用戶設置的一些參數(shù),例如tempalte和render。
3、如果在實例化Vue.js時給出了render選項,那么template其實是無效的,因為不會進入模板編譯的流程,而是直接使用render選項中提供的渲染函數(shù)。
4、Vue.js在官方文檔的template選項中也給出了相應的提示。如果沒有render選項,那么需要獲取模板并將模板編譯成渲染函數(shù)(render函數(shù))賦值給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方法會返回參數(shù)中提供的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會觸發(fā)警告,提示用戶template選項是無效的。
14、獲取模板之后,下一步是將模板編譯成渲染函數(shù),通過執(zhí)行compileToFunctions函數(shù)可以將模板編譯成渲染函數(shù)并設置到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、將模板編譯成代碼字符串并將代碼字符串轉換成渲染函數(shù)的過程是在compileToFunctions函數(shù)中完成的,其內部實現(xiàn)如下
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); <!-- 將代碼字符串轉換為函數(shù) --> const res = {}; res.render = createFunction(compiled.render); return (cache[key] = res) } function createFunction(code){ return new Function(code); }
1)首先,將options屬性混合到空對象中,其目的是讓options稱為可選參數(shù)。
2)檢查緩存中是否已經存在編譯后的模板。如果模板已經被編譯,就會直接返回緩存中的結果,不會重復編譯,保證不做無用功來提升性能。
3)調用compile函數(shù)來編譯模板,將模板編譯成代碼字符串并存儲在compiled中的render屬性中。
4)調用createFunction函數(shù)將代碼字符串轉換成函數(shù)。其實現(xiàn)原理箱單簡單,使用new Function(code)就可以完成。
5)在代碼字符串被new Function(code)轉換成函數(shù)之后,當調用函數(shù)時,代碼字符串會被執(zhí)行。例如
const code = 'console.log("Hello Berwin")'; const render = new Function(code); render();//Hello Berwin
6)最后,將渲染函數(shù)返回給調用方。
16、當通過compileToFunctions函數(shù)得到渲染函數(shù)之后,將渲染函數(shù)設置到this.$options上。
四、只包含運行時版本的vm.$mount的實現(xiàn)原理
(1)只包含運行時版本的vm.mount方法的核心功能。實現(xiàn)如下
Vue.prototype.$mount = function(el){ el = el && inBrower ? query(el) : undefined; return mountComponent(this,el); }
1、$mount方法將ID轉換為DOM元素后,使用mountComponent函數(shù)將Vue.js實例掛載到DOM元素上。
2、將實例掛載到DOM元素上指的是將模板渲染到指定的DOM元素中,而且是持續(xù)性的,以后當數(shù)據(jù)(狀態(tài))發(fā)生變化時,依然可以渲染到指定的DOM元素中。
3、實現(xiàn)這個功能需要開啟watcher。
watcher將持續(xù)觀察模板中用到的所有數(shù)據(jù)(狀態(tài)),當這些數(shù)據(jù)(狀態(tài))被修改時它將得到通知,從而進行渲染操作。這個過程回持續(xù)到實例被銷毀。
export function mountComponent(vm,el){ if(!vm.$options.render){ vm.$options.render = createEmptyVNode; if(process.env.NODE_ENV !== 'production'){ <!-- 在開發(fā)環(huán)境發(fā)出警告 --> } } }
4、mountComponent方法會判斷實例上是否存在渲染函數(shù)。如果不存在,則設置一個默認的渲染函數(shù)createEmptyVNode,該渲染函數(shù)執(zhí)行后,會返回一個注釋類型的VNode節(jié)點。
5、事實上,如果在mountComponent方法中發(fā)現(xiàn)實例上沒有渲染函數(shù),則會將el參數(shù)指定頁面中的元素節(jié)點替換成一個注釋節(jié)點,并且在開發(fā)環(huán)境下在瀏覽器的控制臺中給出警告。
(2)Vue.js實例在不同的階段會觸發(fā)不同的生命周期鉤子,在掛載實例之前會觸發(fā)beforeMount鉤子函數(shù)。
export function mountComponent(vm,el){ if(!vm.$options.render){ vm.$options.render = createEmptyVNode; if(process.env.NODE_ENV !== 'production'){ <!-- 在開發(fā)環(huán)境發(fā)出警告 --> } callHook(vm,'beforeMount') } }
1、鉤子函數(shù)觸發(fā)后,將執(zhí)行真正的掛載操作。掛載操作與渲染類似,不同的是渲染指的是渲染一次,而掛載指的是持續(xù)性渲染。掛載之后,每當狀態(tài)發(fā)生變化時,都會進行渲染操作。
(3)mountComponent具體實現(xiàn)
export function mountComponent(vm,el){ if(!vm.$options.render){ vm.$options.render = createEmptyVNode; if(process.env.NODE_ENV !== 'production'){ <!-- 在開發(fā)環(huán)境發(fā)出警告 --> } <!-- 觸發(fā)生命周期鉤子 --> callHook(vm,'beforeMount'); <!-- 掛載 --> vm._watcher = new Watcher(vm,()=>{ vm._update(vm._render()) },noop); <!-- 觸發(fā)生命周期鉤子 --> callHook(vm,'mounted'); return vm; } }
1、vm._update作用:調用虛擬DOM中的patch方法來執(zhí)行節(jié)點的比對與渲染操作。
2、vm._render作用:執(zhí)行渲染函數(shù),得到一份新的VNode節(jié)點樹。
3、vm._update(vm._render())作用:先調用渲染函數(shù)得到一份最新的VNode節(jié)點樹,然后通過vm._update方法對最新的VNode和上一次渲染用到的舊VNode進行對比并更新DOM節(jié)點。簡單來說,就是執(zhí)行了渲染操作。
(4)掛載是持續(xù)性的,而持續(xù)性的關鍵就在于new Watcher這行代碼。
1、Watcher的第二個參數(shù)支持函數(shù),并且當它是函數(shù)時,會同時觀察函數(shù)中所讀取的所有Vue.js實例上的響應式數(shù)據(jù)。
2、當watcher執(zhí)行函數(shù)時,函數(shù)中所讀取的數(shù)據(jù)都將會觸發(fā)getter去全局找到watcher并將其收集到函數(shù)的依賴列表中。即,函數(shù)中讀取的所有數(shù)據(jù)都將被watcher觀察。這些數(shù)據(jù)中的任何一個發(fā)生變化時,watcher都將得到通知。
3、當數(shù)據(jù)發(fā)生變化時,watcher會一次又一次地執(zhí)行函數(shù)進入渲染流程,如此反復,這個過程會持續(xù)到實例被銷毀。
4、掛載完畢后,會觸發(fā)mounted鉤子函數(shù)。
如果不懂watcher,其實可以去掉看,就簡單很多
export function mountComponent(vm,el){ if(!vm.$options.render){ vm.$options.render = createEmptyVNode; if(process.env.NODE_ENV !== 'production'){ <!-- 在開發(fā)環(huán)境發(fā)出警告 --> } <!-- 觸發(fā)生命周期鉤子 --> callHook(vm,'beforeMount'); <!-- 掛載 --> vm._update(vm._render()) <!-- 觸發(fā)生命周期鉤子 --> callHook(vm,'mounted'); return vm; } }
這樣,是不是很容易理解了。整個mountComponent
,一句關鍵代碼:vm._update(vm._render())
,表示通過執(zhí)行vm._render()得到VNode,再把VNode傳入vm._update()
,vm._update()
得功能是 將傳入的VNode 變成 真實Dom渲染到頁面。
簡單地總結一下:
$mount()的思路就是, 判斷 用戶傳入的option有沒有render函數(shù),
1.有的話就走運行時版本,
2.沒有的話就自動生成render函數(shù),然后在執(zhí)行運行時版本(其實這就是編譯時版本,比運行時版本多了異步生成render函數(shù)的步驟)。
執(zhí)行運行時版本的時候,
通過render()獲得Vnode把Vnode傳入_update() 實現(xiàn)渲染
到此這篇關于Vue源碼學習記錄之手寫vm.$mount方法 的文章就介紹到這了,更多相關vue vm.$mount方法 內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Vue3 去除 vue warn 及生產環(huán)境去除console.log的方法
這篇文章主要介紹了Vue3 去除 vue warn 及生產環(huán)境去除console.log的方法,本文結合實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-06-06在vue項目中使用Jquery-contextmenu插件的步驟講解
今天小編就為大家分享一篇關于在vue項目中使用Jquery-contextmenu插件的步驟講解,小編覺得內容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-01-01