本文所有代码均保存在HouyunCheng / mini-2vdom

虚拟DOM只是实现MVVM的一种方案,或者说是视图更新的一种策略,是实现最小化更新的diff算法的操作对象。

创建扫描器

所有编译行为的第一步都是遍历整个字符串,于是我们创建Scanner类,专门用于扫描整个字符串。

class Scanner {
constructor(text) {
this.text = text;
// 指针
this.pos = 0;
// 尾巴 剩余字符
this.tail = text;
} /**
* 路过指定内容
*
* @memberof Scanner
*/
scan(tag) {
if (this.tail.indexOf(tag) === 0) {
// 直接跳过指定内容的长度
this.pos += tag.length;
// 更新tail
this.tail = this.text.substring(this.pos);
}
} /**
* 让指针进行扫描,直到遇见指定内容,返回路过的文字
*
* @memberof Scanner
* @return str 收集到的字符串
*/
scanUntil(stopTag) {
// 记录开始扫描时的初始值
const startPos = this.pos;
// 当尾巴的开头不是stopTg的时候,说明还没有扫描到stopTag
while (!this.eos() && this.tail.indexOf(stopTag) !== 0 ) {
// 改变尾巴为当前指针这个字符到最后的所有字符
this.tail = this.text.substring(++this.pos);
} // 返回经过的文本数据
return this.text.substring(startPos, this.pos).trim();
} /**
* 判断指针是否到达文本末尾(end of string)
*
* @memberof Scanner
*/
eos() {
return this.pos >= this.text.length;
}
}

scanUntil方法用于扫描字符串,并将扫描过的内容返回,用于收集为token。整个扫描会分段进行,直到字符串的结尾。

转换为没有嵌套结构的tokens

先看代码,我们先实例化Scanner用于扫描整个传入字符串,同时初始化一个tokens数组用于保存token和一个word用于保存sanner收集到的字符串。

整个转化行为会持续到字符串的末尾,而scanscanUntil交替进行,不断获取<>之间的内容(即标签和属性)或者><之间的内容(即标签内的内容,包括文本和子标签)。

为了区分开始标签和结束标签,我们在生成的token数组中的第一项添加#/作为开始或结束的标记,第二项为标签名,第三项,我们放入开始标签中收集到的属性,而不是将属性单独放在一个token中,这样做是为了简化后边将tokens转化为嵌套结构的操作。

于是,我们得到了由形如[类型标记, 标签名, 数据, 文本]组成的二维数组。

这里对是一个标签否有属性这一点使用了非常简单粗暴的实现,即看<>中收集到的字符串中是否有空格,有空格则判断为有属性,没空格则判断为没有属性。

在收集标签属性的时候,顺便使用propsParser对标签属性进行了简单解析。

/**
* 将html字符串转为无嵌套结构的token,返回tokens数组
*
* @param {string} html
* @return {array}
*/
function collectTokens(html) {
const scanner = new Scanner(html);
const tokens = []; let word = '';
while (!scanner.eos()) {
// 扫描文本
const text = scanner.scanUntil('<');
scanner.scan('<');
tokens[tokens.length - 1] && tokens[tokens.length - 1].push(text);
// 扫描标签<>中的内容
word = scanner.scanUntil('>');
scanner.scan('>');
// 如果没有扫描到值,就跳过本次进行下一次扫描
if (!word) continue;
// 区分开始标签 # 和结束标签 /
if (word.startsWith('/')) {
tokens.push(['/', word.slice(1)]);
} else {
// 如果有属性存在,则解析属性
const firstSpaceIdx = word.indexOf(' ');
if (firstSpaceIdx === -1) {
tokens.push(['#', word, {}]);
} else {
// 解析属性
const data = propsParser(word.slice(firstSpaceIdx))
tokens.push(['#', word.slice(0, firstSpaceIdx), data]);
}
}
} return tokens;
}

使用propsParser简单解析标签属性

propsParser中,我们同样使用Scanner进行扫描,用=进行分割,分别得到keyvalue

由于某些属性是单属性的,比如字符串<button loading disabled class="btn">中的loading,以=分割的话会得到loading disabled class作为key,这显然是错误的。于是我们同样使用简单粗暴的方式,用是否有空格来判断是否有单属性,同时将单属性的值设置为true

由于这里直接使用了"="进行扫描,所以当前的程序不支持单引号,同时="之间不能有空格。

同时,这里只是对标签属性进行了简单的拆分,并没有对classstyle内的属性进行拆分。那是之后的步骤。当然,也可以放在这里进行。

function propsParser(propsStr) {
propsStr = propsStr.trim();
const scanner = new Scanner(propsStr);
const props = {}; while(!scanner.eos()) {
let key = scanner.scanUntil('='); // 对单属性的处理
const spaceIdx = key.indexOf(' ');
if (spaceIdx !== -1) {
const keys = key.replace(/\s+/g, ' ').split(' '); const len = keys.length;
for (let i = 0; i < len - 1; i++) {
props[keys[i]] = true;
}
key = keys[len - 1].trim();
}
scanner.scan('="'); const val = scanner.scanUntil('"');
props[key] = val || true;
scanner.scan('"');
} return props;
}

生成有嵌套结构的tokens

在之前生成的tokens是没有嵌套结构的,是一个简单的二维数组。在这里,我们要将其转换有嵌套结构的tokens

对于嵌套结构,通常使用来生成,遇到开始标签(这里为#)则压栈,遇到结束标签(这里为/)则出栈。

在这里,我们使用stack来保存栈状态,用collector来收集嵌套的内容,在压栈和出栈的同时也修改collector的指向,以保证嵌套层次的准确性。

同时,我们将嵌套结构放在token的第三个元素的位置。得到形如[类型标记, 标签名, 子节点, 数据, 文本]tokens

function nestTokens(tokens) {
const nestedTokens = [];
const stack = [];
let collector = nestedTokens; for (let i = 0, len = tokens.length; i < len; i++) {
const token = tokens[i]; switch (token[0]) {
case '#':
// 收集当前token
collector.push(token);
// 压入栈中
stack.push(token);
// 由于进入了新的嵌套结构,新建一个数组保存嵌套结构
// 并修改collector的指向
token.splice(2, 0, []);
collector = token[2];
break;
case '/':
// 出栈
stack.pop();
// 将收集器指向上一层作用域中用于存放嵌套结构的数组
collector = stack.length > 0
? stack[stack.length - 1][2]
: nestedTokens;
break;
default:
collector.push(token);
}
} return nestedTokens;
}

整合tokenizer函数

有了以上两个函数函数之后,我们可以将其整合为一个函数,方便之后调用。

function tokenizer(html) {
return nestTokens(collectTokens(html));
}

将tokens转换为虚拟DOM

这一步相对来说就简单很多,只需要安装tokens的结构把相应的数据取出即可。

同时,在这里我们对classstyle属性进行解析,将形如{class: "item active"}class属性转换为

{
class: {
item: true,
active: true
}
}

的形式。

将形如{style: "border: 1px solid red; height: 300px"}转换为

{
style: {
border: "border: 1px solid red",
height: "300px"
}
}

的形式。

同时将在data中的属性key提取出来。由于当前的虚拟DOM还没有上树,所有elm属性为undefined。对于子节点,我们使用递归将子节点追加到children数组中。

于是最终我们得到形如

{
sel: "div",
children: [{
sel: "p",
data: {},
elm: undefined,
text: "文本",
key: "1",
}
}],
data: {class: {container: true}, id: "main"},
elm: undefined,
text: undefined,
key: undefined,
}

的虚拟DOM结构。

以下是tokens2vdom的代码实现。

function tokens2vdom(tokens) {
const vdom = {}; for (let i = 0, len = tokens.length; i < len; i++) {
const token = tokens[i];
vdom['sel'] = token[1];
vdom['data'] = token[3]; // 解析类名
if (vdom['data']['class']) {
vdom['data']['class'] = classParser(vdom['data']['class']);
} // 解析行类样式
if (vdom['data']['style']) {
vdom['data']['style'] = styleParser(vdom['data']['style']);
} // 添加key
if (vdom['data']['key']) {
vdom['key'] = vdom['data']['key'];
delete vdom['data']['key'];
} else {
vdom['key'] = undefined;
} if (token[4]) {
vdom['text'] = token[token.length - 1];
} else {
vdom['text'] = undefined;
} vdom['elm'] = undefined; const children = token[2];
if (children.length === 0) {
vdom['children'] = undefined;
continue;
}; vdom['children'] = []; for (let j = 0; j < children.length; j++) {
vdom['children'].push(tokens2vdom([children[j]]));
} if (vdom['children'].length === 0) {
delete vdom['children'];
}
} return vdom;
}

整合toVDOM函数

到这里我们的需求就基本实现了,我们将之前的函数整合为一个函数即可。

function toVDOM (html) {

  const tokens = tokenizer(html);
const vdom = tokens2vdom(tokens); return vdom;
}

虚拟DOM的结构参照 snabbdom/snabbdom

本文完整的代码实现可以查看 HouyunCheng / mini-2vdom

将HTML字符串编译为虚拟DOM对象的基础实现的更多相关文章

  1. React 16 源码瞎几把解读 【一】 从jsx到一个react 虚拟dom对象

    一.jsx变createElement 每一个用jsx语法书写的react组件最后都会变成 react.createElement(...)这一坨东西, // 转变前 export default ( ...

  2. JS操作DOM对象——JS基础知识(四)

    一.JavaScript的三个重要组成部分 (1)ECMAScript(欧洲计算机制造商协会) 制定JS的规范 (2)DOM(文档对象模型)重点学习对象 处理网页内容的方法和接口 (3)BOM(浏览器 ...

  3. jquery 字符串转dom对象及对该对象使用选择器查询

    <script> $(document).ready(function () { var htmlStr = '<div id="outerDiv">< ...

  4. 为什么虚拟DOM更优胜一筹

    注意: 虚拟DOM只是实现MVVM的一种方案,或者说是视图更新的一种策略.没有虚拟DOM比MVVM更好一说. 我们回顾传统MVC框架,如backbone,它是将某个模板编译成模板函数,需要更新时,是自 ...

  5. React入门-JSX和虚拟dom

    1.JSX理解 举例: const element = <h1>Hello, world!</h1>; 这被称为 JSX,是一个 JavaScript 的语法扩展.建议在 Re ...

  6. 详解Vue中的虚拟DOM

    摘要: 什么是虚拟DOM? 作者:浪里行舟 Fundebug经授权转载,版权归原作者所有. 前言 Vue.js 2.0引入Virtual DOM,比Vue.js 1.0的初始渲染速度提升了2-4倍,并 ...

  7. react快速上手一(使用js语法,创建虚拟DOM元素)

    1.装包,引包 首先需要安装两个包 react ,react-dom cnpm i react react-dom 介绍下这两个包: react:专门用来创建React组件,组件生命周期等这些东西. ...

  8. 对vue虚拟dom的研究

    Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象. ...

  9. [react] 什么是虚拟dom?虚拟dom比操作原生dom要快吗?虚拟dom是如何转变成真实dom并渲染到页面的?

    壹 ❀ 引 虚拟DOM(Virtual DOM)在前端领域也算是老生常谈的话题了,若你了解过vue或者react一定避不开这个话题,因此虚拟DOM也算是面试中常问的一个点,那么通过本文,你将了解到如下 ...

随机推荐

  1. SQLServer 2008快速导出架构和数据脚本

    https://jingyan.baidu.com/article/454316ab715218f7a7c03a9d.html

  2. python之commands和subprocess入门介绍(可执行shell命令的模块)

    一.commands模块 1.介绍 当我们使用Python进行编码的时候,但是又想运行一些shell命令,去创建文件夹.移动文件等等操作时,我们可以使用一些Python库去执行shell命令. com ...

  3. 让 Java 中 if else 更优雅的几个小技巧

    对于一个高级 crud 工程师‍而言,if else 是写代码时使用频率最高的关键词之一,然而有时过多的 if else 会让我们优雅的 crud 代码显得不那么优雅,并且感到脑壳疼

  4. Baolu Aggregate TPS Report

    1.说明 这是一个基于JMeter官方的Aggregate Report的监听器改进而来的监听器!!! 2.插件背景 早在很久之前,宝路就曾经改造过JMeter的Aggregate Report 的源 ...

  5. Python基础之数据类型详解(2)

    今天继续昨天的python基本数据类型详解,按照上一篇博文的格式,接下来讲解列表.元组.字典以及集合. 列表 1.用途按位置存放多个值2.定义在[]内用逗号分割开多个任意类型的元素 # 定义列表 # ...

  6. 【linux】驱动-9-设备树插件

    目录 前言 9. Linux设备树插件 9.1 格式 9.2 设备树插件的编译和加载 9.2.1 单独使用dtc工具编译 9.2.2 内核dtc工具编译设备树插件 9.2.3 加载设备树插件 9.2. ...

  7. 学习笔记-angular 使用uuid

    import { UUID } from 'angular2-uuid'; let uuid = UUID.UUID().replace(/-/g, '').toLocaleUpperCase(); ...

  8. 重磅!解锁Apache Flink读写Apache Hudi新姿势

    感谢阿里云 Blink 团队Danny Chan的投稿及完善Flink与Hudi集成工作. 1. 背景 Apache Hudi 是目前最流行的数据湖解决方案之一,Data Lake Analytics ...

  9. UnitTwoSummary

    目录 一.设计策略与程序分析 第一次作业 第二次作业 第三次作业 二.可扩展性检查与分析 三.bug 四.总结与反思 一.设计策略与程序分析 第一次作业 设计思路 输入,调度器,电梯分别设置成三个线程 ...

  10. 腾讯高级工程师带你完整体验Node.js开发实战

    Node.js拥有广大的 JavaScript程序员基础并且完全开源,它被广泛地用在 Web服务.开发工作流.客户端应用等诸多领域.在 Web 服务开发这个领域,业界对 Node.js 的接受程度最高 ...