这篇文章主要介绍了Vue 中的compile操做方法,写的十分的全面细致,具备必定的参考价值,对此有须要的朋友能够参考学习下。若有不足之处,欢迎批评指正。css
在 Vue 里,模板编译也是很是重要的一部分,里面也很是复杂,此次探究不会深刻探究每个细节,而是走一个全景概要,来吧,你们和我一块儿去一探究竟。html
初体验前端
咱们看了 Vue 的初始化函数就会知道,在最后一步,它进行了 vm.mount 在两个地方定义过,分别是在 entry-runtime-with-compiler.js(简称:eMount) 和 runtime/index.js(简称:rMount) 这两个文件里,那么这两个有什么区别呢?vue
// entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount // 这个 $mount 其实就是 rMount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
const options = this.$options
if (!options.render) {
...
//欢迎加入前端全栈开发交流圈一块儿吹水聊天学习交流:864305860
if(template) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
...
}
return mount.call(this, el, hydrating)
}//欢迎加入前端全栈开发交流圈一块儿吹水聊天学习交流:864305860
复制代码
其实 eMount 最后仍是去调用的 rMount,只不过在 eMount 作了必定的操做,若是你提供了 render 函数,那么它会直接去调用 rMount,若是没有,它就会去找你有没有提供 template,若是你没有提供 template,它就会用 el 去查询 dom 生成 template,最后经过编译返回了一个 render 函数,再去调用 eMount。 从上面能够看出,最重要的一部分就是 compileToFunctions 这个函数,它最后返回了 render 函数,关于这个函数,它有点复杂,我画了一张图来看一看它的关系,可能会有偏差,但愿大侠们能够指出。node
编译三步走webpack
看一下这个编译的总体过程,咱们其实能够发现,最核心的部分就是在这里传进去的 baseCompile 作的工做: parse: 第一步,咱们须要将 template 转换成抽象语法树(AST)。 optimizer: 第二步,咱们对这个抽象语法树进行静态节点的标记,这样就能够优化渲染过程。 generateCode: 第三步,根据 AST 生成一个 render 函数字符串。 好了,咱们接下来就一个一个慢慢看。web
解析器面试
在解析器中有一个很是重要的概念 AST,你们能够去自行了解一下。 在 Vue 中,ASTNode 分几种不一样类型,关于 ASTNode 的定义在 flow/compile.js 里面,请看下图: 正则表达式
咱们用一个简单的例子来讲明一下:express
<div id="demo">
<h1>Latest Vue.js Commits</h1>
<p>{{1 + 1}}</p>
</div>
复制代码
咱们想想这段代码会生成什么样的 AST 呢?
咱们这个例子最后生成的大概就是这么一棵树,那么 Vue 是如何去作这样一些解析的呢?咱们继续看。 在 parse 函数中,咱们先是定义了很是多的全局属性以及函数,而后调用了 parseHTML 这么一个函数,这也是 parse 最核心的函数,这个函数会不断的解析模板,填充 root,最后把 root(AST) 返回回去。
parseHTML
在这个函数中,最重要的是 while 循环中的代码,而在解析过程当中发挥重要做用的有这么几个正则表达式。
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
//欢迎加入前端全栈开发交流圈一块儿吹水聊天学习交流:864305860
复制代码
Vue 经过上面几个正则表达式去匹配开始结束标签、标签名、属性等等。 关于 while 的详细注解我放在我仓库里了,有兴趣的能够去看看。 在 while 里,其实就是不断的去用 html.indexOf('<') 去匹配,而后根据返回的索引的不一样去作不一样的解析处理:
parse 函数就是不断的重复这个工做,而后将 template 转换成 AST,在解析过程当中,其实对于标签与标签之间的空格,Vue 也作了优化处理,有些元素之间的空格是没用的。 compile 其实要说要说很是多的篇幅,可是这里只能简单的理一下思路,具体代码还须要各位下去深扣。
优化器
从代码中的注释咱们能够看出,优化器的目的就是去找出 AST 中纯静态的子树: 把纯静态子树提高为常量,每次从新渲染的时候就不须要建立新的节点了 在 patch 的时候就能够跳过它们 optimize 的代码量没有 parse 那么多,咱们来看看:
export function optimize (root: ?ASTElement, options: CompilerOptions) {
// 判断 root 是否存在
if (!root) return
// 判断是不是静态的属性
// 'type,tag,attrsList,attrsMap,plain,parent,children,attrs'
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// 判断是不是平台保留的标签,html 或者 svg 的
isPlatformReservedTag = options.isReservedTag || no
// 第一遍遍历: 给全部静态节点打上是不是静态节点的标记
markStatic(root)
// 第二遍遍历:标记全部静态根节点
markStaticRoots(root, false)
}//欢迎加入前端全栈开发交流圈一块儿吹水聊天学习交流:864305860
复制代码
下面两段代码我都剪切了一部分,由于有点多,这里就不贴太多代码了,详情请参考个人仓库。
第一遍遍历
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
...
}
}
复制代码
其实 markStatic 就是一个递归的过程,不断地去检查 AST 上的节点,而后打上标记。 刚刚咱们说过,AST 节点分三种,在 isStatic 这个函数中咱们对不一样类型的节点作了判断:
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))//欢迎加入前端全栈开发交流圈一块儿吹水聊天学习交流:864305860
}
复制代码
能够看到 Vue 对下面几种状况作了处理:
当这个节点的 type 为 2,也就是表达式节点的时候,很明显它不是一个静态节点,因此返回 false 当 type 为 3 的时候,也就是文本节点,那它就是一个静态节点,返回 true 若是你在元素节点中使用了 v-pre 或者使用了
标签,就会在这个节点上加上 pre 为 true,那么这就是个静态节点
若是它是静态节点,那么须要它不能有动态的绑定、不能有 v-if、v-for、v-else 这些指令,不能是 slot 或者 component 标签、不是咱们自定义的标签、没有父节点或者元素的父节点不能是带 v-for 的 template、 这个节点的属性都在 type,tag,attrsList,attrsMap,plain,parent,children,attrs 里面,知足这些条件,就认为它是静态的节点。
接下来,就开始对 AST 进行递归操做,标记静态的节点,至于里面作了哪些操做,能够到上面那个仓库里去看,这里就不展开了。
第二遍遍历
第二遍遍历的过程是标记静态根节点,那么咱们对静态根节点的定义是什么,首先根节点的意思就是他不能是叶子节点,起码要有子节点,而且它是静态的。在这里 Vue 作了一个说明,若是一个静态节点它只拥有一个子节点而且这个子节点是文本节点,那么就不作静态处理,它的成本大于收益,不如直接渲染。
一样的,咱们在函数中不断的递归进行标记,最后在全部静态根节点上加上 staticRoot 的标记,关于这段代码也能够去上面的仓库看一看。
代码生成器
在这个函数中,咱们将 AST 转换成为 render 函数字符串,代码量仍是挺多的,咱们能够来看一看。
能够看到在最后代码生成阶段,最重要的函数就是 genElement 这个函数,针对不一样的指令、属性,咱们会选择不一样的代码生成函数。最后咱们按照 AST 生成拼接成一个字符串,以下所示:
在 render 这个函数字符串中,咱们会看到一些函数,那么这些函数是在什么地方定义的呢?咱们能够在 core/instance/index.js 这个文件中找到这些函数:
在编译结束后,咱们根据不一样的指令、属性等等去选择须要调用哪个处理函数,最后拼接成一个函数字符串。
咱们能够很清楚的看到,最后生成了一个 render 渲染字符串,那么咱们要如何去使用它呢?其实在后面进行渲染的时候,咱们进行了 new Function(render) 的操做,而后咱们就可以正常的使用 render 函数了。
结语
感谢您的观看,若有不足之处,欢迎批评指正。
本次给你们推荐一个免费的学习群,里面归纳移动应用网站开发,css,html,webpack,vue node angular以及面试资源等。 对web开发技术感兴趣的同窗,欢迎加入Q群:864305860,无论你是小白仍是大牛我都欢迎,还有大牛整理的一套高效率学习路线和教程与您免费分享,同时天天更新视频资料。 最后,祝你们早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 这就是编译的一些参数
const state = new CodegenState(options)
// 生成 render 字符串
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return $[code]}`,
staticRenderFns: state.staticRenderFns
}//欢迎加入前端全栈开发交流圈一块儿吹水聊天学习交流:864305860
}
复制代码with(this){return _c('div',{attrs:{"id":"demo"}},[(1>0)?_c('h1',[_v("Latest Vue.js Commits")]):_e(),...}
复制代码// v-once
target._o = markOnce
// 转换
target._n = toNumber
target._s = toString
// v-for
target._l = renderList
// slot
target._t = renderSlot
// 是否相等
target._q = looseEqual
// 检测数组里是否有相等的值
target._i = looseIndexOf
// 渲染静态树
target._m = renderStatic
// 过滤器处理
target._f = resolveFilter
// 检查关键字
target._k = checkKeyCodes
// v-bind
target._b = bindObjectProps
// 建立文本节点
target._v = createTextVNode
// 建立空节点
target._e = createEmptyVNode
// 处理 scopeslot
target._u = resolveScopedSlots
// 处理事件绑定
target._g = bindObjectListeners
// 建立 VNode 节点
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
//欢迎加入前端全栈开发交流圈一块儿吹水聊天学习交流:864305860
复制代码