将HTML字符串编译为虚拟DOM对象的基础实现
本文所有代码均保存在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
收集到的字符串。
整个转化行为会持续到字符串的末尾,而scan
和scanUntil
交替进行,不断获取<
和>
之间的内容(即标签和属性)或者>
和<
之间的内容(即标签内的内容,包括文本和子标签)。
为了区分开始标签和结束标签,我们在生成的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
进行扫描,用=
进行分割,分别得到key
和value
。
由于某些属性是单属性的,比如字符串<button loading disabled class="btn">
中的loading
,以=
分割的话会得到loading disabled class
作为key
,这显然是错误的。于是我们同样使用简单粗暴的方式,用是否有空格来判断是否有单属性,同时将单属性的值设置为true
。
由于这里直接使用了"
和="
进行扫描,所以当前的程序不支持单引号,同时=
和"
之间不能有空格。
同时,这里只是对标签属性进行了简单的拆分,并没有对class
和style
内的属性进行拆分。那是之后的步骤。当然,也可以放在这里进行。
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
的结构把相应的数据取出即可。
同时,在这里我们对class
和style
属性进行解析,将形如{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对象的基础实现的更多相关文章
- React 16 源码瞎几把解读 【一】 从jsx到一个react 虚拟dom对象
一.jsx变createElement 每一个用jsx语法书写的react组件最后都会变成 react.createElement(...)这一坨东西, // 转变前 export default ( ...
- JS操作DOM对象——JS基础知识(四)
一.JavaScript的三个重要组成部分 (1)ECMAScript(欧洲计算机制造商协会) 制定JS的规范 (2)DOM(文档对象模型)重点学习对象 处理网页内容的方法和接口 (3)BOM(浏览器 ...
- jquery 字符串转dom对象及对该对象使用选择器查询
<script> $(document).ready(function () { var htmlStr = '<div id="outerDiv">< ...
- 为什么虚拟DOM更优胜一筹
注意: 虚拟DOM只是实现MVVM的一种方案,或者说是视图更新的一种策略.没有虚拟DOM比MVVM更好一说. 我们回顾传统MVC框架,如backbone,它是将某个模板编译成模板函数,需要更新时,是自 ...
- React入门-JSX和虚拟dom
1.JSX理解 举例: const element = <h1>Hello, world!</h1>; 这被称为 JSX,是一个 JavaScript 的语法扩展.建议在 Re ...
- 详解Vue中的虚拟DOM
摘要: 什么是虚拟DOM? 作者:浪里行舟 Fundebug经授权转载,版权归原作者所有. 前言 Vue.js 2.0引入Virtual DOM,比Vue.js 1.0的初始渲染速度提升了2-4倍,并 ...
- react快速上手一(使用js语法,创建虚拟DOM元素)
1.装包,引包 首先需要安装两个包 react ,react-dom cnpm i react react-dom 介绍下这两个包: react:专门用来创建React组件,组件生命周期等这些东西. ...
- 对vue虚拟dom的研究
Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象. ...
- [react] 什么是虚拟dom?虚拟dom比操作原生dom要快吗?虚拟dom是如何转变成真实dom并渲染到页面的?
壹 ❀ 引 虚拟DOM(Virtual DOM)在前端领域也算是老生常谈的话题了,若你了解过vue或者react一定避不开这个话题,因此虚拟DOM也算是面试中常问的一个点,那么通过本文,你将了解到如下 ...
随机推荐
- 如何安装jenkins并简单的使用
如何安装jenkins并使用 一.jenkins 简介: Jenkins是基于Java开发的一种持续集成工具,用于监控持续重复的工作,功能包括 : 1.持续的软件版本发布/测试项目: 2.监控外部调用 ...
- golang 矩阵乘法、行列式、求逆矩阵
package matrix import ( "math" "github.com/astaxie/beego" ) type Matrix4 struct ...
- 一个通用驱动Makefile-V2-支持编译多目录
目录 前言 1. 特点 2. 分析 2.1 简要原理 2.2 具体分析 3. 源码 前言 该 Makefile 已经通过基于内核 Linux5.4 版本验证通过. 因为编写这通用驱动 Makefile ...
- Spring Boot 轻量替代框架 Solon 1.3.15 发布
Solon 是一个微型的Java开发框架.项目从2018年启动以来,参考过大量前人作品:历时两年,4000多次的commit:内核保持0.1m的身材,超高的跑分,良好的使用体验.支持:RPC.REST ...
- 提高Python的性能
01 使用哈希表的数据结构 如果在程序中遇到大量搜索操作时,并且数据中没有重复项,则可以使用查找而不是循环.举例如下: items = ['a', 'b',..,'100m'] #1000s of ...
- Kubernetes 部署策略详解-转载学习
Kubernetes 部署策略详解 参考:https://www.qikqiak.com/post/k8s-deployment-strategies/ 在Kubernetes中有几种不同的方式发布应 ...
- 美团点评技术专家 帮你快速上手跨平台开发框架Flutter
Flutter并没有开创新的概念,它背后的框架原理和底层设计思想,与原生Android/iOS开发并没有本质区别,甚至从React.Native中吸收了不少优秀的设计理念. Flutter是Googl ...
- 蒙特卡洛——使用CDF反函数生成非均匀随机数
均匀随机数生成 先来说说均匀随机数生成,这是非均匀随机数的生成基础. 例如,我们现在有drand()函数,可以随机生成[0,1]范围内的均匀随机数. 要求一个drand2()函数,能够生成[0 ...
- Debian10 安装MyCLI
1 概述 Debian10安装MyCLI. 环境: Debian10 Python3.7 2 准备环境 2.1 Python 首先确保安装了Python: apt install python3 若是 ...
- Spring Boot demo系列(二):简单三层架构Web应用
2021.2.24 更新 1 概述 这是Spring Boot的第二个Demo,一个只有三层架构的极简Web应用,持久层使用的是MyBatis. 2 架构 一个最简单的Spring Boot Web应 ...