背景
在上一篇中,咱们保障了代码不报错,并且参数也胜利传进去了,但未实现任何的逻辑,这一章咱们的工作就是实现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解析