优质中文开源软件代码项目资源技术共享平台
传播开源的理念,推广开源项目
学习是对自己最棒的投资!与君共勉!
云服务器主机测评推荐,开源软件代码项目技术资源共享!

网站首页 > web开发 > JavaScript 正文

Vue2模版编译流程详解

longtao100 2023-10-10 10:54:20 JavaScript 19 ℃ 0 评论

目录

为了更好理解 vue 的模板编译这里我整理了一份模板编译的整体流程,如下所示,下面将用源码解读的方式来找到模板编译中的几个核心步骤,进行详细说明:

1、起步

这里我使用 webpack 来打包 vue 文件,来分析 vue 在模板编译中的具体流程,如下所示,下面是搭建的项目结构和文件内容:

项目结构

├─package-lock.json
├─package.json
├─src
|├─App.vue
|└index.js
├─dist
|└main.js
├─config
|   └webpack.config.js

App.vue

webpack.config.js

const { VueLoaderPlugin } = require('vue-loader')
​
module.exports = {
mode: 'development',
module: {
  rules: [
    {
      test: /.vue$/,
      loader: 'vue-loader'
    },
    // 它会应用到普通的 `.js` 文件
    // 以及 `.vue` 文件中的 `\n' +
  '\n' +
  '\n',
filename: 'App.vue',
template: {
  type: 'template',
  content: '\n
\n{{ count }}\n
\n',  start: 10,  end: 53,  attrs: {} }, script: {  type: 'script',  content: '\n' +    'export default {\n' +    'props: {},\n' +    'data() {\n' +    '  return {\n' +    '    count: 0\n' +    '  }\n' +    '}\n' +    '}\n',  start: 74,  end: 156,  attrs: {} }, .... }

template-loader

template-loader 的作用是将 import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&" 模块编译成 render 函数并导出,以下是编译产物:

// 编译前
{{ count }}
​ // 编译后 var render = function render() { var _vm = this,  _c = _vm._self._c return _c("div", { attrs: { id: "box" } }, [  _vm._v("\n" + _vm._s(_vm.count) + "\n"), ]) } var staticRenderFns = [] render._withStripped = true ​ export { render, staticRenderFns }

template-loader 核心原理是通过 vue/compiler-sfc 将模板转换成为 render 函数,并返回 template 编译产物

module.exports = function (source) {
const loaderContext = this
  ...
// 接收模板编译核心库
const { compiler, templateCompiler } = resolveCompiler(ctx, loaderContext)
​
  ...
​
// 开启编译
const compiled = compiler.compileTemplate(finalOptions)
​
  ...
​
// 编译后产出,code就是render函数
const { code } = compiled
​
// 导出template模块
return code + `\nexport { render, staticRenderFns }`
}

2、模板编译流程

vue/compiler-sfc 是模板编译的核心库,在 vue2.7 版本中使用,而 vue2.7 以下的版本都是使用vue-template-compiler,本质两个包的功能是一样的,都可以将模板语法编译为 JavaScript,接下来我们来解析一下在模板编译过程中使用的方法:

parseHTML 阶段

可以将 vue 文件中的模板语法转义为 AST,为后续创建 dom 结构做预处理

export function parseHTML(html, options: HTMLParserOptions) {
// 存储解析后的标签
const stack: any[] = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0
let last, lastTag
// 循环 html 字符串结构
while (html) {
// 记录当前最新html
last = html
if (!lastTag || !isPlainTextElement(lastTag)) {
// 获取以 < 为开始的位置
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// 解析注释
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment && options.comment) {
options.comment(
html.substring(4, commentEnd),
index,
index + commentEnd + 3
)
}
advance(commentEnd + 3)
continue
}
}
// 解析条件注释
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
// 解析 Doctype
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// 解析截取结束标签
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) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
let text, rest, next
if (textEnd >= 0) {
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0, textEnd)
}
// 纯文本节点
if (textEnd < 0) {
text = html
}
// 截取文本节点
if (text) {
advance(text.length)
}
if (options.chars && text) {
options.chars(text, index - text.length, index)
}
} else {
let endTagLength = 0
const stackedTag = lastTag.toLowerCase()
const reStackedTag =
reCache[stackedTag] ||
(reCache[stackedTag] = new RegExp(
'([\s\S]*?)(]*>)',
'i'
))
const rest = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(//g, '$1') // #7298
.replace(//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)
}
if (html === last) {
options.chars && options.chars(html)
break
}
}
// 清空闭合标签
parseEndTag()
// 截取标签,前后推进位置
function advance(n) {
index += n
html = html.substring(n)
}
// 解析开始标签
function parseStartTag() {
const start = html.match(startTagOpen)
if (start) {
const match: any = {
tagName: start[1],
attrs: [],
start: index
}
advance(start[0].length)
let end, attr
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)
}
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
// 匹配处理开始标签
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)
}
}
const unary = isUnaryTag(tagName) || !!unarySlash
const l = match.attrs.length
const attrs: ASTAttr[] = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines =
tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
if (__DEV__ && options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length
attrs[i].end = args.end
}
}
if (!unary) {
stack.push({
tag: tagName,
lowerCasedTag: tagName.toLowerCase(),
attrs: attrs,
start: match.start,
end: match.end
})
lastTag = tagName
}
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
// 解析结束标签
function parseEndTag(tagName?: any, start?: any, end?: any) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
// 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
}
if (pos >= 0) {
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i--) {
if (__DEV__ && (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(stack[i].tag, start, end)
}
}
// Remove the open elements from the stack
stack.length = pos
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
}
}

genElement 阶段

genElement 会将 AST 预发转义为字符串代码,后续可将其包装成 render 函数的返回值

// 将AST预发转义成render函数字符串
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) {
// 处理循环结构
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
// 处理条件语法
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 {
// 处理组件和dom元素
 ...
return code
}
}

通过genElement函数包装处理后,将vue 模板的 template 标签部分转换为 render 函数,如下所示:

const compiled = compiler.compileTemplate({
source: '\n' +
'
\n' + '{{ count }}\n' + '\n' + '
\n' }); const { code } = compiled; // 编译后 var render = function render() { var _vm = this, _c = _vm._self._c return _c("div", { attrs: { id: "box" } }, [ _vm._v("\n" + _vm._s(_vm.count) + "\n"), _c("button", { on: { add: _vm.handleAdd } }, [_vm._v("+")]), ]) } var staticRenderFns = [] render._withStripped = true

compilerToFunction 阶段

将 genElement 阶段编译的字符串产物,通过 new Function将 code 转为函数

export function createCompileToFunctionFn(compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions(
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
...
// 编译
const compiled = compile(template, options)
// 将genElement阶段的产物转化为function
function createFunction(code, errors) {
try {
return new Function(code)
} catch (err: any) {
errors.push({ err, code })
return noop
}
}
const res: any = {}
const fnGenErrors: any[] = []
// 将code转化为function
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
...
}
}

为了方便理解,使用断点调试,来看一下 compileTemplate 都经历了哪些操作:

首先会判断是否需要预处理,如果需要预处理,则会对 template 模板进行预处理并返回处理结果,此处跳过预处理,直接进入 actuallCompile 函数

这里可以看到本身内部还有一层编译函数对 template 进行编译,这才是最核心的编译方法,而这个 compile 方法来源于 createCompilerCreator

createCompilerCreator 返回了两层函数,最终返回值则是 compile 和 compileToFunction,这两个是将 template 转为 render 函数的关键,可以看到 template 会被解析成 AST 树,最后通过 generate 方法转义成函数 code,接下来我们看一下parse函数中是如何将 template 转为 AST 的。

继续向下 debug 后,会走到 parseHTML 函数,这个函数是模板编译中用来解析 HTML 结构的核心方法,通过回调 + 递归最终遍历整个 HTML 结构并将其转化为 AST 树。

parseHTML 阶段

使用 parseHTML 解析成的 AST 创建 render 函数和 Vdom

genElement 阶段

将 AST 结构解析成为虚拟 dom 树

最终编译输出为 render 函数,得到最终打包构建的产物。

3、总结

到此我们应该了解了 vue 是如何打包构建将模板编译为渲染函数的,有了渲染函数后,只需要将渲染函数的 this 指向组件实例,即可和组件的响应式数据绑定。vue 的每一个组件都会对应一个渲染 Watcher ,他的本质作用是把响应式数据作为依赖收集,当响应式数据发生变化时,会触发 setter 执行响应式依赖通知渲染 Watcher 重新执行 render 函数做到页面数据的更新。

以上就是Vue2模版编译流程详解的详细内容,更多关于Vue2模版编译的资料请关注开源网www.osweb.cn其它相关文章!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

请填写验证码
开源分类
最近发表
开源网标签
开源网归档