前言
在深入 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;
};