专注、坚持

深入 Vue(二)—— Vue 2 的运行时与编译器

2021.02.22 by kingcos

前言

在深入 Vue 源码前,我们再尝试弄清楚一些模糊的概念。在早期使用 Vue CLI 创建 Vue 2 项目时,会有以下选项:

➜  demo vue init webpack my-project

? Project name my-project
? Project description A Vue.js project
? Author kingcos <2821836721v@gmail.com>
? Vue build (Use arrow keys)
❯ Runtime + Compiler: recommended for most users
  Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific H
TML) are ONLY allowed in .vue files - render functions are required elsewhere

即:

运行时 + 编译器:推荐多数用户选择 仅运行时:体积大约小 6KB(min+gzip),但模版(或任何 Vue 指定的 HTML)可在 .vue 文件中使用 - 其它地方需要 render 函数才可使用

实践

差异测试

话不多说,我们直接按两个选项分别建立 demo 工程。其中最明显的差异位于 main.js 的 Vue 对象构造:

import App from './App'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  render: h => h(App)
})

// -----

// Runtime + Compiler
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  components: { App },
  template: '<App/>'
})

// app.vue 实现完全一致
<template>
  <div id="app">
    <h3>{{ count }}</h3>
    <button type="button" v-on:click="increase">+</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      count: 666
    }
  },
  methods: {
    increase() {
      this.count += 1
    }
  }
}
</script>
<style></style>

从注释可区分出,上方仅运行时,App.vue 将通过 render 函数渲染;下方运行时 + 编译器,则直接设置模版为 <App/> 元素。我们尝试将仅运行时的 new Vue 实现替换为运行时 + 编译器版本,并重新运行,此时页面将无法正常加载,并在 console 输出以下内容:

[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
[Vue 警告]:正在使用 Vue 的仅运行时版本,其模板编译器将不可用。可将模板预编译为渲染(render)函数,或使用包含编译器的版本构建。

(found in <Root>)

此时我们改成 render 即可正常加载(以下代码在运行时 + 编译器版本下也可运行):

new Vue({
  el: '#app',
  components: { App },
  // template: '<App/>'
  render: (fn) => {
    return fn('App', App)
  }
})

我们在 render 函数中打个断点,其整体调用链路为:

Vue -> Vue._init -> Vue.$mount(挂载) -> mountComponent -> Watcher -> Wacther.get -> updateComponent -> Vue._render -> render(渲染)

打包

那么有个问题是,当我们使用 Webpack 等工具打包时,是如何区分使用仅运行时还是运行时 + 编译器的呢?我继续对比了两个 demo 的工程配置,最终找到了差异所在:

// ./build/webpack.base.conf.js
module.exports = {
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js', // <--- HERE
    }
  }
}

这里直接决定了打包时对 import Vue from 'vue' 的引入路径。即运行时 + 编译器将引入完整版的 vue.esm.js,否则将默认引入运行时

<!DOCTYPE html>
<html>
 <head></head>
 <body>
  <div id="app">
   <h3>666</h3>
   <button type="button">+</button>
  </div>
  <!-- Runtime-only default -->
  <script type="text/javascript" src="/static/js/vendor.71a44962bfab27b57f40.js"></script>
  <!-- Add 'vue$': 'vue/dist/vue.esm.js' -->
  <script type="text/javascript" src="/static/js/vendor.ee987f4daf07f4f02d0e.js"></script>
 </body>
</html>
// vendor.71a44962bfab27b57f40.js.map

{"version":3,"sources":["webpack:///./node_modules/vue/dist/vue.runtime.esm.js"]}

// vendor.ee987f4daf07f4f02d0e.js.map
{"version":3,"sources":["webpack:///./node_modules/vue/dist/vue.esm.js"]}

源码入口

Vue 仅运行时和运行时 + 编译器的差异主要体现在「编译器」。在上一篇文章中,我们实现了 compile 函数以对 Vue 模版进行编译转换为浏览器可解析的 HTML。Vue 源码中包含了不同版本的入口:

// vue/src/platforms/web/
entry-compiler.ts                  // 编译器入口
entry-runtime-esm.ts               // 运行时 ES Module 入口 
entry-runtime-with-compiler-esm.ts // 运行时+编译器 ESM 入口
entry-runtime-with-compiler.ts     // 运行时+编译器入口
entry-runtime.ts                   // 运行时入口

关于渲染与挂载的源码分析,请关注下篇内容。

Tips

脚手架

脚手架(中国大陆),亦称为鹰架(台湾)、棚架(香港)和排栅,是一种临时性的建筑工具,架设在正在组建或重建的楼房或建筑物,亦用于轮船等大型的移动式物品,供施工人员在墙壁等高处施工。

—— 维基百科

在前端工程实践中,脚手架一词经常会出现,其来自于英文中的 scaffold。泛指 Vue CLI 等一些开箱即用的工具链,你也可以在 Vue 官网 https://vuejs.org/guide/scaling-up/tooling.html 中查看 Vue 相关的工程脚手架。

当然,Vue 应用也可以不通过脚手架创建,如下我们可以直接通过 CDN 引入相关 JS 即可实现:

<div id="app">
    <h3>{{ count }}</h3>
    <button type="button" v-on:click="increase">+</button>
</div>

<!-- development version, includes helpful console warnings -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<!-- production version, optimized for size and speed -->
<!-- <script src="https://cdn.jsdelivr.net/npm/vue@2"></script> -->
<script>
var app = new Vue({
  el: '#app',
  data: {
    count: 666
  },
  methods: {
    increase() {
      this.count += 1
    }
  }
})
</script>

Vue 到底是什么?

我们在上一篇文章中将 miniVue 定义为一个类(class),但其实在 JavaScript 中本身并没有类这一类型。我们使用 typeof(miniVue) 也将获得 function 即函数。

那为什么 Vue 在构造时需要使用 new 关键字?原来 JavaScript 中还有一种特殊的函数 —— 构造函数,通常使用首字母大写的形式命名。使用 new 调用构造函数后将默认返回对象(object),其中可以使用 this 构造对象。

function MyVue (options) {
  console.log(this)
  if (!(this instanceof MyVue)) {
    console.warn('MyVue is a constructor and should be called with the `new` keyword')
  }
}
var a = MyVue({})     // undefined, this === window
var b = new MyVue({}) // MyVue, this -> object(MyVue)

在 Vue 2 源码 vue/src/core/instance/index.ts 中,Vue 就是如下构造函数的实现:

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

因此传入构造函数的内容将作为 options 参数整体传入并参与到后续的使用中。

createEmptyVNode

createEmptyVNode 即创建一个空节点函数:

var createEmptyVNode = function (text) {
    if (text === void 0) { text = ''; }
    var node = new VNode();
    node.text = text;
    node.isComment = true;
    return node;
};

参考