共计 52864 个字符,预计需要花费 133 分钟才能阅读完成。
在最开始的章节提到过,咱们在应用 vue-cli
创立我的项目的时候,提供了两个版本供咱们应用,Runtime Only
版本和 Runtime + Compiler
版本。Runtime Only
版本是不蕴含编译器的,在我的项目打包的时候会把模板编译成 render
函数,也叫预编译。Runtime + Compiler
版本蕴含编译器,能够把编译过程放在运行时做。
入口
这一块代码量比拟多,次要是对各种状况做了一些边界解决。这里只关注主流程。对细节感兴趣的搭档们能够自行去钻研。个别咱们应用 Runtime + Compiler
版本可能比拟多一些,先来找到入口:
// src/platforms/web/entry-runtime-with-compiler.js | |
Vue.prototype.$mount = function ( | |
el?: string | Element, | |
hydrating?: boolean | |
): Component { | |
// ... | |
if (!options.render) { | |
// 模版就绪,进入编译阶段 | |
if (template) { | |
// 编译模版,失去 动静渲染函数和动态渲染函数 | |
const {render, staticRenderFns} = compileToFunctions(template, { | |
// 在非生产环境下,编译时记录标签属性在模版字符串中开始和完结的地位索引 | |
outputSourceRange: process.env.NODE_ENV !== 'production', | |
shouldDecodeNewlines, | |
shouldDecodeNewlinesForHref, | |
// 界定符,默认 {{}} | |
delimiters: options.delimiters, | |
// 是否保留正文 | |
comments: options.comments | |
}, this) | |
} | |
} | |
} |
compileToFunctions
办法就是把 template
编译而失去 render
以及 staticRenderFns
。
compileToFunctions
// src/platforms/web/compiler/index.js | |
import {baseOptions} from './options' | |
import {createCompiler} from 'compiler/index' | |
const {compile, compileToFunctions} = createCompiler(baseOptions) | |
export {compile, compileToFunctions} |
createCompiler
// src/compiler/index.js | |
export const createCompiler = createCompilerCreator(function baseCompile ( | |
template: string, | |
options: CompilerOptions | |
): CompiledResult { | |
// 将模版解析为 AST | |
const ast = parse(template.trim(), options) | |
// 优化 AST,动态标记 | |
if (options.optimize !== false) {optimize(ast, options) | |
} | |
// 生成渲染函数,,将 ast 转换成可执行的 render 函数的字符串模式 | |
// code = {// render: `with(this){return ${_c(tag, data, children, normalizationType)}}`, | |
// staticRenderFns: [_c(tag, data, children, normalizationType), ...] | |
// } | |
const code = generate(ast, options) | |
return { | |
ast, | |
render: code.render, | |
staticRenderFns: code.staticRenderFns | |
} | |
}) |
createCompiler
是通过调用 createCompilerCreator
返回的,这里传入了一个 baseCompile
函数作为参数,这个函数是重点,编译的外围过程就是在这个函数中执行的。
createCompilerCreator
// src/compiler/create-compiler.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 = [] | |
// 日志,负责记录将 error 和 tip | |
let warn = (msg, range, tip) => {(tip ? tips : errors).push(msg) | |
} | |
if (options) {if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { | |
// $flow-disable-line | |
const leadingSpaceLength = template.match(/^\s*/)[0].length | |
warn = (msg, range, tip) => {const data: WarningMessage = { msg} | |
if (range) {if (range.start != null) {data.start = range.start + leadingSpaceLength} | |
if (range.end != null) {data.end = range.end + leadingSpaceLength} | |
} | |
(tip ? tips : errors).push(data) | |
} | |
} | |
// 合并配置项 options 到 finalOptions | |
// 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 || null), | |
options.directives | |
) | |
} | |
// copy other options | |
for (const key in options) {if (key !== 'modules' && key !== 'directives') {finalOptions[key] = options[key] | |
} | |
} | |
} | |
finalOptions.warn = warn | |
// 外围编译函数,传递模版字符串和最终的配置项,失去编译后果 | |
const compiled = baseCompile(template.trim(), finalOptions) | |
if (process.env.NODE_ENV !== 'production') {detectErrors(compiled.ast, warn) | |
} | |
// 将编译期间产生的谬误和提醒挂载到编译后果上 | |
compiled.errors = errors | |
compiled.tips = tips | |
return compiled | |
} | |
return { | |
compile, | |
compileToFunctions: createCompileToFunctionFn(compile) | |
} | |
} | |
} |
createCompilerCreator
返回了一个 createCompiler
函数,createCompiler
返回了一个对象,包含了 compile
和 compileToFunctions
,这个 compileToFunctions
对应的就是 $mount
中调用的 compileToFunctions
办法。在 createCompiler
函数内定义了 compile
办法,并把它传递给 createCompileToFunctionFn
,compile
次要目标就是对特有平台的配置项做一些合并,如 web 平台和解决一些在编译期间产生的谬误。
createCompileToFunctionFn
// src/compiler/to-function.js | |
export function createCompileToFunctionFn (compile: Function): Function {const cache = Object.create(null) | |
return function compileToFunctions ( | |
template: string, | |
options?: CompilerOptions, | |
vm?: Component | |
): CompiledFunctionResult { | |
// 传递进来的编译选项 | |
options = extend({}, options) | |
const warn = options.warn || baseWarn | |
delete options.warn | |
/* istanbul ignore if */ | |
if (process.env.NODE_ENV !== 'production') { | |
// detect possible CSP restriction | |
// 检测可能的 CSP 限度 | |
try {new Function('return 1') | |
} catch (e) {if (e.toString().match(/unsafe-eval|CSP/)) { | |
warn( | |
'It seems you are using the standalone build of Vue.js in an' + | |
'environment with Content Security Policy that prohibits unsafe-eval.' + | |
'The template compiler cannot work in this environment. Consider' + | |
'relaxing the policy to allow unsafe-eval or pre-compiling your' + | |
'templates into render functions.' | |
) | |
} | |
} | |
} | |
// check cache | |
// 如果有缓存,则跳过编译,间接从缓存中获取上次编译的后果 | |
const key = options.delimiters | |
? String(options.delimiters) + template | |
: template | |
if (cache[key]) {return cache[key] | |
} | |
// compile | |
// 执行编译函数,失去编译后果 | |
const compiled = compile(template, options) | |
// check compilation errors/tips | |
// 查看编译期间产生的 errors/tips,别离输入到控制台 | |
if (process.env.NODE_ENV !== 'production') {if (compiled.errors && compiled.errors.length) {if (options.outputSourceRange) { | |
compiled.errors.forEach(e => { | |
warn(`Error compiling template:\n\n${e.msg}\n\n` + | |
generateCodeFrame(template, e.start, e.end), | |
vm | |
) | |
}) | |
} else { | |
warn(`Error compiling template:\n\n${template}\n\n` + | |
compiled.errors.map(e => `- ${e}`).join('\n') + '\n', | |
vm | |
) | |
} | |
} | |
if (compiled.tips && compiled.tips.length) {if (options.outputSourceRange) {compiled.tips.forEach(e => tip(e.msg, vm)) | |
} else {compiled.tips.forEach(msg => tip(msg, vm)) | |
} | |
} | |
} | |
// turn code into functions | |
// 转换编译失去的字符串代码为函数,通过 new Function(code) 实现 | |
const res = {} | |
const fnGenErrors = [] | |
res.render = createFunction(compiled.render, fnGenErrors) | |
res.staticRenderFns = compiled.staticRenderFns.map(code => {return createFunction(code, fnGenErrors) | |
}) | |
// check function generation errors. | |
// this should only happen if there is a bug in the compiler itself. | |
// mostly for codegen development use | |
/* istanbul ignore if */ | |
// 解决下面代码转换过程中呈现的谬误 | |
if (process.env.NODE_ENV !== 'production') {if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) { | |
warn( | |
`Failed to generate render function:\n\n` + | |
fnGenErrors.map(({err, code}) => `${err.toString()} in\n\n${code}\n`).join('\n'), | |
vm | |
) | |
} | |
} | |
// 缓存编译后果 | |
return (cache[key] = res) | |
} | |
} |
createCompileToFunctionFn
返回了 compileToFunctions
这个就是咱们要找的最终定义所在了,它次要做了这么几件事:
- 执行编译函数失去编译后果。
- 将编译失去的字符串代码转换成可执行的函数。
- 解决异样
- 缓存
小结
通过以上的代码能够看出真正编译个过程就在 createCompilerCreator
函数传递的 baseCompile
中,次要分为这么几个局部:
-
将模板解析成 AST
const ast = parse(template.trim(), options)
-
优化 AST (动态标记)
optimize(ast, options)
-
生成代码字符串
const code = generate(ast, options)
之所以在真正编译之前做了这么多前戏,目标就是为了对不同平台做一些解决。上面次要针对这三个局部看看做了一些什么事件。
parse
parse
次要作用就是将模板解析成 AST,它是一种形象语法树。这个过程比较复杂,它会在每个节点的 AST 对象上设置元素的所有信息,比方:父节点、子节点、标签信息、属性信息、插槽信息等等。在期间会用到许多正则表达式对模板进行匹配。
parse
// src/compiler/parser/index.js | |
export function parse ( | |
template: string, | |
options: CompilerOptions | |
): ASTElement | void { | |
warn = options.warn || baseWarn | |
// 是否为 pre 标签 | |
platformIsPreTag = options.isPreTag || no | |
// 必须应用 props 进行绑定的属性 | |
platformMustUseProp = options.mustUseProp || no | |
// 获取标签的命名空间 | |
platformGetTagNamespace = options.getTagNamespace || no | |
// 是否是保留标签(html + svg) | |
const isReservedTag = options.isReservedTag || no | |
// 是否为一个组件 | |
maybeComponent = (el: ASTElement) => !!( | |
el.component || | |
el.attrsMap[':is'] || | |
el.attrsMap['v-bind:is'] || | |
!(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag)) | |
) | |
// 别离获取 options.modules 下的 class、model、style 三个模块中的 transformNode、preTransformNode、postTransformNode 办法 | |
// 负责解决元素节点上的 class、style、v-model | |
transforms = pluckModuleFunction(options.modules, 'transformNode') | |
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode') | |
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') | |
// 界定符,比方: {{}} | |
delimiters = options.delimiters | |
const stack = [] | |
// 空格选项 | |
const preserveWhitespace = options.preserveWhitespace !== false | |
const whitespaceOption = options.whitespace | |
// 根节点,以 root 为根,解决后的节点都会依照层级挂载到 root 下,最初 return 的就是 root,一个 ast 语法树 | |
let root | |
// 以后元素的父元素 | |
let currentParent | |
let inVPre = false | |
let inPre = false | |
let warned = false | |
function warnOnce (msg, range) {if (!warned) { | |
warned = true | |
warn(msg, range) | |
} | |
} | |
// 因为代码比拟长,前面几个办法独自拿进去。function closeElement (element) {/*...*/} | |
function trimEndingWhitespace (el) {/*...*/} | |
function checkRootConstraints (el) {/*...*/} | |
parseHTML(template, {/*...*/}) | |
return root | |
} |
parse
接管两个参数 template
和 options
,也就是模板字符串和配置选项,options
定义在 /src/platforms/web/compiler/options
中,这里次要是不同的平台(web 和 weex)的配置选项不同。
closeElement
// src/compiler/parser/index.js | |
function closeElement (element) { | |
// 移除节点开端的空格 | |
trimEndingWhitespace(element) | |
// 以后元素不再 pre 节点内,并且也没有被解决过 | |
if (!inVPre && !element.processed) { | |
// 别离解决元素节点的 key、ref、插槽、自闭合的 slot 标签、动静组件、class、style、v-bind、v-on、其它指令和一些原生属性 | |
element = processElement(element, options) | |
} | |
// 解决根节点上存在 v-if、v-else-if、v-else 指令的状况 | |
// 如果根节点存在 v-if 指令,则必须还提供一个具备 v-else-if 或者 v-else 的同级别节点,避免根元素不存在 | |
// tree management | |
if (!stack.length && element !== root) { | |
// allow root elements with v-if, v-else-if and v-else | |
if (root.if && (element.elseif || element.else)) {if (process.env.NODE_ENV !== 'production') {checkRootConstraints(element) | |
} | |
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.`, | |
{start: element.start} | |
) | |
} | |
} | |
// 建设父子关系,将本人放到父元素的 children 数组中,而后设置本人的 parent 属性为 currentParent | |
if (currentParent && !element.forbidden) {if (element.elseif || element.else) {processIfConditions(element, currentParent) | |
} else {if (element.slotScope) { | |
// scoped slot | |
// keep it in the children list so that v-else(-if) conditions can | |
// find it as the prev node. | |
const name = element.slotTarget || '"default"' | |
; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element | |
} | |
currentParent.children.push(element) | |
element.parent = currentParent | |
} | |
} | |
// 设置子元素,将本人的所有非插槽的子元素设置到 element.children 数组中 | |
// final children cleanup | |
// filter out scoped slots | |
element.children = element.children.filter(c => !(c: any).slotScope) | |
// remove trailing whitespace node again | |
trimEndingWhitespace(element) | |
// check pre state | |
if (element.pre) {inVPre = false} | |
if (platformIsPreTag(element.tag)) {inPre = false} | |
// 别离为 element 执行 model、class、style 三个模块的 postTransform 办法 | |
// 然而 web 平台没有提供该办法 | |
// apply post-transforms | |
for (let i = 0; i < postTransforms.length; i++) {postTransforms[i](element, options) | |
} | |
} |
closeElement
办法次要做了三件事:
- 如果元素没有被解决,调用
processElement
解决元素上的一些属性。 - 设置以后元素的父元素。
- 设置以后元素的子元素。
trimEndingWhitespace
// src/compiler/parser/index.js | |
function trimEndingWhitespace (el) { | |
// remove trailing whitespace node | |
if (!inPre) { | |
let lastNode | |
while ((lastNode = el.children[el.children.length - 1]) && | |
lastNode.type === 3 && | |
lastNode.text === ' ' | |
) {el.children.pop() | |
} | |
} | |
} |
trimEndingWhitespace
作用就是删除元素中空白的文本节点。
checkRootConstraints
// src/compiler/parser/index.js | |
function checkRootConstraints (el) {if (el.tag === 'slot' || el.tag === 'template') { | |
warnOnce(`Cannot use <${el.tag}> as component root element because it may ` + | |
'contain multiple nodes.', | |
{start: el.start} | |
) | |
} | |
if (el.attrsMap.hasOwnProperty('v-for')) { | |
warnOnce( | |
'Cannot use v-for on stateful component root element because' + | |
'it renders multiple elements.', | |
el.rawAttrsMap['v-for'] | |
) | |
} | |
} |
checkRootConstraints
的作用是对根元素的查看,不能应用 slot
和 template
作为根元素,不能在有状态组件的根元素上应用 v-for
,因为它会渲染出多个元素。
parseHTML
// src/compiler/parser/index.js | |
parseHTML(template, { | |
warn, | |
expectHTML: options.expectHTML, | |
isUnaryTag: options.isUnaryTag, | |
canBeLeftOpenTag: options.canBeLeftOpenTag, | |
shouldDecodeNewlines: options.shouldDecodeNewlines, | |
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref, | |
shouldKeepComment: options.comments, | |
outputSourceRange: options.outputSourceRange, | |
/** | |
* 开始解决标签 | |
* @param {*} tag 标签名 | |
* @param {*} attrs [{name: attrName, value: attrVal, start, end}, ...] 模式的属性数组 | |
* @param {*} unary 自闭合标签 | |
* @param {*} start 标签在 html 字符串中的开始索引 | |
* @param {*} end 标签在 html 字符串中的完结索引 | |
*/ | |
start (tag, attrs, unary, start, end) { | |
// 如果存在命名空间,,则继承父命名空间 | |
// 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 (process.env.NODE_ENV !== 'production') {if (options.outputSourceRange) { | |
element.start = start | |
element.end = end | |
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {cumulated[attr.name] = attr | |
return cumulated | |
}, {}) | |
} | |
attrs.forEach(attr => {if (invalidAttributeRE.test(attr.name)) { | |
warn( | |
`Invalid dynamic argument expression: attribute names cannot contain ` + | |
`spaces, quotes, <, >, / or =.`, | |
{start: attr.start + attr.name.indexOf(`[`), | |
end: attr.start + attr.name.length | |
} | |
) | |
} | |
}) | |
} | |
// 非服务端渲染的状况下,模版中不应该呈现 <style></style>、<script></script> 标签 | |
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.', | |
{start: element.start} | |
) | |
} | |
// 为 element 对象别离执行 class、style、model 模块中的 preTransforms 办法 | |
// 解决存在 v-model 指令的 input 标签,别离解决 input 为 checkbox、radio、其它的状况 | |
// apply pre-transforms | |
for (let i = 0; i < preTransforms.length; i++) {element = preTransforms[i](element, options) || element | |
} | |
if (!inVPre) { | |
// 是否存在 v-pre 指令,存在则设置 element.pre = true | |
processPre(element) | |
if (element.pre) { | |
// 存在 v-pre 指令,则设置 inVPre 为 true | |
inVPre = true | |
} | |
} | |
// 如果 pre 标签,则设置 inPre 为 true | |
if (platformIsPreTag(element.tag)) {inPre = true} | |
if (inVPre) { | |
// 存在 v-pre 指令,这样的节点只会渲染一次,将节点上的属性都设置到 el.attrs 数组对象中,作为动态属性,数据更新时不会渲染这部分内容 | |
processRawAttrs(element) | |
} else if (!element.processed) { | |
// structural directives | |
// 解决 v-for 属性 | |
processFor(element) | |
// 解决 v-if、v-else-if、v-else | |
processIf(element) | |
// 解决 v-once 指令 | |
processOnce(element) | |
} | |
// root 不存在,以后解决的元素为第一个元素,即组件的 根 元素 | |
if (!root) { | |
root = element | |
if (process.env.NODE_ENV !== 'production') { | |
// 查看根元素,不能应用 slot、template、v-for | |
checkRootConstraints(root) | |
} | |
} | |
if (!unary) { | |
// 非自闭合标签,通过 currentParent 记录以后元素,下一个元素在解决的时候,就晓得本人的父元素是谁 | |
currentParent = element | |
// 而后将 element push 到 stack 数组,未来解决到以后元素的闭合标签时用 | |
stack.push(element) | |
} else { | |
// 阐明以后元素为自闭合标签 | |
closeElement(element) | |
} | |
}, | |
/** | |
* 解决完结标签 | |
* @param {*} tag 完结标签的名称 | |
* @param {*} start 完结标签的开始索引 | |
* @param {*} end 完结标签的完结索引 | |
*/ | |
end (tag, start, end) { | |
// 完结标签对应的开始标签的 AST 对象 | |
const element = stack[stack.length - 1] | |
// pop stack | |
stack.length -= 1 | |
currentParent = stack[stack.length - 1] | |
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {element.end = end} | |
// 设置父子关系 | |
closeElement(element) | |
}, | |
// 解决文本,基于文本生成 ast 对象,将该 AST 放到它的父元素的 children 中。chars (text: string, start: number, end: number) { | |
// 异样解决 | |
if (!currentParent) {if (process.env.NODE_ENV !== 'production') {if (text === template) { | |
warnOnce( | |
'Component template requires a root element, rather than just text.', | |
{start} | |
) | |
} else if ((text = text.trim())) { | |
warnOnce(`text "${text}" outside root element will be ignored.`, | |
{start} | |
) | |
} | |
} | |
return | |
} | |
// IE textarea placeholder bug | |
/* istanbul ignore if */ | |
if (isIE && | |
currentParent.tag === 'textarea' && | |
currentParent.attrsMap.placeholder === text | |
) {return} | |
const children = currentParent.children | |
if (inPre || text.trim()) {// 文本在 pre 标签内 或者 text.trim() 不为空 | |
text = isTextTag(currentParent) ? text : decodeHTMLCached(text) | |
} else if (!children.length) {// 阐明文本不在 pre 标签内而且 text.trim() 为空,而且以后父元素也没有子节点,则将 text 置为空 | |
// remove the whitespace-only node right after an opening tag | |
text = '' | |
} else if (whitespaceOption) { | |
// 压缩解决 | |
if (whitespaceOption === 'condense') { | |
// in condense mode, remove the whitespace node if it contains | |
// line break, otherwise condense to a single space | |
text = lineBreakRE.test(text) ? '':' ' | |
} else {text = ' '} | |
} else {text = preserveWhitespace ? '' :''} | |
// 如果通过解决后 text 还存在 | |
if (text) {if (!inPre && whitespaceOption === 'condense') { | |
// 不在 pre 节点中,并且配置选项中存在压缩选项,则将多个间断空格压缩为单个 | |
// condense consecutive whitespaces into single space | |
text = text.replace(whitespaceRE, ' ') | |
} | |
let res | |
// 基于 text 生成 AST 对象 | |
let child: ?ASTNode | |
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { | |
// 文本中存在表达式(即有界定符(占位符))child = { | |
type: 2, | |
expression: res.expression, | |
tokens: res.tokens, | |
text | |
} | |
} else if (text !== '' || !children.length || children[children.length - 1].text !==' ') { | |
// 纯文本节点 | |
child = { | |
type: 3, | |
text | |
} | |
} | |
// push 到父元素的 children 中 | |
if (child) {if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { | |
child.start = start | |
child.end = end | |
} | |
children.push(child) | |
} | |
} | |
}, | |
// 解决正文节点 | |
comment (text: string, start, end) { | |
// adding anything as a sibling to the root node is forbidden | |
// comments should still be allowed, but ignored | |
// 禁止将任何内容作为 root 的节点的同级进行增加,正文应该被容许,然而会被疏忽 | |
// 如果 currentParent 不存在,阐明正文和 root 为同级,疏忽 | |
if (currentParent) { | |
// 正文节点的 AST | |
const child: ASTText = { | |
type: 3, | |
text, | |
isComment: true | |
} | |
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { | |
// 记录节点的开始索引和完结索引 | |
child.start = start | |
child.end = end | |
} | |
// push 到父元素的 children 中 | |
currentParent.children.push(child) | |
} | |
} | |
}) | |
return root | |
} |
对模板的解析次要是通过 parseHTML
函数,他定义在 src/compiler/parser/html-parser
中:
// src/compiler/parser/html-parser | |
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ | |
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ | |
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*` | |
const qnameCapture = `((?:${ncname}\\:)?${ncname})` | |
const startTagOpen = new RegExp(`^<${qnameCapture}`) | |
const startTagClose = /^\s*(\/?)>/ | |
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) | |
const doctype = /^<!DOCTYPE [^>]+>/i | |
// #7298: escape - to avoid being passed as HTML comment when inlined in page | |
const comment = /^<!\--/ | |
const conditionalComment = /^<!\[/ | |
// Special Elements (can contain anything) | |
export const isPlainTextElement = makeMap('script,style,textarea', true) | |
const reCache = {} | |
const decodingMap = { | |
'<': '<', | |
'>': '>', | |
'"': '"','&':'&',' ':'\n','	':'\t',''':"'" | |
} | |
const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g | |
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g | |
// #5992 | |
const isIgnoreNewlineTag = makeMap('pre,textarea', true) | |
const shouldIgnoreFirstNewline = (tag, html) => tag && isIgnoreNewlineTag(tag) && html[0] === '\n' | |
function decodeAttr (value, shouldDecodeNewlines) { | |
const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr | |
return value.replace(re, match => decodingMap[match]) | |
} | |
/** | |
* 通过循环遍历 html 模版字符串,顺次解决其中的各个标签,以及标签上的属性 | |
* @param {*} html html 模版 | |
* @param {*} options 配置项 | |
*/ | |
export function parseHTML (html, options) {const stack = [] | |
const expectHTML = options.expectHTML | |
// 是否是自闭合标签 | |
const isUnaryTag = options.isUnaryTag || no | |
// 是否能够只有开始标签 | |
const canBeLeftOpenTag = options.canBeLeftOpenTag || no | |
// 记录以后在原始 html 字符串中的开始地位 | |
let index = 0 | |
let last, lastTag | |
while (html) { | |
last = html | |
// Make sure we're not in a plaintext content element like script/style | |
// 确保不是在 script、style、textarea 这样的纯文本元素中 | |
if (!lastTag || !isPlainTextElement(lastTag)) { | |
// 找第一个 < 字符 | |
let textEnd = html.indexOf('<') | |
// textEnd === 0 阐明在结尾找到了 | |
// 别离解决可能找到的正文标签、条件正文标签、Doctype、开始标签、完结标签 | |
// 每解决完一种状况,就会截断(continue)循环,并且重置 html 字符串,将解决过的标签截掉,下一次循环解决残余的 html 字符串模版 | |
if (textEnd === 0) { | |
// Comment: | |
// 解决正文标签 <!-- xx --> | |
if (comment.test(html)) { | |
// 正文标签的完结索引 | |
const commentEnd = html.indexOf('-->') | |
if (commentEnd >= 0) { | |
// 是否应该保留 正文 | |
if (options.shouldKeepComment) { | |
// 失去:正文内容、正文的开始索引、完结索引 | |
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3) | |
} | |
// 调整 html 和 index 变量 | |
advance(commentEnd + 3) | |
continue | |
} | |
} | |
// 解决条件正文标签:<!--[if IE]> | |
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment | |
if (conditionalComment.test(html)) { | |
// 完结地位 | |
const conditionalEnd = html.indexOf(']>') | |
if (conditionalEnd >= 0) { | |
// 调整 html 和 index 变量 | |
advance(conditionalEnd + 2) | |
continue | |
} | |
} | |
// 解决 Doctype,<!DOCTYPE html> | |
const doctypeMatch = html.match(doctype) | |
if (doctypeMatch) {advance(doctypeMatch[0].length) | |
continue | |
} | |
/** | |
* 解决开始标签和完结标签是这整个函数中的核型局部 | |
* 这两局部就是在结构 element ast | |
*/ | |
// 解决完结标签,比方 </div> | |
const endTagMatch = html.match(endTag) | |
if (endTagMatch) { | |
const curIndex = index | |
advance(endTagMatch[0].length) | |
// 解决完结标签 | |
parseEndTag(endTagMatch[1], curIndex, index) | |
continue | |
} | |
// 解决开始标签 | |
const startTagMatch = parseStartTag() | |
if (startTagMatch) { | |
// 进一步解决上一步失去后果,并最初调用 options.start 办法 | |
// 真正的解析工作都是在这个 start 办法中做的 | |
handleStartTag(startTagMatch) | |
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {advance(1) | |
} | |
continue | |
} | |
} | |
let text, rest, next | |
if (textEnd >= 0) { | |
// 能走到这儿,阐明尽管在 html 中匹配到到了 <xx,然而这不属于上述几种状况,// 它就只是一个一般的一段文本:< 我是文本 | |
// 于是从 html 中找到下一个 <,直到 <xx 是上述几种状况的标签,则完结,// 在这整个过程中始终在调整 textEnd 的值,作为 html 中下一个无效标签的开始地位 | |
// 截取 html 模版字符串中 textEnd 之后的内容,rest = <xx | |
rest = html.slice(textEnd) | |
// 这个 while 循环就是解决 <xx 之后的纯文本状况 | |
// 截取文本内容,并找到无效标签的开始地位(textEnd)while (!endTag.test(rest) && | |
!startTagOpen.test(rest) && | |
!comment.test(rest) && | |
!conditionalComment.test(rest) | |
) { | |
// 则认为 < 前面的内容为纯文本,而后在这些纯文本中再次找 < | |
next = rest.indexOf('<', 1) | |
// 如果没找到 <,则间接完结循环 | |
if (next < 0) break | |
// 走到这儿阐明在后续的字符串中找到了 <,索引地位为 textEnd | |
textEnd += next | |
// 截取 html 字符串模版 textEnd 之后的内容赋值给 rest,持续判断之后的字符串是否存在标签 | |
rest = html.slice(textEnd) | |
} | |
// 走到这里,阐明遍历完结,有两种状况,一种是 < 之后就是一段纯文本,要不就是在前面找到了无效标签,截取文本 | |
text = html.substring(0, textEnd) | |
} | |
// 如果 textEnd < 0,阐明 html 中就没找到 <,那阐明 html 就是一段文本 | |
if (textEnd < 0) {text = html} | |
// 将文本内容从 html 模版字符串上截取掉 | |
if (text) {advance(text.length) | |
} | |
// 解决文本 | |
// 基于文本生成 ast 对象,而后将该 ast 放到它的父元素的中 | |
// 即 currentParent.children 数组中 | |
if (options.chars && text) {options.chars(text, index - text.length, index) | |
} | |
} else { | |
// 解决 script、style、textarea 标签的闭合标签 | |
let endTagLength = 0 | |
// 开始标签的小写模式 | |
const stackedTag = lastTag.toLowerCase() | |
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i')) | |
// 匹配并解决开始标签和完结标签之间的所有文本,比方 <script>xx</script> | |
const rest = html.replace(reStackedTag, function (all, text, endTag) { | |
endTagLength = endTag.length | |
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') { | |
text = text | |
.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298 | |
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1') | |
} | |
if (shouldIgnoreFirstNewline(stackedTag, text)) {text = text.slice(1) | |
} | |
if (options.chars) {options.chars(text) | |
} | |
return '' | |
}) | |
index += html.length - rest.length | |
html = rest | |
parseEndTag(stackedTag, index - endTagLength, index) | |
} | |
// 到这里就解决完结,如果 stack 数组中还有内容,则阐明有标签没有被闭合,给出提示信息 | |
if (html === last) {options.chars && options.chars(html) | |
if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {options.warn(`Mal-formatted tag at end of template: "${html}"`, {start: index + html.length}) | |
} | |
break | |
} | |
} | |
// Clean up any remaining tags | |
parseEndTag() | |
/** | |
* 重置 html,html = 从索引 n 地位开始的向后的所有字符 | |
* index 为 html 在 原始的 模版字符串 中的的开始索引,也是下一次该解决的字符的开始地位 | |
* @param {*} n 索引 | |
*/ | |
function advance (n) { | |
index += n | |
html = html.substring(n) | |
} | |
/** | |
* 解析开始标签,比方:<div id="app"> | |
* @returns {tagName: 'div', attrs: [[xx], ...], start: index } | |
*/ | |
function parseStartTag () {const start = html.match(startTagOpen) | |
if (start) { | |
// 处理结果 | |
const match = { | |
// 标签名 | |
tagName: start[1], | |
// 属性,占位符 | |
attrs: [], | |
// 标签的开始地位 | |
start: index | |
} | |
/** | |
* 调整 html 和 index,比方:* html = 'id="app">' | |
* index = 此时的索引 | |
* start[0] = '<div' | |
*/ | |
advance(start[0].length) | |
let end, attr | |
// 解决 开始标签 内的各个属性,并将这些属性放到 match.attrs 数组中 | |
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) { | |
attr.start = index | |
advance(attr[0].length) | |
attr.end = index | |
match.attrs.push(attr) | |
} | |
// 开始标签的完结,end = '>' 或 end = '/>' | |
if (end) {match.unarySlash = end[1] | |
advance(end[0].length) | |
match.end = index | |
return match | |
} | |
} | |
} | |
/** | |
* 进一步解决开始标签的解析后果 ——— match 对象 | |
* 解决属性 match.attrs,如果不是自闭合标签,则将标签信息放到 stack 数组,待未来解决到它的闭合标签时再将其弹出 stack,示意该标签处理完毕,这时标签的所有信息都在 element ast 对象上了 | |
* 接下来调用 options.start 办法解决标签,并依据标签信息生成 element ast,* 以及解决开始标签上的属性和指令,最初将 element ast 放入 stack 数组 | |
* | |
* @param {*} match {tagName: 'div', attrs: [[xx], ...], start: index } | |
*/ | |
function handleStartTag (match) { | |
const tagName = match.tagName | |
// /> | |
const unarySlash = match.unarySlash | |
if (expectHTML) {if (lastTag === 'p' && isNonPhrasingTag(tagName)) {parseEndTag(lastTag) | |
} | |
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {parseEndTag(tagName) | |
} | |
} | |
// 一元标签,比方 <hr /> | |
const unary = isUnaryTag(tagName) || !!unarySlash | |
// 解决 match.attrs,失去 attrs = [{name: attrName, value: attrVal, start: xx, end: xx}, ...] | |
// 比方 attrs = [{name: 'id', value: 'app', start: xx, end: xx}, ... | |
const l = match.attrs.length | |
const attrs = new Array(l) | |
for (let i = 0; i < l; i++) {const args = match.attrs[i] | |
// 比方:args[3] => 'id',args[4] => '=',args[5] => 'app' | |
const value = args[3] || args[4] || args[5] || ''const shouldDecodeNewlines = tagName ==='a'&& args[1] ==='href' | |
? options.shouldDecodeNewlinesForHref | |
: options.shouldDecodeNewlines | |
// attrs[i] = {id: 'app'} | |
attrs[i] = {name: args[1], | |
value: decodeAttr(value, shouldDecodeNewlines) | |
} | |
// 非生产环境,记录属性的开始和完结索引 | |
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {attrs[i].start = args.start + args[0].match(/^\s*/).length | |
attrs[i].end = args.end | |
} | |
} | |
// 如果不是自闭合标签,则将标签信息放到 stack 数组中,待未来解决到它的闭合标签时再将其弹出 stack | |
// 如果是自闭合标签,则标签信息就没必要进入 stack 了,间接解决泛滥属性,将他们都设置到 element ast 对象上,就没有解决 完结标签的那一步了,这一步在解决开始标签的过程中就进行了 | |
if (!unary) {// 将标签信息放到 stack 数组中,{ tag, lowerCasedTag, attrs, start, end} | |
stack.push({tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end }) | |
// 标识以后标签的完结标签为 tagName | |
lastTag = tagName | |
} | |
/** | |
* 调用 start 办法,次要做了以下 6 件事件: | |
* 1、创立 AST 对象 | |
* 2、解决存在 v-model 指令的 input 标签,别离解决 input 为 checkbox、radio、其它的状况 | |
* 3、解决标签上的泛滥指令,比方 v-pre、v-for、v-if、v-once | |
* 4、如果根节点 root 不存在则设置以后元素为根节点 | |
* 5、如果以后元素为非自闭合标签则将本人 push 到 stack 数组,并记录 currentParent,在接下来解决子元素时用来通知子元素本人的父节点是谁 | |
* 6、如果以后元素为自闭合标签,则示意该标签要解决完结了,让本人和父元素产生关系,以及设置本人的子元素 | |
*/ | |
if (options.start) {options.start(tagName, attrs, unary, match.start, match.end) | |
} | |
} | |
/** | |
* 解析完结标签,比方:</div> | |
* 最次要的事就是:* 1、解决 stack 数组,从 stack 数组中找到以后完结标签对应的开始标签,而后调用 options.end 办法 | |
* 2、解决完完结标签之后调整 stack 数组,保障在失常状况下 stack 数组中的最初一个元素就是下一个完结标签对应的开始标签 | |
* 3、解决一些异常情况,比方 stack 数组最初一个元素不是以后完结标签对应的开始标签,还有就是 | |
* br 和 p 标签独自解决 | |
* @param {*} tagName 标签名,比方 div | |
* @param {*} start 完结标签的开始索引 | |
* @param {*} end 完结标签的完结索引 | |
*/ | |
function parseEndTag (tagName, start, end) { | |
let pos, lowerCasedTagName | |
if (start == null) start = index | |
if (end == null) end = index | |
// 倒序遍历 stack 数组,找到第一个和以后完结标签雷同的标签,该标签就是完结标签对应的开始标签的形容对象 | |
// 实践上,不出异样,stack 数组中的最初一个元素就是以后完结标签的开始标签的形容对象 | |
// Find the closest opened tag of the same type | |
if (tagName) {lowerCasedTagName = tagName.toLowerCase() | |
for (pos = stack.length - 1; pos >= 0; pos--) {if (stack[pos].lowerCasedTag === lowerCasedTagName) {break} | |
} | |
} else { | |
// If no tag name is provided, clean shop | |
pos = 0 | |
} | |
// 如果在 stack 中始终没有找到雷同的标签名,则 pos 就会 < 0,进行前面的 else 分支 | |
if (pos >= 0) { | |
// 这个 for 循环负责敞开 stack 数组中索引 >= pos 的所有标签 | |
// 为什么要用一个循环,下面说到失常状况下 stack 数组的最初一个元素就是咱们要找的开始标签,// 然而有些异常情况,就是有些元素没有给提供完结标签,比方:// stack = ['span', 'div', 'span', 'h1'],以后解决的完结标签 tagName = div | |
// 匹配到 div,pos = 1,那索引为 2 和 3 的两个标签(span、h1)阐明就没提供完结标签 | |
// 这个 for 循环就负责敞开 div、span 和 h1 这三个标签,// 并在开发环境为 span 和 h1 这两个标签给出”未匹配到完结标签的提醒”// Close all the open elements, up the stack | |
for (let i = stack.length - 1; i >= pos; i--) { | |
if (process.env.NODE_ENV !== 'production' && | |
(i > pos || !tagName) && | |
options.warn | |
) { | |
options.warn(`tag <${stack[i].tag}> has no matching end tag.`, | |
{start: stack[i].start, end: stack[i].end } | |
) | |
} | |
if (options.end) { | |
// 走到这里,阐明下面的异常情况都解决完了,调用 options.end 解决失常的完结标签 | |
options.end(stack[i].tag, start, end) | |
} | |
} | |
// 将方才解决的那些标签从数组中移除,保障数组的最初一个元素就是下一个完结标签对应的开始标签 | |
// Remove the open elements from the stack | |
stack.length = pos | |
// lastTag 记录 stack 数组中未解决的最初一个开始标签 | |
lastTag = pos && stack[pos - 1].tag | |
} else if (lowerCasedTagName === 'br') { | |
// 以后解决的标签为 <br /> 标签 | |
if (options.start) {options.start(tagName, [], true, start, end) | |
} | |
} else if (lowerCasedTagName === 'p') { | |
// 解决 <p> 标签 | |
if (options.start) {options.start(tagName, [], false, start, end) | |
} | |
// 解决 </p> 标签 | |
if (options.end) {options.end(tagName, start, end) | |
} | |
} | |
} | |
} |
parseHTML
的次要逻辑就是循环解析,用正则做匹配,而后做不同的解决,在匹配的过程中利用 advance
函数一直扭转索引,直到解析结束。
通过正则能够匹配到正文标签、文档类型标签、开始标签、完结标签。通过 handleStartTag
办法解析开始标签,将非一元标签构建进去的 AST 对象推入 stack
中,通过 parseEndTag
办法对闭合标签做解析,也就是倒序的 stack
,找到第一个和以后完结标签雷同的标签,该标签就是完结标签对应的开始标签的形容对象。
AST 元素节点总共有 3 种类型,type
为 1 示意是一般元素,为 2 示意是表达式,为 3 示意是纯文本。parse
的作用就是利用正则将模板字符创解析成 AST 语法树。
optimize
在模板解析之后,生成 AST 树,接下来就是对 AST 的一些优化。
optimize
// src/compiler/optimizer.js | |
/** | |
* Goal of the optimizer: walk the generated template AST tree | |
* and detect sub-trees that are purely static, i.e. parts of | |
* the DOM that never needs to change. | |
* | |
* Once we detect these sub-trees, we can: | |
* | |
* 1. Hoist them into constants, so that we no longer need to | |
* create fresh nodes for them on each re-render; | |
* 2. Completely skip them in the patching process. | |
*/ | |
export function optimize (root: ?ASTElement, options: CompilerOptions) {if (!root) return | |
/** | |
* options.staticKeys = 'staticClass,staticStyle' | |
* isStaticKey = function(val) {return map[val] } | |
*/ | |
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) | |
} |
markStatic
// src/compiler/optimizer.js | |
function markStatic (node: ASTNode) { | |
// 通过 node.static 来标识节点是否为 动态节点 | |
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、动态插槽内容在热重载时失败 | |
*/ | |
if (!isPlatformReservedTag(node.tag) && | |
node.tag !== 'slot' && | |
node.attrsMap['inline-template'] == null | |
) { | |
// 递归终止条件,如果节点不是平台保留标签 && 也不是 slot 标签 && 也不是内联模版,则间接完结 | |
return | |
} | |
// 遍历子节点,递归调用 markStatic 来标记这些子节点的 static 属性 | |
for (let i = 0, l = node.children.length; i < l; i++) {const child = node.children[i] | |
markStatic(child) | |
// 如果子节点是非动态节点,则将父节点更新为非动态节点 | |
if (!child.static) {node.static = false} | |
} | |
// 如果节点存在 v-if、v-else-if、v-else 这些指令,则顺次标记 block 中节点的 static | |
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) {node.static = false} | |
} | |
} | |
} | |
} | |
/** | |
* 判断节点是否为动态节点:* 通过自定义的 node.type 来判断,2: 表达式 => 动静,3: 文本 => 动态 | |
* 但凡有 v-bind、v-if、v-for 等指令的都属于动静节点 | |
* 组件为动静节点 | |
* 父节点为含有 v-for 指令的 template 标签,则为动静节点 | |
*/ | |
function isStatic (node: ASTNode): boolean {if (node.type === 2) { // expression | |
// 比方:{{msg}} | |
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) && | |
Object.keys(node).every(isStaticKey) | |
)) | |
} |
markStaticRoots
// src/compiler/optimizer.js | |
function markStaticRoots (node: ASTNode, isInFor: boolean) {if (node.type === 1) {if (node.static || node.once) { | |
// 曾经是 static 的节点或者是 v-once 指令的节点 | |
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. | |
if (node.static && node.children.length && !( | |
node.children.length === 1 && | |
node.children[0].type === 3 | |
)) { | |
// 除了自身是一个动态节点外,必须满足领有 children,并且 children 不能只是一个文本节点 | |
node.staticRoot = true | |
return | |
} else {node.staticRoot = false} | |
// 以后节点不是动态根,递归遍历其子节点,标记动态根 | |
if (node.children) {for (let i = 0, l = node.children.length; i < l; i++) {markStaticRoots(node.children[i], isInFor || !!node.for) | |
} | |
} | |
// 如果节点存在 v-if、v-else-if、v-else 指令,则为 block 节点标记动态根 | |
if (node.ifConditions) {for (let i = 1, l = node.ifConditions.length; i < l; i++) {markStaticRoots(node.ifConditions[i].block, isInFor) | |
} | |
} | |
} | |
} |
在模板不是所有数据都是响应式的,有些数据是首次渲染之后就不会在发生变化的,通过遍历生成的模板 AST 树,对这些节点进行标记,就能够在 patch
的过程中跳过它们,从而进步比照的性能。
在 optimize
中实际上就是通过 markStatic(root)
对动态节点进行标记和 应用 markStaticRoots(root, false)
标记动态根。
generate
在生成 AST 语法树后,对 AST 进行优化,标记动态节点和动态根。最初就是通过 generate
生成代码字符串。
generate
// src/compiler/codegen/index.js | |
export function generate ( | |
ast: ASTElement | void, | |
options: CompilerOptions | |
): CodegenResult {const state = new CodegenState(options) | |
// fix #11483, Root level <script> tags should not be rendered. | |
const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")' | |
return {render: `with(this){return ${code}}`, | |
staticRenderFns: state.staticRenderFns | |
} | |
} |
state
是 CodegenState
的一个实例,在生成代码的时候会用到其中的一些属性和办法。generate
函数次要是通过 genElement
生成 code
, 而后在用 with(this){return ${code}}
将其包裹起来。
genElement
// src/compiler/codegen/index.js | |
export function genElement (el: ASTElement, state: CodegenState): string {if (el.parent) {el.pre = el.pre || el.parent.pre} | |
if (el.staticRoot && !el.staticProcessed) { | |
// 解决动态根节点 | |
return genStatic(el, state) | |
} else if (el.once && !el.onceProcessed) { | |
// 解决带有 v-once 指令的节点 | |
return genOnce(el, state) | |
} else if (el.for && !el.forProcessed) { | |
// 解决 v-for | |
return genFor(el, state) | |
} else if (el.if && !el.ifProcessed) { | |
// 解决 v-if | |
return genIf(el, state) | |
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) { | |
// 解决子节点 | |
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 { | |
// 自定义组件和原生标签 | |
let data | |
if (!el.plain || (el.pre && state.maybeComponent(el))) { | |
// 非一般元素或者带有 v-pre 指令的组件走这里,解决节点的所有属性 | |
data = 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 | |
} | |
} |
genElement
的作用次要就是对 AST 节点上的属性应用不同办法做解决,而生成代码函数。
genStatic
// src/compiler/codegen/index.js | |
// hoist static sub-trees out | |
function genStatic (el: ASTElement, state: CodegenState): string { | |
// 标记以后动态节点曾经被解决 | |
el.staticProcessed = true | |
// Some elements (templates) need to behave differently inside of a v-pre | |
// node. All pre nodes are static roots, so we can use this as a location to | |
// wrap a state change and reset it upon exiting the pre node. | |
const originalPreState = state.pre | |
if (el.pre) {state.pre = el.pre} | |
// 将生成的代码增加到 staticRenderFns 中 | |
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`) | |
state.pre = originalPreState | |
// 返回 _m 函数,state.staticRenderFns.length - 1 示意数组中的下标 | |
return `_m(${state.staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})` | |
} |
genOnce
// src/compiler/codegen/index.js | |
function genOnce (el: ASTElement, state: CodegenState): string { | |
// 标记以后节点的 v-once 指令已被解决 | |
el.onceProcessed = true | |
if (el.if && !el.ifProcessed) { | |
// 含有 v-if 并且 V-if 没有被解决,则解决 V-if 最终生成一段三元运算符的代码 | |
return genIf(el, state) | |
} else if (el.staticInFor) { | |
// 阐明以后节点是被包裹在还有 v-for 指令节点外部的动态节点 | |
let key = '' | |
let parent = el.parent | |
while (parent) {if (parent.for) { | |
key = parent.key | |
break | |
} | |
parent = parent.parent | |
} | |
if (!key) { | |
process.env.NODE_ENV !== 'production' && state.warn( | |
`v-once can only be used inside v-for that is keyed. `, | |
el.rawAttrsMap['v-once'] | |
) | |
return genElement(el, state) | |
} | |
// 生成 _o 函数 | |
return `_o(${genElement(el, state)},${state.onceId++},${key})` | |
} else { | |
// 下面状况都不合乎,阐明是简略的动态节点,生成 _m 函数 | |
return genStatic(el, state) | |
} | |
} |
genFor
// src/compiler/codegen/index.js | |
export function genFor ( | |
el: any, | |
state: CodegenState, | |
altGen?: Function, | |
altHelper?: string | |
): string { | |
const exp = el.for | |
const alias = el.alias | |
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''const iterator2 = el.iterator2 ? `,${el.iterator2}` :'' | |
// 提醒 v-for 在组件上时必须应用 key | |
if (process.env.NODE_ENV !== 'production' && | |
state.maybeComponent(el) && | |
el.tag !== 'slot' && | |
el.tag !== 'template' && | |
!el.key | |
) { | |
state.warn(`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` + | |
`v-for should have explicit keys. ` + | |
`See https://vuejs.org/guide/list.html#key for more info.`, | |
el.rawAttrsMap['v-for'], | |
true /* tip */ | |
) | |
} | |
// 标记以后节点上的 v-for 指令已被解决 | |
el.forProcessed = true // avoid recursion | |
/** | |
* 生成 _l 函数,比方:* v-for="(item,index) in data" | |
* | |
* _l((data), function (item, index) {* return genElememt(el, state) | |
* }) | |
*/ | |
return `${altHelper || '_l'}((${exp}),` + | |
`function(${alias}${iterator1}${iterator2}){` + | |
`return ${(altGen || genElement)(el, state)}` + | |
'})' | |
} |
genIf
// src/compiler/codegen/index.js | |
export function genIf ( | |
el: any, | |
state: CodegenState, | |
altGen?: Function, | |
altEmpty?: string | |
): string { | |
// 标记以后节点的 v-if 指令曾经被解决 | |
el.ifProcessed = true // avoid recursion | |
// 失去三元表达式,condition ? render1 : render2 | |
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty) | |
} | |
function genIfConditions ( | |
conditions: ASTIfConditions, | |
state: CodegenState, | |
altGen?: Function, | |
altEmpty?: string | |
): string { | |
// 长度为空,间接返回一个空节点渲染函数 | |
if (!conditions.length) {return altEmpty || '_e()' | |
} | |
// 顺次从 conditions 获取第一个 condition | |
const condition = conditions.shift() | |
if (condition.exp) { | |
// 通过对 condition.exp 去生成一段三元运算符的代码, | |
// : 后是递归调用,如果有多个 conditions,就生成多层三元运算 | |
return `(${condition.exp})?${genTernaryExp(condition.block) | |
}:${genIfConditions(conditions, state, altGen, altEmpty) | |
}` | |
} else {return `${genTernaryExp(condition.block)}` | |
} | |
// v-if with v-once should generate code like (a)?_m(0):_m(1) | |
function genTernaryExp (el) { | |
return altGen | |
? altGen(el, state) | |
: el.once | |
? genOnce(el, state) | |
: genElement(el, state) | |
} | |
} |
genChildren
// src/compiler/codegen/index.js | |
export function genChildren ( | |
el: ASTElement, | |
state: CodegenState, | |
checkSkip?: boolean, | |
altGenElement?: Function, | |
altGenNode?: Function | |
): string | void { | |
const children = el.children | |
if (children.length) {const el: any = children[0] | |
// optimize single v-for | |
if (children.length === 1 && | |
el.for && | |
el.tag !== 'template' && | |
el.tag !== 'slot' | |
) { | |
// 一个子节点 && 节点上有 v-for && 不是 template 标签 && 不是 slot | |
// 则间接调用 genElement,从而进入到 genFor | |
const normalizationType = checkSkip | |
? state.maybeComponent(el) ? `,1` : `,0` | |
: `` | |
return `${(altGenElement || genElement)(el, state)}${normalizationType}` | |
} | |
const normalizationType = checkSkip | |
? getNormalizationType(children, state.maybeComponent) | |
: 0 | |
// 生成代码的一个函数 | |
const gen = altGenNode || genNode | |
// 返回一个数组,数组的每个元素都是一个子节点的渲染函数,// 格局:['_c(tag, data, children, normalizationType)', ...] | |
return `[${children.map(c => gen(c, state)).join(',')}]${normalizationType ? `,${normalizationType}` : '' | |
}` | |
} | |
} |
genSlot
// src/compiler/codegen/index.js | |
function genSlot (el: ASTElement, state: CodegenState): string { | |
// 插槽名称 | |
const slotName = el.slotName || '"default"' | |
// 解决子节点 | |
const children = genChildren(el, state) | |
// 最终返回 _t 函数 | |
let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}` | |
const attrs = el.attrs || el.dynamicAttrs | |
? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({ | |
// slot props are camelized | |
name: camelize(attr.name), | |
value: attr.value, | |
dynamic: attr.dynamic | |
}))) | |
: null | |
const bind = el.attrsMap['v-bind'] | |
if ((attrs || bind) && !children) {res += `,null`} | |
if (attrs) {res += `,${attrs}` | |
} | |
if (bind) {res += `${attrs ? '':',null'},${bind}` | |
} | |
return res + ')' | |
} |
genProps
// src/compiler/codegen/index.js | |
function genProps (props: Array<ASTAttr>): string { | |
// 动态属性 | |
let staticProps = `` | |
// 动静属性 | |
let dynamicProps = `` | |
// 遍历属性数组 | |
for (let i = 0; i < props.length; i++) { | |
// 属性 | |
const prop = props[i] | |
// 属性值 | |
const value = __WEEX__ | |
? generateValue(prop.value) | |
: transformSpecialNewlines(prop.value) | |
if (prop.dynamic) { | |
// 动静属性,`dAttrName,dAttrVal,...` | |
dynamicProps += `${prop.name},${value},` | |
} else { | |
// 动态属性,'attrName,attrVal,...' | |
staticProps += `"${prop.name}":${value},` | |
} | |
} | |
// 去掉动态属性最初的逗号 | |
staticProps = `{${staticProps.slice(0, -1)}}` | |
if (dynamicProps) { | |
// 如果存在动静属性则返回:// _d(动态属性字符串,动静属性字符串) | |
return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])` | |
} else { | |
// 阐明属性数组中不存在动静属性,间接返回动态属性字符串 | |
return staticProps | |
} | |
} |
genData
// src/compiler/codegen/index.js | |
export function genData (el: ASTElement, state: CodegenState): string { | |
let data = '{' | |
// directives first. | |
// directives may mutate the el's other properties before they are generated. | |
const dirs = genDirectives(el, state) | |
if (dirs) data += dirs + ',' | |
// key | |
if (el.key) {data += `key:${el.key},` | |
} | |
// ref | |
if (el.ref) {data += `ref:${el.ref},` | |
} | |
if (el.refInFor) {data += `refInFor:true,`} | |
// pre | |
if (el.pre) {data += `pre:true,`} | |
// record original tag name for components using "is" attribute | |
if (el.component) {data += `tag:"${el.tag}",` | |
} | |
// 为节点执行模块 (class、style) 的 genData 办法,// module data generation functions | |
for (let i = 0; i < state.dataGenFns.length; i++) {data += state.dataGenFns[i](el) | |
} | |
// attributes | |
if (el.attrs) {data += `attrs:${genProps(el.attrs)},` | |
} | |
// DOM props | |
if (el.props) {data += `domProps:${genProps(el.props)},` | |
} | |
// event handlers | |
// 自定义事件, 如 {`on${eventName}:handleCode` } 或者 {`on_d(${eventName}:handleCode`, `${eventName},handleCode`) } | |
if (el.events) {data += `${genHandlers(el.events, false)},` | |
} | |
// 带 .native 修饰符的事件,if (el.nativeEvents) {data += `${genHandlers(el.nativeEvents, true)},` | |
} | |
// slot target | |
// only for non-scoped slots | |
// 非作用域插槽 | |
if (el.slotTarget && !el.slotScope) {data += `slot:${el.slotTarget},` | |
} | |
// scoped slots | |
// 作用域插槽 | |
if (el.scopedSlots) {data += `${genScopedSlots(el, el.scopedSlots, state)},` | |
} | |
// component v-model | |
if (el.model) {data += `model:{value:${el.model.value},callback:${el.model.callback},expression:${el.model.expression}},` | |
} | |
// inline-template | |
if (el.inlineTemplate) {const inlineTemplate = genInlineTemplate(el, state) | |
if (inlineTemplate) {data += `${inlineTemplate},` | |
} | |
} | |
data = data.replace(/,$/, '') +'}' | |
// v-bind dynamic argument wrap | |
// v-bind with dynamic arguments must be applied using the same v-bind object | |
// merge helper so that class/style/mustUseProp attrs are handled correctly. | |
if (el.dynamicAttrs) {data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})` | |
} | |
// v-bind data wrap | |
if (el.wrapData) {data = el.wrapData(data) | |
} | |
// v-on data wrap | |
if (el.wrapListeners) {data = el.wrapListeners(data) | |
} | |
return data | |
} |
genData
函数就是依据 AST 元素节点的属性结构出一个 data
对象字符串,这个在前面创立 VNode 的时候的时候会作为参数传入。
genComponent
// src/compiler/codegen/index.js | |
function genComponent ( | |
componentName: string, | |
el: ASTElement, | |
state: CodegenState | |
): string { | |
// 所有的子节点 | |
const children = el.inlineTemplate ? null : genChildren(el, state, true) | |
// 返回 `_c(compName, data, children)` | |
// compName 是 is 属性的值 | |
return `_c(${componentName},${genData(el, state)}${children ? `,${children}` : '' | |
})` | |
} |
举个栗子
模板
<ul :class="bindCls" class="list" v-if="isShow"> | |
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li> | |
</ul> |
parse 生成 AST
ast = { | |
'type': 1, | |
'tag': 'ul', | |
'attrsList': [], | |
'attrsMap': { | |
':class': 'bindCls', | |
'class': 'list', | |
'v-if': 'isShow' | |
}, | |
'if': 'isShow', | |
'ifConditions': [{ | |
'exp': 'isShow', | |
'block': // ul ast element | |
}], | |
'parent': undefined, | |
'plain': false, | |
'staticClass': 'list', | |
'classBinding': 'bindCls', | |
'children': [{ | |
'type': 1, | |
'tag': 'li', | |
'attrsList': [{ | |
'name': '@click', | |
'value': 'clickItem(index)' | |
}], | |
'attrsMap': {'@click': 'clickItem(index)', | |
'v-for': '(item,index) in data' | |
}, | |
'parent': // ul ast element | |
'plain': false, | |
'events': { | |
'click': {'value': 'clickItem(index)' | |
} | |
}, | |
'hasBindings': true, | |
'for': 'data', | |
'alias': 'item', | |
'iterator1': 'index', | |
'children': [ | |
'type': 2, | |
'expression': '_s(item)+":"+_s(index)' | |
'text': '{{item}}:{{index}}', | |
'tokens': [{ '@binding': 'item'}, | |
':', | |
{'@binding': 'index'} | |
] | |
] | |
}] | |
} |
optimize 优化 AST
ast = { | |
'type': 1, | |
'tag': 'ul', | |
'attrsList': [], | |
'attrsMap': { | |
':class': 'bindCls', | |
'class': 'list', | |
'v-if': 'isShow' | |
}, | |
'if': 'isShow', | |
'ifConditions': [{ | |
'exp': 'isShow', | |
'block': // ul ast element | |
}], | |
'parent': undefined, | |
'plain': false, | |
'staticClass': 'list', | |
'classBinding': 'bindCls', | |
'static': false, | |
'staticRoot': false, | |
'children': [{ | |
'type': 1, | |
'tag': 'li', | |
'attrsList': [{ | |
'name': '@click', | |
'value': 'clickItem(index)' | |
}], | |
'attrsMap': {'@click': 'clickItem(index)', | |
'v-for': '(item,index) in data' | |
}, | |
'parent': // ul ast element | |
'plain': false, | |
'events': { | |
'click': {'value': 'clickItem(index)' | |
} | |
}, | |
'hasBindings': true, | |
'for': 'data', | |
'alias': 'item', | |
'iterator1': 'index', | |
'static': false, | |
'staticRoot': false, | |
'children': [ | |
'type': 2, | |
'expression': '_s(item)+":"+_s(index)' | |
'text': '{{item}}:{{index}}', | |
'tokens': [{ '@binding': 'item'}, | |
':', | |
{'@binding': 'index'} | |
], | |
'static': false | |
] | |
}] | |
} |
generate 生成代码
with (this) {return (isShow) ? | |
_c('ul', { | |
staticClass: "list", | |
class: bindCls | |
}, | |
_l((data), function (item, index) { | |
return _c('li', { | |
on: {"click": function ($event) {clickItem(index) | |
} | |
} | |
}, | |
[_v(_s(item) + ":" + _s(index))]) | |
}) | |
) : _e()} |
最终生成了许多简写函数,比方 _c
、_t
、_l
、_m
, 这些函数都定义在:
// src/core/instance/render-helpers/index.js | |
export function installRenderHelpers (target: any) { | |
target._o = markOnce | |
target._n = toNumber | |
target._s = toString | |
target._l = renderList | |
target._t = renderSlot | |
target._q = looseEqual | |
target._i = looseIndexOf | |
target._m = renderStatic | |
target._f = resolveFilter | |
target._k = checkKeyCodes | |
target._b = bindObjectProps | |
target._v = createTextVNode | |
target._e = createEmptyVNode | |
target._u = resolveScopedSlots | |
target._g = bindObjectListeners | |
target._d = bindDynamicKeys | |
target._p = prependModifier | |
} |
到此,Vue 的源码大体梳理结束,在此期间查看了许多大佬的文章,感激大佬们的自私分享,感激敌对的前端圈。
相干链接
Vue 源码解读(预):手写一个简易版 Vue
Vue 源码解读(一):筹备工作
Vue 源码解读(二):初始化和挂载
Vue 源码解读(三):响应式原理
Vue 源码解读(四):更新策略
Vue 源码解读(五):render 和 VNode
Vue 源码解读(六):update 和 patch
Vue 源码解读(七):模板编译
如果感觉还对付的话,给个赞吧!!!也能够来我的集体博客逛逛 https://www.mingme.net/