Study Notes
本博主会持续更新各种前端的技术,如果各位道友喜欢,可以关注、收藏、点赞下本博主的文章。
Vue.js 源码剖析-模板编译
Vue 模板编译
-
为什么需要模板编译
Vue 2.x 使用 VNode 描述视图以及各种交互,用户自己编写 VNode 比较复杂
-
模板编译的目的
- 将模板(template)字符串转换为渲染函数(render)
- 用户只需要编写类似 HTML 的代码 - Vue 模板,通过编译器将模板转换为返回 VNode 的 render 函数
- .vue 文件在 webpack 构建的过程中会被转换成 render 函数
沙盒工具
官方提供 Vue 2.x 模板编译沙盒
Vue 2.6 Template Explorer 模板编译沙盒
Vue 3 Template Explorer 模板编译沙盒
分析 render 函数
function anonymous() {
with (this) {
return _c('div', [
_m(0),
message ? _c('p', [_v(_s(message))]) : _c('p', [_v('No message.')]),
]);
}
}
- _c 是 createElement() 方法,定义的位置 src/core/instance/render.js 中
- 相关的渲染函数(_开头的方法定义),在 src/core/instance/render-helps/index.js 中
// src/core/instance/render.js
// 对编译生成的render进行渲染的方法
// 具体可前往上一章虚拟Dom源码剖析
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
// src/core/instance/render-helps/index.js
// 生成文本虚拟节点
target._v = createTextVNode;
// 用于渲染静态资源的方法
target._m = renderStatic;
// 将数据转换为字符串格式
target._s = toString;
// src/core/vdom/vnode.js
// 生成文本虚拟节点
export function createTextVNode(val: string | number) {
return new VNode(undefined, undefined, undefined, String(val));
}
// src/core/instance/render-helpers/render-static.js
/**
* Runtime helper for rendering static trees.
* 运行时,用于渲染静态抽象语法树
*/
export function renderStatic(
index: number,
isInFor?: boolean,
): VNode | Array {
// static trees can be rendered once and cached on the contructor options
// so every instance shares the same cached trees
// 静态树可以渲染一次并缓存在构造器选项上,每个实例共享相同的缓存中的静态树
const renderFns = this.$options.staticRenderFns;
const cached = renderFns.cached || (renderFns.cached = []);
let tree = cached[index];
// if has already-rendered static tree and not inside v-for,
// we can reuse the same tree by doing a shallow clone.
// 如果已经渲染了静态树而不是在v-for内部,
// 我们可以通过执行浅复制来重用同一棵树。
if (tree && !isInFor) {
return Array.isArray(tree) ? cloneVNodes(tree) : cloneVNode(tree);
}
// otherwise, render a fresh tree.
// 否则,渲染一棵新的树
tree = cached[index] = renderFns[index].call(this._renderProxy, null, this);
markStatic(tree, `__static__${index}`, false);
return tree;
}
// src/shared/util.js
/**
* Convert a value to a string that is actually rendered.
* 将值转换为实字符串。
*/
export function toString(val: any): string {
return val == null
? ''
: typeof val === 'object'
? JSON.stringify(val, null, 2)
: String(val);
}
编译过程
编译入口分析
- src/platforms/web/entry-runtime-with-compiler.js
// 把 template 转换成 render 函数
const { render, staticRenderFns } = compileToFunctions(
template,
{
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments,
},
this,
);
- src/platforms/web/compiler/index.js
/* @flow */
import { baseOptions } from './options';
import { createCompiler } from 'compiler/index';
// baseOptions 平台相关的options
const { compile, compileToFunctions } = createCompiler(baseOptions);
export { compile, compileToFunctions };
- src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions,
): CompiledResult {
// 把模板转换成 ast 抽象语法树
// 抽象语法树,用来以树形的方式描述代码结构
const ast = parse(template.trim(), options);
// 优化抽象语法树
optimize(ast, options);
// 把抽象语法树生成字符串形式的 js 代码
const code = generate(ast, options);
return {
// 抽象语法树
ast,
// 渲染函数
render: code.render,
// 静态渲染函数,生成静态 VNode 树
staticRenderFns: code.staticRenderFns,
};
});
- src/compiler/create-compiler.js
// baseOptions 平台相关的options
// src/platforms/web/compiler/options.js 中定义
export function createCompilerCreator(baseCompile: Function): Function {
return function createCompiler(baseOptions: CompilerOptions) {
function compile(
template: string,
options?: CompilerOptions,
): CompiledResult {
const finalOptions = Object.create(baseOptions);
const errors = [];
const tips = [];
finalOptions.warn = (msg, tip) => {
(tip ? tips : errors).push(msg);
};
if (options) {
// merge custom modules
// 合并自定义模块
if (options.modules) {
finalOptions.modules = (baseOptions.modules || []).concat(
options.modules,
);
}
// merge custom directives
// 合并自定义指令
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives),
options.directives,
);
}
// copy other options
// 克隆其他配置
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key];
}
}
}
const compiled = baseCompile(template, finalOptions);
if (process.env.NODE_ENV !== 'production') {
errors.push.apply(errors, detectErrors(compiled.ast));
}
compiled.errors = errors;
compiled.tips = tips;
return compiled;
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile),
};
};
}
解析 - parse
解析器将模板解析为抽象语树 AST,只有将模板解析成 AST 后,才能基于它做优化或者生成代码字符串。
/**
* Convert HTML string to AST.
* 将HTML字符串转换为AST
*/
export function parse(
template: string,
options: CompilerOptions,
): ASTElement | void {
warn = options.warn || baseWarn;
// 解析 options
// no: false
platformIsPreTag = options.isPreTag || no;
platformMustUseProp = options.mustUseProp || no;
platformGetTagNamespace = options.getTagNamespace || no;
// 根据key获取options.modules对应的数据
transforms = pluckModuleFunction(options.modules, 'transformNode');
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');
// delimiters完整版的vue才有,只有在编译的时候才会使用到,
// 作用是改变差值表达式使用的符号
// https://cn.vuejs.org/v2/api/#delimiters
delimiters = options.delimiters;
const stack = [];
const preserveWhitespace = options.preserveWhitespace !== false;
let root;
let currentParent;
let inVPre = false;
let inPre = false;
let warned = false;
function warnOnce(msg) {
if (!warned) {
warned = true;
warn(msg);
}
}
function endPre(element) {
// check pre state
if (element.pre) {
inVPre = false;
}
if (platformIsPreTag(element.tag)) {
inPre = false;
}
}
// 对模板解析
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldKeepComment: options.comments,
// 解析过程中的回调函数,生成 AST
start(tag, attrs, unary) {
// check namespace.
// inherit parent ns if there is one
const ns =
(currentParent && currentParent.ns) || platformGetTagNamespace(tag);
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs);
}
// 生成AST(抽象语法树)
let element: ASTElement = createASTElement(tag, attrs, currentParent);
if (ns) {
element.ns = ns;
}
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true;
process.env.NODE_ENV !== 'production' &&
warn(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
`<${tag}>` +
', as they will not be parsed.',
);
}
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element;
}
// 跳过这个元素和它的子元素的编译过程。
// 可以用来显示原始 Mustache 标签。
// 跳过大量没有指令的节点会加快编译。
// https://cn.vuejs.org/v2/api/#v-pre
if (!inVPre) {
processPre(element);
if (element.pre) {
inVPre = true;
}
}
if (platformIsPreTag(element.tag)) {
inPre = true;
}
if (inVPre) {
// 处理el属性
processRawAttrs(element);
} else if (!element.processed) {
// structural directives
// 结构化指令的处理
// v-for
processFor(element);
// v-if
processIf(element);
// v-once
processOnce(element);
// element-scope stuff
// 处理el
processElement(element, options);
}
// 检查根元素约束
function checkRootConstraints(el) {
if (process.env.NODE_ENV !== 'production') {
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.',
);
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.',
);
}
}
}
// tree management
// 抽象语法树管理
if (!root) {
root = element;
checkRootConstraints(root);
} else if (!stack.length) {
// allow root elements with v-if, v-else-if and v-else
// 允许根元素带有v-if,v-else-if和v-else
if (root.if && (element.elseif || element.else)) {
checkRootConstraints(element);
// 将if语法树添加到根语法树里
addIfCondition(root, {
exp: element.elseif,
block: element,
});
} else if (process.env.NODE_ENV !== 'production') {
warnOnce(
`Component template should contain exactly one root element. ` +
`If you are using v-if on multiple elements, ` +
`use v-else-if to chain them instead.`,
);
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
// 处理if语法树
processIfConditions(element, currentParent);
} else if (element.slotScope) {
// scoped slot
currentParent.plain = false;
const name = element.slotTarget || '"default"';
(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[
name
] = element;
} else {
currentParent.children.push(element);
element.parent = currentParent;
}
}
if (!unary) {
currentParent = element;
stack.push(element);
} else {
endPre(element);
}
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options);
}
},
end() {
// remove trailing whitespace
// 删除尾部空格
const element = stack[stack.length - 1];
const lastNode = element.children[element.children.length - 1];
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
element.children.pop();
}
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
endPre(element);
},
// 处理文本元素
chars(text: string) {
if (!currentParent) {
if (process.env.NODE_ENV !== 'production') {
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.',
);
} else if ((text = text.trim())) {
warnOnce(`text "${text}" outside root element will be ignored.`);
}
}
return;
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (
isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return;
}
const children = currentParent.children;
text =
inPre || text.trim()
? isTextTag(currentParent)
? text
: decodeHTMLCached(text)
: // only preserve whitespace if its not right after a starting tag
preserveWhitespace && children.length
? ' '
: '';
if (text) {
let expression;
if (
!inVPre &&
text !== ' ' &&
(expression = parseText(text, delimiters))
) {
children.push({
type: 2,
expression,
text,
});
} else if (
text !== ' ' ||
!children.length ||
children[children.length - 1].text !== ' '
) {
children.push({
type: 3,
text,
});
}
}
},
// 处理注释元素
comment(text: string) {
currentParent.children.push({
type: 3,
text,
isComment: true,
});
},
});
return root;
}
Vue 官网查看
v-if/v-for 结构化指令只能在编译阶段处理,如果我们要在 render 函数处理条件或循环只能使用
js 中的 if 和 for
在模板中使用的 v-if 和 v-for:
- {{ item.name }}
No items found.
这些都可以在渲染函数中用 JavaScript 的 if/else 和 map 来重写:
props: ['items'],
render: function (createElement) {
if (this.items.length) {
return createElement('ul', this.items.map(function (item) {
return createElement('li', item.name)
}))
} else {
return createElement('p', 'No items found.')
}
}
- AST tree 生成沙盒
优化 - optimize
- 优化抽象语法树
- 静态节点 --> 永远不会更改的节点
- 标记非静态节点和标记静态根节点
- 检测静态节点,设置为静态,不需要在每次重新渲染的时候重新生成节点
- patch 阶段跳过静态节点
src/compiler/index.js
// 优化抽象语法树
optimize(ast, options);
src/compiler/optimizer.js
export function optimize(root: ?ASTElement, options: CompilerOptions) {
if (!root) return;
isStaticKey = genStaticKeysCached(options.staticKeys || '');
isPlatformReservedTag = options.isReservedTag || no;
// first pass: mark all non-static nodes.
// 第一次通过:标记非静态节点。
markStatic(root);
// second pass: mark static roots.
// 第二遍:标记静态根节点。
markStaticRoots(root, false);
}
function markStatic(node: ASTNode) {
// 判断当前的astNode是否是静态的
node.static = isStatic(node);
// 元素节点
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
// 不要将组件插槽的内容设为静态
// 1.组件无法更改插槽节点
// 2.静态插槽内容无法进行热重装
// 是组件,不是slot,没有inline-template,直接返回,阻止向下执行
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return;
}
// 遍历children
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i];
// 标记非静态
markStatic(child);
if (!child.static) {
// 如果有一个child不是静态,设置当前节点不是静态
node.static = false;
}
}
// 处理条件渲染
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block;
// 标记非静态
markStatic(block);
if (!block.static) {
// 如果有一个block不是静态,设置当前节点不是静态
node.static = false;
}
}
}
}
}
function markStaticRoots(node: ASTNode, isInFor: boolean) {
// 判断是否为元素节点
if (node.type === 1) {
// 判断当前节点是否是静态节点或者是否只渲染一次
if (node.static || node.once) {
// 标记其在for循环中是否是静态的
node.staticInFor = isInFor;
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
// 如果一个元素内只有文本节点,此时这个元素不是静态的Root
// vue 认为这种优化会带来负面的影响
// 判断当前节点是静态节点,并且存在子节点,并且当前节点的子节点不能只存在一个文本节点
// 如果不满足,优化成本将大于收益
if (
node.static &&
node.children.length &&
!(node.children.length === 1 && node.children[0].type === 3)
) {
// 标记根节点为静态
node.staticRoot = true;
return;
} else {
// 标记根节点为非静态
node.staticRoot = false;
}
// 检测当前节点的子节点中是否有静态的Root
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for);
}
}
// 检测当前节点的条件渲染中是否有静态的Root
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor);
}
}
}
}
function isStatic(node: ASTNode): boolean {
if (node.type === 2) {
// expression 表达式
return false;
}
if (node.type === 3) {
// text 文本
return true;
}
return !!(
node.pre ||
(!node.hasBindings && // no dynamic bindings
!node.if &&
!node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in 不能是内置组件
isPlatformReservedTag(node.tag) && // not a component 不能是组件
!isDirectChildOfTemplateFor(node) && // 不能是v-for下的直接节点
Object.keys(node).every(isStaticKey))
);
}
生成 - generate
- 将抽象语法树转换为字符串形式的 js 代码
src/compiler/index.js
// 将抽象语法树转换为字符串形式的 js 代码
const code = generate(ast, options);
src/compiler/codegen/index.js
export function generate(
ast: ASTElement | void,
options: CompilerOptions,
): CodegenResult {
// CodegenState 代码生成过程中使用的状态对象
const state = new CodegenState(options);
// 如果存在ast,生成代码,否则返回_c("div")
const code = ast ? genElement(ast, state) : '_c("div")';
return {
render: `with(this){return ${code}}`,
// 静态渲染函数,即静态根节点的渲染函数
staticRenderFns: state.staticRenderFns,
};
}
export function genElement(el: ASTElement, state: CodegenState): string {
// el.staticProcessed用于判断当前静态根节点是否已处理,防止重复处理
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state);
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state);
} else if (el.for && !el.forProcessed) {
return genFor(el, state);
} else if (el.if && !el.ifProcessed) {
return genIf(el, state);
// 以下处理非静态节点
} else if (el.tag === 'template' && !el.slotTarget) {
return genChildren(el, state) || 'void 0';
} else if (el.tag === 'slot') {
return genSlot(el, state);
} else {
// component or element
// 处理组件和内置标签
let code;
if (el.component) {
code = genComponent(el.component, el, state);
} else {
// 生成元素的属性/指令/事件等
// 处理各种指令,包括genDirective(model/text/html)
const data = el.plain ? undefined : genData(el, state);
// 将节点转换为相应的代码
const children = el.inlineTemplate ? null : genChildren(el, state, true);
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`;
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
return code;
}
}
// hoist static sub-trees out
function genStatic(el: ASTElement, state: CodegenState): string {
// 将状态置为已处理
el.staticProcessed = true;
// 将静态根节点转换为生成vnode的js代码
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`);
return `_m(${state.staticRenderFns.length - 1}${
el.staticInFor ? ',true' : ''
})`;
}