背景
在上一篇中,我们保证了代码不报错,并且参数也成功传进去了,但未实现任何的逻辑,这一章我们的工作就是完成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),比如 hello
包含两种类型,为元素类型,
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;
}
总结
解析节点---->解析变量---->根据绑定的类型关联指令--->新增订阅者
|
|--->双向绑定监听值变化--->值变化触发事件
点击这里查看代码和效果
参考
点击余下链接,查看该系列其他文章