關於Vue源碼學習的博客, HcySunYang的 Vue2.1.7源碼學習是我所見過講的最清晰明瞭的博客了,很是適合想了解Vue源碼的同窗入手。本文是在看了這篇博客以後進一步的學習心得。
注意:本文所用Vue版本爲2.5.13
PS:本文有點草率,以後會重寫改進。
關於學習源碼,我有話要說~
一開始我學習Vue的源碼,是將 Vue.js 這個文件下載下來逐行去看……由於我聽信了我同事說的「不過一萬多行代碼,實現也很簡單,能夠直接看。」結果可想而知,花了十幾個小時看完代碼,還經過打斷點看流程,除了學習到一些新的js語法、一些優雅的代碼寫法、和對整個代碼熟悉了以外,沒啥其餘收穫。
其實,這是一個丟西瓜撿芝麻的行爲,沒有明確的目的籠統的看源碼,最終迷失在各類細枝末節上了。
因此呢,我看源碼的經驗教訓有以下幾點:html
這裏咱們來解決從哪裏開始看代碼的流程,重點是找到Vue構造函數的實現。
首先,找到 package.json
文件,從中找到編譯命令 "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
,這裏 rollup
是相似於 Webpack 的打包工具,打包文件在 script/config.js
中,找到該文件。找 entry
入口關鍵字(不會rollup,但配置方式和 Webpack 差不太多)。入口文件有好多配置,咱們就找到會生成 dist/vue.js
的配置項:前端
// 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 },
好,這裏就找到了 web/entry-runtime-with-compiler.js
這個路徑,完整路徑應該是 src/platform/web/entry-runtime-with-compiler.js
。在這個文件中咱們找到一個Vue對象import進來了。vue
import Vue from './runtime/index'
咱們順着找到到 src/platform/web/runtime/index.js
這個文件,在文件中發現導入文件git
import Vue from 'core/index'
就順着這個思路找,最終找到 src/core/instance/index.js
這個文件。
完整找到Vue實例入口文件的流程以下:github
package.json script/config.js src/platform/web/entry-runtime-with-compiler.js src/platform/web/runtime/index.js src/core/index.js src/core/instance/index.js
簡單看看Vue構造函數的樣子~web
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
能夠看到Vue的構造函數,裏面只作了 this._init(options)
行爲。這個 _init
方法在執行 initMixin
方法的時候定義了。找到同目錄下的 init.js
文件。express
export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ 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 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 { // 合併配置項 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) // 初始化代理 } else { vm._renderProxy = vm } vm._self = vm // 暴露對象自身 initLifecycle(vm) // 初始化生命週期 initEvents(vm) // 初始化事件:on,once,off,emit initRender(vm) // 初始化渲染:涉及到Virtual DOM callHook(vm, 'beforeCreate') // 觸發 beforeCreate 生命週期鉤子 initInjections(vm) // 在初始化 data/props 前初始化Injections initState(vm) // 初始化狀態選項 initProvide(vm) // 在初始化 data/props 後初始化Provide // 有關inject和provide請查閱 https://cn.vuejs.org/v2/api/#provide-inject callHook(vm, 'created') // 觸發 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) } // 若是Vue配置項中有el,直接掛在到DOM中 if (vm.$options.el) { vm.$mount(vm.$options.el) } } }
抓住重點,咱們是要來學習State的。從上面代碼中能夠找到initState方法的執行,這就是咱們此行的目的——State數據選項。除此以外還有其餘重要方法的初始化方式,這將會在以後的博客中繼續討論和學習。編程
以前是簡單提一下學習源碼的方法論和如何開始學習Vue源碼學習。而且找到了咱們要學習的State所在,如今進入正題:json
瞭解Vue的數據選項的運行機制。
在Vue2.1.7源碼學習中,做者已經很是很是很是清晰明瞭的幫咱們分析了data的實現。在此基礎上開始好好學習其餘數據選項的實現邏輯。api
這裏我經過本身的思路再來整理下項目中data的實現。
注:因爲這一部分已經被各種源碼解析博客講爛了,而要把這部分講清楚要大量篇幅。因此我就不貼代碼了。仍是那句話,抓重點!咱們主要研究的是data以外的實現方式。關於data的實現和mvvm的逐步實現,Vue2.1.7源碼學習中講的很是清晰明瞭。
如下是我整理的思路,有興趣的同窗能夠順着個人思路去看看。
在 state.js 中找到 initState,並順利找到 initData 函數。initData中主要作了如下幾步操做:
vm.name
來修改和獲取 data 中的 name 的值。重點在 observe
方法,因而咱們根據 import 關係找到 src/core/observer/index.js
文件。observe
方法經過傳入的值最終返回一個Observer類的實例對象。
找到Observer類,在構造函數中爲當前類建立Dep實例,而後判斷數據,若是是數組,觸發 observeArray 方法,遍歷執行 observe 方法;若是是對象,觸發walk方法。
找到walk方法,方法中遍歷了數據對象,爲對象每一個屬性執行 defineReactive 方法。
找到 defineReactive 方法,該方法爲 mvvm 數據變化檢測的核心。爲對象屬性添加 set 和 get 方法。重點來了, vue 在 get 方法中執行 dep.depend()
方法,在 set 方法中執行 dep.notify()
方法。這個先很少講,最後進行聯結說明。
找到同目錄下的 dep.js
文件,文件不長。定義了 Dep 類和pushTarget
、popTarget
方法。在 Dep 類中有咱們以前提到的 depend
和 notify
方法。看下兩個方法的實現:
depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }
在 depend
方法中,Dep.target 就是一個 Watcher 實例,它的 addDep
方法最終會調用到 Dep 的 addSubs
方法。subs 是 Watcher 數組。即將當前 watcher 存到 Dep 的 subs 數組中。
在 notify
方法中,將 Watcher 數組 subs 遍歷,執行他們的 update
方法。update
最終會去執行 watcher
的回調函數。
即在 get 方法中將 watcher 添加到 dep,在 set 方法中經過 dep 對 watcher 進行回調函數觸發。
這裏其實已經實現了數據監聽,接着咱們來看看 Watcher,其實 Watcher 就是Vue中 watch 選項的實現了。說到 watch 選項咱們都知道它用來監聽數據變化。Watcher 就是實現這個過程的玩意啦~
Watcher的構造函數最終調用了 get
方法,代碼以下:
get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }
get
方法作了以下幾步:
getter
方法。Dep.target
恢復到上一個值,而且將當前 Watcher 從 Dep 的 subs 中去除。其中要注意的是,在第二步中數據的 getter
方法會執行到 dep.depend()
方法,depend
方法將當前 watcher 加入到 subs 中。至於步驟一和三還不太理解。挖個坑先~
這樣 watcher 就監測上數據了。那怎麼使用呢?固然是數據變化時使用咯。當監測的數據變化時,執行數據 setter 方法,而後執行 dep 的 notify
方法。因爲咱們以前已經將 watcher 都收集到 dep 的 subs 中,notify
方法遍歷執行 watcher 的 update
方法,update
方法最終遍歷執行回調函數。
observe
方法,建立 Observer 執行 walk
爲對象數據添加setter 和 getterdep.depend()
收集 watcher,在 setter 方法中執行 dep.notify()
方法,最終遍歷執行 watcher 數組的回調函數。getter
方法觸發 dep.depend()
dep.depend()
方法將當前 Watcher(Dep.target)傳遞給Dep的subs(watcher數組)中。setter
方法,觸發 dep.notify()
方法,遍歷 Dep 中的 subs(watcher數組),執行 Watcher 的回調函數。嗯……就是這樣~以後把挖的坑填上!
說完了 Data 的監聽流程,說說 watch 應該就不難啦~
找到 src/core/instance/state.js
的 initWatch
函數,該方法用來遍歷 Vue 實例中的 watch 項,最終全部 watch 都會執行 createWatcher
方法。
繼續看 createWatcher
方法,這個方法也很簡單,最終返回 vm.$watch(keyOrFn, handler, options)
。咱們繼續往下找~
在 stateMixin
方法中找到了定義 Vue 的 $watch 方法屬性。來看看怎麼實現的:
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() } }
若是回調函數 cb 是一個對象,那麼返回並執行 createWatcher
函數,最終仍是會走到 $watch 方法中。
不然,建立一個 Watcher 實例,當這個實例建立後,目標數據有任何變化 watch 選項中都能監聽到了。若是是有 immediate 參數,那麼當即執行一次Watcher的回調函數。最後返回一個解除監聽的方法,執行了 Watcher 的 teardown 方法。
那麼問題來了,爲何watch選項監聽數據的方法中參數是以下寫法呢?
watch: { a: function(val, oldVal){ console.log(val) } }
能夠找到 src/core/instance/observer/watcher.js
中找到 run
方法。能夠看到 this.cb.call(this.vm, value, oldValue)
這裏的 cb 回調函數傳遞的參數就是 value 和 oldValue。
這裏說個基礎知識,函數使用 call 方法執行,第一個參數是方法的this值,以後纔是真正的參數。
run () { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } }
小結:watch 選項其實就是爲指定數據建立 Watcher 實例,接收回調函數的過程。
接下來咱們看看props,官網對props的定義以下:
props 能夠是數組或對象,用於接收來自父組件的數據。
找到 initProps
方法。
function initProps (vm: Component, propsOptions: Object) { const propsData = vm.$options.propsData || {} const props = vm._props = {} // cache prop keys so that future props updates can iterate using Array // instead of dynamic object key enumeration. const keys = vm.$options._propKeys = [] const isRoot = !vm.$parent // root instance props should be converted observerState.shouldConvert = isRoot for (const key in propsOptions) { keys.push(key) const value = validateProp(key, propsOptions, propsData, vm) /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { const hyphenatedKey = hyphenate(key) if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) { warn( `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`, vm ) } defineReactive(props, key, value, () => { if (vm.$parent && !isUpdatingChildComponent) { warn( `Avoid mutating a prop directly since the value will be ` + `overwritten whenever the parent component re-renders. ` + `Instead, use a data or computed property based on the prop's ` + `value. Prop being mutated: "${key}"`, vm ) } }) } else { defineReactive(props, key, value) } if (!(key in vm)) { proxy(vm, `_props`, key) } } observerState.shouldConvert = true }
能夠看到,props 和 data 相似。在 initProps
中無非作了兩步:defineReactive
和 proxy
,這兩個方法咱們在提到 data 的時候講過了。defineReactive
爲數據設置 setter、getter,proxy
方法將 props
中的屬性映射到 Vue 實例 vm 上,便於咱們能夠用 vm.myProps
來獲取數據。
至此,我有個疑問:data與props有何不一樣呢?
data使用的是observe方法,建立一個Observer對象,Observer對象最終是執行了defineReactive方法。而props是遍歷選項屬性,執行defineReactive方法。中間可能就多了個Observer對象,那麼這個Observer對象的做用到底在哪呢?通過實踐props屬性改變後界面也會改變。說明mvvm對props也是成立的。
另外,data和props有個不一樣的地方就是props是不建議改變的。詳見單向數據流
小結:邏輯和data相似,都是監聽數據。不一樣之處呢……再研究研究~
再來講說computed,找到初始化computed方法 src/core/instance/state.js
中的 initComputed
方法,去除非關鍵代碼後看到其實主要有倆個行爲,爲 computed 屬性建立 Watcher,而後執行 defineComputed
方法。
function initComputed (vm: Component, computed: Object) { ... for (const key in computed) { ... if (!isSSR) { watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } if (!(key in vm)) { defineComputed(vm, key, userDef) } ... } }
defineComputed 作了兩步行爲:一是定義 sharedPropertyDefinition 的 getter 和 setter,二是將 sharedPropertyDefinition 的屬性傳給vm,即 Object.defineProperty(target, key, sharedPropertyDefinition)
。自此,咱們能夠經過 vm.computedValue
來獲取計算屬性結果了。
小結:computed其實也就是一個數據監聽行爲,與data和props不一樣之處就是在get函數中須要進行邏輯計算處理。
繼續在 state.js
中看到 initMethods
方法。顧名思義,這是初始化methods的方法。實現很簡單,代碼以下:
function initMethods (vm: Component, methods: Object) { const props = vm.$options.props for (const key in methods) { if (process.env.NODE_ENV !== 'production') { if (methods[key] == null) { warn( `Method "${key}" has an undefined value in the component definition. ` + `Did you reference the function correctly?`, vm ) } if (props && hasOwn(props, key)) { warn( `Method "${key}" has already been defined as a prop.`, vm ) } if ((key in vm) && isReserved(key)) { warn( `Method "${key}" conflicts with an existing Vue instance method. ` + `Avoid defining component methods that start with _ or $.` ) } } vm[key] = methods[key] == null ? noop : bind(methods[key], vm) } }
重點在最後一句。前面都排除重名和空值錯誤的,最後將 methods 中的方法傳給 vm,方法內容若是爲空則方法什麼都不作。不然調用 bind
方法執行該函數。
找到這個 bind
方法,位置在 src/shared/util.js
中。
export function bind (fn: Function, ctx: Object): Function { function boundFn (a) { const l: number = arguments.length return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx) } // record original fn length boundFn._length = fn.length return boundFn }
該方法返回一個執行 methods
中函數的方法(這種方法的執行方式比較快)。
小結:將methods的方法用bind函數優化執行過程。而後將methods中的各個方法傳給Vue實例對象。
本文純屬我的理解,若有任何問題,請及時指出,不勝感激~
最後提出一個看源碼的當心得:
我發現……看源碼、跟流程,儘可能將注意力集中在 方法的執行和 類的實例化行爲上。對於變量的獲取和賦值、測試環境警報提示,簡略看下就行,避免逐行閱讀代碼拉低效率。
至此,Vue中的幾個數據選項都學習了一遍了。關鍵在於理解mvvm的過程。data 理解以後,props、watch、computed 都好理解了。methods 和 mvvm 無關……
經過四個早上的時間把文章寫出來了~對 Vue 的理解深入了一些,可是仍是能感受到有不少未知的知識點等着我去發掘。加油吧!今年專一於 Vue 前端學習,把 Vue 給弄懂!
鑑於前端知識碎片化嚴重,我但願可以系統化的整理出一套關於Vue的學習系列博客。
本文源碼已收入到GitHub中,以供參考,固然能留下一個star更好啦^-^。
https://github.com/violetjack/VueStudyDemos
VioletJack,高效學習前端工程師,喜歡研究提升效率的方法,也專一於Vue前端相關知識的學習、整理。
歡迎關注、點贊、評論留言~我將持續產出Vue相關優質內容。
新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571...
CSDN: http://blog.csdn.net/violetja...
簡書: http://www.jianshu.com/users/...
Github: https://github.com/violetjack