本文介绍通过 render函数创建DOM的基本过程(仅仅核心部分),更多的细节也可以参考 Vue 框架源码自行探索 。

Render => Virtual-DOM
/* 模拟数据 */
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 框架源码核心

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的更多相关文章

  1. openlayers5-webpack 入门开发系列一初探篇(附源码下载)

    前言 openlayers5-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载 ...

  2. leaflet-webpack 入门开发系列一初探篇(附源码下载)

    前言 leaflet-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载地址 w ...

  3. 【Windows10 IoT开发系列】配置篇

    原文:[Windows10 IoT开发系列]配置篇 Windows10 For IoT是Windows 10家族的一个新星,其针对不同平台拥有不同的版本.而其最重要的一个版本是运行在Raspberry ...

  4. ESP8266开发之旅 进阶篇② 闲聊Arduino IDE For ESP8266烧录配置

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  5. 【webpack 系列】进阶篇

    本文将继续引入更多的 webpack 配置,建议先阅读[webpack 系列]基础篇的内容.如果发现文中有任何错误,请在评论区指正.本文所有代码都可在 github 找到. 打包多页应用 之前我们配置 ...

  6. iOS开发系列--Swift进阶

    概述 上一篇文章<iOS开发系列--Swift语言>中对Swift的语法特点以及它和C.ObjC等其他语言的用法区别进行了介绍.当然,这只是Swift的入门基础,但是仅仅了解这些对于使用S ...

  7. 旨在脱离后端环境的前端开发套件 - IDT Server篇

    IDT,一个基于Nodejs的,旨在脱离后端环境的前端开发套件,目的就是能让前端开发完全脱离后端的环境,无论后端是什么模板引擎(主流),都能应付自如. IDT主要包括两大部分:Server + Bui ...

  8. 前端开发【第2篇:CSS】

    鸡血 样式的属性多达几千个,但别担心,按照80-20原则,常用的也就几十个,你完全可以掌握它. Css初识 HTML的诞生 早期只有HTML的时候为了让HTML更美观一点,当时页面的开发者会把颜色写到 ...

  9. [置顶]【实用 .NET Core开发系列】- 导航篇

    前言 此系列从出发点来看,是 上个系列的续篇, 上个系列因为后面工作的原因,后面几篇没有写完,后来.NET Core出来之后,注意力就转移到了.NET Core上,所以再也就没有继续下去,此是原因之一 ...

  10. openlayers4 入门开发系列之风场图篇

    前言 openlayers4 官网的 api 文档介绍地址 openlayers4 api,里面详细的介绍 openlayers4 各个类的介绍,还有就是在线例子:openlayers4 官网在线例子 ...

随机推荐

  1. STM32串口缓冲区

    在嵌入式开发中,外设通信(如UART.SPI.I2C)的数据接收常面临两大挑战:不定时.不定量数据的实时处理和高频率数据流下的稳定性保障.传统的轮询方式效率低下,而中断驱动的接收逻辑又容易因处理延迟导 ...

  2. 【电子DIY神器】通吃各种5线步进电机!I2C接口控制28BYJ-48五线四相步进电机

    总线单极性步进电机驱动板 摘要 总线单极性步进电机扩展板采用紧凑型设计,兼容XIAO系列主控板直连或独立使用,支持级联16个模块.板载ULN2003达林顿管驱动芯片(单通道500mA/整片2.5A), ...

  3. Nerf和3DGS神经重建技术在自动驾驶模拟中的应用

    验证自动驾驶软件需要数百万公里的测试.这不仅意味着系统开发周期长,而且系统的复杂度也会不断增加,同时,大规模的实车测试也会耗费巨量的资源并且可能会面临未知的安全问题.aiSim这样的虚拟仿真工具可以减 ...

  4. Java 里的对象在虚拟机里面是怎么存储的?

    Java 中的对象在虚拟机里的存储 在 Java 中,对象在虚拟机中的存储方式取决于 JVM 内存模型,主要存储在 堆(Heap) 中.对象的内存布局和管理方式会影响对象的创建.访问和销毁.下面详细解 ...

  5. 记录一次mysql数据库修复过程

    1. 场景 最近在使用小皮面板进行靶场搭建的时候,发现数据库一直无法启动,而在虚拟机里是可以启动了,这就很奇怪了.意识到我的本地已经安装了mysql,可能产生了冲突,但是当我兴冲冲启动本地mysql的 ...

  6. Octotree插件 - 可以列出github项目的目录结构

    Octotree - GitHub code tree

  7. 【经验】Git仓库多账号管理与部署|SSH密钥设置

    生成 SSH 密钥 先打开一个git窗口,生成ssh密钥. 如果打开的不是git窗口,而是cmd窗口,则需要先切换到C:\Users\用户名\.ssh目录下. 下面这条指令的your_email和yo ...

  8. Oracle链接服务器导致SQL Server异常终止

    现象 首先该链接服务器是使用 OraOLEDB provider (OLEDB Provider for Oracle)创建的,在使用该链接服务器的SQL语句中出现特殊字符 "--" ...

  9. 基于onnxruntime结合PyQt快速搭建视觉原型Demo

      我在日常工作中经常使用PyQt和onnxruntime来快速生产demo软件,用于展示和测试,这里,我将以Yolov12为例,展示一下我的方案.   首先我们需要使用Yolov12训练一个模型,并 ...

  10. python3验证手机号码

    import redef check_phone_right(self, phone_number): """检测号码是否正确""" pho ...