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版本 複製程式碼

什麼時候我們需要使用編譯器?
編譯器: 把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有很多打包後的版本

它們都依賴於都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複製程式碼