编译
web 模式的编译主要做了 3 件事:
- 解析 template 生成 AST
- AST 转换
- 生成代码
/*** web 编译* @param {string} template - 待编译的模板字符串* @param {string} options - 配置对象*/
function compile(template, options = {}) {return baseCompile(template, extend({}, parserOptions, options, {nodeTransforms: [...DOMNodeTransforms, ...(options.nodeTransforms || [])],directiveTransforms: extend({}, DOMDirectiveTransforms, opotins.directiveTransforms || {}),transformHoist: null}))
}/*** 基础编译* baseCompile 主要做了 3 件事:* 1、解析 template 生成 AST* 2、AST 转换* 3、生成代码*/
function baseCompile(template, options = {}) {const prefixIdentifiers = false// 1、解析 template 生成 ASTconst ast = isString(template) ? baseParse(template, options) : templateconst [nodeTransforms, directiveTransforms] = getBaseTransformPreset()// 2、AST 转换transform(ast, extend({}, options, {prefixIdentifiers,nodeTransforms: [...nodeTransforms,...(options.nodeTransforms || [])],directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {})}))// 3、生成代码return generage(ast, extend({}, options, {prefixIdentifiers}))
}
解析 template 生成 AST
问题:AST 是什么?
答:AST 是抽象语法树,用于描述节点在模板中完整的映射信息。
问题:为什么 AST 的根节点是虚拟节点?
答:因为 Vue3 中支持
<Fragment>
语法,即:组件可以有多个根节点,但是树状结构只能有一个根节点,所以需要在外层套一个虚拟节点。
HTML 的嵌套结构的解析过程其实就是一个递归解析元素节点的过程。
/*** 解析 template 生成 AST* baseParse 主要做了 2 件事:* 1、创建解析上下文* 2、解析子节点,并创建 AST 根节点*/
function baseParse(content, options = {}) {// 1、创建解析上下文const context = createParserContext(content, options)const start = getCursor(context)// 2、解析子节点,并创建 AST 根节点return createRoot(parseChildren(context, 0/* DATA */, []), getSelection(context, start))
}// 默认解析配置
const defaultParserOptions = {delimiters: [`{{`, `}}`],getNamespace: () => 0/* HTML */,getTextMode: () => 0/* DATA */,isVoidTag: NO,isPreTag: NO,isCustomElement: NO,decodeEntities: (rawText) => rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),onError: defaultOnError
}/*** 创建解析器上下文*/
function createParserContext(content, options) {return {options: extend({}, defaultParserOptions, options), // 解析相关配置column: 1, // 当前代码的列号line: 1, // 当前代码的行号offset: 0, // 当前代码相对于原始代码的偏移量originalSource: content, // 最初的原始代码source: content, // 当前代码inPre: false, // 当前代码是否在 pre 标签内inVPre: false // 当前代码是否在 VPre 指定的环境下}
}/*** 解析子节点* 目的:解析并创建 AST 节点数组* parseChildren 主要做了 2 件事:* 1、自顶向下分析代码,生成 nodes* 2、空白字符管理*/
function parseChildren(context, mode, ancestors) {const parent = last(ancestors) // 父节点const ns = parent ? parent.ns : 0/* HTML */const nodes = []// 1、自顶向下分析代码,生成 nodes(AST 节点数组)// 自顶向下遍历代码,然后根据不同情况去解析代码while (!isEnd(context, mode, ancestors)) {const s = context.sourcelet node = undefinedif (mode === 0/* DATA */ || mode === 1/* RCDATA */) {// 处理 {{ 插值代码if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {node = parseInterpolation(context, mode)}// 处理 < 开头的代码else if (mode === 0/* DATA */ && s[0] === '<') {// s 长度为 1,说明代码结尾是 <,报错if (s.length === 1) {emitError(context, 5/* EOF_BEFORE_TAG_NAME */, 1)}// 处理 <! 开头的代码else if (s[1] === '!') {// 处理注释节点if (startsWith(s, '<!--')) {node = parseComment(context)}// 处理 <!DOCTYPE 节点else if (startsWith(s, '<!DOCTYPE')) {node = parseBogusComment(context)}// 处理 <![CDATA] 节点else if (startsWith(s, '<![CDATA]')) {if (ns !== 0/* HTML */) {node = parseCDATA(context, ancestors)} else {emitError(context, 1/* CDATA_IN_HTML_CONTENT */)node = parseBogusComment(context)}}// 处理其他情况else {emitError(context, 11/* INCORRECTLY_OPENED_COMMENT */)node = parseBogusComment(context)}}// 处理 </ 结束标签else if (s[1] === '/') {// s 长度为2,说明代码结尾是 </,报错if (s.length === 2) {emitError(context, 5/* EOF_BEFORE_TAG_NAME */, 2)}// </> 缺少结束标签,报错else if (s[2] === '>') {emitError(context, 14/* MISSING_END_TAG_NAME */, 2)advanceBy(context, 3)continue}// 多余的结束标签else if (/[a-z]/i.test(s[2])) {emitError(context, 23/* X_INVALID_END_TAG */)parseTag(context, 1/* END */, parent)continue} else {emitError(context, 12/* INVALID_FIRST_CHARACTER_OF_TAG_NAME */, 2)node = parseBogusComment(context)}}// 解析标签元素节点else if (/[a-z]/i.test(s[1])) {node = parseElement(context, ancestors)} else if (s[1] === '?') {emitError(context, 21/* UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME */, 1)node = parseBogusComment(context)} else {emitError(context, 12/* INVALID_FIRST_CHARACTER_OF_TAG_NAME */, 1)}}}// 解析普通文本节点if (!node) {node = parseText(context, mode)}// 如果 node 是数组,则遍历添加if (isArray(node)) {for (let i = 0; i < node.length; i++) {pushNode(nodes, node[i])}}// 否则,添加单个 nodeelse {pushNode(nodes, node)}}// 2、空白字符管理(用于提高编译效率)let removedWhitespace = falseif (mode !== 2/* RAWTEXT */) {if (!context.inPre) {for (let i = 0; i < nodes.length; i++) {const node = nodes[i]if (node.type === 2 /* TEXT */) {if (!/[Atr\n\f]/.test(node.content)) {// 匹配空白字符const prev = nodes[i - 1]const next = nodes[i + 1]// 如果 空白字符是开头或者结尾节点 或者 空白字符与注释节点相连 或者 空白字符在两个元素之间并包含换行符 那么这些空白字符节点都应该被移除if (!prev || !next || prev.type === 3/* COMMENT */ || next.type === 3/* COMMENT */ ||(prev.type === 1/* ELEMENT */ && next.type === 1/* ELEMENT */ && /[\r\n]/.test(node.content))) {removedWhitespace = truenodes[i] = null}// 否则压缩这些空白字符到一个空格else {node.content = ''}}// 替换内容中的空白空间到一个空格else {node.content = node.content.replace(/[\t\r\n\f]+/g, '')}}// 生产环境移除注释节点else if (!(process.env.NODE_ENV !== 'production') && node.type === 3/* COMMENT */) {removedWhitespace = truenodes[i] = null}}}// 根据 HTML 规范删除前导换行符else if (parent && context.options.isPreTag(parent.tag)) {const first = nodes[0]if (first && first.type === 2/* TEXT */) {first.content = first.content.replace(/^\r?\n/, '')}}}// 过滤空白字符节点return removedWhitespace ? nodes.filter(Boolean) : nodes
}
解析子节点
解析注释节点
/*** 解析注释节点*/
function parseComment(context) {const start = getCursor(context)let contentconst match = /--(\!)?>/.exec(context.source) // 注释结束符// 如果没有匹配的注释结束符,则报错if (!match) {content = context.source.slice(4)advanceBy(context, context.source.length)emitError(context, 7/* EOF_IN_COMMENT */)} else {// 非法的注释符号if (match.index <= 3) {emitError(context, 0/* ABRUPT_CLOSING_OF_EMPTY_COMMENT */)}// 注释结束符不正确if (match[1]) {emitError(context, 10/* INCORRECTLY_CLOSED_COMMENT */)}content = context.source.slice(4, match.index) // 获取注释的内容const s = context.source.slice(0, match.index) // 截取到注释结尾之间的代码,用于后续判断嵌套注释let prevIndex = 1, nestedIndex = 0// 判断嵌套注释符的情况,存在即报错while ((nestedIndex = s.indexOf('<!--', prevIndex)) !== -1) {advanceBy(context, nestedIndex - prevIndex + 1)if (nestedIndex + 4 < s.length) {emitError(context, 16/* NESTED_COMMENT */)}prevIndex = nestedIndex + 1}// 前进代码到注释符结束后,目的是用来前进代码,更新 context 解析上下文advanceBy(context, match.index + match[0].length - prevIndex + 1)}return {type: 3/* COMMENT */,content,loc: getSelection(context, start)}
}/*** 前进代码*/
function advanceBy(context, numberOfCharacters) {const { source } = context// 更新 context 的 offset?line?column?advancePositionWithMutation(context, source, numberOfCharacters)// 更新 context 的 sourcecontext.source = source.slice(numberOfCharacters)
}/*** 更新 context 的 offset?line?column?*/
function advancePositionWithMutation(pos, source, numberOfCharacters = source.length) {let linesCount = 0let lastNewLinePos = -1for (let i = 0; i < numberOfCharacters; i++) {if (source.charCodeAt(i) === 10/* newline char code */) {linesCount++lastNewLinePos = i}}pos.offset += numberOfCharacterspos.line += linesCountpos.column = lastNewLinePos === -1 ? pos.column + numberOfCharacters : numberOfCharacters - lastNewLinePosreturn pos
}
解析插值
/*** 解析插值*/
function parseInterpolation(context, mode) {// 从配置中获取插值开始和结束分隔符,默认是 {{ 和 }}const [open, close] = context.options.delimitersconst closeIndex = context.source.indexOf(close, open, length)// 没有结束符,报错if (closeIndex === -1) {emitError(context, 25/* X_MISSING_INTERPOLATION_END */)return undefined}const start = getCursor(context)// 代码前进到插值开始分隔符后advanceBy(context, open.length)const innerStart = getCursor(context) // 内部插值开始位置const innerEnd = getCursor(context) // 内部插值结束位置const rawContentLength = closeIndex - open.length // 插值原始内容的长度const rawContent = context.source.slice(0, rawContentLength) // 插值原始内容const preTrimContent = parseTextData(context, rawContentLength, mode) // 获取插值的内容,并前进代码到插值的内容后const content = preTrimContent.trim()const startOffset = preTrimContent.indexOf(contend) // 内容相对于插值开始分隔符的头偏移// 更新内部插值开始位置if (startOffset > 0) {advancePositionWithMutation(innerStart, rawContent, startOffset)}// 内容相对于插值结束分隔符的尾偏移const endOffset = rawContentLength - (preTrimContent.length - content.length - startOffset)// 更新内部插值结束位置advancePositionWithMutation(innerEnd, rawContent, endOffset)// 前进代码到插值结束分隔符后advanceBy(context, close.length)// 返回一个描述节点的对象return {type: 5/* INTERPOLATION */, // 插值节点content: { // 表达式节点的对象type: 4/* SIMPLE_EXPRESSION */, // 表达式节点isStatic: false,isConstant: false,content, // 插值的内容loc: getSelection(context, innerStart, innerEnd) // 内容代码开头和结束的位置信息},loc: getSelection(context, start) // 插值代码开头和结束的位置信息}
}
解析普通文本
/*** 解析普通文本*/
function parseText(context, mode) {const endTokens = ['<', context.options.delimiters[0]] // 文本结束符 // CDATA 标记 XML 中的纯文本if (mode === 3/* CDATA */) {endTokens.push(']]>')}let endIndex = context.source.length// 遍历文本结束符,匹配找到结束的位置for (let i = 0; i < endTokens.length; i++) {const index = context.source.indexOf(endTokens[i], 1)if (index !== -1 && endIndex > index) {endIndex = index}}// 获取文本的内容,并前进代码到文本的内容后const start = getCursor(context)const content = parseTextData(context, endIndex, mode)return {type: 2/* TEXT */,content,loc: getSelection(context, start)}
}
解析元素节点
/*** 解析元素节点* parseElement 主要做了 3 件事:* 1、解析开始标签* 2、解析子节点* 3、解析结束标签*/
function parseElement(context, ancestors) {const wasInPre = context.inPre // 是否在 pre 标签内const wasInVPre = context.inVPre // 是否在 v-pre 指令内const isPreBoundary = context.inPre && !wasInPre // 是否在 pre 标签的边界const isVPreBoundary = context.inVPre && !wasInVPre // 是否在 v-pre 指令的边界// 获取当前元素的父标签节点const parent = last(ancestors)// 1、解析开始标签,生成一个标签节点,并前进代码到开始标签后const element = parseTag(context, 0/* Start */, parent)// 如果是自闭和标签,直接返回标签节点if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {return element}// 下面是处理子节点的逻辑// 先把标签节点添加到 ancestors,入栈ancestors.push(element)const mode = context.options.getTextMode(element, parent)// 2、递归解析子节点,传入 ancestorsconst children = parseChildren(context, mode, ancestors)// ancestors 出栈ancestors.pop()// 添加到 children 属性中element.children = children// 结束标签if (startsWithEndTagOpen(context.source, element.tag)) {// 3、解析结束标签,并前进代码到结束标签后parseTag(context, 1/* End */, parent)}else {emitError(context, 24/* X_MISSING_END_TAG */, 0, element.loc.start);if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {const first = children[0];if (first && startsWith(first.loc.source, '<!--')) {emitError(context, 8/* EOF_IN SCRIPT_HTML COMMENT_LIKE_TEXT */)}}}// 更新标签节点的代码位置,结束位置到结束标签后element.loc = getSelection(context, element.loc.start)if (isPreBoundary) {context.inPre = false}if (isVPreBoundary) {context.inVPre = false}return element
}/*** 解析并创建一个标签节点*/
function parseTag(context, type, parent) {const start = getCursor(context) // 开始标签// 1、匹配标签文本结束的位置const match = /^<\/?([a-z][^\t\r\n\f/>]*)/i.exec(context.source)const tag = match[1]const ns = context.options.getNamesxpace(tag, parent)// 前进代码到标签文本结束位置advanceBy(context, match[0].length)// 2、前进代码到标签文本后面的空白字符后advanceSpaces(context)// 保存当前状态以防我们需要用 v-pre 重新解析属性const cursor = getCursor(context)const currentSource = context.source// 3、解析标签中的属性,并前进代码到属性后let props = parseAttributes(context, type)// 4、检查是不是一个 pre 标签if (context.options.isPreTag(tag)) {context.inPre = true}// 5、检查属性中有没有 v-pre 指令,如果有,则设置 context.inVPre 为 true,并重置上下文 context 和 重新解析属性if (!context.inVPre && props.some(p => p.type === 7/* DIRECTIVE */ && p.name === 'pre')) {context.inVPre = true;// 重置 contextextend(context, cursor);context.source = currentSource;// 重新解析属性,并把 v-pre 过滤了props = parseAttributes(context, type).filter(p => p.name !== 'v-pre');}let isSelfClosing = false; // 是否是闭合标签// 6、判断是否自闭合标签if (context.source.length === 0) {emitError(context, 9/* EOF IN TAG */);} else {isSelfClosing = startsWith(context.source, '/>')if (type === 1/* End */ && isSelfClosing) {// 结束标签不应该是自闭和标签emitError(context, 4/* END_TAG_WITH_TRAILING_SOLIDUS */);}// 前进代码到闭合标签后advanceBy(context, isSelfClosing ? 2 : 1);}let tagType = 0/* ELEMENT */const options = context.options// 7、判断标签类型是组件、插槽还是模板if (!context.inVPre && !options.isCustomElement(tag)) {const hasVIs = props.some(p => p.type === 7/* DIRECTIVE */ && p.name === 'is') // 判断是否有 is 属性if (options.isNativeTag && !hasVIs) {if (!options.isNativeTag(tag)) {tagType = 1/* COMPONENT*/}} else if (hasVIs || isCoreComponent(tag) || (options.isBuiltInComponent && options.isBuiltInComponent(tag)) || /^[A-Z]/.test(tag) || tag === 'component') {tagType = 1/* COMPONENT */}if (tag === 'slot') {tagType = 2/* SLOT */} else if (tag === 'template' && props.some(p => {return (p.type === 7/* DIRECTIVE */ && isSpecialTemplateDirective(p.name));})) {tagType = 3/* TEMPLATE */}}// 返回描述标签节点的对象return {type: 1/* ELEMENT */, // 节点类型ns,tag, // 标签名tagType, // 标签类型props,isSelfClosing, // 是否是闭合标签children: [], // 标签子节点数组(初始化为空)loc: getSelection(context, start), // 文本的开头和结尾信息codegenNode: undefined}
}
问题:在 parseTag 的过程中,如果解析的属性有
v-pre
标签,为什么要回到之前的 context,重新解析一次?答:避免标签中的内容不会被其他属性影响。
创建 AST 根节点
/*** 创建 AST 根节点*/
function createRoot(children, loc = locStub) {return {type: 0/* ROOT */,children,helpers: [],components: [],directives: [],hoists: [],imports: [],cached: 0,temps: 0,codegenNode: undefined,loc}
}
AST 转换
AST 转换流程:
-
创建 transform 上下文
-
遍历 AST 节点
-
静态提升
好处:针对静态节点不用每次在 render 阶段都执行一次 createVNode 创建 vnode 对象。
-
创建根代码生成节点
AST 转换其实就是语法分析阶段,把 AST 节点做一层转换,构造出语义化更强信息更丰富的 codegenCode,它在后续的代码生成阶段起非常重要的作用。
创建 transform 上下文
/*** 获取节点和指令转换的方法*/
function getBaseTransformPreset(prefixIdentifiers) {return [[transformOnce,transformIf,transformFor,transformExpression,transformSlotOutlet,transformElement,trackSlotScopes,transformText,],{on: transformOn,bind: transformBind,model: transformModel}]
}/*** Node 环境的转换* transform 主要做了 4 件事:* 1、创建 transform 上下文* 2、遍历 AST 节点* 3、静态变量提升* 4、创建根代码的生成节点*/
function transform(root, options) {// 1、创建 transform 上下文const context = createTransformContext(root, options)// 2、遍历 AST 节点traverseNode(root, context)// 3、静态变量提升if (options.hoistStatic) {hoistStatic(root, context)}// 4、创建根代码的生成节点if (!options.ssr) {createRootCodegen(root, context)}root.helpers = [...context.helpers]root.components = [...context.components]root.directives = [...context.directives]root.imports = [...context.imports]root.hoists = context.hoistsroot.temps = context.tempsroot.cached = context.cached
}/*** 创建转换上下文*/
function createTransformContext(root, { prefixIdentifiers = false, hoistStatic = false, cacheHandlers = false, nodeTransforms = [], directiveTransforms = {}, transformHoist = null, isBuiltInComponents = NOOP, expressionPlugins = [], scopeId = null, ssr = false, onError = defaultOnError }) {// transform 过程中的一些配置 和 状态数据等const context = {// 配置prefixIdentifiers,hoistStatic,cacheHandlers,nodeTransforms,directiveTransforms,transformHoist,isBuiltInComponents,expressionPlugins,scopeId,ssr,onError,// 状态数据root,helpers: new Set(),components: new Set(),directives: new Set(),hoists: [],imports: new Set(),temps: 0,cached: 0,identifiers: {},scopes: {vFor: 0,vSlot: 0,vPre: 0,vOnce: 0},parent: null,currentNode: root,childIndex: 0,// methodshelper(name) {context.helpers.add(name)return name},helperString(name) {return `_${helperNameMap[context.helper(name)]}`},replaceNode(node) {context.parent.children[context.childIndex] = context.currentNode = node},removeNode(node) {const list = context.parent.childrenconst removalIndex = node ? list.indexOf(node) : context.currentNode ? context.childIndex : -1// 移除当前节点if (!node || node === context.currentNode) {context.currentNode = nullcontext.onNodeRemoved()}// 移除兄弟节点else {if (context.childIndex > removalIndex) {context.childIndex--context.onNodeRemoved()}}// 移除节点context.parent.children.splice(removalIndex, 1)},onNodeRemoved: () => { },addIdentifiers(exp) { },removeIdentifiers(exp) { },hoist(exp) {context.hoists.push(exp)const identifier = createSimpleExpression(`_hoisted_${context.hoists.length}`, false, exp.loc, true)identifier.hoisted = expreturn identifier},cache(exp, isVNode = false) {return createCacheExpression(++context.cached, exp, isVNode)}}return context
}/*** 遍历 AST 节点*/
function traverseNode(node, context) {context.currentNode = node// 节点转换函数const { nodeTransforms } = contextconst exitFns = []for (let i = 0; i < nodeTransforms.length; i++) {// 有些转换函数会涉及一个退出函数,在处理完子节点后执行const onExit = nodeTransforms[i](node, context)if (onExit) {if (isArray(onExit)) {exitFns.push(...onExit)} else {exitFns.push(onExit)}}// 节点被移除if (!context.currentNode) return// 因为在转换的过程中节点可能被替代,恢复到之前的节点else {node = context.currentNode}}switch (node.type) {case 3/* COMMENT */:if (!context.ssr) {// 需要导入 createComment 符主函数context.helper(CREATE_COMMENT)}breakcase 5/* INTERPOLATION */:// 需要导入 toString 辅助函数if (!context.ssr) {context.helper(TO_DISPLAY_STRING)}breakcase 9/* IF */:// 递归遍历每个分支节点for (let i = 0; i < node.branches.length; i++) {traverseNode(node.branches[i], context)}breakcase 10/* IF_BRANCH */:case 11/* FOR */:case 1/* ELEMENT */:case 0/* ROOT */:// 遍历子节点traverseChildren(node, context)break}// 执行转换函数返回的退出函数let i = exitFns.lengthwhile (i--) { }
}
遍历 AST 节点
转换元素节点
/*** 转换元素节点*/
const transformElement = (node, context) => {// 如果节点不会 元素 或 组件,则直接返回if (!(node.type === 1/* ELEMENT */ && (node.tagType === 0/* ELEMENT */ || node.tagType === 1/* COMPONENT */))) return// 返回退出函数,在所有子表达式处理并合并后执行return function postTransformElement() {// 转换的目标是创建一个实现 VNodeCall 接口的代码生成节点const { tag, props } = nodeconst isComponent = node.tagType === 1/* COMPONENT */const vnodeTag = isComponent ? resolveComponentType(node, context) : `"${tag}"`const isDynamicComponent = isObject(vnodeTag) && vnodeTag.callee === RESOLVE_DYNAMIC_COMPONENTlet vnodeProps // 属性let vnodeChildren // 子节点// 标记更新的类型标识,用于运行时优化let vnodePatchFlaglet patchFlag = 0// 动态绑定的属性let vnodeDynamicPropslet dynamicPropNameslet vnodeDirectives// 动态组件、svg、foreignObejct 标签以及动态绑定 prop 的节点都被视作为一个 Blocklet shouldUseBlock = isDynamicComponent || (!isComponent && (tag === 'svg' || tag === 'foreignObject' || findProp(node, 'key', true)))// 处理 props - 从 AST 节点的 props 对象中进一步解析出指令 directives、动态属性 dynamicPropsNames、更新标识 patchFlagif (props.length > 0) {const propsBuildResult = buildProps(node, context)vnodeProps = propsBuildResult.propspatchFlag = propsBuildResult.patchFlag // 更新标识 - 用于标识节点更新的类型,在组件更新的过程中会用到dynamicPropNames = propsBuildResult.dynamicPropsNames // 动态属性const directives = propsBuildResult.directives // 指令vnodeDirectives = directives && directives.length ? createArrayExpression(directives.map(dir => buildDirectiveArgs(dir, context))) : undefined}// 处理 childrenif (node.children.length > 0) {// 把 KeepAlive 看做是一个 Block,这样可以避免它的子节点的动态节点被父 Block 收集if (vnodeTag === KEEP_ALIVE) {shouldUseBlock = true// 确保它始终更新patchFlag |= 1024/* DYNAMIC_SLOTS */if ((propcess.env.NODE_ENV !== 'production') && node.children.length > 1) {context.onError(createCompilertError(42/* X_KEEP_ALIVE_INVALID_CHILDREN */, {start: node.children[0].loc.start,end: node.children[node.children.length - 1].loc.end,source: ''}))}}// Teleport 不是一个真正的组件,它有专门的运行时处理const shouldBuildAsSlots = isComponent && vnodeTag !== TELEPORT && vnodeTag !== KEEP_ALIVE// 如果节点只有一个子节点,并且子节点是一个普通的文本节点插值或表达式,则直接把节点赋值给 vnode.childrenif (shouldBuildAsSlots) {// 组件有 children,则处理插槽const { slots, hasDynamicSlots } = buildSlots(node, context)vnodeChildren = slotsif (hasDynamicSlots) {patchFlag |= 1024/* DYNAMIC_SLOTS */}} else if (node.children.length === 1 && vnodeTag !== TELEPORT) {const child = node.children[0]const type = child.typeconst hasDyanmicTextChild = type === 5/* INTERPOLATION */ || type === 8/* COMPOUND_EXPRESSION */if (hasDynamicTextChild && !getStaticType(child)) {patchFlag |= 1/* TEXT */}// 如果只是一个普通文本节点、插值或者表达式,直接把节点赋值给 vnodeChildrenif (hasDynamicTextChild || type === 2/* TEXT */) {vnodeChildren = child} else {vnodeChildren = node.children}} else {vnodeChildren = node.children}}// 处理 patchFlag 和 dynamicPropNamesif (patchFlag !== 0) {// 根据 patchFlag 的值从 patchFlagNames 中获取 flag 对应的名字,从而生成注释if ((process.env.NODE_ENV !== 'production')) {if (patchFlag < 0) {vnodePatchFlag = patchFlag + `/*${PatchFlagNames[patchFlag]}*/`} else {const flagNames = object.keys(PatchFlagNames).map(Number).filter(m => n > 0 && patchFlag & n).map(n => PatchFlagNames[n]).join(`,`)vnodePatchFlag = patchFlag + `/* ${flagNames} */`}} else {vnodePatchFlag = String(patchFlag)}// 将 dynamicPropNames 转化为 vnodeDynamicProps 字符串,便于后续对节点生成代码逻辑的处理if (dynamicPropNames && dynamicPropNames.length) {vnodeDynamicProps = stringifyFynamicPropNames(dynamicPropNames)}}// 通过 createVNodeCall 创建了实现 VNodeCall 接口的代码生成节点node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren, vnodeFlag, vnodeDynamicProps, vnodeDirectives, !!shouldUseBlock, false/* disableTrancking */, node.loc)}
}/*** 创建了实现 VNodeCall 接口的代码生成节点*/
function createVNodeCall(context, tag, props, children, patchFlag, dynamicProps, directives, isBlock = false, disableTranking = false, loc = locStub) {if (context) {// context.helper 会将一些 simple 对象添加到 context.helps 数组中,目的是为了后续代码生成阶段生成一些辅助代码if (isBlock) {context.helper(OPEN_BLOCK)context.helper(CREATE_BLOCK)} else {context.helper(CREATE_VNODE)}if (directives) {context.helper(WITH_DIRECTIVES)}}// 返回一个对象,包含了传入的参数return {type: 13/* VNODE_CALL */,tag,props,children,patchFlag,dynamicProps,directives,isBlock,disableTracking,loc}
}
转换表达式节点
/*** 转换表达式节点(只有在 node 环境下的编译或者是 web 端的非生产环境下才会执行)* transformExpression 主要做了 2 件事:* 1、转换插值中的动态表达式* 2、转换元素指令中的动态表达式*/
const transformExpression = (node, context) => {// 处理插值中的动态表达式if (node.type === 5/* INTERPOLATION */) {node.content = processExpression(node.content, context)}// 处理元素指令中的动态表达式else if (node.type === 1/* ELEMENT */) {for (let i = 0; i < node.props.length; i++) {const dir = node.props[i]// v-on 和 v-for 不处理,因为它们都有各自的处理逻辑if (dir.type === 7/* DIRECTIVE */ && dir.name !== 'for') {const exp = dir.expconst arg = dir.argif (exp && exp.type === 4/*SIMPLE EXPRESSION*/ && !(dir.name === 'on' && arg)) {dir.exp = processExpression(exp, context, dir.name === 'slot')}if (arg && argtype === 4 /* SIMPLE_EXPRESSION */ && !arg.isStatic) {dir.arg = processExpresstion(arg, context)}}}}
}
/*processExpression 的实现:1、内部依赖了 @babel/parser 库去解析表达式生成 AST 节点2、依赖了 estree-walker 库去遍历这个 AST 节点,然后对节点分析去判断是否需要加前缀3、接着对 AST 节点修改,最终转换生成新的表达式对象
*/
转换文本节点
/*** 转换文本节点 - 只处理根节点、元素节点、v-for、v-if分支* 目的:合并一些相邻的文本节点,然后为内部每一个文本节点创建一个代码生成节点,*/
const transformText = (node, context) => {// 在节点退出时执行转换,保证所有表达式都已经被处理if (node.type === 0/* ROOT */ || node.type === 1/* ELEMENT */ || node.type === 11/* FOR */ || node.type === 10/* IF_BRANCH */) {// 返回退出函数,因为要保证所有表达式节点都已经被处理才执行转换逻辑return () => {const children = node.childrenlet currentContainer = undefinedlet hasText = false// 遍历节点的子节点数组,把子节点中相邻的文本节点合并成一个for (let i = 0; i < children.length; i++) {const child = children[i]if (isText(child)) {hasText = truefor (let j = i + 1; j < children.length; j++) {const next = children[j]if (isText(next)) {if (!currentContainer) {// 创建复合表达式节点currentContainer = children[i] = {type: 8/* COMPOUND_EXPRESSION */,loc: child.loc,children: [child]}}currentContainer.children.push(`+`, next)children.splice(j, 1)j--}else {currentContainer = undefinedbreak}}}}// 如果是一个带有单个文本子元素的纯元素节点,则什么都不需要转换,因为这种情况在运行时可以直接设置元素的 textContent 来更新文本if (!hasText || (children.length === 1 && (node.type === 0/* ROOT */ || (node.type === 1/* ELEMENT */ &&node.tagType === 0/* ELEMENT*/)))) {return}// 为子文本节点创建一个调用函数表达式的代码生成节点for (let i = 0; i < children.length; i++) {const child = children[i]if (isText(child) || child.type === 8/* COMPOUND_EXPRESSION */) {const callArgs = []// 为 createTextVNode 添加执行参数if (child.type !== 2/* TEXT */ || child.content !== '') {callArgs.push(child)}// 标记动态文本if (!context.ssr && child.type !== 2/* TEXT */) {callArgs.push(`${1/* TEXT */}/* ${PatchFlagNames[1/* TEXT */]} */`)}children[i] = {type: 12/* TEXT_CALL */,content: child,loc: child.loc,codegenNode: createCallExpression(context.helper(CREATETEXT), callArgs)}}}}}
}/*** 创建调用函数表达式的代码生成节点* @description 创建的函数表达式所生成的节点,对应的函数名是 createTextVNode,参数 callArgs 是子节点本身 child,如果是动态插值节点,那么参数还会多一个 TEXT 的 patchFlag*/
function createCallExpression(callee, args = [], loc = locStub) {// 返回一个类型为 JS_CALL_EXPRESSION 的对象,并包含了执行的函数名和参数return {type: 14/* JS_CALL_EXPRESSION */, // 类型loc, // 位置callee, // 函数名arguments: args // 参数}
}
转换 v-if 节点
/*** 转换 v-if 节点* 通过 createStructuralDirectiveTransform 创建的一个结构化指令的转换函数*/
const transformIf = createStructuralDirectiveTransform(/^(if|else|else-if)$/, (node, dir, context) => {return processIf(node, dir, context, (ifNode, branch, isRoot) => {// 返回退出回调函数,当所有子节点转换完成执行return () => {if (isRoot) {// v-if 节点的退出函数// 创建 IF 节点的 codegenNodeifNode.codegenNode = createCodegenNodeForBranch(branch, 0, context)} else {// v-else-if、v-else节点的退出函数// 将此分支的 codegenNode 附加到 上一个条件节点的 codegenNode 的 alternate 中let parentCondition = ifNode.codegenNodewhile (parentCondition.alternate.type === 19/* JS_CONDITIONAL_EXPRESSION */) {parentCondition = parentCondition.alternate}// 更新候选节点parentCondition.alternate = createCodegenNodeForBranch(branch, ifNode.branches.length, 1, context)}}})
})/*** 创建结构指令转换* @param {string} name - 指令名* @param {Function} fn - 构造转换退出函数的方法*/
function createStructuralDirectiveTransform(name, fn) {const matches = isString(name) ? (n) => n === name : (n) => name.test(n)// 返回 transformIf 转换函数return (node, context) => {// 只处理元素节点,因为只有元素节点才会有 v-if 指令if (node.type === 1/* ELEMENT */) {const { props } = node// 结构化指令的转换与插槽无关,插槽相关处理逻辑在 vSlot.ts 中if (node.tagType === 3/* TEMPLATE */ && props.some(isVSlot)) returnconst exitFns = []for (let i = 0; i < props.length; i++) {const prop = props[i]// 删除结构指令以避免无限递归if (prop.type === 7/* DIRECTIVE */ && matches(prop.name)) {props.splice(i, 1)i--// 执行 fn 获取退出函数const onExit = fn(node, prop, context)if (onExit) {exitFns.push(onExit)}}}return exitFns}}
}/*** 处理 if 分支节点*/
function processIf(node, dir, context, processCodegen) {// 处理 if 节点if (dir.name === 'if') {// 创建分支节点const branch = createIfBranch(node, dir)// 创建 IF 节点,替换当前节点const ifNode = {type: 9 /* IF*/,loc: node.loc,branches: [branch]}context.replaceNode(ifNode)if (processCodegen) {return processCodegen(ifNode, branch, true)}}// 处理 v-else-if、v-else 节点else {const siblings = context.parent.childrenlet i = siblings.indexof(node)while (i-- >= -1) {const sibling = siblings[i]if (sibling && siblingtype === 9/* IF */) {// 把节点移动到 IF 节点的 branches 中context.removeNode()const branch = createIfBranch(node, dir)sibling.branches.push(branch)const onExit = processCodegen && processCodegen(sibling, branch, false)// 因为分支已被删除,所以它的子节点需要在这里遍历traverseNode(branch, context)// 执行退出函数if (onExit) {onExit()}// 恢复 currentNode 为 null,因为它已经被移除context.currentNode = null} else {context.onError(createCompilerError(28/* X_V_ELSE_NO_ADJACENT_IF */, node.loc))}break}}
}/*** 创建分支节点*/
function createIfBranch(node, dir) {return {type: 10/* IF_BRANCH */,loc: node.loc,condition: dir.name === 'else' ? undefined : dir.exp,// 如果节点 node 不是 template,那么 children 指向的就是这个单个 node 构造的数组children: node.tagType === 3/* TEMPLATE */ ? node.children : [node]}
}/*** 创建 IF 节点的 codegenNode*/
function createCodegenNodeForBranch(branch, index, context) {if (branch.condition) {// 当分支节点存在 condition 的时候,比如 v-if 和 v-else-if,它通过 createConditionalExpression 返回一个条件表达式节点return createConditionalExpression(branch.condition, createChildrenCodegenNode(branch, index, context), createCallExpression(context.helper(CREATE_COMMENT), [(process.env.NODE_ENV !== 'production') ? '"v-if"' : '""', 'true']))} else {return createChildrenCodegenNode(branch, index, context)}
}/*** 当分支节点存在 condition 时,返回条件表达式节点*/
function createConditionalExpression(test, consequent, alterater, newline = true) {return {type: 19/* JS_CONDIITONAL_EXPRESSION */,test,consequent, // if 分支的子节点对应的代码生成节点alternate,// 后面分支的子节点对应的代码生成节点newline,loc: locStub}
}/*** 判断每个分支子节点是否是 vnodeCall*/
function createChildrenCodegenNode(branch, index, context) {const { helper } = context//根据 index 创建 key 属性const keyProperty = createObjectProperty(`key`, createSimpleExpression(index + '', false))const { children } = branchconst firstChild = children[0]const needFragmentWrapper = children.length !== 1 || firstChild.type !== 1/* ELEMENT */if (needFragmentWrapper) {if (children.length === 1 && firstChild.type === 11/* FOR */) {const vnodeCall = firstChild.codegenNodeinjectProp(vnodeCall, keyProperty, context)return vnodeCall} else {return createVNodeCall(context, helper(FRAGMENT), createObjectExpression([keyProperty]), children, `${64 /* STABLE_FRAGMENT */} /* ${PatchFlagNames[64/* STABLE_FRAGMENT */]}*/`, undefined, undefined, true, false, branch.loc)}} else {const vnodeCall = firstChild.codegenNode// 把 createVNode 改变为 createBlockif (vnodeCall.type === 13/* VNODE_CALL */ && /* 组件节点的 children 会被视为插槽,不需要添加 block */ (firstChildtagType !== 1/* COMPONENT */ || vnodeCall.tag === TELEPORT)) {vnodeCallisBlock = true// 创建 block 的辅助代码helper(OPEN_BLOCK)helper(CREATE_BLOCK)}// 给 branhc 注入 key 属性injectProp(vnodeCall, keyProperty, context)return vnodeCall}
}
静态提升
/*** 静态提升*/
function hoistStatic(root, context) {// 注意:根节点无法实现静态提升walk(root, context, new Map(), isSingleElementRoot(root, root.children[0]));
}/*** 移动*/
function walk(node, context, resultCache, doNotHoistNode = false) {let hasHoistedNode = falselet hasRuntimeConstant = false // 是否包含运行时常量const { children } = nodefor (leti = 0; i < children.length; i++) {const child = children[i]// 普通元素可以静态提升if (child.type === 1/* ELEMENT */ && child.tagType === 0/* ELEMENT */) {let staticType// 获取静态节点的类型,如果是元素,则递归检查它的子节点if (!doNotHoistNode && (staticType = getStaticType(child, resultCache)) > 0) {// 运行时常量,值只有在运行时才能被确定,所以不能静态提升if (staticType === 2/* HAS_RUNTIME_CONSTANT */) {hasRuntimeConstant = true}// 更新 patchFlagchild.codegenNode.patchFlag = -1/* HOISTED */ + ((process.env.NODE_ENV !== 'production') ? `/* HOISTED */` : ``)// 更新节点的 codegenNodechild.codegenNode = context.hoist(child.codegenNode)hasHoistedNode = truecontinue}// 节点可能会包含一些动态子节点,但它的静态属性还是可以被静态提升else {const codegenNode = child.codegenNodeif (codegenNode.type === 13/* VNODE_CALL */) {const flag = getPatchFlag(codegenNode)if ((!flag || flag === 512/* NEED_PATCH */ || flag === 1/* TEXT */) && !hasDynamicKeyOrRef(child) && !hasCachedProps()) {const props = getNodeProps(child)if (props) {codegenNode.props = context.hoist(props)}}}}}// 文本节点也可以静态提升else if (child.type === 12/* TEXT_CALL */) {const staticType = getStaticType(child.content, resultCache)if (staticType > 0) {// 运行时常量,由于它的值只能在运行时才能被确定,所以不能静态提升if (staticType === 2/* HAS_RUNTIME_CONSTANT */) {hasRuntimeConstant = true}child.codegenNode = context.hoist(child.codegenNode)hasHoistedNode = true}}// 递归遍历子节点if (child.type === 1/* ELEMENT */) {walk(child, context, resultCache)} else if (child.type === 11/* FOR */) {walk(child, context, resultCache, child.children.length === 1)} else if (child.type === 9/* IF */) {for (leti = 0; i < child.branches.length; i++) {walk(child.branches[i], context, resultCache, child.branches[i].children.length === 1)}}}// 如果编译配置了 transformHoist,则执行if (!hasRuntimeConstant && hasHoistedNode && context.transformHoist) {context.transformHoist(children, context, node)}
}/*** 提升*/
function hoist(exp) {context.hoists.push(exp)const identifier = createSimpleExpression(`_hoisted_${context.hoists.length}`, false, exp.loc, true)identifier.hoisted = expreturn identifier
}
child.codegenNode = context.hoist(child.codegenNode)
问题:静态提升的好处是针对静态节点不用每次在 render 阶段都执行一次 createVNode 创建 vnode 对象,但它有没有成本呢?为什么?
答:有成本。
- 在初始化时需要判断区分动态节点和静态节点,这会带来一定的性能损耗
- 需要内存去保存静态节点,因此会占用更多的内存
创建根代码生成节点
/*** 创建根代码生成节点*/
function createRootCodegen(root, context) {const { helper } = contextconst { children } = rootconst child = children[0]if (children.length === 1) {// 如果子节点是单个元素节点,则将其转换成一个 blockif (isSingleElementRoot(root, child) && child.codegenNode) {const codegenNode = child.codegenNodeif (codegenNode.type === 13/* VNODE_CALL */) {codegenNode.isBlock = truehelper(OPEN_BLOCK)helper(CREATE_BLOCK)}root.codegenNode = codegenNode} else {root.codegenNode = child;}} else if (children.length > 1) {// 如果子节点是多个节点,则返回一个 fragement 的代码生成节点root.codegenNode = createVNodeCall(context, helper(FRAGMENT), undefined, root.children, `${64 /* STABLE_FRAGMENT */} /* ${PatchFlagNames[64 /* STABLE_FRAGMENT */]} */`, undefined, undefined, true)}
}
生成代码
生成代码流程:
- 创建代码生产上下文
- 生成预设代码
- 生成渲染函数
- 生成资源声明代码
- 生成创建 VNode 树的表达式
/*** 生成代码* @param {Object} ast - 转换后的 AST 树* generate 主要做 5 件事:* 1、创建代码生成上下文* 2、生成预设代码* 3、生成渲染函数* 4、生成资源声明代码* 5、生成创建 VNode 树的表达式*/
function generate(ast, options = {}) {// 1、创建代码生成上下文const context = createCodegenContext(ast, options);const { mode, push, prefixldentifiers, indent, deindent, newline, scopeld, ssr } = context;const hasHelpers = ast.helpers.length > 0;const useWithBlock = !prefixldentifiers && mode !== 'module'const genScopeld = scopeld != null && mode === 'module';// 2、生成预设代码if (mode === 'module') {genModulePreamble(ast, context, genScopeId)} else {genFunctionPreamble(ast, context);}// 3、生成渲染函数if (!ssr) {push(`function render(_ctx, _cache) {`);} else {push('function ssrRender( _ctx, _push,_parent,_attrs) {');}indent();if (useWithBlock) {// 处理带 with 的情况,Web 端运行时编译push(`with (_ctx) {`);indent();if (hasHelpers) {push(`const { ${ast.helpers.map(s => `${helperNameMap[s]}:_${helperNameMap[s]}`).join(',')}} = _Vue`);push(`\n`);newline();}}// 4、生成资源声明代码// 生成自定义组件声明代码if (ast.components.length) {genAssets(ast.components, 'component', context);if (ast.directives.length || ast.temps > 0) {newline();}}// 生成自定义指令声明代码if (ast.directives.length) {genAssets(ast.directives, 'directive', context);if (ast.temps > 0) {newline();}}// 生成临时变量代码if (ast.temps > 0) {push('let');for (let i = 0; i < ast.temps; i++) {push('Si>0?,:]_tempsli');}}// 如果生成了资源的声明代码,则在尾部添加一个换行符,然后再生成一个空行if (ast.components.length || ast.directives.length || ast.temps) {push(`\n`);newline();}// 如果不是 ssr,则再添加一个 return 字符串if (!ssr) {push(`return`);}// 5、生成创建 VNode 树的表达式if (ast.codegenNode) {genNode(ast.codegenNode, context)} else {push(`null`);}if (useWithBlock) {deindent();push(`}`);}deindent();push(`}`);return {ast,code: context.code,map: context.map ? context.map.toJSON() : undefined}
}
创建代码生成上下文
/*** 创建代码生成上下文*/
function createCodegenContext(ast, { mode = 'function', prefixldentifiers = mode === 'module', sourceMap = false, filename = `template.vue.html`, scopeld = null, optimizeBindings = false, runtimeGlobalName = `Vue`, runtimeModuleName = `vue`, ssr = false }) {// 上下文对象 context 维护了 generate 过程的一些配置、状态数据、辅助函数const context = {mode,prefixIdentifiers,sourceMap,filename,scopeId,optimizeBindings,runtimeGlobalName,runtimeModuleName,ssr,source: ast.loc.source,code: ``,column: 1,line: 1,offset: 0,indentLevel: 0,pure: false,map: undefined,helper(key) {return `${SfhelperNameMap[key]}`},// context.code 后追加 code 来更新它的值push(code) {context.code += code},// 增加代码的缩进,让上下文维护的代码缩进 context.indentLevel 加 1indent() {// 执行 newline 方法,添加一个换行符,以及两倍 indentLevel 对应的空格来表示缩进的长度newline(++context.indentLevel)},// 减少代码的缩进,让上下文维护的代码缩进 context.indentLevel 减 1deindent(withoutNewLine = false) {if (withoutNewLine) {--context.indentLevel} else {// 执行 newline 方法去添加一个换行符,并减少两倍 indentLevel 对应的空格的缩进长度newline(--context.indentLevel)}},// 添加换行符newline() {newline(context.indentLevel)}}function newline(n) {context.push('n' + ``.repeat(n))}return context
}
生成预设代码
/*** 生成 module 模式的预设代码*/
function genModulePreamble(ast, context, genScopeId) {const { push, newline, optimizeBindings, runtimeModuleName }= context// 处理 scopeId...// 生成 import 声明代码// ast.helpers 是在 transform 阶段通过 context.helper 添加的if (ast.helpers.length) {if (optimizeBindings) {push(`import { ${ast.helpers.map(s => helperNameMap[s]).join(',')}}} from ${JSON.stringif(runtimeModule)} \n`)push(`\n// Binding optimization for webpack code-split \nconst ${ast.helpers.map(s => `${helperNameMap[s]}= ${helperNameMap[s]}`).join(',')}\n`)} else {push(`import { ${ast.helpers.map(s => `${helperNameMap[s]}as_${helperNameMap[s]}`).join(',')}} from ${JSON.stringify(runtimeModuleName)} \n`)}}// 处理 ssrHelpers...// 处理imports...// 处理scopeId...genHoists(ast.hoists, context)newline()push(`export `)
}/*** 生成静态提升的相关代码*/
function genHoists(hoists, context) {if (!hoists.length) returncontext.pure = trueconst [push, newline] = context// 调用 newline 生成空行newline()// 遍历 hoists 数组生成静态变量提升的方法hoists.forEach((exp, i) => {if (exp) {push(`const_hoisted_${i + 1}=`)genNode(exp, context)newline()}})context.pure = false
}
生成渲染函数
// 无额外定义的函数
生成资源声明代码
/*** 生成自定义组件/指令声明代码*/
function genAssets(assets, type, { helper, push, newline }) {// 获取自定义组件/指令const resolver = helper(type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE)// 遍历 assets 数组,生成自定义组件声明代码for (let i = 0; i < assets.length; i++) {const id = assetslilpush(`const ${toValidAssetId(id, type)}=${resolver}(${JSON.stringify(id)})`)if (i < assets.length - 1) {newline()}}
}/*** 包装变量*/
function toValidAssetId(name, type) {return `_${type}_${name.replace(/[^\w]/g, '_')}`;
}
生成创建 VNode 树的表达式
/*** 生成创建 VNode 树的表达式*/
function genNode(node, context) {if (shared.isString(node)) {context.push(node)return}if (sharedisSymbol(node)) {context.push(context.helper(node))return}// 根据不同类型生成不同代码switch (node.type) {case 1/* ELEMENT*/:case 9 /*IF*/:case 11/* FOR */:genNode(node.codegenNode, context)breakcase 2/* TEXT */:genText(node, context)breakcase 4/* SIMPLE_EXPRESSION */:genExpression(node, context)breakcase 5/* INTERPOLATION */:genInterpolation(node, context)breakcase 12/* TEXT_CALL */:genNode(node.codegenNode, context)breakcase 8/* COMPOUND EXPRESSION */:genCompoundExpression(node, context)breakcase 3/* COMMENT */:breakcase 13/* VNODE_CALL */:genVNodeCall(node, context)breakcase 14/* JS_CALL_EXPRESSION */:genCallExpression(node, context)breakcase 15/* JS_OBJECT_EXPRESSION */:genObjectExpression(node, context)breakcase 17/* JS_ARRAY_EXPRESSION */:genArrayExpression(node, context)breakcase 18/* JS_FUNCTION_EXPRESSION */:genFunctionExpression(node, context)breakcase 19/* JS CONDITIONAL_EXPRESSION */:genConditionalExpression(node, context)breakcase 20/* JS_CACHE_EXPRESSION */:genCacheExpression(node, context)break// SSR only typescase 21/* JS_BLOCK_STATEMENT */:genNodeList(node.body, context, true, false)breakcase 22/* JS_TEMPLATE_LITERAL */:genTemplateLiteral(node, context)breakcase 23/* JS_IF_STATEMENT */:genIfStatement(node, context)breakcase 24/* JS_ASSIGNMENT_EXPRESSION */:genAssignmentExpression(node, context)breakcase 25/* JS_SEQUENCE_EXPRESSION */:genSequenceExpression(node, context)breakcase 26/* JS_RETURN_STATEMENT */:genReturnStatement(node, context)break}
}/*** 生成创建 VNode 节点的表达式代码*/
function genVNodeCall(node, context) {const { push, helper, pure } = contextconst { tag, props, children, patchFlag, dynamicProps, directives, isBlock, disableTracking } = nodeif (directives) {push(helper(WITH_DIRECTIVES) + `(`)}/*isBlock 为 true:生成创建 Block 相关代码isBlock 为 false,生成创建 VNode 的相关代码*/if (isBlock) {push(`${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}),`)}// 是否生成注释代码if (pure) {push(PURE_ANNOTATION)}push(helper(isBlock ? CREATE_BLOCK : CREATE_VNODE) + `(`, node)genNodeList(genNullableArgs([tag, props, children, patchFlag, dynamicProps]), context)push(`)`)if (isBlock) {push(`)`)}if (directives) {push(`,`)genNode(directives, context)push(`)`)}
}/*** 从后往前遍历,直到找到第一个不为 null 的索引,并返回前面(包含)的所有数组*/
function genNullableArgs(args) {let i = args.lengthwhile (i--) {if (args[i] != null) break}return args.slice(0, i + 1).map(arg => arg || `null`)
}/*** 生成参数相关代码*/
function genNodeList(nodes, context, multilines = false, comma = true) {const { push, newline } = context// 遍历 nodes,并判断其类型for (let i = 0; i < nodes.length; i++) {const node = nodes[i]// 字符转,则直接添加到代码中if (shared.isString(node)) {push(node)}// 数组,则生成数组形式代码else if (shared.isArray(node)) {genNodeListAsArray(node, context)}// 对象,则递归执行生成节点代码else {genNode(node, context)}if (i < nodes.length - 1) {if (multilines) {comma && push(',')newline()} else {comma && push(',')}}}
}/*** 生成 nodes 第二个元素 - props 参数*/
function genExpression(node, context) {const { content, isStatic } = node// 往代码中添加 context 内容context.push(isStatic ? JSON.stringify(content) : content, node)
}/*** 生成 nodes 第二个元素 - children 子节点数组*/
function genNodeListAsArray(nodes, context) {const multilines = nodes.length > 3 || nodes.some(n => isArray(n) || !isText$1(n))context.push(`[`)multilines && context.indent()// 递归调用genNodeList(nodes, context, multilines)multilines && context.deindent()context.push(`]`)
}/*** 生成条件表达式代码*/
function genConditionalExpression(node, context) {const { test, consequent, alternate, newline: needNewline } = nodeconst { push, indent, deindent, newline } = context// 生成条件表达式if (test.type === 4/* SIMPLE EXPRESSION*/) {const needsParens = !isSimpleldentifier(test.content)needsParens && push(`(`)genExpression(test, context)needsParens && push(`)`)} else {push(`(`)genNode(test, context)push(`)`)}// 换行加缩进needNewline && indent()context.indentLevel++needNewline || push(` `)// 生成主逻辑代码push(`?`)genNode(consequent, context)context.indentLevel--needNewline && newline()needNewline || push(` `)
}
Vue3 优化
/*** 运行时优化:Vue3 采用 Block 的概念来优化性能,即只更新渲染动态节点*/
const blockStack = []
let currentBlock = null/*** 收集动态的 vnode 节点,在 patch 阶段只对比这些动态 vnode 节点*/
function openBlock(disableTracking = false) {blockStack.push((currentBlock = disableTracking ? null : []));
}/*** 创建 vnode 节点*/
function createVNode(type, props = null, children = null) {// 处理 props 相关逻辑,标准化 class 和 style// ...// 对vnode 类型信息编码// ...// 创建 vnode 对象// ...// 标准化子节点,把不同数据类型的 children 转成数组或者文本类型// ...// 添加动态 vnode 节点到 currentBlock 中if (shouldTrack > 0 && !isBlockNode && currentBlock && patchFlag !== 32/* HYDRATE EVENTS */ && (patchFlag > 0 || shapeFlag & 128/* SUSPENSE */ || shapeFlag & 64/* TELEPORT */ || shapeFlag & 4/* STATEFUL_COMPONENT */ || shapeFlag & 2/* FUNCTION_COMPONENT */)) {currentBlock.push(vnode)}return vnode
}/*** 创建 Block*/
function createBlock(type, props, children, patchFlag, dynamicProps) {// 创建 vnodeconst vnode = createVNode(type, props, children, patchFlag, dynamicProps, true/* isBlock:阻止这个 block 收集自身 */)// 在 vnode 上保留当前 Block 收集的动态子节点vnode.dynamicChildren = currentBlock || EMPTY_ARRblockStack.pop()// 当前 Block 恢复到父 BlockcurrentBlock = blockStack[blockStack.length - 1] || null// 节点本身作为父 Block 收集的子节点if (currentBlock) {currentBlock.push(vnode)}return vnode
}/*** 重新渲染元素*/
const patchElement = (nl, n2, parentComponent, parentSuspense, isSVG, optimized) => {const el = (n2.el = n1.el)const oldProps = (n1 && n1.props) || EMPTY_OBJconst newProps = n2.props || EMPTY_OBJ// 更新 propspatchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)const areChildrenSVG = isSVG && n2.type !== 'foreignObject'// 更新子节点// 如果是 blockVNode,则只需要比较动态子节点就行if (n2.dynamicChildren) {patchBlockChildren(n1.dynamicChildren, n2.dynamicChildren, currentContainer, parentComponent, parentSuspense, isSVG);} else if (!optimized) {patchChildren(n1, n2, currentContainer, currentAnchor, parentComponent, parentSuspense, isSVG)}
}/*** 比较动态子节点*/
const patchBlockChildren = (oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG) => {for (leti = 0; i < newChildren.length; i++) {const oldVNode = oldChildren[i]const newVNode = newChildren[i]// 确定待更新节点的容器const container =// 对于 Fragment,我们需要提供正确的父容器oldVNode.type === Fragment ||// 在不同节点的情况下,将有一个替换节点,我们也需要正确的父容器!isSameVNodeType(oldVNode, newVNode) ||// 组件的情况,我们也需要提供一个父容器oldVNode.shapeFlag & 6/* COMPONENT */? hostParentNode(oldVNode.el) :// 在其他情况下,父容器实际上并没有被使用,所以这里只传递 Block 元素即可fallbackContainerpatch(oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, true)}
}