Vue 模板解釋
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 。
(二)通過 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 方法
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 的某個屬性,具體的過程就不再細說了。