Vue 中的compile操作方法


Posted in Javascript onFebruary 26, 2018

在 Vue 里,模板编译也是非常重要的一部分,里面也非常复杂,这次探究不会深入探究每一个细节,而是走一个全景概要,来吧,大家和我一起去一探究竟。

初体验

我们看了 Vue 的初始化函数就会知道,在最后一步,它进行了 vm.$mount(el) 的操作,而这个 $mount 在两个地方定义过,分别是在 entry-runtime-with-compiler.js(简称:eMount) 和 runtime/index.js(简称:rMount) 这两个文件里,那么这两个有什么区别呢?

// 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) {
 ...
 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)
}

其实 eMount 最后还是去调用的 rMount,只不过在 eMount 做了一定的操作,如果你提供了 render 函数,那么它会直接去调用 rMount,如果没有,它就会去找你有没有提供 template,如果你没有提供 template,它就会用 el 去查询 dom 生成 template,最后通过编译返回了一个 render 函数,再去调用 eMount。

从上面可以看出,最重要的一部分就是 compileToFunctions 这个函数,它最后返回了 render 函数,关于这个函数,它有点复杂,我画了一张图来看一看它的关系,可能会有误差,希望大侠们可以指出。

Vue 中的compile操作方法

编译三步走

看一下这个编译的整体过程,我们其实可以发现,最核心的部分就是在这里传进去的 baseCompile 做的工作:

  • parse: 第一步,我们需要将 template 转换成抽象语法树(AST)。
  • optimizer: 第二步,我们对这个抽象语法树进行静态节点的标记,这样就可以优化渲染过程。
  • generateCode: 第三步,根据 AST 生成一个 render 函数字符串。

好了,我们接下来就一个一个慢慢看。

解析器

在解析器中有一个非常重要的概念 AST,大家可以去自行了解一下。

在 Vue 中,ASTNode 分几种不同类型,关于 ASTNode 的定义在 flow/compile.js 里面,请看下图:

Vue 中的compile操作方法

我们用一个简单的例子来说明一下:

<div id="demo">
 <h1>Latest Vue.js Commits</h1>
 <p>{{1 + 1}}</p>
</div>

我们想一想这段代码会生成什么样的 AST 呢?

Vue 中的compile操作方法

我们这个例子最后生成的大概就是这么一棵树,那么 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 = /^<!\[/

Vue 通过上面几个正则表达式去匹配开始结束标签、标签名、属性等等。

关于 while 的详细注解我放在我仓库里了,有兴趣的可以去看看。

在 while 里,其实就是不断的去用 html.indexOf('<') 去匹配,然后根据返回的索引的不同去做不同的解析处理:

  • __等于 0:__这就代表这是注释、条件注释、doctype、开始标签、结束标签中的某一种
  • __大于等于 0:__这就说明是文本、表达式
  • __小于 0:__表示 html 标签解析完了,可能会剩下一些文本、表达式

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)
}

下面两段代码我都剪切了一部分,因为有点多,这里就不贴太多代码了,详情请参考我的仓库。

第一遍遍历

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)
 ))
}

可以看到 Vue 对下面几种情况做了处理:

当这个节点的 type 为 2,也就是表达式节点的时候,很明显它不是一个静态节点,所以返回 false

当 type 为 3 的时候,也就是文本节点,那它就是一个静态节点,返回 true

如果你在元素节点中使用了 v-pre 或者使用了 <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 函数字符串,代码量还是挺多的,我们可以来看一看。

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
 }
}

可以看到在最后代码生成阶段,最重要的函数就是 genElement 这个函数,针对不同的指令、属性,我们会选择不同的代码生成函数。最后我们按照 AST 生成拼接成一个字符串,如下所示:

with(this){return _c('div',{attrs:{"id":"demo"}},[(1>0)?_c('h1',[_v("Latest Vue.js Commits")]):_e(),...}

在 render 这个函数字符串中,我们会看到一些函数,那么这些函数是在什么地方定义的呢?我们可以在 core/instance/index.js 这个文件中找到这些函数:

// 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)

在编译结束后,我们根据不同的指令、属性等等去选择需要调用哪一个处理函数,最后拼接成一个函数字符串。

我们可以很清楚的看到,最后生成了一个 render 渲染字符串,那么我们要如何去使用它呢?其实在后面进行渲染的时候,我们进行了 new Function(render) 的操作,然后我们就能够正常的使用 render 函数了。

总结

大流程走完之后,我相信大家会对编译过程有一个比较清晰的认识,然后再去挖细节相信也会容易的多了,读源码,其实并不是一个为了读而读的过程,我们可以在源码中学到很多我们可能在日常开发中没有了解到的知识。
至于最后代码生成器中的那一大段代码,我还没有把它注释好,后面应该会将源码注释放到仓库里,不过我也相信大家也能够顺利的去读懂源码。

还有一点要提的是在 render 函数中,Vue 使用了 with 函数,我们平时肯定没见过,因为官方不推荐我们去使用 with,我抱着这样的想法去找了找原因,最后我在知乎上找到了尤大大的回答,这是链接,大家可以去了解下。

以上所述是小编给大家介绍的Vue 中的compile,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
Javascript JSQL,SQL无处不在,
May 05 Javascript
读jQuery之二(两种扩展)
Jun 11 Javascript
调试Node.JS的辅助工具(NodeWatcher)
Jan 04 Javascript
如何使用json在前后台进行数据传输实例介绍
Apr 11 Javascript
jquery如何实现在加载完iframe的内容后再进行操作
Sep 10 Javascript
JS实现超精简响应鼠标显示二级菜单代码
Sep 12 Javascript
createObjectURL方法实现本地图片预览
Sep 30 Javascript
JavaScript中this函数使用实例解析
Feb 21 Javascript
微信小程序实现搜索功能
Mar 10 Javascript
vue 组件之间事件触发($emit)与event Bus($on)的用法说明
Jul 28 Javascript
VUE+Element实现增删改查的示例源码
Nov 23 Vue.js
微信小程序自定义支持图片的弹窗
Dec 21 Javascript
element ui 对话框el-dialog关闭事件详解
Feb 26 #Javascript
vue中简单弹框dialog的实现方法
Feb 26 #Javascript
基于 D3.js 绘制动态进度条的实例详解
Feb 26 #Javascript
vue实现模态框的通用写法推荐
Feb 26 #Javascript
Vue.js 2.0和Cordova开发webApp环境搭建方法
Feb 26 #Javascript
浅谈ajax请求不同页面的微信JSSDK问题
Feb 26 #Javascript
详解Node 定时器
Feb 26 #Javascript
You might like
PHP中改变图片的尺寸大小的代码
2011/07/17 PHP
windows7下安装php的php-ssh2扩展教程
2014/07/04 PHP
浅谈PHP定义命令空间的几个注意点(推荐)
2016/10/29 PHP
php+redis实现商城秒杀功能
2020/11/19 PHP
读jQuery之九 一些瑕疵说明
2011/06/21 Javascript
extjs4 treepanel动态改变行高度示例
2013/12/17 Javascript
JavaScript通过元素的ID和name设置样式
2014/07/08 Javascript
jQuery实现的fixedMenu下拉菜单效果代码
2015/08/24 Javascript
jQuery validate插件submitHandler提交导致死循环解决方法
2016/01/21 Javascript
jquery对象和DOM对象的相互转换详解
2016/10/18 Javascript
jQuery+CSS3实现点赞功能
2017/03/13 Javascript
Bootstrap DateTime Picker日历控件简单应用
2017/03/25 Javascript
Vue press 支持图片放大功能的实例代码
2018/11/09 Javascript
trackingjs+websocket+百度人脸识别API实现人脸签到
2018/11/26 Javascript
vue实现PC端录音功能的实例代码
2019/06/05 Javascript
vue实现中部导航栏布局功能
2019/07/30 Javascript
改变layer confirm弹窗按钮的颜色方法
2019/09/12 Javascript
JS实现随机抽取三人
2019/11/06 Javascript
如何正确解决VuePress本地访问出现资源报错404的问题
2020/12/03 Vue.js
Python实现把utf-8格式的文件转换成gbk格式的文件
2015/01/22 Python
Python操作RabbitMQ服务器实现消息队列的路由功能
2016/06/29 Python
Python爬取当当、京东、亚马逊图书信息代码实例
2017/12/09 Python
使用python 和 lint 删除项目无用资源的方法
2017/12/20 Python
Python读取视频的两种方法(imageio和cv2)
2018/04/15 Python
Python制作exe文件简单流程
2019/01/24 Python
Python使用sklearn实现的各种回归算法示例
2019/07/04 Python
CSS3基础(RGBa、text-shadow、box-shadow、border-radius)
2012/11/13 HTML / CSS
eDreams澳大利亚:预订机票、酒店和度假产品
2017/04/19 全球购物
TheFork葡萄牙:欧洲领先的在线餐厅预订平台
2019/05/27 全球购物
碧欧泉Biotherm加拿大官方网站:法国高端护肤品牌
2019/10/18 全球购物
说明书格式及范文
2014/05/07 职场文书
社区维稳工作方案
2014/06/06 职场文书
机关党员进社区活动总结
2014/07/05 职场文书
表扬通报怎么写
2015/01/16 职场文书
《地震中的父与子》教学反思
2016/02/16 职场文书
go语言中fallthrough的用法说明
2021/05/06 Golang