Vue源码学习笔记2—模板解析

分析准备

[ ].slice.call(lis):将伪数组转换为真数组

        const lis = document.getElementsByTagName('li')
        const lis2 = Array.prototype.slice.call(lis)
        console.log(lis2 instanceof Array) //true
        console.log(lis2[1].innerHTML) //'test2'
        console.log(lis2.forEach) //'function'

Node.nodeType:节点类型

        const elementNode = document.getElementById('app')
        const attrNode = elementNode.getAttributeNode('id')
        const textNode = elementNode.firstChild
        console.log(elementNode.nodeType) // 1
        console.log(attrNode.nodeType) // 2
        console.log(textNode.nodeType) // 3
  1. Document:文档
  2. Element:元素
  3. Attr:属性
  4. Text:文本

DocumentFragment

DocumentFragment:文档片段(高效批量更新多个节点)
document:对应显示的页面,包含n个element,一旦更新document内部的某个元素,界面更新。
documentFragment:内存中保存n个element的容器对象(不与界面关联),如果更新fragment中的某个element,界面不变。

与document相比,最大的区别是DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

    <ul id="fragment_test">
        <li>test1</li>
        <li>test2</li>
        <li>test3</li>
    </ul>
        const ul = document.getElementById('fragment_test')
        // 1. 创建fragment
        const fragment = document.createDocumentFragment()
        // 2. 取出ul中所有子节点保存到fragment
        let child
        // 一个节点只能有一个父亲
        while (child = ul.firstChild) {
            // 先将child从ul中移除,添加为fragment子节点
            fragment.appendChild(child)
        }
        // 3. 更新fragment中所有li的文本
        Array.prototype.slice.call(fragment.childNodes).forEach(node => {
            if (node.nodeType === 1) {
                node.textContent = 'kk'
            }
        })
        // 4. 将fragment插入ul
        ul.appendChild(fragment)

大括号表达式

    <div id="app">
        <p>{{name}}</p>
    </div>
    <script src="./mvvm/compile.js"></script>
    <script src="./mvvm/mvvm.js"></script>
    <script src="./mvvm/observer.js"></script>
    <script src="./mvvm/watcher.js"></script>
    <script>
        new MVVM({
            el: "#app",
            data: {
                name: 'kk'
            }
        })
    </script>

源码分析

以下代码用的是:https://github.com/DMQ/mvvm.git,此版进行简化改造主要说明原理与实现

在这里插入图片描述

function Compile(el, vm) {
    // 保存vm到compile对象
    this.$vm = vm;
    // 将el对应的元素对象保存到compile对象中
    this.$el = this.isElementNode(el) ? el : document.querySelector(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);
    }
}

上述代码所示为上图step into后进入到的compile.js中代码中,如注释所示步骤123即可实现模板解析中的大括号表达式。在第九行打上断点,进入到node2Fragment中

    node2Fragment: function (el) {
        var fragment = document.createDocumentFragment(),
            child;
        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    },

如上所示该部分首先创建一个空的fragment,然后将el中所有子节点转移到fragment,返回fragment。

接下来编译fragment中所有层次的子节点

    init: function () {
        this.compileElement(this.$fragment);
    },

compileElement函数如下所示。

    compileElement: function (el) {
        // 取出最外层所有子节点
        var childNodes = el.childNodes,
            // 保存compile对象
            me = this;
        // 遍历所有子节点(text/element)
        [].slice.call(childNodes).forEach(function (node) {
            //得到节点的文本内容
            var text = node.textContent;
            // 创建正则对象(匹配大括号表达式)
            var reg = /\{\{(.*)\}\}/;
            //判断节点是否是一个元素节点
            if (me.isElementNode(node)) {
                // 解析指令
                me.compile(node);
                //判断节点是否是大括号格式的文本节点
            } else if (me.isTextNode(node) && reg.test(text)) {
                // 编译大括号表达式文本节点
                me.compileText(node, RegExp.$1.trim());
            }
            // 如果当前节点还有子节点,通过递归调用实现所有层次节点的编译
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

开始时取出的childNodes = NodeList(3) [text, p, text],通过遍历所有子节点,根据正则对象得到匹配出的表达式字符串,从data中取出表达式对应的属性值,将属性值设置为文本节点的textContent。{{name}}符合大括号格式的文本节点,进入compileText中

    compileText: function (node, exp) {
        compileUtil.text(node, this.$vm, exp);
    },
var compileUtil = {
    text: function (node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
        bind: function (node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));
        new Watcher(vm, exp, function (value, oldValue) {
            updaterFn && updaterFn(node, value, oldValue);
        });
    },
        _getVMVal: function (vm, exp) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function (k) {
            val = val[k];
        });
        return val;
    },
};

通过bind函数得到需要更新节点的函数,调用函数。通过_getVMVal从vm得到表达式所对应的值更新节点。
最后将编译好的fragment添加页面的el元素中。

事件指令

    <div id="app">
        <p>{{name}}</p>
        <button v-on:click="show">tips</button>
    </div>
    <script>
        new MVVM({
            el: "#app",
            data: {
                name: 'kk'
            },
            methods: {
                show() {
                    alert(this.name)
                }
            }
        })
    </script>

源码分析

以下代码用的是:https://github.com/DMQ/mvvm.git,此版进行简化改造主要说明原理与实现

步骤一,取出el元素中所有子节点保存到一个fragment对象中。与前面大致一样
在这里插入图片描述

步骤二中对元素节点的事件指令属性进行解析。
compile函数如下

    compile: function (node) {
        var nodeAttrs = node.attributes,
            me = this;
        [].slice.call(nodeAttrs).forEach(function (attr) {
            var attrName = attr.name;
            if (me.isDirective(attrName)) {
                var exp = attr.value;
                var dir = attrName.substring(2);
                // 事件指令
                if (me.isEventDirective(dir)) {
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                    // 普通指令
                } else {
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
                // 移除此指令属性
                node.removeAttribute(attrName);
            }
        });
    },

首先得到标签的所有属性nodeAttrs,遍历所有属性nodeAttrs。得到属性名attrName = “v-on:click”。判断是否是指令属性。如果是则继续执行,得到属性值exp = “show”,从属性名中得到指令名dir = “on:click”。判断是否为事件指令,进行解析处理事件指令。指令解析完后移除此指令属性。其中事件处理过程的事件处理函数eventHandler如下

    // 事件处理
    eventHandler: function (node, vm, exp, dir) {
        var eventType = dir.split(':')[1],
            fn = vm.$options.methods && vm.$options.methods[exp];
        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false);
        }
    },

得到事件名(类型)eventType = “click”,从methods中得到表达式所对应的函数(事件回调函数)fn = ƒ show()。如果事件名和时间回调函数都存在,则给节点绑定事件名和回调函数(强制绑定this为vm)的DOM事件监听。

一般指令

    <style>
    <div id="app">
        <p v-text="msg"></p>
        <p v-html="msg"></p>
        <p class="kk" v-class="myclass">hello world</p>
    </div>
        new MVVM({
            el: "#app",
            data: {
                msg: '<a href="http://www.baidu.com">百度</a>',
                myclass: 'xx'
            }
        })

源码分析

以下代码用的是:https://github.com/DMQ/mvvm.git,此版进行简化改造主要说明原理与实现

步骤一取出el元素中所有子节点保存到一个fragment对象中。与前面大致一样,步骤二中对元素节点的一般指令属性进行解析。得到指令名和指令值,从data中根据表达式得到对应的值。
在这里插入图片描述

// 指令处理集合
var compileUtil = {
    // 解析v-text
    text: function (node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // 解析v-html
    html: function (node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },
        // 解析v-class
    class: function (node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },
    bind: function (node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));
        new Watcher(vm, exp, function (value, oldValue) {
            updaterFn && updaterFn(node, value, oldValue);
        });
    },
};

根据指令名确定需要操作元素节点的属性

var updater = {
    textUpdater: function (node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },

    htmlUpdater: function (node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },

    classUpdater: function (node, value, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');

        var space = className && String(value) ? ' ' : '';

        node.className = className + space + value;
    },
 };

该篇为学习过程的学习笔记,若有不足和错误,欢迎指正!

猜你喜欢

转载自blog.csdn.net/xicc1112/article/details/105967497