>[success] # 简单的了解虚拟DOM(二)
`Virtual DOM`(虚拟dom)构成 是使用`VNode`(虚拟节点),虚拟节点实际是一种**js对象表现形式**,通过创建一个简单`VNode`(虚拟节点)类
* 创建一个Vnode class 用来生成Vnode 对象
~~~
class VNode {
constructor(tag, data, children, text, elm) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点的一些数据信息,比如props、attrs等数据*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
}
}
~~~
* 使用VNode 类去转换 一个vue模板
~~~html
<template>
<span class="demo" v-show="isShow"> // 这里是第一个部分的VNode 对象
This is a span. // 这里是第二个部分的VNode 对象
</span>
</template>
~~~
~~~
function render() {
const tag = 'span'
const data = {
// 指令集和
directives: [
{
rawName: 'v-show',
expression: 'isShow',
name: 'show',
value: true,
},
],
/* 静态class */
staticClass: 'demo',
}
const text = undefined
const elm = undefined
const children = [
new VNode(
undefined,
undefined,
undefined,
'This is a span.',
undefined
),
]
return new VNode(tag, data, children, text, elm)
}
console.log(render())
~~~
* 打印效果
~~~
VNode {
tag: 'span',
data: { directives: [ [Object] ], staticClass: 'demo' },
children: [
VNode {
tag: undefined,
data: undefined,
children: undefined,
text: 'This is a span.',
elm: undefined
}
],
text: undefined,
elm: undefined
}
~~~
>[danger] ##### 将VNode 二次封装
`VNode` 虚拟节点其实和`dom节点`思路一样,空节点,文本节点等
* 空节点类
~~~
function createEmptyVNode () {
const node = new VNode();
node.text = '';
return node;
}
~~~
* 文本节点类
~~~
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val));
}
~~~
* 克隆节点类
~~~
function cloneVNode (node) {
const cloneVnode = new VNode(
node.tag,
node.data,
node.children,
node.text,
node.elm
);
return cloneVnode;
}
~~~
>[info] ## template 模板 编译
上面情况是**人为思想去拆解各个部分参数作为VNode参数**,实际情况下会利用`ast`语法树做一个语法分析 (你可以参看babel章节的ast讲解), 获取到 `ast` 语法树的结构对应属性在和`VNode`对接形成`VNode`对象
整体思路就是依次循环字符串,找到符合匹配的规则进行保存,因此需要一个指针方法`advance`,头部的指针指向接下来需要匹配的部分
![](https://img.kancloud.cn/f5/dd/f5dd766677e6f93c4e782ad9f9f96981_1055x172.png)
* advance(43);
![](https://img.kancloud.cn/96/37/963794ad395f4a640d9438d855482860_1038x146.png)
>[danger] ##### ast 结构
~~~
<div :class="c" class="demo" v-if="isShow">
<span v-for="item in sz">{{item}}</span>
</div>
~~~
~~~
{
/* 标签属性的map,记录了标签上属性 */
'attrsMap': {
':class': 'c',
'class': 'demo',
'v-if': 'isShow'
},
/* 解析得到的:class */
'classBinding': 'c',
/* 标签属性v-if */
'if': 'isShow',
/* v-if的条件 */
'ifConditions': [
{
'exp': 'isShow'
}
],
/* 标签属性class */
'staticClass': 'demo',
/* 标签的tag */
'tag': 'div',
/* 子标签数组 */
'children': [
{
'attrsMap': {
'v-for': "item in sz"
},
/* for循环的参数 */
'alias': "item",
/* for循环的对象 */
'for': 'sz',
/* for循环是否已经被处理的标记位 */
'forProcessed': true,
'tag': 'span',
'children': [
{
/* 表达式,_s是一个转字符串的函数 */
'expression': '_s(item)',
'text': '{{item}}'
}
]
}
]
}
~~~
>[danger] 将html 解析ast
~~~
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // abc-aaa
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; // <aaa:asdads>
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div>
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 > <div>
// 将html 变成ast 抽象语法树
export function parseHTML(html) {
let root = null; // ast语法树的树根
let currentParent; // 标识当前父亲是谁
let stack = [];
const ELEMENT_TYPE = 1; // dom 元素类型 1
const TEXT_TYPE = 3; // dom 文本类型三
function createASTElement(tagName, attrs) {
return {
tag: tagName,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null
}
}
function start(tagName, attrs) {
// 遇到开始标签 就创建一个ast元素
let element = createASTElement(tagName, attrs);
if (!root) {
root = element;
}
currentParent = element; // 把当前元素标记成父ast树
stack.push(element); // 将开始标签创建一个ast元素放到栈中
}
function chars(text) {
text = text.replace(/\s/g, '');
if (text) {
currentParent.children.push({
text,
type: TEXT_TYPE
})
}
}
function end(tagName) {
let element = stack.pop(); // 拿到的是ast对象
// 我要标识当前这个p是属于这个div的儿子的
currentParent = stack[stack.length - 1];
if (currentParent) {
element.parent = currentParent;
currentParent.children.push(element); // 实现了一个树的父子关系
}
}
console.log(html)
// 将html 字符串解析成ast 语法树
// 现在循环整个html 字符串安装对应规则生成对应的ast表示
while (html) {
let textEnd = html.indexOf('<')
// 如果textEnd索引为0,就说明是一个标签 可能是开始标签或者结束标签
// 例如 <p>woooo</p>
if (textEnd === 0) {
let startTagMatch = parseStartTag(); // 通过这个方法获取到匹配的结果 tagName,attrs
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs); // 1解析开始标签
continue; // 如果开始标签匹配完毕后 继续下一次 匹配
}
let endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1]); // 2解析结束标签
continue;
}
}
// 如果不是< 括号开头的说明不是标签 是文本例如 <p>我是文本</p>
let text
if (textEnd >= 0) {
// 获取文本内容
text = html.substring(0, textEnd);
}
if (text) {
advance(text.length);
chars(text); // 3解析文本
}
}
// 每次从已经匹配过得地方开始截取
function advance(n) {
html = html.substring(n);
}
function parseStartTag() {
/**
* < div id = "app" ><p> www </p> </div>
* ["<div", "div", index: 0, input: "<div id="app ">↵ <p>www</p>↵ </div>", groups: undefined]
*/
let start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length); // 将标签删除 将匹配到的内容冲html中截取掉
let end, attr
// 开始获取标签中的元素存储到attrs 中
// 如果不是结束标签并且dom中有属性就循环取出来
// 上面已经html 截取了一次因此现在 如果有属性的展示效果 class="name"></div>
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
// 将属性进行解析
advance(attr[0].length); // 将属性去掉
match.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5]
});
}
if (end) { // 去掉开始标签的 >
advance(end[0].length);
console.log(match)
/*
{
"tagName": "div",
"attrs": [{
"name": "id",
"value": "app"
}]
}
*/
return match
}
}
}
return root;
}
~~~
* ast 转换后结构
* 再将获取ast 语法树的结构装进VNode中
~~~
// 拼出基本格式
function generate(el) {
// 开始处理模板的基本模型 基本模型拆分理解
// 1._c的函数包裹
// 第一个参数是dom 标签节点,第二个参数是dom 上面的属性,第三个参数如果有子节点
// 就这接着重复_c的效果
let children = genChildren(el); // 子节点
// 拼出基本结构 el.tag 中tag 是ast中一个属性
let code = `_c("${el.tag}",${
el.attrs.length?genProps(el.attrs):'undefined'
}${
children? `,${children}` :''
})
`
return code;
}
~~~
>[danger] ##### 可参考
[参考链接](https://juejin.cn/book/6844733705089449991/section/6844733705232056334)
- 工程化 -- Node
- vscode -- 插件
- vscode -- 代码片段
- 前端学会调试
- 谷歌浏览器调试技巧
- 权限验证
- 包管理工具 -- npm
- 常见的 npm ci 指令
- npm -- npm install安装包
- npm -- package.json
- npm -- 查看包版本信息
- npm - package-lock.json
- npm -- node_modules 层级
- npm -- 依赖包规则
- npm -- install 安装流程
- npx
- npm -- 发布自己的包
- 包管理工具 -- pnpm
- 模拟数据 -- Mock
- 页面渲染
- 渲染分析
- core.js && babel
- core.js -- 到底是什么
- 编译器那些术语
- 词法解析 -- tokenize
- 语法解析 -- ast
- 遍历节点 -- traverser
- 转换阶段、生成阶段略
- babel
- babel -- 初步上手之了解
- babel -- 初步上手之各种配置(preset-env)
- babel -- 初步上手之各种配置@babel/helpers
- babel -- 初步上手之各种配置@babel/runtime
- babel -- 初步上手之各种配置@babel/plugin-transform-runtime
- babel -- 初步上手之各种配置(babel-polyfills )(未来)
- babel -- 初步上手之各种配置 polyfill-service
- babel -- 初步上手之各种配置(@babel/polyfill )(过去式)
- babel -- 总结
- 各种工具
- 前端 -- 工程化
- 了解 -- Yeoman
- 使用 -- Yeoman
- 了解 -- Plop
- node cli -- 开发自己的脚手架工具
- 自动化构建工具
- Gulp
- 模块化打包工具为什么出现
- 模块化打包工具(新) -- webpack
- 简单使用 -- webpack
- 了解配置 -- webpack.config.js
- webpack -- loader 浅解
- loader -- 配置css模块解析
- loader -- 图片和字体(4.x)
- loader -- 图片和字体(5.x)
- loader -- 图片优化loader
- loader -- 配置解析js/ts
- webpack -- plugins 浅解
- eslit
- plugins -- CleanWebpackPlugin(4.x)
- plugins -- CleanWebpackPlugin(5.x)
- plugin -- HtmlWebpackPlugin
- plugin -- DefinePlugin 注入全局成员
- webapck -- 模块解析配置
- webpack -- 文件指纹了解
- webpack -- 开发环境运行构建
- webpack -- 项目环境划分
- 模块化打包工具 -- webpack
- webpack -- 打包文件是个啥
- webpack -- 基础配置项用法
- webpack4.x系列学习
- webpack -- 常见loader加载器
- webpack -- 移动端px转rem处理
- 开发一个自己loader
- webpack -- plugin插件
- webpack -- 文件指纹
- webpack -- 压缩css和html构建
- webpack -- 清里构建包
- webpack -- 复制静态文件
- webpack -- 自定义插件
- wepack -- 关于静态资源内联
- webpack -- source map 对照包
- webpack -- 环境划分构建
- webpack -- 项目构建控制台输出
- webpack -- 项目分析
- webpack -- 编译提速优护体积
- 提速 -- 编译阶段
- webpack -- 项目优化
- webpack -- DefinePlugin 注入全局成员
- webpack -- 代码分割
- webpack -- 页面资源提取
- webpack -- import按需引入
- webpack -- 摇树
- webpack -- 多页面打包
- webpack -- eslint
- webpack -- srr打包后续看
- webpack -- 构建一个自己的配置后续看
- webpack -- 打包组件和基础库
- webpack -- 源码
- webpack -- 启动都做了什么
- webpack -- cli做了什么
- webpack - 5
- 模块化打包工具 -- Rollup
- 工程化搭建代码规范
- 规范化标准--Eslint
- eslint -- 扩展配置
- eslint -- 指令
- eslint -- vscode
- eslint -- 原理
- Prettier -- 格式化代码工具
- EditorConfig -- 编辑器编码风格
- 检查提交代码是否符合检查配置
- 整体流程总结
- 微前端
- single-spa
- 简单上手 -- single-spa
- 快速理解systemjs
- single-sap 不使用systemjs
- monorepo -- 工程
- Vue -- 响应式了解
- Vue2.x -- 源码分析
- 发布订阅和观察者模式
- 简单 -- 了解响应式模型(一)
- 简单 -- 了解响应式模型(二)
- 简单 --了解虚拟DOM(一)
- 简单 --了解虚拟DOM(二)
- 简单 --了解diff算法
- 简单 --了解nextick
- Snabbdom -- 理解虚拟dom和diff算法
- Snabbdom -- h函数
- Snabbdom - Vnode 函数
- Snabbdom -- init 函数
- Snabbdom -- patch 函数
- 手写 -- 虚拟dom渲染
- Vue -- minVue
- vue3.x -- 源码分析
- 分析 -- reactivity
- 好文
- grpc -- 浏览器使用gRPC
- grcp-web -- 案例
- 待续