>[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)