学习vue2.5源码之第五篇——compiler简述

template 与 render

在使用vue的时候,我们有两种方式来创建我们的HTML页面,第一种情况,也是大多情况下,我们会使用模板template的方式,因为这更易读易懂也是官方推荐的方法;第二种情况是使用render函数来生成HTML,它比template更接近编译器,这也对我们的JavaScript的编程能力要求更高。

实际上使用render函数的方式会比使用template的效率更高,因为vue会先将template编译成render函数,然后再走之后的流程,也就是说使用template会比render多走编译成render这一步,而这一步就是由我们的compiler来实现的啦,本节讲述的就是vue源码中的编译器compiler,它是如何一步步将template最后转换为render。

$mount小插曲

由于在vue实例的那一篇我漏掉了$mount的具体实现,而理解$mount的流程也会帮助我们更好地引入compiler,那我们就快速地看看$mount(假如觉得只想看编译部分的同学可以跳过这一段哦

entry-runtime-with-compiler.js

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
  // 没有render时将template转化为render
  if (!options.render) {
    let template = options.template

    // 有template
    if (template) {
      // 判断template类型(#id、模板字符串、dom元素)
      // template是字符串
      if (typeof template === 'string') {
        // template是#id
        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
            )
          }
        }
      // template是dom元素
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        // 无效template
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    // 无template
    } else if (el) {
      // 如果 render 函数和 template 属性都不存在,挂载 DOM 元素的 HTML 会被提取出来用作模板
      template = getOuterHTML(el)
    }

    // 执行template => compileToFunctions()
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  // 有render
  return mount.call(this, el, hydrating)
}

整体流程:

  • 用一个变量mount把原来的$mount方法存起来,再重写$mount方法
  • 然后对el进行处理,el可以是dom节点或者是节点的选择器字符串,若是后者的话在通过query(el)进行转换
  • el不能是html或者body元素(也就是说不能直接将vue绑定在html或者body标签上)
  • 若没有render函数
    • 若有template,判断template类型(#id、模板字符串、dom元素)
    • render函数和template都不存在,挂载DOM元素的HTML会被提取出来用作template
    • 执行template => compileToFunctions(),将template转换为render
  • 若有render函数
    • 走原来的$mount方法

这里就证明了使用template的话还是会先转换为render再进行下一步的操作,我们接着看下一步发生了什么吧~

runtime/index.js

上一个文件中的vue是来自这里的,我们在这里可以看到

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

可以看出这个$mount方法返回的是mountComponent这个方法,我们又继续找找

instance/lifecycle.js

原来mountComponent是在lifecycle.js中,兜兜转转我们又回到了实例的这一块来~

export function mountComponent () {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false
  
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

这里的操作就是在调用beforeMount钩子前检查选项里有没有render函数,没有的话我们就给它建个空的,然后我们执行vm._update(vm._render(), hydrating),再用watcher进行数据绑定,然后调用mounted钩子。关于_update_render的实现我们先卖个关子~ 等我们学到虚拟dom实现的时候再看。

compiler 整体流程

前面搞了这么多前戏,终于开始讲compiler了~ 还记得刚提到重写的$mount方法吗,里面将template转换为render是通过compileToFunctions方法实现的,我们看看他的来头,之后的逻辑会有点绕但是不难理解,提醒~~~~ 对于绕来绕去的源码有一个好的方法就是写demo + 打断点!根据你的需求去打断点看一下输出的内容是否符合你的预期,这会对你理解源码很有帮助哦,在后面的学习中我们也会用例子去分析~~~ 跟随着compileToFunctions的源头,我们走起!~

platforms/web/compiler/index.js

const { compile, compileToFunctions } = createCompiler(baseOptions)

src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile () {
  ...
})

src/compiler/create-compiler.js

export function createCompilerCreator (baseCompile){
  return function createCompiler (baseOptions) {
    function compile (template, options) {
      const finalOptions = Object.create(baseOptions)

      // merge
      if (options) {
        // merge custom modules
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        // merge custom directives
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives),
            options.directives
          )
        }
        // copy other options
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      // finalOptions 合并 baseOptions 和 options
      const compiled = baseCompile(template, finalOptions)
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

呼~一下子贴这么多代码,好怕被打,我们可以先把路找着,具体代码再慢慢看。
createCompilerCreator是个高阶函数,接受一个函数baseCompile,返回了一个函数createCompilercreateCompiler函数里又有一个compile函数,里面调用了baseCompile和最初传入的baseOptions,最后返回compile函数和compileToFunctions函数。emmm...有点乱呵,我画个图给你们将就着看吧。。。

我们先看create-compiler.js中的createCompilerCreator函数中的createCompiler函数中的compile函数中(好累。。。):

先是将参数baseOptions和传入的options进行合并得到finalOptions,再进行最关键一步(终于!):const compiled = baseCompile(template, finalOptions)

baseCompile函数就是最外层createCompilerCreator函数的一个参数,这个关键的流程我们等下就看,我们先继续,由baseCompile得到了我们想要的结果compiled,再返回给上一个函数createCompiler,在return中有我们要的一个函数,就是我们最开始调用的compileToFunctions,原来他就是通过一个函数将我们的compile结果转换为compileToFunctions

我们去看看这个转换函数createCompileToFunctionFn,然后对比一下转换前后两者的差别。在src/compiler/to-function.js
文件中,我就不贴代码了,你们自己对着源码看吧,我说一下里面主要完成的操作就是执行了compile函数得到原来的值再进行转化,再将其存进缓存中。

而原compile返回的结构是:

{
    ast,
    render,
    staticRenderFns
}

经过转化后没有了ast,而且将renderstaticRenderFns转换为函数的形式:

{
    render,
    staticRenderFns
}

看完了整体流程,我们看回很关键的函数baseCompile

baseCompile

function baseCompile (template, options) {
  const ast = parse(template.trim(), options)
  optimize(ast, options)
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}

其实这个函数很短但是阐述了我们编译的全过程

parse -> optimize -> generate

step 1 :先对template进行parse得到抽象语法树AST

step 2 :将AST进行静态优化

step 3 :由AST生成render

返回的格式就是

{
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
}

最后我放上来自web平台中的baseOptions的配置含义,方便你们以后看源码可以查询

{
  expectHTML: true, // 是否期望HTML,不知道是啥反正web中的是true
  modules, // klass和style,对模板中类和样式的解析
  directives, // v-model、v-html、v-text
  isPreTag, // v-pre标签
  isUnaryTag, // 单标签,比如img、input、iframe
  mustUseProp, // 需要使用props绑定的属性,比如value、selected等
  canBeLeftOpenTag, // 可以不闭合的标签,比如tr、td等
  isReservedTag, // 是否是保留标签,html标签和SVG标签
  getTagNamespace, // 命名空间,svg和math
  staticKeys: genStaticKeys(modules) // staticClass,staticStyle。
}

这三个步骤在接下来的文章里我们会进行更详细的分析~ 对于compiler的概念和整体的流程都基本讲完啦,谢谢你们的支持,如有分析错误之处可以随意提出来,我们一起探讨探讨~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容