>  기사  >  웹 프론트엔드  >  Vue에서 컴파일 작업 방법을 사용하는 방법

Vue에서 컴파일 작업 방법을 사용하는 방법

亚连
亚连원래의
2018-06-04 15:05:322734검색

这篇文章主要介绍了Vue 中的compile操作方法,非常不错,具有参考借鉴价值,需要的朋友参考下吧

在 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 函数,关于这个函数,它有点复杂,我画了一张图来看一看它的关系,可能会有误差,希望大侠们可以指出。

编译三步走

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

  • parse: 第一步,我们需要将 template 转换成抽象语法树(AST)。

  • optimizer: 第二步,我们对这个抽象语法树进行静态节点的标记,这样就可以优化渲染过程。

  • generateCode: 第三步,根据 AST 生成一个 render 函数字符串。

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

解析器

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

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

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

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

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

我们这个例子最后生成的大概就是这么一棵树,那么 Vue 是如何去做这样一些解析的呢?我们继续看。

在 parse 函数中,我们先是定义了非常多的全局属性以及函数,然后调用了 parseHTML 这么一个函数,这也是 parse 最核心的函数,这个函数会不断的解析模板,填充 root,最后把 root(AST) 返回回去。

parseHTML

在这个函数中,最重要的是 while 循环中的代码,而在解析过程中发挥重要作用的有这么几个正则表达式。

const attribute = /^\s*([^\s"&#39;<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|&#39;([^&#39;]*)&#39;+|([^\s"&#39;=<>`]+)))?/
const ncname = &#39;[a-zA-Z_][\\w\\-\\.]*&#39;
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('23a9defae34cf7eec58e22582e91c85b 标签,就会在这个节点上加上 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) : &#39;_c("p")&#39;
 return {
 render: `with(this){return $[code]}`,
 staticRenderFns: state.staticRenderFns
 }
}

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

with(this){return _c(&#39;p&#39;,{attrs:{"id":"demo"}},[(1>0)?_c(&#39;h1&#39;,[_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,我抱着这样的想法去找了找原因,最后我在知乎上找到了尤大大的回答,这是链接,大家可以去了解下。

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

javascript变量提升和闭包理解

浅谈angular4.0中路由传递参数、获取参数最nice的写法

Vue 仿QQ左滑删除组件功能

위 내용은 Vue에서 컴파일 작업 방법을 사용하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.