前端开发系列123-进阶篇之generate Virtual-DOM
本文介绍通过 render函数创建DOM的基本过程(仅仅核心部分),更多的细节也可以参考 Vue 框架源码自行探索 。

/* 模拟数据 */
function render() {
let name = "刘毅";
return _c("a", { id: "app", title: "标题" } , _c("p", null, _v("hello")),
_c("span", null, _v("My name is" + _s(name))));
}
function _c() {
return createElement(...arguments);
}
function _v(text) {
return createTextNode(text);
}
/* 关键:用于处理插值模板 */
function _s(val) {
return val == null ?'': (typeof val === 'object'?JSON.stringify(val):val);
}
/* 创建节点函数 */
function createElement(tag, data = {}, ...children) {
return v_node(tag, data, null, children, null);
}
/* 创建文本内容 */
function createTextNode(text) {
return v_node(null, null, null, null, text);
}
/* 创建虚拟 DOM 方法:把数据组织成对象返回 */
function v_node(tag, data, key, children, text) {
return { tag, data, key, children, text }
}
let vNode = render();
console.log('vNode', vNode);
/* 打印输出 */
// vNode
// { tag: 'a',
// data: { id: 'app', title: '标题' },
// key: null,
// text: null,
// children:
// [ { tag: 'p', data: null, key: null, children: [Array], text: null },
// { tag: 'span', data: null,key: null,children: [Array],text: null }
// ]
// }
给出上面代码生成的虚拟 DOM对应的对象结构图。

在 Vue 框架中,我们主要三种方式来渲染标签。
1、实例化 Vue 的过程中,通过 el 来选择实例挂载的标签。
2、实例化 Vue 的过程中,通过 template 标签字符串模板来渲染标签。
3、实例化 Vue 的过程中,直接通过 render 函数的方式来渲染标签,这也是底层的方法。
我们给出对应的 Vue 渲染标签(组件)的对应代码。
<script src="./node_modules/vue/dist/vue.js"></script>
<div id="app1">1111</div>
<div id="app2">2222</div>
<div id="app3">3333</div>
<script>
/* 第一种方式: 通过 配置项中的 el 参数来挂载 */
let vm1 = new Vue({
el: "#app1"
});
/* 第二种方式:通过template 模板 */
let vm2 = new Vue({
template: `<div class="box">我是模板内容</div>`
});
vm2.$mount("#app2");
/* 第三种方式:通过 render 函数渲染 */
let vm3 = new Vue({
render(c) {
return c('div', {
attrs: {
title: "标题",
idx: 1
},
class: {
'is-red': true,
}
}, [
c('a', '我是a'),
c('span', {class: "span-class"}, '我是span'),
])
}
});
vm3.$mount("#app3");
/* 测试数据 */
console.log(vm1.$el);
console.log(vm2.$el);
console.log(vm3.$el);
</script>
在上面的代码中,我们通过三种方式来进行渲染,它们将生成下面的标签结构。
<div id="app1">1111</div>
<div class="box">我是模板内容</div>
<div title="标题" idx="1" class="is-red"><span class="span-class">我是span</span><a>我是a</a></div>
在三种渲染的方式中,其中el 把挂载渲染的标签到页面,template 会直接执行替换操作,render函数同 template 一致。我们知道,无论使用什么样的方式来渲染 Vue框架的内部最终都是使用 render函数来进行处理的。
接下来,我这里通过代码简单模拟 render 函数渲染生成标签和虚拟 DOM 的过程。为了保持基本一致,我这里改造下上文的代码,并提供 Vue 这个构造函数(Class),并把涉及到的诸多方法都写到Vue原型对象上面以供实例化对象调用。
/* 第一部分代码:主要处理模板编译 */
/* 形如:abc-123 */
const nc_name = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
/* 形如:<aaa:bbb> */
const q_nameCapture = `((?:${nc_name}\\:)?${nc_name})`;
/* 形如:<div 匹配开始标签的左半部分 */
const startTagOpen = new RegExp(`^<${q_nameCapture}`);
/* 匹配开始标签的右半部分(>) 形如`>`或者` >`前面允许存在 N(N>=0)个空格 */
const startTagClose = /^\s*(\/?)>/;
/* 匹配闭合标签:形如 </div> */
const endTag = new RegExp(`^<\\/${q_nameCapture}[^>]*>`);
/* 匹配属性节点:形如 id="app" 或者 id='app' 或者 id=app 等形式的字符串 */
const att=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<div>`]+)))?/
/* 匹配插值语法:形如 {{msg}} */
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
/* 标记节点类型(文本节点) */
let NODE_TYPE_TEXT = 3;
/* 标记节点类型(元素节点) */
let NODE_TYPE_ELEMENT = 1;
function compiler(html) {
let stack = []; /* 数组模拟栈结构 */
let currentParent;
let root = null;
/* 推进函数:每处理完一部分模板就向前推进删除一段 */
function advance(n) {
html = html.substring(n);
}
function start(tag, attrs) {
let element = createASTElement(tag, attrs);
if (!root) {
root = element;
}
currentParent = element;
stack.push(element);
}
function end(tagName) {
let element = stack.pop();
currentParent = stack[stack.length - 1];
if (currentParent) {
element.parent = currentParent;
currentParent.children.push(element);
}
}
/* 文本处理函数:<span> hello <span> => text的值为 " hello "*/
function chars(text) {
/* 1.先处理文本字符串中所有的空格,全部替换为空 */
// text = text.replace(/\s/g, '');
/* 2.把数据组织成{text:"hello",type:3}的形式保存为当前父节点的子元素 */
if (text) {
currentParent.children.push({
text,
nodeType: NODE_TYPE_TEXT
})
}
}
function createASTElement(tag, attrs) {
return {
tag,
attrs,
children: [],
parent: null,
nodeType: NODE_TYPE_ELEMENT
}
}
/* 解析开始标签部分:主要提取标签名和属性节点 */
function parser_start_html() {
/* 00-正则匹配 <div id="app" title="标题">模板结构*/
let start = html.match(startTagOpen);
if (start) {
/* 01-提取标签名称 形如 div */
const tagInfo = {
tag: start[1],
attrs: []
};
/* 删除<div部分 */
advance(start[0].length);
/* 02-提取属性节点部分 形如:id="app" title="标题"*/
let attr, end;
while (!(end = html.match(startTagClose)) && (attr = html.match(att))) {
tagInfo.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5]
});
advance(attr[0].length);
}
/* 03-处理开始标签 形如 >*/
if (end) {
advance(end[0].length);
return tagInfo;
}
}
}
while (html) {
let textTag = html.indexOf('<');
/* 如果以<开头 */
if (textTag == 0) {
/* (1) 可能是开始标签 形如:<div id="app"> */
let startTagMatch = parser_start_html();
if (startTagMatch) {
start(startTagMatch.tag, startTagMatch.attrs);
continue;
}
/* (2) 可能是结束标签 形如:</div>*/
let endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1]);
continue;
}
}
/* 文本内容的处理 */
let text;
if (textTag >= 0) {
text = html.substring(0, textTag);
}
if (text) {
advance(text.length);
chars(text);
}
}
return root;
}
/* ****************** */
function generateAttrs(attrs) {
/* 1.初始化空字符 */
let str = '';
/* 2.遍历属性节点数组,并按既定格式拼接 */
attrs.forEach((attr, idx) => {
/* 2.1 如果属性节点名称为 style那么则对 value进行中间处理 */
if (attr.name === 'style') {
let obj = {};
attr.value.split(';').forEach(item => {
let [key, value] = item.split(':');
obj[key] = value
});
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}); /* 循环后:str === id:"app",title:"标题", */
/* 3.拼接上外层的{},并去掉{}中最后一个逗号(,)*/
str = `{ ${str.slice(0, -1)} }`;
return str;
}
function generateChildren(el) {
let children = el.children;
return (children && children.length > 0) ? `${children.map(c => generate(c)).join(',')}` : false;
}
function generate(node) {
return node.nodeType == 1 ? generateRenderString(node) : generateText(node);
}
function generateText(node) {
let tokens = [];
let match, index;
/* 获取文本内容 */
let text = node.text;
// console.log('node', node);
/*如果是全局匹配 那么每次匹配的时候都需要将 lastIndex 调整到0*/
let lastIndex = defaultTagRE.lastIndex = 0;
/* 正则匹配(匹配插值语法部分的内容) */
while (match = defaultTagRE.exec(text)) {
index = match.index;
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)));
}
tokens.push(`_s(${match[1].trim()})`);
lastIndex = index + match[0].length;
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return `_v(${tokens.join(' + ')})`;
}
/* 核心函数:完成每个部分字符串(标签名 && 属性节点 && 子节点)的拼接 */
function generateRenderString(el) {
let children = generateChildren(el);
return `_c("${el.tag}",${el.attrs.length ? generateAttrs(el.attrs) : 'null'}${ children ? `,${children}` : ''})`;
}
function compilerToFunction(template) {
/* Html->AST */
let root = compiler(template);
/* AST->RenderString */
let renderString = generateRenderString(root);
/* RenderString->RenderFunction */
return new Function(`with(this){ return ${renderString}}`);
}
/* 第二部分代码:主要处理虚拟 DOM 的生成 */
class Vue {
constructor(options) {
this.$options = options;
/* 如果传入了 el | el + template */
if (this.$options.el) this.$mount(this.$options.el);
}
$mount(el) {
let v_node;
/* 挂载函数 */
el = document.querySelector(el);
/* 考虑:el + template + render函数的优先级关系 */
if (!this.$options.render) {
// 对模板进行编译
let template = this.$options.template; // 取出模板
/* 如果没有仅仅是传入 el的情况那么就获取outerHTML */
if (!template && el) {
template = el.outerHTML;
}
/* 无论传入的是 el || template */
/* 最终根据 template 标签字符串创建 render 函数 */
this.$options.render = compilerToFunction(template).bind(this);
v_node = this.$options.render();
} else {
/* 如何创建? */
this.render = this.$options.render;
v_node = this.render(this._c.bind(this));
}
console.log('v_node', v_node);
}
_c() {
/* 创建标签节点 */
return this.createElement(...arguments);
}
_v(text) {
/* 创建文本节点 */
return this.createTextNode(text);
}
_s(val) {
/* 编译插值 */
return val == null ? '':(typeof val === 'object'?JSON.stringify(val):val)
}
createElement(tag, data = {}, ...children) {
/* 创建标签节点的实现函数 */
return this.v_node(tag, data, null, children, null);
}
createTextNode(text) {
/* 创建文本内容的实现函数 */
return this.v_node(null, null, null, null, text);
}
v_node(tag, data, key, children, text) {
/* 创建虚拟 DOM :把所有的数据都组织成对象返回 */
return { tag, data, key, children, text }
}
}
/* 第三部分:测试代码 */
/* 第一种方式 */
new Vue({
el: "#app"
});
/* 第二种方式 */
new Vue({
el: "#app",
template: `<a id="app" title="标题">
<p>hello</p>
<span>My name is {{name}} </span>
</a>`
});
/* 第三种方式 */
let vm3 = new Vue({
render(c) {
return c('div', {
id: "testID"
}, c('a', '我是a'))
}
});
vm3.$mount("#app");

前端开发系列123-进阶篇之generate Virtual-DOM的更多相关文章
- openlayers5-webpack 入门开发系列一初探篇(附源码下载)
前言 openlayers5-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载 ...
- leaflet-webpack 入门开发系列一初探篇(附源码下载)
前言 leaflet-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载地址 w ...
- 【Windows10 IoT开发系列】配置篇
原文:[Windows10 IoT开发系列]配置篇 Windows10 For IoT是Windows 10家族的一个新星,其针对不同平台拥有不同的版本.而其最重要的一个版本是运行在Raspberry ...
- ESP8266开发之旅 进阶篇② 闲聊Arduino IDE For ESP8266烧录配置
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
- 【webpack 系列】进阶篇
本文将继续引入更多的 webpack 配置,建议先阅读[webpack 系列]基础篇的内容.如果发现文中有任何错误,请在评论区指正.本文所有代码都可在 github 找到. 打包多页应用 之前我们配置 ...
- iOS开发系列--Swift进阶
概述 上一篇文章<iOS开发系列--Swift语言>中对Swift的语法特点以及它和C.ObjC等其他语言的用法区别进行了介绍.当然,这只是Swift的入门基础,但是仅仅了解这些对于使用S ...
- 旨在脱离后端环境的前端开发套件 - IDT Server篇
IDT,一个基于Nodejs的,旨在脱离后端环境的前端开发套件,目的就是能让前端开发完全脱离后端的环境,无论后端是什么模板引擎(主流),都能应付自如. IDT主要包括两大部分:Server + Bui ...
- 前端开发【第2篇:CSS】
鸡血 样式的属性多达几千个,但别担心,按照80-20原则,常用的也就几十个,你完全可以掌握它. Css初识 HTML的诞生 早期只有HTML的时候为了让HTML更美观一点,当时页面的开发者会把颜色写到 ...
- [置顶]【实用 .NET Core开发系列】- 导航篇
前言 此系列从出发点来看,是 上个系列的续篇, 上个系列因为后面工作的原因,后面几篇没有写完,后来.NET Core出来之后,注意力就转移到了.NET Core上,所以再也就没有继续下去,此是原因之一 ...
- openlayers4 入门开发系列之风场图篇
前言 openlayers4 官网的 api 文档介绍地址 openlayers4 api,里面详细的介绍 openlayers4 各个类的介绍,还有就是在线例子:openlayers4 官网在线例子 ...
随机推荐
- Unity Shader模板测试-描边
Unity Shader模板测试描边效果,常用于 rpg 项目中主角被遮挡的情况,将被遮挡的部分的轮廓描边绘制出来,这样可以在任何情况都能知道主角在哪里.(还有另外一种就是使用X光效果,但这种效果不需 ...
- Navicat Premium 16激活教程(NavicatCracker)
1.安装Navicat Premium 16 (注意版本,这里以此版本为例):并下载激活工具 1.1.Navicat Premium 下载路径: http://www.navicat.com.cn/d ...
- memcached DRDOS攻击实验
memcached DRDOS攻击实验 一.前提 关于drdos DRDoS(Distributed Reflection Denial of Service) 指的是利用IP Spoofing技术, ...
- 什么情况下会触发 Java 的 Full GC?
什么情况下会触发 Java 的 Full GC? Full GC(完全垃圾回收)是 Java 中的一个重要垃圾回收阶段,它会回收 整个堆内存,包括 新生代 和 老年代.触发 Full GC 的条件通常 ...
- Linux部署调度工具xxl-job
背景: Pentaho Data Integration(kettle)作为用户规模最多的开源ETL工具,强大简洁的功能深受广大ETL从业者的欢迎.但kettle本身的调度监控功能却非常弱.Penta ...
- 【解决方法】edge浏览器不小心删除收藏夹怎么办?
C:\Users\用户名\AppData\Local\Microsoft\Edge\User Data\Default 进入该目录,找到名为Bookmarks或Bookmarks.bak或Bookma ...
- VS2019 webApi(.net core2.2版本)上传到Gitee
一.本地创建项目 创建本地项目,依次点击下一步,在选择"目标框架"时选择2.2, 二.配置swagger 1.添加依赖项 2.修改Startup.cs public void Co ...
- 【笔记】Git|将git仓库中所有的 commit 合成一个,清空所有 git 提交记录
在对代码进行开源时,我们往往并不希望代码开发过程中的提交记录被其他人看到,因为提交的过程中往往会涵盖一些敏感信息.因此会存在 将仓库中所有 commit 合成一个 的需求. 直觉上,往往会用 reba ...
- k8s入门操作
kubectl -->apiserver 管理工具 管理k8s集群 增删改查node kubectl get service/node/replicaset/deployment/statefu ...
- Vite 3 来了!新增功能 + 如何迁移
@charset "UTF-8"; .markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; o ...