當學習成為了習慣,知識也就變成了常識。 感謝各位的 點贊、收藏和評論。
新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn
文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。
目標
深入理解 Vue 的初始化過程,再也不怕 面試官 的那道面試題:new Vue(options)
發生了什麼?
找入口
想知道 new Vue(options)
都做了什麼,就得先找到 Vue 的建構函式是在哪宣告的,有兩個辦法:
-
從 rollup 配置檔案中找到編譯的入口,然後一步步找到 Vue 建構函式,這種方式 費勁
-
通過編寫示例程式碼,然後打斷點的方式,一步到位,簡單
我們就採用第二種方式,寫示例,打斷點,一步到位。
- 在
/examples
目錄下增加一個示例檔案 ——test.html
,在檔案中新增如下內容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 原始碼解讀</title>
</head>
<body>
<div id="app">
{{ msg }}
</div>
<script src="../dist/vue.js"></script>
<script>
debugger
new Vue({
el: '#app',
data: {
msg: 'hello vue'
}
})
</script>
</body>
</html>
- 在瀏覽器中開啟控制檯,然後開啟
test.html
,則會進入斷點除錯,然後找到 Vue 建構函式所在的檔案
點選檢視演示動圖,動圖地址:https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d839ea6f3e5d4adcaf1ea9a8f6ff1a70~tplv-k3u1fbpfcp-watermark.awebp
得到 Vue 建構函式在 /src/core/instance/index.js
檔案中,接下來正式開始原始碼閱讀,帶著目標去閱讀。
在閱讀過程中如遇到看不明白的地方,可通過編寫示例程式碼,然後使用瀏覽器的除錯功能進行一步步除錯,配合理解,如果還是理解不了,就做個備註繼續向後看,也許你看到其它地方,就突然明白這個地方在做什麼,或者回頭再來看,就會懂了,原始碼這個東西,一定要多看,要想精通,一遍兩遍肯定是不夠的
原始碼解讀 —— Vue 初始化過程
Vue
/src/core/instance/index.js
import { initMixin } from './init'
// Vue 建構函式
function Vue (options) {
// 呼叫 Vue.prototype._init 方法,該方法是在 initMixin 中定義的
this._init(options)
}
// 定義 Vue.prototype._init 方法
initMixin(Vue)
export default Vue
Vue.prototype._init
/src/core/instance/init.js
/**
* 定義 Vue.prototype._init 方法
* @param {*} Vue Vue 建構函式
*/
export function initMixin (Vue: Class<Component>) {
// 負責 Vue 的初始化過程
Vue.prototype._init = function (options?: Object) {
// vue 例項
const vm: Component = this
// 每個 vue 例項都有一個 _uid,並且是依次遞增的
vm._uid = uid++
// a flag to avoid this being observed
vm._isVue = true
// 處理元件配置項
if (options && options._isComponent) {
/**
* 每個子元件初始化時走這裡,這裡只做了一些效能優化
* 將元件配置物件上的一些深層次屬性放到 vm.$options 選項中,以提高程式碼的執行效率
*/
initInternalComponent(vm, options)
} else {
/**
* 初始化根元件時走這裡,合併 Vue 的全域性配置到根元件的區域性配置,比如 Vue.component 註冊的全域性元件會合併到 根例項的 components 選項中
* 至於每個子元件的選項合併則發生在兩個地方:
* 1、Vue.component 方法註冊的全域性元件在註冊時做了選項合併
* 2、{ components: { xx } } 方式註冊的區域性元件在執行編譯器生成的 render 函式時做了選項合併,包括根元件中的 components 配置
*/
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 設定代理,將 vm 例項上的屬性代理到 vm._renderProxy
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化元件例項關係屬性,比如 $parent、$children、$root、$refs 等
initLifecycle(vm)
/**
* 初始化自定義事件,這裡需要注意一點,所以我們在 <comp @click="handleClick" /> 上註冊的事件,監聽者不是父元件,
* 而是子元件本身,也就是說事件的派發和監聽者都是子元件本身,和父元件無關
*/
initEvents(vm)
// 解析元件的插槽資訊,得到 vm.$slot,處理渲染函式,得到 vm.$createElement 方法,即 h 函式
initRender(vm)
// 呼叫 beforeCreate 鉤子函式
callHook(vm, 'beforeCreate')
// 初始化元件的 inject 配置項,得到 result[key] = val 形式的配置物件,然後對結果資料進行響應式處理,並代理每個 key 到 vm 例項
initInjections(vm) // resolve injections before data/props
// 資料響應式的重點,處理 props、methods、data、computed、watch
initState(vm)
// 解析元件配置項上的 provide 物件,將其掛載到 vm._provided 屬性上
initProvide(vm) // resolve provide after data/props
// 呼叫 created 鉤子函式
callHook(vm, 'created')
// 如果發現配置項上有 el 選項,則自動呼叫 $mount 方法,也就是說有了 el 選項,就不需要再手動呼叫 $mount,反之,沒有 el 則必須手動呼叫 $mount
if (vm.$options.el) {
// 呼叫 $mount 方法,進入掛載階段
vm.$mount(vm.$options.el)
}
}
}
resolveConstructorOptions
/src/core/instance/init.js
/**
* 從元件建構函式中解析配置物件 options,併合並基類選項
* @param {*} Ctor
* @returns
*/
export function resolveConstructorOptions (Ctor: Class<Component>) {
// 配置專案
let options = Ctor.options
if (Ctor.super) {
// 存在基類,遞迴解析基類建構函式的選項
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// 說明基類建構函式選項已經發生改變,需要重新設定
Ctor.superOptions = superOptions
// 檢查 Ctor.options 上是否有任何後期修改/附加的選項(#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// 如果存在被修改或增加的選項,則合併兩個選項
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
// 選項合併,將合併結果賦值為 Ctor.options
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
resolveModifiedOptions
/src/core/instance/init.js
/**
* 解析建構函式選項中後續被修改或者增加的選項
*/
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
let modified
// 建構函式選項
const latest = Ctor.options
// 密封的建構函式選項,備份
const sealed = Ctor.sealedOptions
// 對比兩個選項,記錄不一致的選項
for (const key in latest) {
if (latest[key] !== sealed[key]) {
if (!modified) modified = {}
modified[key] = latest[key]
}
}
return modified
}
mergeOptions
/src/core/util/options.js
/**
* 合併兩個選項,出現相同配置項時,子選項會覆蓋父選項的配置
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
// 標準化 props、inject、directive 選項,方便後續程式的處理
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// 處理原始 child 物件上的 extends 和 mixins,分別執行 mergeOptions,將這些繼承而來的選項合併到 parent
// mergeOptions 處理過的物件會含有 _base 屬性
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, 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)
}
}
// 合併選項,childVal 優先順序高於 parentVal
function mergeField (key) {
// strats = Object.create(null)
const strat = strats[key] || defaultStrat
// 值為如果 childVal 存在則優先使用 childVal,否則使用 parentVal
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
initInjections
/src/core/instance/inject.js
/**
* 初始化 inject 配置項
* 1、得到 result[key] = val
* 2、對結果資料進行響應式處理,代理每個 key 到 vm 例項
*/
export function initInjections (vm: Component) {
// 解析 inject 配置項,然後從祖代元件的配置中找到 配置項中每一個 key 對應的 val,最後得到 result[key] = val 的結果
const result = resolveInject(vm.$options.inject, vm)
// 對 result 做 資料響應式處理,也有代理 inject 配置中每個 key 到 vm 例項的作用。
// 不不建議在子元件去更改這些資料,因為一旦祖代元件中 注入的 provide 發生更改,你在元件中做的更改就會被覆蓋
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
resolveInject
/src/core/instance/inject.js
/**
* 解析 inject 配置項,從祖代元件的 provide 配置中找到 key 對應的值,否則用 預設值,最後得到 result[key] = val
* inject 物件肯定是以下這個結構,因為在 合併 選項時對元件配置物件做了標準化處理
* @param {*} inject = {
* key: {
* from: provideKey,
* default: xx
* }
* }
*/
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
// inject 配置項的所有的 key
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
// 遍歷 key
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// 跳過 __ob__ 物件
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
// 拿到 provide 中對應的 key
const provideKey = inject[key].from
let source = vm
// 遍歷所有的祖代元件,直到 根元件,找到 provide 中對應 key 的值,最後得到 result[key] = provide[provideKey]
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
// 如果上一個迴圈未找到,則採用 inject[key].default,如果沒有設定 default 值,則丟擲錯誤
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}
initProvide
/src/core/instance/inject.js
/**
* 解析元件配置項上的 provide 物件,將其掛載到 vm._provided 屬性上
*/
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
總結
Vue 的初始化過程(new Vue(options))都做了什麼?
-
處理元件配置項
-
初始化根元件時進行了選項合併操作,將全域性配置合併到根元件的區域性配置上
-
初始化每個子元件時做了一些效能優化,將元件配置物件上的一些深層次屬性放到 vm.$options 選項中,以提高程式碼的執行效率
-
-
初始化元件例項的關係屬性,比如 $parent、$children、$root、$refs 等
-
處理自定義事件
-
呼叫 beforeCreate 鉤子函式
-
初始化元件的 inject 配置項,得到
ret[key] = val
形式的配置物件,然後對該配置物件進行淺層的響應式處理(只處理了物件第一層資料),並代理每個 key 到 vm 例項上 -
資料響應式,處理 props、methods、data、computed、watch 等選項
-
解析元件配置項上的 provide 物件,將其掛載到 vm._provided 屬性上
-
呼叫 created 鉤子函式
-
如果發現配置項上有 el 選項,則自動呼叫 $mount 方法,也就是說有了 el 選項,就不需要再手動呼叫 $mount 方法,反之,沒提供 el 選項則必須呼叫 $mount
-
接下來則進入掛載階段
連結
- 配套視訊,關注微信公眾號回覆:"精通 Vue 技術棧原始碼原理視訊版" 獲取
- 精通 Vue 技術棧原始碼原理 專欄
- github 倉庫 liyongning/Vue 歡迎 Star
感謝各位的:點贊、收藏和評論,我們下期見。
當學習成為了習慣,知識也就變成了常識。 感謝各位的 點贊、收藏和評論。
新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn
文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。