Vue原始碼解析(1)-入口檔案
在學習Vue原始碼之前,首先要做的一件事情,就是去GitHub上將Vue原始碼clone下來,目前我這裡分析的Vue版本是V2.5.21,下面開始分析:
一、原始碼的目錄結構:
Vue的原始碼都在src目錄下,分為6個不同功能的檔案
src ├── compiler# 編譯相關:包括把模板解析成 ast 語法樹,ast 語法樹優化,程式碼生成等功能。 ├── core# 核心程式碼:包括內建元件、全域性 API 封裝,Vue 例項化、觀察者、虛擬 DOM、工具函式等等 ├── platforms# 不同平臺的支援: 2個目錄代表2個主要入口,分別打包成執行在web上和weex上的Vue.js ├── server# 服務端渲染:所有服務端渲染相關的邏輯,跑在服務端的Node.js,把元件渲染為伺服器端的HTML字串,將它們直接傳送到瀏覽器 ├── sfc# .vue 檔案解析:把.vue檔案內容解析成一個JavaScript的物件 ├── shared# 共享程式碼:被瀏覽器端和服務端所共享的工具方法
二、原始碼的構建:
Vue原始碼是用rollup構建的,在package.json檔案下,可以看到構建的指令碼如下:
"scripts": { "build": "node scripts/build.js", "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer", "build:weex": "npm run build -- weex" }
按照指令碼的檔案路徑,可以在scripts/build.js找到對應的構建檔案,在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)
上面這段程式碼的意思是先從config配置檔案讀取配置,再通過process.argv[2]獲取命令列引數,對構建配置做過濾,構建出不同用途的 Vue.js。
我們再來看一下最主要的邏輯scripts/config.js檔案裡面的內容:
const builds = { // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify 'web-runtime-cjs': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.common.js'), format: 'cjs', banner }, // Runtime+compiler CommonJS build (CommonJS) 'web-full-cjs': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.common.js'), format: 'cjs', alias: { he: './entity-decoder' }, banner }, // Runtime only (ES Modules). Used by bundlers that support ES Modules, // e.g. Rollup & Webpack 2 'web-runtime-esm': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.esm.js'), format: 'es', banner }, // Runtime+compiler CommonJS build (ES Modules) '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-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 }, // ... }
這裡的單個配置是遵循 Rollup 的構建規則:
entry:表示構建的入口 JS 檔案地址
dest:表示構建後的 JS 檔案地址
format:表示構建的格式(cjs:表示構建出來的檔案遵循 CommonJS 規範;es:構建出來的檔案遵循 ES Module 規範;umd:構建出來的檔案遵循 UMD 規範)
三、Runtime Only vs Runtime + Compiler
Runtime Only:
我們在使用 Runtime Only 版本的 Vue.js 的時候,通常需要藉助如 webpack 的 vue-loader 工具把 .vue 檔案編譯成 JavaScript,因為是在編譯階段做的,所以它只包含執行時的 Vue.js 程式碼,因此程式碼體積也會更輕量。
Runtime + Compiler:
我們如果沒有對程式碼做預編譯,但又使用了 Vue 的 template 屬性並傳入一個字串,則需要在客戶端編譯模板,因為在 Vue.js 2.0 中,最終渲染都是通過render
函式,如果寫 template
屬性,則需要編譯成 render
函式,那麼這個編譯過程會發生執行時,所以需要帶有編譯器的版本,很顯然,這個編譯過程對效能會有一定損耗,所以通常我們更推薦使用 Runtime-Only 的 Vue.js。
四、原始碼入口:
在 web 應用下,我們來分析 Runtime + Compiler 構建出來的 Vue.js,它的入口是src/platforms/web/entry-runtime-with-compiler.js
/* @flow */ import config from 'core/config' import { warn, cached } from 'core/util/index' import { mark, measure } from 'core/util/perf' import Vue from './runtime/index' import { query } from './util/index' import { compileToFunctions } from './compiler/index' import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat' const idToTemplate = cached(id => { const el = query(id) return el && el.innerHTML }) const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this } const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`vue ${this._name} compile`, 'compile', 'compile end') } } } return mount.call(this, el, hydrating) } /** * Get outerHTML of elements, taking care * of SVG elements in IE as well. */ function getOuterHTML (el: Element): string { if (el.outerHTML) { return el.outerHTML } else { const container = document.createElement('div') container.appendChild(el.cloneNode(true)) return container.innerHTML } } Vue.compile = compileToFunctions export default Vue
上面這個檔案首先import Vue from './runtime/index'匯入了vue物件,然後對其prototype做了一些操作,所以我們找到./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, isChrome } from 'core/util/index' // install platform patch function Vue.prototype.__patch__ = inBrowser ? patch : noop // public mount method Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) } export default Vue
從上面這個檔案中可以看到,import Vue from 'core/index'匯入vue,所以我們再去core/index找到vue,
在core/index中,initGlobalAPI定義了一些vue的全域性的API的方法,可以在src/core/global-api/index.js檔案裡面找到:
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) 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
從以上檔案中,終於看到初始化vue的API的程式碼!
然後我們去'./instance/index'找到Vue,它實際上就是一個用Function實現的類,我們使用的時候就是通過new Vue例項化的,後面很多xxxMixin的函式呼叫,它們的功能都是給 Vue 的 prototype 上擴充套件一些方法
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' 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) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue