【讀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 源碼】會按照一個系列去更新。分享本身學習的同時,也但願與更多的同行交流所得,如此而已。

相關文章
相關標籤/搜索