>[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`,頭部的指針指向接下來需要匹配的部分

* advance(43);

>[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 -- 案例
- 待續