1. 程式人生 > >Vue 模板解釋

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 的某個屬性,具體的過程就不再細說了。