Vue原始碼: 建構函式入口

小諾哥發表於2019-05-02

Vue架構設計

螢幕快照 2019-05-02 下午7 13 45

Vue目錄設計

├── scripts ------------------------------- 構建相關的指令碼/配置檔案
│   ├── git-hooks ------------------------- 存放git鉤子的目錄
│   ├── alias.js -------------------------- 別名配置
│   ├── config.js ------------------------- 生成rollup配置的檔案
│   ├── build.js -------------------------- 對 config.js 中所有的rollup配置進行構建
│   ├── ci.sh ----------------------------- 持續整合執行的指令碼
│   ├── release.sh ------------------------ 用於自動釋出新版本的指令碼
├── dist ---------------------------------- 構建後檔案的輸出目錄
├── examples ------------------------------ 存放一些使用Vue開發的應用案例
├── flow ---------------------------------- 型別宣告,使用開源專案 [Flow](https://flowtype.org/)
├── packages ------------------------------ 存放獨立釋出的包的目錄
├── test ---------------------------------- 包含所有測試檔案
├── src ----------------------------------- 原始碼
│   ├── compiler -------------------------- 編譯器程式碼的存放目錄,將 template 編譯為 render 函式
│   ├── core ------------------------------ 存放通用的,與平臺無關的程式碼
│   │   ├── observer ---------------------- 響應系統,包含資料觀測的核心程式碼
│   │   ├── vdom -------------------------- 包含虛擬DOM建立(creation)和打補丁(patching)的程式碼
│   │   ├── instance ---------------------- 包含Vue建構函式設計相關的程式碼
│   │   ├── global-api -------------------- 包含給Vue建構函式掛載全域性方法(靜態方法)或屬性的程式碼
│   │   ├── components -------------------- 包含抽象出來的通用元件
│   ├── server ---------------------------- 包含服務端渲染(server-side rendering)的相關程式碼
│   ├── platforms ------------------------- 包含平臺特有的相關程式碼,不同平臺的不同構建的入口檔案也在這裡
│   │   ├── web --------------------------- web平臺
│   │   │   ├── entry-runtime.js ---------- 執行時構建的入口,不包含模板(template)到render函式的編譯器,所以不支援 `template` 選項,我們使用vue預設匯出的就是這個執行時的版本。大家使用的時候要注意
│   │   │   ├── entry-runtime-with-compiler.js -- 獨立構建版本的入口,它在 entry-runtime 的基礎上新增了模板(template)到render函式的編譯器
│   │   │   ├── entry-compiler.js --------- vue-template-compiler 包的入口檔案
│   │   │   ├── entry-server-renderer.js -- vue-server-renderer 包的入口檔案
│   │   │   ├── entry-server-basic-renderer.js -- 輸出 packages/vue-server-renderer/basic.js 檔案
│   │   ├── weex -------------------------- 混合應用
│   ├── sfc ------------------------------- 包含單檔案元件(.vue檔案)的解析邏輯,用於vue-template-compiler包
│   ├── shared ---------------------------- 包含整個程式碼庫通用的程式碼
├── package.json -------------------------- 不解釋
├── yarn.lock ----------------------------- yarn 鎖定檔案
├── .editorconfig ------------------------- 針對編輯器的編碼風格配置檔案
├── .flowconfig --------------------------- flow 的配置檔案
├── .babelrc ------------------------------ babel 配置檔案
├── .eslintrc ----------------------------- eslint 配置檔案
├── .eslintignore ------------------------- eslint 忽略配置
├── .gitignore ---------------------------- git 忽略配置

複製程式碼

Vue.js構建版本

完整版: 構建後檔案包括編譯器+執行時
編譯器: 負責把模板字串變異為JS的Render函式
執行時: 負責建立Vue.js例項, 渲染檢視, 使用虛擬DOM演算法重新渲染
UMD: 支援通過script標籤在瀏覽器引入
CJS: 用來支援一些低版本打包工具, 因為它們package.json檔案的main欄位只包含執行時的CJS版本
ESM: 用來支援現代打包工具, 這些打包工具package.json的module欄位只包含執行時候的ESM版本
複製程式碼
螢幕快照 2019-05-02 下午5 34 44

什麼時候我們需要使用編譯器?

編譯器: 把template變異為Render函式。

// 用到了template就需要編譯器
new Vue({
   template: '<div></div>'
})

// 如果本身就是Render函式不需要編譯器
new Vue({
   render (h) {
      return h('div', this.hi)
  }
})
複製程式碼

我們如果使用vue-loader, 那麼*.vue檔案模板會在構建時候預編譯成JS, 所以打包完成的檔案實際上不需要編譯器的, 只需要引入執行時版本(體積小)即可。

如果確實需要使用完整版只需要在打包工具中配置一個別名。

// webpack

resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
        }
    },
複製程式碼

關於開發環境與生產環境

我們知道Vue有很多打包後的版本

螢幕快照 2019-05-02 下午6 30 40

它們都依賴於都process.env.NODE_ENV環境變數, 根據其值來決定選擇什麼模式。 所以我們可以在打包工具中配置這些環境變數。

在webpack中配置環境變數

var webpack = require('webpack');

module.exports = {
   ...,

    plugins: [
        // 配置全域性變數的外掛
        new webpack.DefinePlugin({
           'NODE_ENV': JSON.stringify('production')
        })
    ]
};

複製程式碼

建構函式的入口

一步步找到Vue的建構函式入口。

執行npm run dev

通過檢視package.json檔案下的scripts命令。

"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
複製程式碼

scripts/config.js為開啟的對應配置檔案, process.env.TARGET為web-full-dev。 在scripts/config.js找到對應的配置物件

const builds = {
    // 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
    },
}


當然主要生成配置物件是這段程式碼

複製程式碼

function genConfig (name) { // opts為builds裡面對應key的基礎配置物件 const opts = builds[name] // config是真正要返回的配置物件 const config = { input: opts.entry, external: opts.external, plugins: [ flow(), alias(Object.assign({}, aliases, opts.alias)) ].concat(opts.plugins || []), output: { file: opts.dest, format: opts.format, banner: opts.banner, name: opts.moduleName || 'Vue' }, onwarn: (msg, warn) => { if (!/Circular/.test(msg)) { warn(msg) } } }

// built-in vars const vars = { WEEX: !!opts.weex, WEEX_VERSION: weexVersion, VERSION: version } // feature flags Object.keys(featureFlags).forEach(key => { vars[process.env.${key}] = featureFlags[key] }) // build-specific env // 根據不同的process.env.NODE_ENV載入不同的打包後版本 if (opts.env) { vars['process.env.NODE_ENV'] = JSON.stringify(opts.env) } config.plugins.push(replace(vars))

if (opts.transpile !== false) { config.plugins.push(buble()) }

Object.defineProperty(config, '_name', { enumerable: false, value: name })

return config }

if (process.env.TARGET) { module.exports = genConfig(process.env.TARGET) } else { exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig) }


### 找到打包入口檔案

根據配置物件的entry欄位:

複製程式碼

entry: resolve('web/entry-runtime-with-compiler.js')


以及resolve函式

複製程式碼

const aliases = require('./alias') const resolve = p => { // web/ weex /server const base = p.split('/')[0] if (aliases[base]) { // 拼接完整的入口檔案 return path.resolve(aliases[base], p.slice(base.length + 1)) } else { return path.resolve(__dirname, '../', p) } }

aliases.js檔案

複製程式碼

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') }


找到真正的入口檔案為: vue-dev/src/platforms/web/entry-runtime-with-compiler.js。

在entry-runtime-with-compiler.js檔案中發現
複製程式碼

import Vue from './runtime/index'

其實這裡主要做的是掛載$mount()方法, 可以看我之前寫的文章[mount掛載函式](https://juejin.im/post/5c8531995188251bbf2edf82)。

OK回到繼續回到我們之前話題, 在vue-dev/src/platforms/web/runtime/index.js下發現這裡還不是真正的Vue建構函式

複製程式碼

import Vue from './instance/index'

不過也馬上接近了, 繼續查詢vue-dev/src/core/instance/index.js, 很明顯這裡才是真正的建構函式。

複製程式碼

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 Vue (options) { // 提示必須使用new Vue() if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the new keyword') } // 執行初始化操作, 一般_字首方法都是內部方法 // __init()方法是initMixin裡繫結的 this._init(options) }

// 在Vue原型上掛載方法 initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue)

export default Vue


### initMixin()

複製程式碼

export function initMixin (Vue: Class) { Vue.prototype._init = function (options?: Object) { // 快取this const vm: Component = this // a uid vm._uid = uid++

// 這裡只要是開啟config.performance進行效能除錯時候一些元件埋點
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  startTag = `vue-perf-start:${vm._uid}`
  endTag = `vue-perf-end:${vm._uid}`
  mark(startTag)
}

// a flag to avoid this being observed
// 標識一個物件是 Vue 例項, 避免再次被observed
vm._isVue = true
// merge options
// options是new Vue(options)配置物件
// _isComponent是一個內部屬性, 用於建立元件
if (options && options._isComponent) {
  // optimize internal component instantiation
  // since dynamic options merging is pretty slow, and none of the
  // internal component options needs special treatment.
  initInternalComponent(vm, options)
} else {
  // 定義例項屬性$options: 用於當前 Vue 例項的初始化選項
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}
// expose real self
// 定義一個內部屬性_self
vm._self = vm
// 執行各種初始化操作
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  vm._name = formatComponentName(vm, false)
  mark(endTag)
  measure(`vue ${vm._name} init`, startTag, endTag)
}
// 執行掛載操作
if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}
複製程式碼

} }



## 參考閱讀

深入淺出vue.js複製程式碼

相關文章