Vue模板编译简单解析

compile的作用:

  • 解析指令(属性节点)与插值表达式(文本节点),并替换模板数据,初始化视图;
  • 将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图;

什么是DocumentFragments?

DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的 Document 使用,用于存储已排好版的或尚未打理好格式的 XML 片段。最大的区别是因为 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

实现virtualNode,将挂载点内的所有节点存储到DocumentFragment中
virtualNode(node) {
    let fragment = document.createDocumentFragment()
    // 把el中所有的子节点挨个添加到文档片段中
    let childNodes = node.childNodes
    // 由于childNodes是一个类数组,所以我们要把它转化成为一个数组,以使用forEach方法
    Array.from(childNodes).forEach(node => {
        // 把所有的字节点添加到fragment中
        fragment.appendChild(node)
    })
    return fragment
}

接下来我们要做的事情就是解析fragment里面的节点:compile(fragment)
我们要递归遍历fragment里面的所有子节点,根据节点类型进行判断,如果是文本节点则按插值表达式进行解析,如果是属性节点则按指令进行解析。在解析属性节点的时候,我们还要进一步判断:是不是由v-开头的指令,或者是特殊字符,如@、:开头的指令。

class Compile {
    constructor(el, vm) {
        this.el = typeof el === "string" ? document.querySelector(el) : el
        this.vm = vm
        // 解析模板内容
        if (this.el) {
        const fragment = this.virtualNode(this.el)
        this.compile(fragment)
        this.el.appendChild(fragment)
        }
    }
    // 解析fragment里面的节点
    compile(fragment) {
        let childNodes = fragment.childNodes
        this.toArray(childNodes).forEach(node => {
            // 如果是元素节点,则解析指令
            if (this.isElementNode(node)) {
                this.compileElementNode(node)
            }

            // 如果是文本节点,则解析差值表达式
            if (this.isTextNode(node)) {
                this.compileTextNode(node)
            }

            // 递归解析
            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }
        })
    }
}

接下来我们要做的就只剩下解析指令,并通过watcher把解析后的结果通知给视图了。

这里有两个问题大家需要注意一下:

如果是复杂数据的情形,例如插值表达式:{{dad.son.name}}或者

,我们拿到v-text的属性值是字符串dad.son.name,我们是无法通过vm.$data['dad.son.name']拿到数据的。因此,如果数据是复杂数据的情形,我们需要实现getVMData()setVMData()方法进行数据的获取与修改。
在vue中,methods里面的方法里面的this是指向vue实例,因此,在我们通过v-on指令给节点绑定方法的时候,我们需要把该方法的this指向绑定为vue实例。

let CompileUtils = {
    getVMData(vm, expr) {
        let data = vm.$data
        expr.split('.').forEach(key => {
            data = data[key]
        })
        return data
    },
    setVMData(vm, expr,value) {
        let data = vm.$data
        let arr = expr.split('.')
        arr.forEach((key,index) => {
            if(index < arr.length -1) {
                data = data[key]
            } else {
                data[key] = value
            }
        })
    },
    // 解析插值表达式
    mustache(node, vm) {
        let txt = node.textContent
        let reg = /\{\{(.+)\}\}/
        if (reg.test(txt)) {
            let expr = RegExp.$1
            node.textContent = txt.replace(reg, this.getVMData(vm, expr))
            new Watcher(vm, expr, newValue => {
                node.textContent = txt.replace(reg, newValue)
            })
        }
    },
    // 解析v-text
    text(node, vm, expr) {
        node.textContent = this.getVMData(vm, expr)
        new Watcher(vm, expr, newValue => {
            node.textContent = newValue
        })
    },
    // 解析v-html
    html(node, vm, expr) {
        node.innerHTML = this.getVMData(vm, expr)
        new Watcher(vm, expr, newValue => {
            node.innerHTML = newValue
        })
    },
    // 解析v-model
    model(node, vm, expr) {
        let that = this
        node.value = this.getVMData(vm, expr)
        node.addEventListener('input', function () {
            // 下面这个写法不能深度改变数据
            // vm.$data[expr] = this.value
            that.setVMData(vm,expr,this.value)
        })
        new Watcher(vm, expr, newValue => {
            node.value = newValue
        })
    },
    // 解析v-on
    eventHandler(node, vm, eventType, expr) {
        // 处理methods里面的函数fn不存在的逻辑
        // 即使没有写fn,也不会影响项目继续运行
        let fn = vm.$methods && vm.$methods[expr]
        
        try {
            node.addEventListener(eventType, fn.bind(vm))
        } catch (error) {
            console.error('抛出这个异常表示你methods里面没有写方法\n', error)
        }
    }
}

你可能感兴趣的:(Vue模板编译简单解析)