V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
answershuto
V2EX  ›  前端开发

聊聊 Vue.js 的 template 编译

  •  
  •   answershuto ·
    answershuto · 2017-10-08 21:50:10 +08:00 · 3028 次点击
    这是一个创建于 2638 天前的主题,其中的信息可能已经有所发展或是发生改变。

    写在前面

    因为对 Vue.js 很感兴趣,而且平时工作的技术栈也是 Vue.js ,这几个月花了些时间研究学习了一下 Vue.js 源码,并做了总结与输出。

    文章的原地址:https://github.com/answershuto/learnVue

    在学习过程中,为 Vue 加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习 Vue 源码的小伙伴有所帮助。

    可能会有理解存在偏差的地方,欢迎提 issue 指出,共同学习,共同进步。

    $mount

    首先看一下 mount 的代码

    /*把原本不带编译的$mount 方法保存下来,在最后会调用。*/
    const mount = Vue.prototype.$mount
    /*挂载组件,带模板编译*/
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && query(el)
    
      /* istanbul ignore if */
      if (el === document.body || el === document.documentElement) {
        process.env.NODE_ENV !== 'production' && warn(
          `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
        )
        return this
      }
    
      const options = this.$options
      // resolve template/el and convert to render function
      /*处理模板 templete,编译成 render 函数,render 不存在的时候才会编译 template,否则优先使用 render*/
      if (!options.render) {
        let template = options.template
        /*template 存在的时候取 template,不存在的时候取 el 的 outerHTML*/
        if (template) {
          /*当 template 是字符串的时候*/
          if (typeof template === 'string') {
            if (template.charAt(0) === '#') {
              template = idToTemplate(template)
              /* istanbul ignore if */
              if (process.env.NODE_ENV !== 'production' && !template) {
                warn(
                  `Template element not found or is empty: ${options.template}`,
                  this
                )
              }
            }
          } else if (template.nodeType) {
            /*当 template 为 DOM 节点的时候*/
            template = template.innerHTML
          } else {
            /*报错*/
            if (process.env.NODE_ENV !== 'production') {
              warn('invalid template option:' + template, this)
            }
            return this
          }
        } else if (el) {
          /*获取 element 的 outerHTML*/
          template = getOuterHTML(el)
        }
        if (template) {
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            mark('compile')
          }
    
          /*将 template 编译成 render 函数,这里会有 render 以及 staticRenderFns 两个返回,这是 vue 的编译时优化,static 静态不需要在 VNode 更新时进行 patch,优化性能*/
          const { render, staticRenderFns } = compileToFunctions(template, {
            shouldDecodeNewlines,
            delimiters: options.delimiters
          }, this)
          options.render = render
          options.staticRenderFns = staticRenderFns
    
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            mark('compile end')
            measure(`${this._name} compile`, 'compile', 'compile end')
          }
        }
      }
      /*Github:https://github.com/answershuto*/
      /*调用 const mount = Vue.prototype.$mount 保存下来的不带编译的 mount*/
      return mount.call(this, el, hydrating)
    }
    

    通过 mount 代码我们可以看到,在 mount 的过程中,如果 render 函数不存在( render 函数存在会优先使用 render )会将 template 进行 compileToFunctions 得到 render 以及 staticRenderFns。譬如说手写组件时加入了 template 的情况都会在运行时进行编译。而 render function 在运行后会返回 VNode 节点,供页面的渲染以及在 update 的时候 patch。接下来我们来看一下 template 是如何编译的。

    一些基础

    首先,template 会被编译成 AST 语法树,那么 AST 是什么?

    在计算机科学中,抽象语法树( abstract syntax tree 或者缩写为 AST ),或者语法树( syntax tree ),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。具体可以查看抽象语法树

    AST 会经过 generate 得到 render 函数,render 的返回值是 VNode,VNode 是 Vue 的虚拟 DOM 节点,具体定义如下:

    export default class VNode {
      tag: string | void;
      data: VNodeData | void;
      children: ?Array<VNode>;
      text: string | void;
      elm: Node | void;
      ns: string | void;
      context: Component | void; // rendered in this component's scope
      functionalContext: Component | void; // only for functional component root nodes
      key: string | number | void;
      componentOptions: VNodeComponentOptions | void;
      componentInstance: Component | void; // component instance
      parent: VNode | void; // component placeholder node
      raw: boolean; // contains raw HTML? (server only)
      isStatic: boolean; // hoisted static node
      isRootInsert: boolean; // necessary for enter transition check
      isComment: boolean; // empty comment placeholder?
      isCloned: boolean; // is a cloned node?
      isOnce: boolean; // is a v-once node?
      /*Github:https://github.com/answershuto*/
      
      constructor (
        tag?: string,
        data?: VNodeData,
        children?: ?Array<VNode>,
        text?: string,
        elm?: Node,
        context?: Component,
        componentOptions?: VNodeComponentOptions
      ) {
        /*当前节点的标签名*/
        this.tag = tag
        /*当前节点对应的对象,包含了具体的一些数据信息,是一个 VNodeData 类型,可以参考 VNodeData 类型中的数据信息*/
        this.data = data
        /*当前节点的子节点,是一个数组*/
        this.children = children
        /*当前节点的文本*/
        this.text = text
        /*当前虚拟节点对应的真实 dom 节点*/
        this.elm = elm
        /*当前节点的名字空间*/
        this.ns = undefined
        /*编译作用域*/
        this.context = context
        /*函数化组件作用域*/
        this.functionalContext = undefined
        /*节点的 key 属性,被当作节点的标志,用以优化*/
        this.key = data && data.key
        /*组件的 option 选项*/
        this.componentOptions = componentOptions
        /*当前节点对应的组件的实例*/
        this.componentInstance = undefined
        /*当前节点的父节点*/
        this.parent = undefined
        /*简而言之就是是否为原生 HTML 或只是普通文本,innerHTML 的时候为 true,textContent 的时候为 false*/
        this.raw = false
        /*静态节点标志*/
        this.isStatic = false
        /*是否作为跟节点插入*/
        this.isRootInsert = true
        /*是否为注释节点*/
        this.isComment = false
        /*是否为克隆节点*/
        this.isCloned = false
        /*是否有 v-once 指令*/
        this.isOnce = false
      }
    
      // DEPRECATED: alias for componentInstance for backwards compat.
      /* istanbul ignore next */
      get child (): Component | void {
        return this.componentInstance
      }
    }
    

    关于 VNode 的一些细节,请参考VNode 节点

    createCompiler

    createCompiler 用以创建编译器,返回值是 compile 以及 compileToFunctions。compile 是一个编译器,它会将传入的 template 转换成对应的 AST 树、render 函数以及 staticRenderFns 函数。而 compileToFunctions 则是带缓存的编译器,同时 staticRenderFns 以及 render 函数会被转换成 Funtion 对象。

    因为不同平台有一些不同的 options,所以 createCompiler 会根据平台区分传入一个 baseOptions,会与 compile 本身传入的 options 合并得到最终的 finalOptions。

    compileToFunctions

    首先还是贴一下 compileToFunctions 的代码。

      /*带缓存的编译器,同时 staticRenderFns 以及 render 函数会被转换成 Funtion 对象*/
      function compileToFunctions (
        template: string,
        options?: CompilerOptions,
        vm?: Component
      ): CompiledFunctionResult {
        options = options || {}
    
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production') {
          // detect possible CSP restriction
          try {
            new Function('return 1')
          } catch (e) {
            if (e.toString().match(/unsafe-eval|CSP/)) {
              warn(
                'It seems you are using the standalone build of Vue.js in an ' +
                'environment with Content Security Policy that prohibits unsafe-eval. ' +
                'The template compiler cannot work in this environment. Consider ' +
                'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
                'templates into render functions.'
              )
            }
          }
        }
        /*Github:https://github.com/answershuto*/
        // check cache
        /*有缓存的时候直接取出缓存中的结果即可*/
        const key = options.delimiters
          ? String(options.delimiters) + template
          : template
        if (functionCompileCache[key]) {
          return functionCompileCache[key]
        }
    
        // compile
        /*编译*/
        const compiled = compile(template, options)
    
        // check compilation errors/tips
        if (process.env.NODE_ENV !== 'production') {
          if (compiled.errors && compiled.errors.length) {
            warn(
              `Error compiling template:\n\n${template}\n\n` +
              compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
              vm
            )
          }
          if (compiled.tips && compiled.tips.length) {
            compiled.tips.forEach(msg => tip(msg, vm))
          }
        }
    
        // turn code into functions
        const res = {}
        const fnGenErrors = []
        /*将 render 转换成 Funtion 对象*/
        res.render = makeFunction(compiled.render, fnGenErrors)
        /*将 staticRenderFns 全部转化成 Funtion 对象 */
        const l = compiled.staticRenderFns.length
        res.staticRenderFns = new Array(l)
        for (let i = 0; i < l; i++) {
          res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors)
        }
    
        // check function generation errors.
        // this should only happen if there is a bug in the compiler itself.
        // mostly for codegen development use
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production') {
          if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
            warn(
              `Failed to generate render function:\n\n` +
              fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
              vm
            )
          }
        }
    
        /*存放在缓存中,以免每次都重新编译*/
        return (functionCompileCache[key] = res) 
      }
    

    我们可以发现,在闭包中,会有一个 functionCompileCache 对象作为缓存器。

      /*作为缓存,防止每次都重新编译*/
      const functionCompileCache: {
        [key: string]: CompiledFunctionResult;
      } = Object.create(null)
    

    在进入 compileToFunctions 以后,会先检查缓存中是否有已经编译好的结果,如果有结果则直接从缓存中读取。这样做防止每次同样的模板都要进行重复的编译工作。

        // check cache
        /*有缓存的时候直接取出缓存中的结果即可*/
        const key = options.delimiters
          ? String(options.delimiters) + template
          : template
        if (functionCompileCache[key]) {
          return functionCompileCache[key]
        }
    

    在 compileToFunctions 的末尾会将编译结果进行缓存

      /*存放在缓存中,以免每次都重新编译*/
      return (functionCompileCache[key] = res) 
    

    compile

      /*编译,将模板 template 编译成 AST 树、render 函数以及 staticRenderFns 函数*/
      function compile (
        template: string,
        options?: CompilerOptions
      ): CompiledResult {
        const finalOptions = Object.create(baseOptions)
        const errors = []
        const tips = []
        finalOptions.warn = (msg, tip) => {
          (tip ? tips : errors).push(msg)
        }
    
        /*做下面这些 merge 的目的因为不同平台可以提供自己本身平台的一个 baseOptions,内部封装了平台自己的实现,然后把共同的部分抽离开来放在这层 compiler 中,所以在这里需要 merge 一下*/
        if (options) {
          // merge custom modules
          /*合并 modules*/
          if (options.modules) {
            finalOptions.modules = (baseOptions.modules || []).concat(options.modules)
          }
          // merge custom directives
          if (options.directives) {
            /*合并 directives*/
            finalOptions.directives = extend(
              Object.create(baseOptions.directives),
              options.directives
            )
          }
          // copy other options
          for (const key in options) {
            /*合并其余的 options,modules 与 directives 已经在上面做了特殊处理了*/
            if (key !== 'modules' && key !== 'directives') {
              finalOptions[key] = options[key]
            }
          }
        }
    
        /*基础模板编译,得到编译结果*/
        const compiled = baseCompile(template, finalOptions)
        if (process.env.NODE_ENV !== 'production') {
          errors.push.apply(errors, detectErrors(compiled.ast))
        }
        compiled.errors = errors
        compiled.tips = tips
        return compiled
      }
    

    compile 主要做了两件事,一件是合并 option (前面说的将平台自有的 option 与传入的 option 进行合并),另一件是 baseCompile,进行模板 template 的编译。

    来看一下 baseCompile

    baseCompile

    function baseCompile (
      template: string,
      options: CompilerOptions
    ): CompiledResult {
      /*parse 解析得到 ast 树*/
      const ast = parse(template.trim(), options)
      /*
        将 AST 树进行优化
        优化的目标:生成模板 AST 树,检测不需要进行 DOM 改变的静态子树。
        一旦检测到这些静态树,我们就能做以下这些事情:
        1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。
        2.在 patch 的过程中直接跳过。
     */
      optimize(ast, options)
      /*根据 ast 树生成所需的 code (内部包含 render 与 staticRenderFns )*/
      const code = generate(ast, options)
      return {
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    }
    

    baseCompile 首先会将模板 template 进行 parse 得到一个 AST 语法树,再通过 optimize 做一些优化,最后通过 generate 得到 render 以及 staticRenderFns。

    parse

    parse 的源码可以参见https://github.com/answershuto/learnVue/blob/master/vue-src/compiler/parser/index.js#L53

    parse 会用正则等方式解析 template 模板中的指令、class、style 等数据,形成 AST 语法树。

    optimize

    optimize 的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程,diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。

    generate

    generate 是将 AST 语法树转化成 render funtion 字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。


    至此,我们的 template 模板已经被转化成了我们所需的 AST 语法树、render function 字符串以及 staticRenderFns 字符串。

    举个例子

    来看一下这段代码的编译结果

    <div class="main" :class="bindClass">
        <div>{{text}}</div>
        <div>hello world</div>
        <div v-for="(item, index) in arr">
            <p>{{item.name}}</p>
            <p>{{item.value}}</p>
            <p>{{index}}</p>
            <p>---</p>
        </div>
        <div v-if="text">
            {{text}}
        </div>
        <div v-else></div>
    </div>
    

    转化后得到 AST 树,如下图:

    img

    我们可以看到最外层的 div 是这颗 AST 树的根节点,节点上有许多数据代表这个节点的形态,比如 static 表示是否是静态节点,staticClass 表示静态 class 属性(非 bind:class )。children 代表该节点的子节点,可以看到 children 是一个长度为 4 的数组,里面包含的是该节点下的四个 div 子节点。children 里面的节点与父节点的结构类似,层层往下形成一棵 AST 语法树。

    再来看看由 AST 得到的 render 函数

    with(this){
        return _c(  'div',
                    {
                        /*static class*/
                        staticClass:"main",
                        /*bind class*/
                        class:bindClass
                    },
                    [
                        _c( 'div', [_v(_s(text))]),
                        _c('div',[_v("hello world")]),
                        /*这是一个 v-for 循环*/
                        _l(
                            (arr),
                            function(item,index){
                                return _c(  'div',
                                            [_c('p',[_v(_s(item.name))]),
                                            _c('p',[_v(_s(item.value))]),
                                            _c('p',[_v(_s(index))]),
                                            _c('p',[_v("---")])]
                                        )
                            }
                        ),
                        /*这是 v-if*/
                        (text)?_c('div',[_v(_s(text))]):_c('div',[_v("no text")])],
                        2
                )
    }
    

    _c,_v,_s,_q

    看了 render function 字符串,发现有大量的_c,_v,_s,_q,这些函数究竟是什么?

    带着问题,我们来看一下core/instance/render

    /*处理 v-once 的渲染函数*/
      Vue.prototype._o = markOnce
      /*将字符串转化为数字,如果转换失败会返回原字符串*/
      Vue.prototype._n = toNumber
      /*将 val 转化成字符串*/
      Vue.prototype._s = toString
      /*处理 v-for 列表渲染*/
      Vue.prototype._l = renderList
      /*处理 slot 的渲染*/
      Vue.prototype._t = renderSlot
      /*检测两个变量是否相等*/
      Vue.prototype._q = looseEqual
      /*检测 arr 数组中是否包含与 val 变量相等的项*/
      Vue.prototype._i = looseIndexOf
      /*处理 static 树的渲染*/
      Vue.prototype._m = renderStatic
      /*处理 filters*/
      Vue.prototype._f = resolveFilter
      /*从 config 配置中检查 eventKeyCode 是否存在*/
      Vue.prototype._k = checkKeyCodes
      /*合并 v-bind 指令到 VNode 中*/
      Vue.prototype._b = bindObjectProps
      /*创建一个文本节点*/
      Vue.prototype._v = createTextVNode
      /*创建一个空 VNode 节点*/
      Vue.prototype._e = createEmptyVNode
      /*处理 ScopedSlots*/
      Vue.prototype._u = resolveScopedSlots
    
      /*创建 VNode 节点*/
      vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
    

    通过这些函数,render 函数最后会返回一个 VNode 节点,在_update 的时候,经过 patch 与之前的 VNode 节点进行比较,得出差异后将这些差异渲染到真实的 DOM 上。

    关于

    作者:染陌

    Email: [email protected] or [email protected]

    Github: https://github.com/answershuto

    Blog:http://answershuto.github.io/

    知乎主页:https://www.zhihu.com/people/cao-yang-49/activities

    知乎专栏:https://zhuanlan.zhihu.com/ranmo

    掘金: https://juejin.im/user/58f87ae844d9040069ca7507

    osChina:https://my.oschina.net/u/3161824/blog

    转载请注明出处,谢谢。

    欢迎关注我的公众号

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2540 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 02:58 · PVG 10:58 · LAX 18:58 · JFK 21:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.