关于vue.js:模仿vue自己动手写响应式框架三-dom解析

6次阅读

共计 3655 个字符,预计需要花费 10 分钟才能阅读完成。

背景

在上一篇中,咱们保障了代码不报错,并且参数也胜利传进去了,但未实现任何的逻辑,这一章咱们的工作就是实现 dom 的解析,在 vue 中,通过构建虚构 dom 构造实现 dom 的高效更新和渲染,模拟这个思路,咱们构建一个超级简略虚构的 dom 构造,本着让所有人都能看得懂的目标,这次构建的 dom 构造不思考性能,不思考设计,以能实现性能为目标。

domParser(dom 解析)

根本的思路:

  • 从参数 el 指定的节点开始遍历
  • 解析节点
  • 解析子节点
  • 递归的形式实现所有节点的解析
(function (global, factory) {global.Vue = factory})(this, function (options) {

    //1. 解析 dom 构造
    domParser();

    function domParser() {
        let el = options.el;
         // 配置的 el 以 '#' 结尾
        if (el.startsWith("#")) {el = el.substr(1);
        }
        let app = document.getElementById(el);
        let virtualDom = document.createDocumentFragment();
        for (let i = 0; i < app.childNodes.length; ++i) {let node = compile(app.childNodes[i]);
            virtualDom.appendChild(node);
        }
        app.innerHTML = '';
        app.appendChild(virtualDom);
    }
})
  • createDocumentFragment创立虚构 dom 的顶层节点
  • 先不思考 compile 的实现,在 compile 中咱们将实现变量的解析,事件挂载等,但这里先不论
  • app.innerHTML = '';暴力的将原始内容全副革除
  • app.appendChild(virtualDom);插入虚构 dom 构造

compile(节点编译)

在 dom 构造中,每一个节点都是一个 Node,Node 有不同的类型,尽管规范中类型较多,但罕用的就两种,元素类型 (Node.ELEMENT_NODE) 和文本类型 (Node.TEXT_NODE),比方<p>hello</p> 蕴含两种类型,<p>为元素类型,hello为文本类型。ELEMENT_NODE 值为 1,TEXT_NODE 为 3,在 compile 中,这两种类型的解析形式不一样,TEXT 只需解析文本即可,ELEMENT 类型须要解析属性。

function compile(node) {let element = node.cloneNode(false);
        for (let i = 0; i < node.childNodes.length; ++i) {element.appendChild(compile(node.childNodes[i]));
        }
        if (element.nodeType == 3) {
            // 文本类型解析
            let vars = parseVariable(element.textContent);
            for (let i = 0; i < vars.length; ++i) {let directive = Directive(element);
                addSubscriber(vars[i], directive);
            }
        } else if (element.nodeType == 1 && element.attributes) {
            // 元素类型解析
            let attrs = element.attributes;
            for (let i = 0; i < attrs.length; ++i) {let name = attrs[i].name;
                if (name.startsWith("v-bind") || name.startsWith(":")) {let directive = Directive(element, name, attr[i].value);
                    addSubscriber(vars[i], directive);
                }
            }
        }
        return element;
    }
  • cloneNode(false)克隆以后节点,false 示意不复制子节点,因为咱们须要对子节点进行逐个的解析
  • parseVariable解析文本中变量,也就是相似 {{param}} 字符串
  • Directive 内容见下文
  • Subscriber 内容见下文
  • 对于元素类型,逐个遍历属性,如果属性名称以 v-bind 或者 : 结尾就认为绑定了变量

parseVariable(变量解析)

function parseVariable(content) {var variables = {};
        let m;
        let variableRegExp = new RegExp("\{\{([^\}]+\)}\}", "g");
        while (m = variableRegExp.exec(content)) {if (!variables[m[1]]) {variables[m[1]] = true;
            }
        }
        var items = [];
        for (k in variables) {items.push(k);
        }
        return items;
    }

这里间接用正则对变量进行解析。

Directive(指令)

这里是借鉴了 vue 的概念,在 vue 中指令能够实现一系列特定的性能,比方指令 v-model 能够将值绑定到变量,v-for能够对变量进行循环。咱们这次只实现页面上应用到指令,咱们在这个系列中将实现以下指令

  • v-model:实现双向绑定
  • v-on:实现事件绑定
  • title:设置 title 属性
  • style:设置 style 属性
function Directive(node, attr, expression) {
        var directive = {node: node}
        if (node.nodeType == 3) {directive.change = function (value) {this.node.textContent = value;}
        } else if (node.nodeType == 1) {if (attr === 'title') {directive.change = function (value) {this.node.title = value;}
            } else if (attr === 'v-model') {directive.change = function (value) {this.node.value = value;}
                node.addEventListener(('input', function (e) {valueTrigger(expression, e.target.value);
                }))
            } else if (attr === 'style') {directive.change = function(name, value) {this.node.style = this.origin.replace("\{\{" + name + "\}\}", value);
                }
            }
        }
        return directive;
    }

定义了一个 directive 的对象,该对象蕴含以下属性

  • node:指令指标节点
  • change:指定具体逻辑

对于文本类型节点(node.nodeType == 3),间接将变量的内容复制给节点,当然这个必定是不对的,会将其余内容笼罩,但请释怀,咱们下一节会解决这个问题,我只不过不想给这一篇引入太多内容,导致大家消化不良。对于元素类型(node.nodeType == 1),如果是双向绑定,会给节点加上一个 input 的事件,监听节点值的变动,并执行 valueTrigger 的逻辑,该函数逻辑下一篇再讲

Subscriber(订阅者)

如果一个节点绑定了变量,那么这个节点就是一个 Subscriber(订阅者),变量值发生变化会调用节点相干指令(Directive),咱们申明一个 subscriber 的变量用于寄存订阅者信息,以变量的名称作为 key,指令作为值,比方有 A 和 B 两个节点绑定了变量name,那么 subscriber 的构造如下

{
    name:[
    {
        node:A,
        change:function(){...}
    },{
        node:B,
        change:function(){...}
    }]
}

addSubscriber代码如下:

var subscriber = Object.create(null);
function addSubscriber(variableName, directive) {let item = subscriber[variableName];
        if (!item) {item = [];
        }
        item.push(directive);
        subscriber[variableName] = item;
}

总结


解析节点 ----> 解析变量 ----> 依据绑定的类型关联指令 ---> 新增订阅者
                     |
                     |---> 双向绑定监听值变动 ---> 值变动触发事件

点击这里查看代码和成果

参考

点击余下链接,查看该系列其余文章

  • 模拟 vue 本人入手写响应式框架(一) – Vue 实现 todo 利用
  • 模拟 vue 本人入手写响应式框架(一) – Vue 对象创立用
  • 模拟 vue 本人入手写响应式框架(三) – dom 解析
正文完
 0