跳到主要内容

认识

2024年03月18日
柏拉文
越努力,越幸运

一、认识


Vue.js 2.0 Compile 编译器Template 模版编译解析为 render 函数。首先对模版进行词法分析和语法分析, 得到 AST 模版。接着, 将模版 AST 进行优化, 标记静态节点, 最后JavaScript 代码,即渲染函数代码。工作流如下:

二、细节


2.1 解析 parse

  1. 遍历 HTML 模版字符串,通过正则表达式匹配 <

  2. 跳过某些不需要处理的标签,比如: 注释标签、条件注释标签、Doctype

  3. 解析开始标签:

    • 得到一个对象,包括 标签名(tagName)、所有的属性(attrs)、标签在 html 模版字符串中的索引位置

    • 进一步处理上一步得到的 attrs 属性,将其变成 [{ name: attrName, value: attrVal, start: xx, end: xx }, ...] 的形式

    • 通过标签名、属性对象和当前元素的父元素生成 AST 对象,其实就是一个 普通的 JS 对象,通过 key、value 的形式记录了该元素的一些信息

    • 接下来进一步处理开始标签上的一些指令,比如 v-prev-forv-ifv-once,并将处理结果放到 AST 对象上

    • 处理结束将 ast 对象存放到 stack 数组

    • 处理完成后会截断 html 字符串,将已经处理掉的字符串截掉

  4. 解析闭合标签

    • 如果匹配到结束标签,就从 stack 数组中拿出最后一个元素,它和当前匹配到的结束标签是一对。

    • 再次处理开始标签上的属性,这些属性和前面处理的不一样,比如:keyrefscopedSlot、样式等,并将处理结果放到元素的 AST 对象上

    • 然后将当前元素和父元素产生联系,给当前元素的 ast 对象设置 parent 属性,然后将自己放到父元素的 ast 对象的 children 数组中

  5. 最后遍历完整个 html 模版字符串以后,返回 ast 对象

2.2 优化 optimize

optimize 的过程其实就是静态标记的过程。

静态标记的过程如下:

  1. 标记静态节点

    • 通过递归的方式标记所有的元素节点

    • 如果节点本身是静态节点,但是存在非静态的子节点,则将节点修改为非静态节点

  2. 标记静态根节点,基于静态节点,进一步标记静态根节点

    • 如果节点本身是静态节点 && 而且有子节点 && 子节点不全是文本节点,则标记为静态根节点

    • 如果节点本身不是静态根节点,则递归的遍历所有子节点,在子节点中标记静态根

标记为静态节点的条件:

  • 非组件

  • 文本节点

  • 节点上没有 v-bindv-forv-if 等指令

2.3 生成 generate

渲染函数生成过程: 实编译器生成的渲染有两类

  • render: 负责生成动态节点的 vnode

  • staticRenderFns: 数组中的静态渲染函数,这些函数负责生成静态节点的 vnode

渲染函数生成的过程,其实就是在遍历 AST 节点,通过递归的方式,处理每个节点,最后生成形如:_c(tag, attr, children, normalizationType) 的结果。tag 是标签名,attr 是属性对象,children 是子节点组成的数组,其中每个元素的格式都是 _c(tag, attr, children, normalizationTYpe) 的形式,normalization 表示节点的规范化类型,是一个数字 012

template 模版如下:

<div id="app">
<div> 我是 静态节点 001 </div>
<div>{{ text }}</div>
<div :class="vBind"> v-bind 指令</div>
<div v-if="vIf"> v-if 指令 </div>
<div v-show="vShow"> v-show 指令 </div>
<button @click="handleClick">按钮</button>
<input type="text" v-model="vModel">
<ul>
<li v-for="(item,index) in vFor" :key="index">{{ item.value }}</li>
</ul>
<MyComponent :componentProp="componentProp"></MyComponent>
</div>

生成的 vm.$options.render 函数如下:

(function anonymous() {
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('div', [_v(" 我是 静态节点 001 ")]), _v(" "), _c('div', [_v(_s(text))]), _v(" "), _c('div', {
class: vBind
}, [_v(" v-bind 指令")]), _v(" "), (vIf) ? _c('div', [_v(" v-if 指令 ")]) : _e(), _v(" "), _c('div', {
directives: [{
name: "show",
rawName: "v-show",
value: (vShow),
expression: "vShow"
}]
}, [_v(" v-show 指令 ")]), _v(" "), _c('button', {
on: {
"click": handleClick
}
}, [_v("按钮")]), _v(" "), _c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (vModel),
expression: "vModel"
}],
attrs: {
"type": "text"
},
domProps: {
"value": (vModel)
},
on: {
"input": function($event) {
if ($event.target.composing)
return;
vModel = $event.target.value
}
}
}), _v(" "), _c('ul', _l((vFor), function(item, index) {
return _c('li', {
key: index
}, [_v(_s(item.value))])
}), 0), _v(" "), _c('mycomponent', {
attrs: {
"componentprop": componentProp
}
})], 1)
}
}
)

三、问题


3.1 .vue 中的 HTML 是真实的 HTML 吗?

.vue 文件中的 template 中写入的 html 不是真实的 html。 原因非常简单, 如果我们写入的是真实的 HTML 节点, 对于 v-ifv-bindkeep-alive 这些东西,浏览器显然是不认识的,所以这些东西理应无法解析。但是这些指令或者组件被正确解析了,所以 Vue 一定在中间做了什么,让假的 HTML 节点被渲染成了真实的 HTML 标签节点。