【读vue 源码】溯源 import Vue from 'vue' 到底作了什么?

阅读资源

vue.js源码托管地址javascript

flow 静态检查工具地址html

rollup 源码构建vue

flow 静态检查工具

flow 是由Facebook出品的javascript静态类型检查工具。vue.js 的源码是采用flow作了静态类型检查。由于javascript是动态类型的语言。语言灵活的同时也很容易引起一些隐蔽的隐患代码。在编译的时候看上去不会报错,但运行的时候就会出现奇奇怪怪的bug。而类型检查,就是在编译期尽早发现由类型错误引发的bug,又不影响代码运行(不须要运行时动态检查类型)。一些复杂的项目使用工具的手段能够加强代码的可读性,增长项目的可维护性。java

flow 类型检查分为两种方式:

  • 类型推断:经过变量使用的上下文来推断出变量类型,而后根据推断来检查类型。
  • 类型注释:注释好变量的类型,经过注释来检查类型。
// 类型推断
// @flow
function split(str) {
    return str.split(' ')
}
split(1)      // Error!
split('abc')  // Works!
复制代码

传入参数 1 的时候,flow 代码检查就会报错,由于 split()方法是字符串原型对象上的方法,它期待的参数类型是字符串,而传入的确实是数字型,因此就会报错。当传入‘abc’字符串后就能够正常执行代码了。node

// 类型注释
// @flow
function concat(a: string, b: string) {
  return a + b;
}

concat("A", "B");   // Works!
concat(1, 2);       // Error!
复制代码

由于加运算符,便可以执行数字的相加,也能够执行字符串的拼接。因此 concat()方法提早注释好参数的类型。接收的参数a和b都是字符串,因此调用concat(1, 2);就会报错。调用concat("A", "B")就能正常执行。webpack

须要注意的是:当一个文件中出现注释@flow 的标记,说明该文件是须要用flow 进行类型检查的,不然不进行 flow 检查。更多内容请查看flow 静态检查工具官方文档。git

vue.js源码中的flow应用

在 Vue.js 的主目录下有 .flowconfig 文件, 它是 Flow 的配置文件。以下:github

[ignore]  // 忽略的文件
.*/node_modules/.*
.*/test/.*
.*/scripts/.*
.*/examples/.*
.*/benchmarks/.*

[include]

[libs] // 这里 [libs] 配置的是 flow,表示指定的库定义都在 flow 文件夹内
flow   // 对应的 flow 目录

[options]
unsafe.enable_getters_and_setters=true
module.name_mapper='^compiler/\(.*\)$' -> '<PROJECT_ROOT>/src/compiler/\1'
module.name_mapper='^core/\(.*\)$' -> '<PROJECT_ROOT>/src/core/\1'
module.name_mapper='^shared/\(.*\)$' -> '<PROJECT_ROOT>/src/shared/\1'
module.name_mapper='^web/\(.*\)$' -> '<PROJECT_ROOT>/src/platforms/web/\1'
module.name_mapper='^weex/\(.*\)$' -> '<PROJECT_ROOT>/src/platforms/weex/\1'
module.name_mapper='^server/\(.*\)$' -> '<PROJECT_ROOT>/src/server/\1'
module.name_mapper='^entries/\(.*\)$' -> '<PROJECT_ROOT>/src/entries/\1'
module.name_mapper='^sfc/\(.*\)$' -> '<PROJECT_ROOT>/src/sfc/\1'
suppress_comment= \\(.\\|\n\\)*\\$flow-disable-line
复制代码

vue.js 内的flow目录说明

flow
├── compiler.js        # 编译相关
├── component.js       # 组件数据结构
├── global-api.js      # Global API 结构
├── modules.js         # 第三方库定义
├── options.js         # 选项相关
├── ssr.js             # 服务端渲染相关
├── vnode.js           # 虚拟 node 相关
复制代码

vue 源码的目录结构

Vue.js 的源码都在 src 目录下,目录结构以下:web

src
├── compiler    # 包含 Vue.js 全部编译相关的代码。
├── core        # 包含了 Vue.js 的核心代码,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等。
├── platforms   # Vue.js 是一个跨平台的 MVVM 框架,它能够跑在 web 上,也能够配合 weex 跑在 native 客户端上。
├── server      # 全部服务端渲染相关的逻辑都在这个目录下。
├── sfc         # vue.js经过 .vue 单文件来编写组件。这个目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象。
├── shared      # 定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的。
复制代码

从整个目录结构来看,做者把功能模块拆的很是的清楚,相关的逻辑都放在同一个目录下来进行维护。可复用的代码也单独成为一个文件夹。vue-cli

vue.js源码构建

Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下。Rollup 是一个javascript 的模块打包工具。相比webpack更为轻量。了解更多请访问Rollup Github 地址

构建脚本

NPM 托管的项目都会有一个package.json的文件,对这个项目加以描述。script 字段用来定义NPM 的执行脚本。vue.js 的执行构建的脚本以下:

"scripts": {
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
  },
复制代码

也就是说,当咱们执行 npm run build的时候,实际上就是执行 node scripts/build.js这条语句。也就是说 scripts/build.js就是构建入口的js文件。

构建过程

1. 从构建的入口文件开始:scripts/build.js

let builds = require('./config').getAllBuilds() //拿到全部的配置

// filter builds via command line arg
if (process.argv[2]) {
  const filters = process.argv[2].split(',')
  builds = builds.filter(b => {
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
  })
} else {
  // filter out weex builds by default
  builds = builds.filter(b => {
    return b.output.file.indexOf('weex') === -1
  })
}

build(builds)
复制代码

上面这部分代码,首先从配置文件scripts/config.js中读取配置相关的数据,在对配置进行相应的过滤,从而构建出不一样用途的vue.js。

2. 查看构建的配置文件:scripts/config.js

const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.dev.js'),
    format: 'cjs',
    env: 'development',
    banner
  },
  'web-runtime-cjs-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.prod.js'),
    format: 'cjs',
    env: 'production',
    banner
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.dev.js'),
    format: 'cjs',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  'web-full-cjs-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.prod.js'),
    format: 'cjs',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime only ES modules build (for bundlers)
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  // Runtime+compiler ES modules build (for bundlers)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
    format: 'es',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.js'),
    format: 'es',
    transpile: false,
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.min.js'),
    format: 'es',
    transpile: false,
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
    format: 'umd',
    env: 'development',
    banner
  },
  // runtime-only production build (Browser)
  'web-runtime-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.min.js'),
    format: 'umd',
    env: 'production',
    banner
  },
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler production build (Browser)
  'web-full-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.min.js'),
    format: 'umd',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // Web compiler (CommonJS).
  'web-compiler': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
  },
  // Web compiler (UMD for in-browser use).
  'web-compiler-browser': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/browser.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'VueTemplateCompiler',
    plugins: [node(), cjs()]
  },
  // Web server renderer (CommonJS).
  'web-server-renderer-dev': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.dev.js'),
    format: 'cjs',
    env: 'development',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-prod': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.prod.js'),
    format: 'cjs',
    env: 'production',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-basic': {
    entry: resolve('web/entry-server-basic-renderer.js'),
    dest: resolve('packages/vue-server-renderer/basic.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'renderVueComponentToString',
    plugins: [node(), cjs()]
  },
  'web-server-renderer-webpack-server-plugin': {
    entry: resolve('server/webpack-plugin/server.js'),
    dest: resolve('packages/vue-server-renderer/server-plugin.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-webpack-client-plugin': {
    entry: resolve('server/webpack-plugin/client.js'),
    dest: resolve('packages/vue-server-renderer/client-plugin.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  // Weex runtime factory
  'weex-factory': {
    weex: true,
    entry: resolve('weex/entry-runtime-factory.js'),
    dest: resolve('packages/weex-vue-framework/factory.js'),
    format: 'cjs',
    plugins: [weexFactoryPlugin]
  },
  // Weex runtime framework (CommonJS).
  'weex-framework': {
    weex: true,
    entry: resolve('weex/entry-framework.js'),
    dest: resolve('packages/weex-vue-framework/index.js'),
    format: 'cjs'
  },
  // Weex compiler (CommonJS). Used by Weex's Webpack loader.
  'weex-compiler': {
    weex: true,
    entry: resolve('weex/entry-compiler.js'),
    dest: resolve('packages/weex-template-compiler/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/weex-template-compiler/package.json').dependencies)
  }
}
复制代码

上面这部分代码是vue.js构建的配置、服务端渲染webpack插件、weex的打包配置。对于单个配置,遵循了Rollup 的构建规则。配置说明:

  • entry属性:构建入口js文件的地址。
  • dest属性:构建完成后的js文件地址
  • format属性:构建文件的格式。'cjs'表示构建出来的文件遵循 CommonJS 规范;'es' 表示构建出来的文件遵循 ES Module 规范; 'umd' 表示构建出来的文件遵循 UMD 规范。
  • banner属性:对vue.js的一个简单的描述。包含做者信息,版本号等。

3. 以一个配置为例探寻构建过程:web-runtime-cjs

'web-runtime-cjs-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.dev.js'),
    format: 'cjs',
    env: 'development',
    banner
  },
复制代码

从配置中可见:入口的js文件地址,与完成后的js地址,均调用了resolve() 方法。

const aliases = require('./alias')
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}
复制代码

resolve() 方法将传入的参数 p 调用split()方法,经过'/'分割成数组,而后取第一个元素设置为base,那么上述案例中 base即为 web。可是base 并非真实路径,而是借助了别名的配置。别名配置的代码以下:scripts/alias

const path = require('path')

const resolve = p => path.resolve(__dirname, '../', p)
// 到真实文件的一个映射关系
module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  sfc: resolve('src/sfc')
}
复制代码

由上述代码可知:web 对应的知识路径是path.resolve(__dirname, '../', 'src/platforms/web')。由此找到它的入口文件是src/platforms/web/entry-runtime.js它通过 Rollup 的构建打包后,最终会在 dist/vue.runtime.common.js

Runtime Only VS Runtime + Compiler

一般咱们利用 vue-cli 去初始化咱们的 Vue.js 项目的时候会询问是用 Runtime Only 版本的仍是 Runtime + Compiler 版本。他们的区别以下:

  • Runtime Only 一般须要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript,将template 编译成render 函数。由于是在编译阶段作的,因此它只包含运行时的 Vue.js 代码,所以代码体积也会更轻量。

  • Runtime + Compiler 咱们若是没有对代码作预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则须要在客户端编译模板,以下所示:

// 须要编译器的版本
new Vue({
  template: '<div>{{ hi }}</div>'
})

// 这种状况不须要
new Vue({
  render (h) {
    return h('div', this.hi)
  }
})
复制代码

综上:由于在 Vue.js 2.0 中,最终渲染都是经过 render 函数,若是写 template 属性,则须要编译成 render函数,那么这个编译过程会发生运行时,因此须要带有编译器的版本。显然,这个编译过程对性能会有必定损耗,因此推荐使用 Runtime Only

vue 的入口

当咱们开发的时候import Vue from 'vue'到底作了些什么?顺着 Runtime Only 构建出来的vue.js 它的入口是在src/platforms/web/entry-runtime.js代码以下:

/* @flow */

import Vue from './runtime/index'

export default Vue 
复制代码

上述代码 导出一个 Vue,而这个Vue是从./runtime/index导入的。

vue静态的全局配置和原型对象上的方法

继续看./runtime/index文件。代码以下:

/* @flow */

import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'

import {
 query,
 mustUseProp,
 isReservedTag,
 isReservedAttr,
 getTagNamespace,
 isUnknownElement
} from 'web/util/index'

import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

// install platform specific utils
// 静态的全局配置
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

// install platform patch function
// 原型__patch__
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
// 定义了原型上的$mount 方法
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component {
 el = el && inBrowser ? query(el) : undefined
 return mountComponent(this, el, hydrating)
}

// devtools global hook
/* istanbul ignore next */
if (inBrowser) {
 setTimeout(() => {
   if (config.devtools) {
     if (devtools) {
       devtools.emit('init', Vue)
     } else if (
       process.env.NODE_ENV !== 'production' &&
       process.env.NODE_ENV !== 'test'
     ) {
       console[console.info ? 'info' : 'log'](
         'Download the Vue Devtools extension for a better development experience:\n' +
         'https://github.com/vuejs/vue-devtools'
       )
     }
   }
   if (process.env.NODE_ENV !== 'production' &&
     process.env.NODE_ENV !== 'test' &&
     config.productionTip !== false &&
     typeof console !== 'undefined'
   ) {
     console[console.info ? 'info' : 'log'](
       `You are running Vue in development mode.\n` +
       `Make sure to turn on production mode when deploying for production.\n` +
       `See more tips at https://vuejs.org/guide/deployment.html`
     )
   }
 }, 0)
}

export default Vue
复制代码

上述代码仍是从core/index文件中导入一个Vue,最后将其导出。在该文件中定义了Vue的一些静态的全局配置,和原型对象上的方法。

经过initGlobalAPI 给 vue 添加静态方法

继续往下看core/index如何定义 Vue 的,代码以下:

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue) // 定义了vue 自己的静态方法

Object.defineProperty(Vue.prototype, '$isServer', {
 get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
 get () {
   /* istanbul ignore next */
   return this.$vnode && this.$vnode.ssrContext
 }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
 value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue
复制代码

一样 它是从./instance/index 文件中导入 Vue ,最后将其导出。该文件经过initGlobalAPI方法 给 vue 添加静态方法。

经过 Mixin 混入往Vue 的原型上添加方法

继续往下,到./instance/index 文件,代码以下:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'


// 终于溯源结束了,Vue就是一个用 Function 实现的类,因此才经过 new 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)
}

// 在vue原型上挂了方法
initMixin(Vue) 
stateMixin(Vue) 
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
复制代码

到此,终于溯源结束了,Vue就是一个用 Function 实现的类,因此才经过 new Vue 去实例化它。该文件中经过 Mixin 混入的方法,往Vue 的原型上添加了方法。

结束

最近一段时间都会认真的去看vue.js的源码。【读vue 源码】会按照一个系列去更新。分享本身学习的同时,也但愿与更多的同行交流所得,如此而已。