關於Vue原始碼學習的部落格, HcySunYang的Vue2.1.7原始碼學習是我所見過講的最清晰明瞭的部落格了,非常適合想了解Vue原始碼的同學入手。本文是在看了這篇部落格之後進一步的學習心得。
注意:本文所用Vue版本為2.5.13
PS:本文有點草率,之後會重寫改進。
關於原始碼學習
關於學習原始碼,我有話要說~
一開始我學習Vue的原始碼,是將 Vue.js 這個檔案下載下來逐行去看……因為我聽信了我同事說的“不過一萬多行程式碼,實現也很簡單,可以直接看。”結果可想而知,花了十幾個小時看完程式碼,還通過打斷點看流程,除了學習到一些新的js語法、一些優雅的程式碼寫法、和對整個程式碼熟悉了之外,沒啥其他收穫。
其實,這是一個丟西瓜撿芝麻的行為,沒有明確的目的籠統的看原始碼,最終迷失在各種細枝末節上了。
所以呢,我看原始碼的經驗教訓有如下幾點:
- 看程式碼,必須帶著問題去找實現程式碼。
- 保持主線,不要糾結於細枝末節。永遠記住你要解決什麼問題。
- 找到一篇優質的部落格、向前輩學習,讓前輩帶著你去學習事半功倍。
- 想看某程式語言的程式碼,必須要有紮實的語言基礎。走路不穩就想跑,會摔得很慘~
- 學習之道,不能盲目。應該找到一種快速有效的方法,來有目的的實現學習目標。不要用戰術上的勤奮來掩蓋戰略上的失誤。看程式碼如此、看書學習亦如此~
如何開始
這裡我們來解決從哪裡開始看程式碼的流程,重點是找到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進來了。
import Vue from `./runtime/index`
我們順著找到到 src/platform/web/runtime/index.js
這個檔案,在檔案中發現匯入檔案
import Vue from `core/index`
就順著這個思路找,最終找到 src/core/instance/index.js
這個檔案。
完整找到Vue例項入口檔案的流程如下:
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建構函式的樣子~
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
檔案。
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資料選項。除此之外還有其他重要方法的初始化方式,這將會在之後的部落格中繼續討論和學習。
學習State
之前是簡單提一下學習原始碼的方法論和如何開始學習Vue原始碼學習。並且找到了我們要學習的State所在,現在進入正題:
瞭解Vue的資料選項的執行機制。
在Vue2.1.7原始碼學習中,作者已經非常非常非常清晰明瞭的幫我們分析了data的實現。在此基礎上開始好好學習其他資料選項的實現邏輯。
通過data理解mvvm
這裡我通過自己的思路再來整理下專案中data的實現。
注:由於這一部分已經被各類原始碼解析部落格講爛了,而要把這部分講清楚要大量篇幅。所以我就不貼程式碼了。還是那句話,抓重點!我們主要研究的是data之外的實現方式。關於data的實現和mvvm的逐步實現,Vue2.1.7原始碼學習中講的非常清晰明瞭。
以下是我整理的思路,有興趣的同學可以順著我的思路去看看。
在 state.js 中找到 initState,並順利找到 initData 函式。initData中主要做了以下幾步操作:
- 獲取data資料,data資料通常是一個方法,執行方法返回data資料。所以說我們要將data寫成函式方法的形式。
- 遍歷data資料,判斷是否有data與props的key同名,如果沒有執行proxy方法,該方法用於將data中的資料同步到vm物件上,所以我們可以通過
vm.name
來修改和獲取 data 中的 name 的值。 - 執行observe方法,監聽data的變化。
重點在 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
方法做了如下幾步:
- 將當前 Watcher 例項傳遞給 Dep 的 Dep.target。
- 執行 Watcher 所監測的資料的
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 和 getter - 在新增 setter 和 getter 時,建立 Dep,在 getter 方法中執行
dep.depend()
收集 watcher,在 setter 方法中執行dep.notify()
方法,最終遍歷執行 watcher 陣列的回撥函式。 - Dep 類似於 Watcher 和 Observer 的中介軟體。
- Watcher 用於監聽變化,並執行回撥函式。
- 當 Watcher 例項建立時,Watcher 例項會將自身傳遞給 Dep.target
- Watcher 呼叫監測資料的
getter
方法觸發dep.depend()
-
dep.depend()
方法將當前 Watcher(Dep.target)傳遞給Dep的subs(watcher陣列)中。 - 當被監測的資料內容發生改變時,執行
setter
方法,觸發dep.notify()
方法,遍歷 Dep 中的 subs(watcher陣列),執行 Watcher 的回撥函式。
嗯……就是這樣~之後把挖的坑填上!
watch實現
說完了 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的定義如下:
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,找到初始化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函式中需要進行邏輯計算處理。
methods實現
繼續在 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.js學習系列
鑑於前端知識碎片化嚴重,我希望能夠系統化的整理出一套關於Vue的學習系列部落格。
Vue.js學習系列專案地址
本文原始碼已收入到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