将html字符串转换为AST(parseHtmlToAst.js)
const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp}]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`);
const startTagClose = /^\s*(\/?)>/;
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
export function parseHtmlToAst(html) {
let root,
currentParent,
stack = [];
while (html) {
let textEnd = html.indexOf('<');
if (textEnd === 0) {
const startTagMatch = parseStartTag();
if (startTagMatch) {
start(startTagMatch);
continue;
}
const endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end();
}
}
if (textEnd > 0) {
chars(html.substring(0, textEnd));
advance(textEnd);
}
}
function parseStartTag() {
let attr, end;
const start = html.match(startTagOpen);
if (start) {
const startTagMatch = {
tagName: start[1],
attrs: [],
};
advance(start[0].length);
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
startTagMatch.attrs.push(attr[0].trim());
advance(attr[0].length);
}
if (end) {
advance(end[0].length);
return startTagMatch;
}
}
}
function advance(n) {
html = html.substring(n);
}
function start(startTagMatch) {
const ele = createASTElement(startTagMatch.tagName, startTagMatch.attrs);
if (!root) {
root = ele;
}
currentParent = ele;
stack.push(ele);
}
function end() {
const ele = stack.pop();
currentParent = stack[stack.length - 1];
if (currentParent) {
ele.parent = currentParent;
currentParent.children.push(ele);
}
}
function chars(containerText) {
const text = containerText.trim();
if (text) {
currentParent.children.push({
type: 3,
text,
});
}
}
function createASTElement(tagName, attrs) {
return {
type: 1,
tag: tagName,
attrs,
parent,
children: [],
};
}
return root;
}
返回一个固定的数据结构(h函数)(h.js)
export function h(sel, data = {}, children) {
let text, selList;
if (children) {
if (Array.isArray(children)) {
selList = changeArr(children);
} else if (primitive(children)) {
text = children;
} else if (children.sel) {
selList = [children];
}
}
return vnode(sel, data, selList, text, undefined);
}
function primitive(s) {
return typeof s === 'string' || typeof s === 'number';
}
function changeArr(children) {
const newArr = [];
for (let i = 0; i < children.length; ++i) {
if (primitive(children[i]))
newArr[i] = vnode(undefined, undefined, undefined, children[i], undefined);
else newArr[i] = children[i];
}
return newArr;
}
function vnode(sel, data, children, text, elm) {
const key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}
将虚拟dom渲染成真实dom(patch.js)
export function patch(realDom, vnode) {
// 根据虚拟dom,生成真实dom
const el = createRealDom(vnode);
console.log(el);
// 将真实dom挂载与dom树上
realDom.appendChild(el);
}
function createRealDom(vnode) {
const { sel, data, children, text } = vnode;
let ele;
if (sel) {
ele = createElement(sel, data);
} else {
ele = createElement('span', data);
}
if (Array.isArray(children)) {
for (let index = 0; index < children.length; index++) {
appendChild(ele, createRealDom(children[index]));
}
} else {
appendChild(ele, createTextNode(text));
}
return ele;
}
function createElement(tagName, options) {
return document.createElement(tagName, options);
}
function createElementNS(namespaceURI, qualifiedName, options) {
return document.createElementNS(namespaceURI, qualifiedName, options);
}
function createTextNode(text) {
return document.createTextNode(text);
}
function appendChild(node, child) {
node.appendChild(child);
}
调用(main.js)
import { parseHtmlToAst } from './parse/parseHtmlToAst';
import { h } from './h';
import { patch } from './patch';
const html = ` <div id='div1' class='my ct'> <div id='brother' class='my ct' style='width: 100%; color: red;' >bbb</div> <span id='span' class='my ct'><div id='div2' class='my ct'>aaa</div></span></div>`;
const ast = parseHtmlToAst(html.trim());
const vNode = getVNode(ast);
function getVNode(ele) {
const vNode = {};
const props = initProps(ele.attrs);
const hData = h(ele.tag, props, ele.text);
Object.assign(vNode, hData);
if (Array.isArray(ele.children)) {
vNode.children = [];
for (let index = 0; index < ele.children.length; index++) {
vNode.children[index] = getVNode(ele.children[index]);
}
}
return vNode;
}
function initProps(data) {
const prop = {};
if (Array.isArray(data)) {
for (let index = 0; index < data.length; index++) {
const propArr = data[index].split('=');
prop[propArr[0]] = delQuotationMark(propArr[1]);
if (propArr[0] === 'style') {
prop[propArr[0]] = initStyle(prop[propArr[0]]);
}
}
}
return prop;
}
function initStyle(style) {
const styleObj = {};
Array.from(style.split(';')).forEach((item) => {
const styleArr = item.trim().split(':');
if (styleArr[0]) {
styleObj[styleArr[0]] = String(styleArr[1].trim());
}
});
return styleObj;
}
function delQuotationMark(value) {
return value.replace(/\'/g, '').replace(/\"/g, '');
}
const container = document.getElementById('container');
patch(container, vNode);
容器(index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="container" style="width: 100%; color: red"></div>
<script src="js/bundle.js"></script>
</body>
</html>
打包配置(webpack.config.js)
const path = require('path');
module.exports = {
// 入口
entry: './src/index.js',
// 出口
output: {
// 虚拟打包路径
publicPath: 'js',
// 打包出来的文件名
filename: 'bundle.js',
},
devServer: {
// 端口号
port: 8080,
// 静态资源文件夹
contentBase: 'www',
},
};
(package.json)
{
"name": "snabbdom-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.44.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.2"
},
"dependencies": {
}
}