• 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 的某个属性,具体的过程就不再细说了。

  • 相关阅读:
    http://blog.csdn.net/steveguoshao/article/details/38414145
    http://www.tuicool.com/articles/EjMJNz
    http://jingyan.baidu.com/article/7f41ecec1b7a2e593d095ce6.html
    Linux 查看当前时间和修改系统时间
    http://m.blog.csdn.net/article/details?id=49132747
    http://www.cnblogs.com/nick-huang/p/4848843.html
    javaScript事件(一)事件流
    jQuery选择器
    超链接a的target属性
    html基础总结版
  • 原文地址:https://www.cnblogs.com/jonas-von/p/9974107.html
Copyright © 2020-2023  润新知