現在的前端三大框架都有它們獨特的模板,模板的做用就是讓開發編碼變得更加簡單,然而我以爲 Vue 在這一點上是作得近乎完美的(固然,只是我的觀點~~),Vue 模板解釋的核心不外乎就是兩個玩意兒,一個是雙大括號表示式,另外一個是模板指令,這兩東西也是咱們在 Vue 項目中都確定會用到的,下面就來詳細介紹他們是如何實現的。前端
(一)建立模板解釋對象vue
function Vue(options) { // 將配置對象保存在實例對象上 this.$options = options // 將配置對象裏面的data屬性保存在實例對象上 let data = this._data = options.data // 保存實例對象,其實也能夠用箭頭函數~~ let me = this // 遍歷data中的屬性,逐一實現數據劫持 Object.keys(data).forEach(function (key) { me._proxy(key) }) // 模板解釋 this.$compile = new Compile(options.el || document.body,this) }
可見,模板解釋是在數據劫持以後實現的,在實現完數據劫持後,建立模板解釋對象,而且保存到實例對象中,這裏面有兩個參數,第一個就是配置對象中的 el ,也就是掛載的 DOM ,第二個就是 vm 。node
(二)經過 Fragment 容器實現初始化正則表達式
function Compile(el, vm) { // 保存vm this.$vm = vm // 保存el,判斷是不是元素節點,若是不是則嘗試經過選擇器字符來解釋 this.$el = this.isElementNode(el) ? el : document.querySelector(el) // 確保$el存在 if(this.$el){ // 1. 取出el中全部子節點, 封裝在一個fragment對象中 this.$fragment = this.node2Fragment(this.$el) // 2. 編譯fragment中全部層次子節點,這個就是模板編譯的核心方法~~~ this.init() // 3. 將fragment添加到el中 this.$el.appendChild(this.$fragment) } }
初始化的過程也是很容易理解,分三步,先將全部的元素轉移到 fragment 容器中,而後在 fragment 容器中進行初始化,最後將這個 fragment 容器塞回原處。其實 fragment 容器並不進入頁面,這裏塞回去的僅僅是那些給初始化的節點而已。上面用到的三個定義在原型上的函數,isElementNode 用於判斷是不是元素節點;node2Fragment 用於將節點中的全部子節點轉移到 fragment 容器中,init 是初始化的核心函數,用於初始化模板數據:數組
Compile.prototype = { // 將節點中的全部子節點轉移到fragment容器中 node2Fragment:function(node){ // 建立一個fragment對象 let fragment = document.createDocumentFragment() // 循環將元素節點中的全部子節點塞入fragment容器中,最終返回塞滿子節點的fragment對象 let child while(child = node.firstChild){ fragment.appendChild(child) } return fragment }, // 判斷是不是元素節點 isElementNode:function (node) { return node.nodeType === 1 } }
(三)初始化,詳解 init 方法app
Compile.prototype = { init:function(){ // 編譯函數 this.compileElement(this.$fragment) }, compileElement:function(el){ // 獲取全部子節點 const childNodes = el.childNodes // 保存compile對象 const me = this // 將類數組轉化爲真數組,遍歷全部子節點 Array.prototype.slice.call(childNodes).forEach(function (node) { // 獲得節點的文本內容 const text = node.textContent // 定義正則表達式,用於匹配大括號表達式 const reg = /\{\{(.*?)\}\}/ // 元素節點 if(me.isElementNode(node)){ // 編譯元素節點的指令屬性 me.compile(node) }else if(me.isTextNode(node) && reg.test(text)){ // 若是是一個大括號表達式的文本節點 me.compileText(node,RegExp.$1) } // 若是子節點還有子節點,遞歸調用 if(node.childNodes && node.childNodes.length){ me.compileElement(node) } }) }, }
首先,init 方法去調用了compileElement 方法,該方法的主要做用就是處理以前準備好的 fragment 容器,將容器中全部子節點取出,而後進行分類處理,若是是一個元素節點,就去編譯元素節點中的指令,若是是一個大括號表達式的文本節點,就去編譯大括號表達式;若是節點裏面還有子節點,則遞歸調用。順着這個思路,先來研究比較簡單的大括號表達式的狀況(就是compileText這個方法):框架
Compile.prototype = { // 編譯大括號表達式,參數node表明節點,exp表明表達式(就是正則匹配到的那個東西) compileText:function(node,exp){ compileUtil.text(node, this.$vm, exp) } } const compileUtil = { // 解釋 v-text 和 雙大括號表達式,由此也能夠看出其實雙大括號表達式跟v-text指令的實現原理是一致的! text:function (node, vm, exp) { this.bind(node,vm,exp,'text') }, // 真正用於解釋指令的函數 bind:function (node, vm, exp, dir) { // 獲取更新函數 const updaterFn = updater[dir + 'Updater'] updaterFn && updaterFn(node,this._getVMVal(vm,exp)) }, // 獲得表達式對應的value _getVMVal:function (vm, exp) { let val = vm._data exp = exp.split('.') exp.forEach(function (key) { val = val[key] }) return val } } // 更新器 const updater = { // 更新節點的textContent textUpdater:function (node, value) { node.textContent = typeof value === 'undefined' ? '' : value } }
從代碼和註釋上已經很好的說明了整個流程了,這裏再簡單的囉嗦一下吧,其實咱們用的雙大括號表達式也是一種指令,由於它跟v-text的處理是徹底一致的,都是在操做節點的textContent屬性。可能會讓人迷糊的是 _getVMVal函數吧,這個函數的做用就是處理多層次對象的,由於表達式不會僅僅是一層的,也多是兩層或者多層次的,好比,data裏面保存了一個person對象,裏面還有name等其餘屬性,然而咱們極可能會在表達式裏面寫person.name這樣相似的多層次的屬性(說句題外話,vue 不會監聽到對象內部屬性的變化,若是是簡單的經過對象.屬性名的方式去改變對象,那麼vue是不知道的~~),這個函數也正是用於處理這種結構的。由於雙大括號跟其餘指令都非常相似的思想,都是在操做 DOM 的某個屬性,具體的過程就再也不細說了。函數