本文但願能夠幫助那些想吃蛋糕,但又以爲蛋糕太大而又不知道從哪下口的人們。javascript
clone
下來後,按照CONTRIBUTING中的Development Setup
中的順序,逐個執行下來$ npm install # watch and auto re-build dist/vue.js $ npm run dev 複製代碼
既然$ npm run dev
命令能夠從新編譯出vue.js
文件,那麼咱們就從scripts
中的dev
開始看吧。html
"dev":"rollup -w -c scripts/config.js --environment TARGET:web-full-dev" 複製代碼
若是這裏你還不清楚
rollup
是作什麼的,能夠戳這裏,簡單來講就是一個模塊化打包工具。具體的介紹這裏就跳過了,由於咱們是來看vue的,若是太跳躍的話,基本就把此次主要想作的事忽略掉了,跳跳跳不必定跳哪裏了,因此在閱讀源碼的時候,必定要牢記此次咱們的目的是什麼。vue
注意上面指令中的兩個關鍵詞scripts/config.js
和web-full-dev
,接下來讓咱們看看script/config.js
這個文件。java
if (process.env.TARGET) { module.exports = genConfig(process.env.TARGET) } else { exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig) } 複製代碼
回憶上面的命令,咱們傳入的TARGET
是web-full-dev
,那麼帶入到方法中,最終會看到這樣一個object
node
'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 }, 複製代碼
雖然這裏咱們還不知道它具體是作什麼的,暫且經過語義來給它補上註釋吧。既然有了入口文件,那麼咱們繼續打開文件web/entry-runtime-with-compiler.js
。OK,打開這個文件後,終於看到了咱們的一個目標關鍵詞git
import Vue from './runtime/index' 複製代碼
江湖規矩,繼續往這個文件裏跳,而後你就會看到:github
import Vue from 'core/index' 複製代碼
是否是又看到了代碼第一行中熟悉的關鍵詞Vue
web
import Vue from './instance/index' 複製代碼
打開instance/index
後,結束了咱們的第一步,已經從package.json中到框架中的文件,找到了Vue
的定義地方。讓咱們再回顧下流程:npm
切記,在看源碼時爲了防止看着看着看跑偏了,咱們必定要按照代碼執行的順序看。json
項目結構中有examples
目錄,讓咱們也建立一個屬於本身的demo在這裏面吧,隨便copy一個目錄,命名爲demo,後面咱們的代碼都經過這個demo來進行測試、觀察。
index.html內容以下:
<!DOCTYPE html> <html> <head> <title>Demo</title> <script src="../../dist/vue.js"></script> </head> <body> <div id="demo"> <template> <span>{{text}}</span> </template> </div> <script src="app.js"></script> </body> </html> 複製代碼
app.js文件內容以下:
var demo = new Vue({ el: '#demo', data() { return { text: 'hello world!' } } }) 複製代碼
上面demo的html中咱們引入了dist/vue.js,那麼window下,就會有Vue
對象,暫且先將app.js的代碼修改以下:
console.dir(Vue); 複製代碼
若是這裏你還不知道
console.dir
,而只知道console.log
,那你就親自試試而後記住他們之間的差別吧。
從控制檯咱們能夠看出,Vue
對象以及原型上有一系列屬性,那麼這些屬性是從哪兒來的,作什麼的,就是咱們後續去深刻的內容。
是否還記得咱們在第一章中找到最終Vue
構造函數的文件?若是不記得了,就再回去看一眼吧,咱們在本章會按照那個順序倒着來看一遍Vue
的屬性掛載。
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
複製代碼
接下來咱們就開始按照代碼執行的順序,先來分別看看這幾個函數究竟是弄啥嘞?
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
複製代碼
initMixin(src/core/instance/init.js)
Vue.prototype._init = function (options?: Object) {} 複製代碼
在傳入的Vue
對象的原型上掛載了_init
方法。
stateMixin(src/core/instance/state.js)
// Object.defineProperty(Vue.prototype, '$data', dataDef) // 這裏$data只提供了get方法,set方法再非生產環境時會給予警告 Vue.prototype.$data = undefined; // Object.defineProperty(Vue.prototype, '$props', propsDef) // 這裏$props只提供了get方法,set方法再非生產環境時會給予警告 Vue.prototype.$props = undefined; Vue.prototype.$set = set Vue.prototype.$delete = del Vue.prototype.$watch = function() {} 複製代碼
若是這裏你還不知道
Object.defineProperty
是作什麼的,我對你的建議是能夠把對象的原型這部分好好看一眼,對於後面的代碼瀏覽會有很大的效率提高,否則雲裏霧裏的,你浪費的只有本身的時間而已。
eventsMixin(src/core/instance/events.js)
Vue.prototype.$on = function() {} Vue.prototype.$once = function() {} Vue.prototype.$off = function() {} Vue.prototype.$emit = function() {} 複製代碼
lifecycleMixin(src/core/instance/lifecycle.js)
Vue.prototype._update = function() {} Vue.prototype.$forceUpdate = function () {} Vue.prototype.$destroy = function () {} 複製代碼
renderMixin(src/core/instance/render.js)
// installRenderHelpers Vue.prototype._o = markOnce Vue.prototype._n = toNumber Vue.prototype._s = toString Vue.prototype._l = renderList Vue.prototype._t = renderSlot Vue.prototype._q = looseEqual Vue.prototype._i = looseIndexOf Vue.prototype._m = renderStatic Vue.prototype._f = resolveFilter Vue.prototype._k = checkKeyCodes Vue.prototype._b = bindObjectProps Vue.prototype._v = createTextVNode Vue.prototype._e = createEmptyVNode Vue.prototype._u = resolveScopedSlots Vue.prototype._g = bindObjectListeners // Vue.prototype.$nextTick = function() {} Vue.prototype._render = function() {} 複製代碼
將上面5個方法執行完成後,instance
中對Vue
的原型一波瘋狂輸出後,Vue
的原型已經變成了:
若是你認爲到此就結束了?答案固然是,不。讓咱們順着第一章整理的圖,繼續回到core/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' // 初始化全局API 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 複製代碼
按照代碼執行順序,咱們看看initGlobalAPI(Vue)
方法內容:
// Object.defineProperty(Vue, 'config', configDef) Vue.config = { devtools: true, …} Vue.util = { warn, extend, mergeOptions, defineReactive, } Vue.set = set Vue.delete = delete Vue.nextTick = nextTick Vue.options = { components: {}, directives: {}, filters: {}, _base: Vue, } // extend(Vue.options.components, builtInComponents) Vue.options.components.KeepAlive = { name: 'keep-alive' …} // initUse Vue.use = function() {} // initMixin Vue.mixin = function() {} // initExtend Vue.cid = 0 Vue.extend = function() {} // initAssetRegisters Vue.component = function() {} Vue.directive = function() {} Vue.filter = function() {} 複製代碼
不難看出,整個Core在instance的基礎上,又對Vue
的屬性進行了一波輸出。經歷完Core後,整個Vue
變成了這樣:
繼續順着第一章整理的路線,來看看runtime又對Vue
作了什麼。
這裏仍是記得先從宏觀入手,不要去看每一個方法的詳細內容。能夠經過
debugger
來暫停代碼執行,而後經過控制檯的console.dir(Vue)
隨時觀察Vue
的變化,
這裏首先針對web平臺,對Vue.config來了一小波方法添加。
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
複製代碼
向options中directives增長了model
以及show
指令:
// extend(Vue.options.directives, platformDirectives) Vue.options.directives = { model: { componentUpdated: ƒ …} show: { bind: ƒ, update: ƒ, unbind: ƒ } } 複製代碼
向options中components增長了Transition
以及TransitionGroup
:
// extend(Vue.options.components, platformComponents) Vue.options.components = { KeepAlive: { name: "keep-alive" …} Transition: {name: "transition", props: {…} …} TransitionGroup: {props: {…}, beforeMount: ƒ, …} } 複製代碼
在原型中追加__patch__
以及$mount
:
// 虛擬dom所用到的方法 Vue.prototype.__patch__ = patch Vue.prototype.$mount = function() {} 複製代碼
以及對devtools的支持。
在entry中,覆蓋了$mount
方法。
掛載compile,compileToFunctions
方法是將template
編譯爲render
函數
Vue.compile = compileToFunctions
複製代碼
至此,咱們完整的過了一遍在web中Vue的構造函數的變化過程:
template
的能力。上一章咱們從宏觀角度觀察了整個Vue構造函數的變化過程,那麼咱們本章將從微觀角度,看看new Vue()後,都作了什麼。
將咱們demo中的app.js修改成以下代碼:
var demo = new Vue({ el: '#demo', data() { return { text: 'hello world!' } } }) 複製代碼
還記得instance/init中的Vue構造函數嗎?在代碼執行了this._init(options)
,那咱們就從_init
入手,開始本章的旅途。
Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ // 瀏覽器環境&支持window.performance&非生產環境&配置了performance if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` // 至關於 window.performance.mark(startTag) mark(startTag) } // a flag to avoid this being observed vm._isVue = true // merge options 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進行合併 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 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) } } 複製代碼
這個方法都作了什麼?
_uid
,_isVue
屬性。_renderProxy
,_self
屬性。initLifecycle
initEvents
initRender
beforeCreate
initInjections
initState
initProvide
created
_name
屬性options
傳入的el
,調用當前實例的$mount
OK,咱們又宏觀的看了整個_init
方法,接下來咱們結合咱們的demo,來細細的看下每一步產生的影響,以及具體調用的方法。
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) function resolveConstructorOptions (Ctor: Class<Component>) { let options = Ctor.options if (Ctor.super) { const superOptions = resolveConstructorOptions(Ctor.super) const cachedSuperOptions = Ctor.superOptions if (superOptions !== cachedSuperOptions) { // super option changed, // need to resolve new options. Ctor.superOptions = superOptions // check if there are any late-modified/attached options (#4976) const modifiedOptions = resolveModifiedOptions(Ctor) // update base extend options if (modifiedOptions) { extend(Ctor.extendOptions, modifiedOptions) } options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions) if (options.name) { options.components[options.name] = Ctor } } } return options } 複製代碼
還記得咱們在第三章中,runtime對Vue
的變動以後,options變成了什麼樣嗎?若是你忘了,這裏咱們再回憶一下:
Vue.options = { components: { KeepAlive: { name: "keep-alive" …} Transition: {name: "transition", props: {…} …} TransitionGroup: {props: {…}, beforeMount: ƒ, …} }, directives: { model: { componentUpdated: ƒ …} show: { bind: ƒ, update: ƒ, unbind: ƒ } }, filters: {}, _base: ƒ Vue } 複製代碼
咱們將上面的代碼進行拆解,首先將this.constructor
傳入resolveConstructorOptions
中,由於咱們的demo中沒有進行繼承操做,因此在resolveConstructorOptions
方法中,沒有進入if,直接返回獲得的結果,就是在runtime
中進行處理後的options
選項。而options
就是咱們在調用new Vue({})
時,傳入的options
。此時,mergeOptions方法變爲:
vm.$options = mergeOptions( { components: { KeepAlive: { name: "keep-alive" …} Transition: {name: "transition", props: {…} …} TransitionGroup: {props: {…}, beforeMount: ƒ, …} }, directives: { model: { componentUpdated: ƒ …} show: { bind: ƒ, update: ƒ, unbind: ƒ } }, filters: {}, _base: ƒ Vue }, { el: '#demo', data: ƒ data() }, vm ) 複製代碼
接下來開始調用mergeOptions
方法。打開文件後,咱們發如今引用該文件時,會當即執行一段代碼:
// config.optionMergeStrategies = Object.create(null) const strats = config.optionMergeStrategies 複製代碼
仔細往下看後面,還有一系列針對strats
掛載方法和屬性的操做,最終strats
會變爲:
其實這些散落在代碼中的掛載操做,有點沒想明白尤大沒有放到一個方法裏去統一處理一波?
繼續往下翻,看到了咱們進入這個文件的目標,那就是mergeOptions
方法:
function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object { debugger; if (process.env.NODE_ENV !== 'production') { // 根據用戶傳入的options,檢查合法性 checkComponents(child) } if (typeof child === 'function') { child = child.options } // 標準化傳入options中的props normalizeProps(child, vm) // 標準化注入 normalizeInject(child, vm) // 標準化指令 normalizeDirectives(child) const extendsFrom = child.extends if (extendsFrom) { parent = mergeOptions(parent, extendsFrom, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } const options = {} let key for (key in parent) { mergeField(key) } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options } 複製代碼
由於咱們這裏使用了最簡單的hello world
,因此在mergeOptions
中,能夠直接從30行開始看,這裏初始化了變量options
,32行、35行的for
循環分別根據合併策略進行了合併。看到這裏,恍然大悟,原來strats
是定義一些標準合併策略,若是沒有定義在其中,就使用默認合併策略defaultStrat
。
這裏有個小細節,就是在循環子options時,僅合併父options中不存在的項,來提升合併效率。
讓咱們繼續來用最直白的方式,回顧下上面的過程:
// 初始化合並策略 const strats = config.optionMergeStrategies strats.el = strats.propsData = function (parent, child, vm, key) {} strats.data = function (parentVal, childVal, vm) {} constants.LIFECYCLE_HOOKS.forEach(hook => strats[hook] = mergeHook) constants.ASSET_TYPES.forEach(type => strats[type + 's'] = mergeAssets) strats.watch = function(parentVal, childVal, vm, key) {} strats.props = strats.methods = strats.inject = strats.computed = function(parentVal, childVal, vm, key) {} strats.provide = mergeDataOrFn // 默認合併策略 const defaultStrat = function (parentVal, childVal) { return childVal === undefined ? parentVal : childVal } function mergeOptions (parent, child, vm) { // 本次demo沒有用到省略前面代碼 ... const options = {} let key for (key in parent) { mergeField(key) } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options } 複製代碼
怎麼樣,是否是清晰多了?本次的demo通過mergeOptions
以後,變爲了以下:
OK,由於咱們本次是來看_init
的,因此到這裏,你須要清除Vue
經過合併策略,將parent與child進行了合併便可。接下來,咱們繼續回到_init
對options
合併處理完以後作了什麼?
在merge完options後,會判斷若是是非生產環境時,會進入initProxy方法。
if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } vm._self = vm 複製代碼
帶着霧水,進入到方法定義的文件,看到了Proxy
這個關鍵字,若是這裏你還不清楚,能夠看下阮老師的ES6,上面有講。
vm._renderProxy = new Proxy(vm, handlers)
,這裏的handlers
,因爲咱們的options中沒有render,因此這裏取值是hasHandler。這部分具體是作什麼用的,暫且知道有這麼個東西,主線仍是不要放棄,繼續回到主線吧。
初始化了與生命週期相關的屬性。
function initLifecycle (vm) { const options = vm.$options // 省去部分與本次demo無關代碼 ... vm.$parent = undefined vm.$root = vm vm.$children = [] vm.$refs = {} vm._watcher = null vm._inactive = null vm._directInactive = false vm._isMounted = false vm._isDestroyed = false vm._isBeingDestroyed = false } 複製代碼
function initEvents (vm) { vm._events = Object.create(null) vm._hasHookEvent = false // 省去部分與本次demo無關代碼 ... } 複製代碼
function initRender (vm: Component) { vm._vnode = null // the root of the child tree vm._staticTrees = null // v-once cached trees vm.$slots = {} vm.$scopedSlots = {} vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) vm.$createElement= (a, b, c, d) => createElement(vm, a, b, c, d, true) vm.$attrs = {} vm.$listeners = {} } 複製代碼
調用生命週期函數beforeCreate
因爲本demo沒有用到注入值,對本次vm並沒有實際影響,因此這一步暫且忽略,有興趣能夠自行翻閱。
本次的只針對這最簡單的demo,分析
initState
,可能忽略了不少過程,後續咱們會針對更復雜的demo來繼續分析一波。
這裏你能夠先留意到幾個關鍵詞Observer
,Dep
,Watcher
。每一個Observer
都有一個獨立的Dep
。關於Watcher
,暫時沒用到,可是請相信,立刻就能夠看到了。
因爲本demo沒有用到,對本次vm並沒有實際影響,因此這一步暫且忽略,有興趣能夠自行翻閱。
這裏知道爲何在
created
時候,無法操做DOM了嗎?由於在這裏,尚未涉及到實際的DOM渲染。
這裏前面有個if判斷,因此當你若是沒有在
new Vue
中的options
沒有傳入el
的話,就不會觸發實際的渲染,就須要本身手動調用了$mount
。
這裏的$mount
最終會調向哪裏?還記得咱們在第三章看到的compiler
所作的事情嗎?就是覆蓋Vue.prototype.$mount
,接下來,咱們一塊兒進入$mount
函數看看它都作了什麼吧。
// 只保留與本次相關代碼,其他看太多會影響視線 const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) const options = this.$options if (!options.render) { let template = getOuterHTML(el) if (template) { const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns } } return mount.call(this, el, hydrating) } 複製代碼
這裏在覆蓋$mount
以前,先將原有的$mount
保留至變量mount
中,整個覆蓋後的方法是將template
轉爲render
函數掛載至vm
的options
,而後調用調用原有的mount
。因此還記得mount
來自於哪嘛?那就繼續吧runtime/index
,方法很簡單,調用了生命週期中mountComponent
。
// 依然只保留和本demo相關的內容 function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el callHook(vm, 'beforeMount') let updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm } 複製代碼
OK,精彩的部分來了,一個Watcher
,盤活了整個咱們前面鋪墊的一系列東西。打開src/core/observer/watcher.js
,讓咱們看看Watcher
的構造函數吧。爲了清楚的看到Watcher
的流程。依舊只保留方法咱們須要關注的東西:
constructor (vm, expOrFn, cb, options, isRenderWatcher) { this.vm = vm vm._watcher = this vm._watchers.push(this) this.getter = expOrFn this.value = this.get() } get () { pushTarget(this) let value const vm = this.vm value = this.getter.call(vm, vm) popTarget() this.cleanupDeps() return value } 複製代碼
Watcher
的構造函數中,本次傳入的updateComponent
做爲Wather
的getter
。get
方法調用時,又經過pushTarget
方法,將當前Watcher
賦值給Dep.target
getter
,至關於調用vm._update
,先調用vm._render
,而這時vm._render
,此時會將已經準備好的render
函數進調用。render
函數中又用到了this.text
,因此又會調用text
的get
方法,從而觸發了dep.depend()
dep.depend()
會調回Watcher
的addDep
,這時Watcher
記錄了當前dep
實例。dep.addSub(this)
,dep
又記錄了當前Watcher
實例,將當前的Watcher
存入dep.subs
中。demo
尚未使用的,也就是當this.text
發生改變時,會觸發Observer
中的set
方法,從而觸發dep.notify()
方法來進行update
操做。最後這段文字太乾了,能夠本身經過斷點,耐心的走一遍整個過程。若是沒有耐心看完這段描述,能夠看看筆者這篇文章100行代碼帶你玩vue響應式。
就這樣,Vue
的數據響應系統,經過Observer
、Watcher
、Dep
完美的串在了一塊兒。也但願經歷這個過程後,你能對真正的對這張圖,有必定的理解。
固然,$mount
中還有一步被我輕描淡寫了,那就是這部分,將template轉換爲render,render實際調用時,會經歷_render
, $createElement
, __patch__
, 方法,有興趣能夠本身瀏覽下'src/core/vdom/'目錄下的文件,來了解vue
針對虛擬dom的使用。
若是你喜歡,能夠繼續瀏覽筆者關於vue template轉換部分的文章《Vue對template作了什麼》。