|
这是第 346篇不掺水的,想要了解更多,请戳下方卡片关注我们吧~Vue2模版编译流程详解http://zoo.zhengcaiyun.cn/blog/article/vue2图片vue 中有这样一张响应式系统的流程图,vue 会将模板语法编译成 render 函数,通过 render 函数渲染生成 Virtual dom,但是官方并没有对模板编译有详细的介绍,这篇文章带大家一起学习下 vue 的模板编译。为了更好理解 vue 的模板编译这里我整理了一份模板编译的整体流程,如下所示,下面将用源码解读的方式来找到模板编译中的几个核心步骤,进行详细说明:图片1、起步这里我使用 webpack 来打包 vue 文件,来分析 vue 在模板编译中的具体流程,如下所示,下面是搭建的项目结构和文件内容:项目结构├─package-lock.json├─package.json├─src| ├─App.vue| └index.js├─dist| └main.js├─config| └webpack.config.jsApp.vue {{ count }} webpack.config.jsconst { VueLoaderPlugin } = require('vue-loader')module.exports = { mode: 'development', module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, // 它会应用到普通的 `.js` 文件 // 以及 `.vue` 文件中的 `\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-loadertemplate-loader 的作用是将 import { render, staticRenderFns } from "./App.vuevue&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 = trueexport { 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)) { // 获取以 ') 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) ) { // ]*>)', '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 = 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 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 = truecompilerToFunction 阶段将 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 函数做到页面数据的更新。参考文献vue 2 官方文档 ( https://v2.cn.vuejs.org/ )看完两件事如果你觉得这篇内容对你挺有启发,我想邀请你帮我两件小事1.点个「在看」,让更多人也能看到这篇内容(点了「在看」,bug -1 )2.关注公众号「政采云技术」,持续为你推送精选好文招贤纳士政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队。团队现有 80 余个前端小伙伴,平均年龄 27 岁,近 4 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、智能化平台、性能体验、云端应用、数据分析、错误监控及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给ZooTeam@cai-inc.com
|
|