模板编译template的背后,究竟发生了什么事?带你了解template的纸短情长
解析模板編譯template的背后發生了什么
- 一、📑初識模板編譯
- 1、vue組件中使用render代替template
- 2、模板編譯總結
- 二、??感受模板編譯的美
- 1、with語法
- (1)例子展示🌰
- (2)知識點歸納
- 三、📈編譯模板
- 1、編譯模板碎碎念
- 2、編譯模板過程
- (1)初始化一個npm環境
- (2)安裝編譯器
- (3)新建新文件
- (4)了解縮寫函數
- (5)編譯插值
- (6)編譯表達式
- (7)編譯屬性和動態屬性
- (8)編譯條件
- (9)編譯循環
- (10)編譯事件
- (11)編譯v-model
- 3、模板編譯總結
- 四、🔑組件渲染/更新過程
- 1、初識組件渲染/更新
- 2、組件渲染/更新過程
- (1)初次渲染過程
- 1)解析模板為render函數
- 2)觸發響應式
- 3)執行render函數,生成vnode
- (2)更新過程
- 1)更新過程細述
- 2)完成流程圖
- (3)異步渲染
- 3、小結
- 五、??結束語
依稀記得我們在vue時,最上方總是有一個 template 包圍著。而很多時候,我們也沒有很在意的去意識到 <template></template> 究竟是什么。
在今天的這篇文章中,就帶大家一起來了解,模板編譯 template 的背后,究竟發生了什么事情?
一起來了解模板編譯的紙短情長🚋🚋🚋
一、📑初識模板編譯
1、vue組件中使用render代替template
template ,即模板。模板是 vue 開發中最常用的部分,即與vue的使用關聯最緊密的原理。它不是 html ,它有指令、有插值、也有 JS 表達式,那它,到底是什么呢?我們來看個例子。
在 vue 中定義一個組件,通常會使用 template 模板字符串來定義一個組件。比如:
Vue.component('heading',{template:`xxx` })一般情況下,模板的定義是上面這種情況。同時,在程序編譯期間,模板會將 template 的這種字符串類型,編譯成 render 函數。
但是呢,在有些復雜的情況下,可能就不能用 template 函數了,這個時候會考慮直接用 render 函數來定義一個組件。比如:
Vue.component('heading',{render: function(createElement){return createElement('h' + this.level, //tag props[ //childrencreateElement('a',{attrs:{name:'headerId',href:'#' + 'headerId'}},'this is a tag')])} })就像上面這樣子,我們也可以通過使用一個 render 函數來定義一個組件。
2、模板編譯總結
看完上面的例子,我們來做個小結?
- template,即模板。這個模板會編譯成 render 函數,其中 render 函數用的是 with 語法。
- 過程:模板→ render 函數→ vnode →組件渲染和更新過程。
- vue 組件可以用 render 函數代替 template 。
- React 一直都用 render ,沒有模板(這里僅作知識補充,不做講解)。
二、??感受模板編譯的美
1、with語法
(1)例子展示🌰
先來了解模板編譯中一個很重要的知識點, with 語法。下面先用一個例子來展示with語法與普通語法的不同。
不使用with語法執行程序時:
const obj = {a: 100, b: 200}console.log(obj.a) console.log(obj.b) console.log(obj.c) //undefined使用with語法執行程序時:
//使用with,能改變 {} 內自由變量的查找方式 // 將 {} 內自由變量,當作 obj 的屬性來查找 with(obj){console.log(a)console.log(b)console.log(c) //會報錯!!! }(2)知識點歸納
看完上面with語法的例子,我們來對 with 語法做一個知識點歸納。
- with 語法會改變 {} 內自由變量的查找規則,當作 obj 屬性來查找;
- 如果在當前 {} 內找不到匹配的 obj 屬性,就會報錯;
- with 要謹慎使用,它打破了作用域規則,會讓其易讀性變差。
三、📈編譯模板
1、編譯模板碎碎念
在前面中我們講過,模板它不是 html ,它有指令、有插值、也有JS表達式,它能實現判斷、也能實現循環。
試想一下模板為什么不是 html ?
思考一下,假如你在寫程序時,能用 html 寫出一個判斷或者循環出來嗎?答案自然時不行的。
所以說, html 只是一個靜態的標簽語言,你寫什么它就顯示什么,它沒有辦法實現一個邏輯,或者做循環和判斷。
因此,對于前端瀏覽器而言,只有 JS 才能實現判斷和循環等各種邏輯功能。
所以,模板一定是轉換為某種 JS 代碼之后才進行運行的。而這個模板怎么轉換成 js 代碼的這個過程,就稱為編譯模板。
那這個模板是怎么轉的呢?接下來我們來看下編譯模板的過程。
2、編譯模板過程
(1)初始化一個npm環境
首先先建立一個新文件,可以命名為 vue-template-complier-demo 。之后用以下命令行初始化一個npm的環境:
npm init -y(2)安裝編譯器
npm 安裝模板編譯器。命令行如下:
npm install vue-template-compiler --save(3)新建新文件
在根目錄下初始化新建一個 index.js 文件,并引入 vue-template-compiler 。代碼如下:
//引入vue-template-compiler const compiler = require('vue-template-compiler')// 編譯 const res = compiler.compile(template) console.log(res.render)接下來我們就來看下,模板中的插值、表達式、屬性和動態屬性等等類型的編譯,到底是怎么樣的?
(4)了解縮寫函數
以下vue源碼中的縮寫函數先了解,將在下面的講解中用到。
// 從 vue 源碼中找到縮寫函數的含義 function installRenderHelpers (target) {target._c = createElement;target._o = markOnce;target._n = toNumber;target._s = toString;target._l = renderList;target._t = renderSlot;target._q = looseEqual;target._i = looseIndexOf;target._m = renderStatic;target._f = resolveFilter;target._k = checkKeyCodes;target._b = bindObjectProps;target._v = createTextVNode;target._e = createEmptyVNode;target._u = resolveScopedSlots;target._g = bindObjectListeners;target._d = bindDynamicKeys;target._p = prependModifier; }(5)編譯插值
//引入vue-template-compiler const compiler = require('vue-template-compiler')// 插值 const template = `<p>{{message}}</p>` // with(this){return createElement('p',[createTextVNode(toString(message))])} // h -> vnode // createElement -> vnode// 編譯 const res = compiler.compile(template) console.log(res.render)編譯以上內容,打印結果如下:
從上圖中可以看到,插值類型的模板最終被編譯成一個 with 語句,并且這個 with 語句的參數都指向了 this 。
同時,大家可以看到,里面有一個 _c , _v , _s。那這幾個元素是什么呢?
這個就是上面第四點中提到的 vue 源碼中的縮寫函數。 _c 對應的就是源碼中的 createElement, _v 對應的就是源碼中的 createTextVNode ,_s 對應的就是源碼中的 toString 。
所以,以上編譯后的 with 語句 with(this){return _c('p',[_v(_s(message))])} ,事實上就是 with(this){return createElement('p',[createTextVNode(toString(message))])} 。
以上這個語句的意思為,編譯創建一個 p 元素,之后呢, p 元素就沒有子元素了,于是就創建它的文本節點 message ,同時 message 是字符串的形式存在,因此要進行 toString 。
額外再補充一個知識點, createElement 其實就等于我們平常所說的 h 函數,返回的是一個 虛擬DOM 節點。
以上就是一個插值模板編譯的過程,下面再用幾個例子讓大家熟悉。
(6)編譯表達式
//引入vue-template-compiler const compiler = require('vue-template-compiler')// 表達式 const template = `<p>{{flag ? message : 'no message found'}}</p>` // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}// 編譯 const res = compiler.compile(template) console.log(res.render)編譯以上內容,打印結果如下:
依據上面插值的分析方法,我們來分析表達式的模板編譯過程。
表達式編譯后的結果返回了一個虛擬 DOM 節點,同樣地,查詢 vue 源碼中的縮寫函數我們可以發現, with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])} 最終的結果等于 with(this){return createElement('p',[createTextVnode(toString(flag ? message : 'no message found'))])} 。
先創建了一個 p 元素,之后 p 元素沒有子元素了,于是創建文本節點,最終 toString 三目表達式里面的內容。
(7)編譯屬性和動態屬性
//引入vue-template-compiler const compiler = require('vue-template-compiler')// 屬性和動態屬性 const template = `<div id="div1" class="container"><img :src="imgUrl"/></div> ` // with(this){return _c('div', // {staticClass:"container",attrs:{"id":"div1"}}, // [ // _c('img',{attrs:{"src":imgUrl}})])}// 編譯 const res = compiler.compile(template) console.log(res.render)編譯以上內容,打印結果如下:
依據上面的分析方法,我們來分析屬性和動態屬性的模板編譯過程。
屬性和動態屬性編譯后的結果返回了一個虛擬 DOM 節點,同樣地,查詢 vue 源碼中的縮寫函數我們可以發現, with(this){return _c('div',{staticClass:"container",attrs:{"id":"div1"}},[_c('img',{attrs:{"src":imgUrl}})])} 最終的結果等于 with(this){return createElement('div',{staticClass:"container",attrs:{"id":"div1"}},[createElement('img',{attrs:{"src":imgUrl}})])} 。
此時我們可以看到,返回的 vnode 節點中,包含 class 名字, container 。此時 div 有一個 id 選擇器,這個 id 選擇器是該 div 的一個屬性,于是就通過attrs來表示。
最外層結束后,里面還有一層, img 。 img 可以視其為跟 div 一樣的標簽,于是先創建 img 元素,又因為 img 綁定了一個具體的值,就像是 div 里面綁定了 id 選擇器。所以在創建完 img 的值之后,繼續用 attrs 來傳遞 img 所綁定的值。
(8)編譯條件
// 條件 const template = `<div><p v-if="flag === 'a'">A</p><p v-else>B</p></div> ` // with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}編譯以上內容,打印結果如下:
依據上面的分析方法,我們來分析條件的模板編譯過程。
對于條件來說,首先是先創建一個 div 元素,之后呢,模板編譯把 v-if 和 v-else 分割成一個三目表達式的方式來進行編譯。
(9)編譯循環
// 循環 const template = `<ul><li v-for="item in list" :key="item.id">{{item.title}}</li></ul> ` // with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}編譯以上內容,打印結果如下:
依據上面的分析方法,我們來分析循環的模板編譯過程。
對于以上循環來說,首先會創建一個 ul 元素,之后查詢 _l 的縮寫函數我們知道它是 renderlist , 所以 list 列表會被 renderList 函數進行編譯。
最后渲染后的 item 被當作函數的參數進行傳遞,并列返回對應 item 的 li 列表元素。
(10)編譯事件
// 事件 const template = `<button @click="clickHandler">submit</button> ` // with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}編譯以上內容,打印結果如下:
依據上面的分析方法,我們來分析事件的模板編譯過程。
對于事件來說,首先會創建一個 button 元素,之后 @click 即 v-on:click 會被編譯成 on:{"click":clickHandler} 。最后是 _v ,即 createTextVNode 。創建一個 submit 的文本節點,將 click 的內容提交上去。
(11)編譯v-model
// v-model const template = `<input type="text" v-model="name">` // 主要看 input 事件 // with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}編譯以上內容,打印結果如下:
依據上面的分析方法,我們來分析雙向綁定v-model的模板編譯過程。
對于 v-model 來說,主要看的是 input 事件。 v-model 的背后,綁定的是 name 和 value 這兩個語法糖。之后通過 attrs 去創建 類型type 為 text 的屬性。
最終是 input 事件, input 事件綁定 $event ,最后, name 的值就等同于 $event.target.value ,這樣,數據就實現了雙向綁定。
3、模板編譯總結
看完上述的內容,我們來對模板編譯做個小結:
(1)從render函數到vnode
模板編譯后是一個 render 函數,執行 render 函數后返回一個 vnode ;
(2)vnode到patch和diff
基于 vnode 的基礎上,再執行 patch 和 diff ;
(3)模板編譯工具
在平常的開發中,我們可以使用 webpack 、 vue-loader 等構建工具,在開發環境下編譯模板。
四、🔑組件渲染/更新過程
1、初識組件渲染/更新
講完上完的內容,我們再來講一個與編譯模板關聯性很強的知識點:組件渲染/更新過程。
一個組件,從渲染到頁面上開始,再到修改 data 去觸發更新(數據驅動視圖),其背后的原理是什么,又需要掌握哪些要點呢?
事實上,組件在渲染之前,會先進行模板編譯,模板 template 會編譯成 render 函數。
之后就是數據的監聽了,這就要談到響應式數據。vue的響應式通過操作 Object.defineProperty() ,去監聽 getter 和 setter 方法,來使得數據實時更新。
監聽完數據之后,就是執行 render 函數,生成 vnode 。
到了 vnode (即 vdom )這一步之后,會進行 patch(elem,vnode) 和 patch(vnode,newVnode) 的比較。
關于響應式原理和vdom的解讀,如有需要可以查看我的前兩篇文章進行學習,這里不再展開細述~
2、組件渲染/更新過程
組件渲染和更新過程主要經過以下三個步驟:初次渲染過程→更新過程→異步渲染。
接下來就這三個步驟進行一一講解。
(1)初次渲染過程
初次渲染過程,即組件第一次渲染是怎么樣的,怎么把模板渲染到頁面上。具體有以下三個步驟:
- 解析模板為 render 函數;
- 觸發響應式,監聽 data 屬性 getter 和 setter ;
- 執行 render 函數,生成 vnode ,進行 patch(elem,vnode) 。
下面就這三個步驟來進行一一講解。
1)解析模板為render函數
在開發環境下,解析模板為 render 函數一般是由 vue-loader 這個插件來處理的。還有一種情況就是,用戶直接用 cdn 的方式引入 vuejs 的文件進行本地代碼練習,這種情況下,解析模板為 render 函數就是在瀏覽器環境運行的。
小知識了解完,我們來看下這個步驟。
解析模板為 render 函數,即解析 template 為 render 函數,這個就是上述文章中說的編譯模板。
2)觸發響應式
在編譯完模板之后, render 函數有了,我們來開始監聽 data 屬性。
監聽 data 屬性,這個時候我們就需要觸發響應式,也就是渲染數據。
那在這個階段怎么渲染數據呢?
這個階段我們需要執行 render 函數, render 函數會觸發 getter 方法,因為數據沒有進行更新,只是進行渲染。只有在進行渲染的時候才會操作 setter 方法。
3)執行render函數,生成vnode
最后,當數據渲染完畢后,就會執行第一步生成的 render 函數,然后生成虛擬 DOM 節點 vnode ,之后進行 patch(elem,vnode) 。
(2)更新過程
1)更新過程細述
更新過程,即 data 修改之后,組件是怎么更新的。
在這個階段呢,將會修改 data ,并且觸發 setter (注意:在此之前 data 在 getter 中已經被監聽)。
觸發完 setter 之后,重新執行 render 函數,并生成 newVnode ,最后進行 patch(vnode, newVnode) 的diff比較。
2)完成流程圖
接下來我們用一張流程圖來完整的回顧渲染和更新的過程。
(3)異步渲染
在渲染和更新結束之后,我們的程序可能還有可能會發生多個程序同時加載,這就涉及到一個異步渲染問題。
異步渲染問題,我們用 $nextTick 來作為例子講解。
假設我們現在要實現一個功能,當我們點擊按鈕時,打印出列表的項數。這個時候我們大多人可能會這么操作。
<template><div id="app"><!-- ref的設置時為了方便后續可以用來:取節點的DOM元素 --><ul ref="ul1"><li v-for="(item, index) in list" :key="index">{{item}}</li></ul><button @click="addItem">添加一項</button></div> </template><script> export default {name: 'app',data() {return {list: ['a', 'b', 'c']}},methods: {addItem() {this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)// 獲取 DOM 元素const ulElem = this.$refs.ul1console.log( ulElem.childNodes.length )}} } </script>此時瀏覽器的顯示效果如下:
細心的小伙伴已經發現,瀏覽器并沒有按照我們所想的打印。當頁面上的列表顯示 6項 內容時,此時控制臺只打印 3項 ;當顯示 9項 時,此時控制臺直接只打印 6項 。
那這究竟時為什么呢?
其實,當我們點擊的那一刻, data 發生變化,但是 DOM 并不會立刻進行渲染。所以等到我們點擊完成的時候,獲取的元素還是原來觸發的內容,而不會增添上新的內容。
那我們所期望的是,當點擊之后立刻觸發 DOM 渲染并拿到最新的值。這個時候就需要用到 nextTick 。具體代碼如下:
<script> export default {name: 'app',data() {return {list: ['a', 'b', 'c']}},methods: {addItem() {this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)// 1. 異步渲染,$nextTick 待 DOM 渲染完再回調,// 即NextTick函數會在多次data修改完并且全部DOM渲染完再觸發,僅在最后觸發一次// 2. 頁面渲染時會將 data 的修改做整合this.$nextTick(() => {// 獲取 DOM 元素const ulElem = this.$refs.ul1console.log( ulElem.childNodes.length )})}} } </script>我們通過給獲取 DOM 元素的代碼外面再嵌套一層 $nextTick 函數,來達到我們想要的效果。在此過程中,當我們點擊結束后, data 的值發生變化,此時 $nextTick 會等待DOM全部渲染完成之后再進行回調。
最終瀏覽器的打印效果如下:
所以,也就是說, $nextTick 通過匯總 data 的修改,最后再一次性更新視圖。
這樣可以減少 DOM 的操作次數,大大的提高了性能。
3、小結
經過上述一系列的講解,我們可以把內容分割成以下兩個要點:
- 要理解清楚渲染和響應式、渲染和模板編譯、渲染和vdom的關系。
- 要理解組件渲染/更新的過程:初次渲染過程→更新過程→異步渲染。
五、??結束語
從模板編譯,到組件渲染更新過程,我們了解了整個 template 背后的全過程。相信通過本文的學習,大家對模板編譯有了一個更深的認識。
關于模板編譯的內容就講到這里啦!如有不理解或文章有誤,歡迎評論區留言或私信我交流~
- 關注公眾號 星期一研究室 ,不定期分享學習干貨,更多有趣的專欄待你解鎖~
- 如果這篇文章對你有幫助,記得 點個贊加個關注 再走哦~
- 我們下期見!🥂🥂🥂
總結
以上是生活随笔為你收集整理的模板编译template的背后,究竟发生了什么事?带你了解template的纸短情长的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 红魔:首款国产 49 英寸 OLED 显
- 下一篇: 『软件测试5』测开岗只要求会黑白盒测试?