模板编译
Vue 使用 Template 语法,编译时将 Template 转为 render 函数。
JSX-in-Vue
跳过编译过程,使用 JSX 语法编译 render 函数,最终都是靠 render 函数生成虚拟 DOM。
使用 JSX
export default defineComponent({
props: {
level: {
type: Number,
required: true,
},
},
setup(props, { slots }) {
const tag = "h" + props.level;
// 利用 JSX 写 h()
return () => <tag>{slots.default()}</tag>;
},
});
h 函数,可以处理动态性更高的场景。h 函数内部也是调用 createVnode 来返回虚拟 DOM。对于那些创建虚拟 DOM 的函数,我们统一称为 h 函数。
JSX 的本质
本质是 createVnode 的语法糖
const element = <h1 id="app">Hello World</h1>;
const element = createVnode("h1", { id: "app" }, "Hello World");
抉择
在 TodoList 组件使用 JSX 写 render 函数 👇
return () => (
<div>
<input type="text" vModel={title.value} />
<button onClick={addTodo}>click</button>
<ul>
{todos.value.length ? (
todos.value.map((todo) => {
return <li>{todo.title}</li>;
})
) : (
<li>no data</li>
)}
</ul>
</div>
);
- 对动态性要求很高的场景使用 JSX,本质还是在写 JavaScript
- JSX 相比于 template 还有一个优势,是可以在一个文件内返回多个组件
- 当然我们接受一些操作上的限制(Template),但同时也会获得一些系统优化的收益(Vue 编译期间进行静态标记的优化)
编译优化
Template
<div id="app">
<div @click="()=>console.log(xx)" name="hello">{{name}}</div>
<h1>技术摸鱼</h1>
<p :id="name" class="app">极客时间</p>
</div>
Vue3 编译后结果 🆚
import {
toDisplayString as _toDisplayString,
createElementVNode as _createElementVNode,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
const _hoisted_1 = { id: "app" };
const _hoisted_2 = /*#__PURE__*/ _createElementVNode(
"h1",
null,
"技术摸鱼",
-1 /* HOISTED */
);
const _hoisted_3 = ["id"];
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("div", _hoisted_1, [
_createElementVNode(
"div",
{
onClick: _cache[0] || (_cache[0] = () => _ctx.console.log(_ctx.xx)),
name: "hello",
},
_toDisplayString(_ctx.name),
1 /* TEXT */
),
_hoisted_2,
_createElementVNode(
"p",
{
id: _ctx.name,
class: "app",
},
"极客时间",
8 /* PROPS */,
_hoisted_3
),
])
);
}
- 首先,静态的标签和属性会放在
_hoisted
变量中,并且放在 render 函数之外。重复执行 render 的时候,代码里的 h1 这个纯静态的标签,就不需要进行额外地计算,并且静态标签在虚拟 DOM 计算的时候,会直接跳过 Diff 过程。 - @click 函数增加了一个 cache 缓存层,这样实现出来的效果也是和静态提升类似,尽可能高效地利用缓存。
- 最后,那些带冒号的属性是动态属性,因而存在使用一个数字去标记标签的动态情况。
比如在 p 标签上,使用 8 这个数字标记当前标签时,只有 props 是动态的。而在虚拟 DOM 计算 Diff 的过程中,可以忽略掉 class 和文本的计算,这也是 Vue 3 的虚拟 DOM 能够比 Vue 2 快的一个重要原因。
PatchFlag
位运算做静态标记
export const enum PatchFlags {
TEXT = 1,// 动态的文本节点
CLASS = 1 << 1, // 2 动态的 class
STYLE = 1 << 2, // 4 动态的 style
PROPS = 1 << 3, // 8 动态属性,不包括类名和样式
FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 Fragment
KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
NEED_PATCH = 1 << 9, // 512
DYNAMIC_SLOTS = 1 << 10, // 动态 slot
HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff
BAIL = -2 // 一个特殊的标志,指代差异算法
}
HoistStatic
静态提升
<div>
<p>静态标记</p>
<p>{{ message }}</p>
</div>
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
const _hoisted_1 = /*#__PURE__*/ _createElementVNode(
"p",
null,
"静态提升",
-1 /* HOISTED */
);
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("div", null, [
_hoisted_1,
_createElementVNode(
"p",
null,
_toDisplayString(_ctx.message),
1 /* TEXT */
),
])
);
}
CacheHandler
事件监听缓存
<div>
<p @click="handleClick">wuli一giao</p>
</div>
import {
createElementVNode as _createElementVNode,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("div", null, [
_createElementVNode(
"p",
{
onClick:
_cache[0] ||
(_cache[0] = (...args) =>
_ctx.handleClick && _ctx.handleClick(...args)),
},
"wuli一giao"
),
])
);
}
SSR
借助服务端渲染,将静态标签直接转化为文本。省去编译生成 VNode 再转成真实 DOM 的开销。
<div>
<p>测试</p>
<p>测试1</p>
<p>测试2</p>
<p>测试3</p>
<p>测试4</p>
<p @click="handleClick">wuli一giao</p>
</div>
import { mergeProps as _mergeProps } from "vue";
import { ssrRenderAttrs as _ssrRenderAttrs } from "vue/server-renderer";
export function ssrRender(
_ctx,
_push,
_parent,
_attrs,
$props,
$setup,
$data,
$options
) {
const _cssVars = { style: { color: _ctx.color } };
_push(
`<div${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}>
<p>测试</p>
<p>测试1</p>
<p>测试2</p>
<p>测试3</p>
<p>测试4</p>
<p>wuli一giao</p>
</div>`
);
}
StaticNode
在客户端渲染的时候,只要标签嵌套得足够多,编译时也会将其转化为 HTML 字符串。
<div>
<p>giao1</p>
<p>giao2</p>
<p>giao3</p>
<p>giao4</p>
<p>giao5</p>
<p>giao6</p>
<p>giao7</p>
<p>giao8</p>
<p>giao9</p>
<p>giao10</p>
</div>
import {
createElementVNode as _createElementVNode,
createStaticVNode as _createStaticVNode,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
const _hoisted_1 = /*#__PURE__*/ _createStaticVNode(
"<p>giao1</p><p>giao2</p><p>giao3</p><p>giao4</p><p>giao5</p><p>giao6</p><p>giao7</p><p>giao8</p><p>giao9</p><p>giao10</p>",
10
);
const _hoisted_11 = [_hoisted_1];
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", null, _hoisted_11);
}