最近一直在讀Vue源碼,也寫了一系列的源碼探秘文章。
但,收到很多朋友的反饋都是:源碼晦澀難懂,時常看著看著就不知道我在看什麼了,感覺缺乏一點動力,如果你可以出點面試中會問到的源碼相關的面試題,通過面試題去看源碼,那就很棒棒。
看到大家的反饋,我絲毫沒有猶豫:安排!!
我通過三篇文章整理了大廠面試中會經常問到的一些Vue面試題,通過源碼角度去回答,拋棄純概念型回答,相信一定會讓面試官對你刮目相看。
請說一下響應式數據的原理?Vue實現響應式數據的核心API是Object.defineProperty。
其實默認Vue在初始化數據時,會給data中的屬性使用Object.defineProperty重新定義所有屬性,當頁面取到對應屬性時。會進行依賴收集(收集當前組件的watcher) 如果屬性發生變化會通知相關依賴進行更新操作。
這裡,我用一張圖來說明Vue實現響應式數據的流程:
首先,第一步是初始化用戶傳入的data數據。這一步對應源碼src/core/instance/state.js的 112 行function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
// ...
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
// ...
}
// observe data
observe(data, true /* asRootData */)
}
第二步是將數據進行觀測,也就是在第一步的initData的最後調用的observe函數。對應在源碼的src/core/observer/index.js的 110 行/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}這裡會通過new Observer(value)創建一個Observer實例,實現對數據的觀測。
第三步是實現對對象的處理。對應源碼src/core/observer/index.js的 55 行。/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// ...
}
第四步就是循環對象屬性定義響應式變化了。對應源碼src/core/observer/index.js的 135 行。/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend() // 收集依賴
// ...
}
return value
},
set: function reactiveSetter (newVal) {
// ...
dep.notify() // 通知相關依賴進行更新
}
})
}
第五步其實就是使用defineReactive方法中的Object.defineProperty重新定義數據。在get中通過dep.depend()收集依賴。當數據改變時,攔截屬性的更新操作,通過set中的dep.notify()通知相關依賴進行更新。Vue 中是如何檢測數組變化?Vue中檢測數組變化核心有兩點:
Vue 將 data 中的數組,進行了原型鏈重寫。指向了自己定義的數組原型方法,這樣當調用數組 api 時,就可以通知依賴更新。如果數組中包含著引用類型,會對數組中的引用類型再次進行觀測。這裡用一張流程圖來說明:
❝這裡第一步和第二步和上題請說一下響應式數據的原理?是相同的,就不展開說明了。
❞第一步同樣是初始化用戶傳入的 data 數據。對應源碼src/core/instance/state.js的 112 行的initData函數。第二步是對數據進行觀測。對應源碼src/core/observer/index.js的 124 行。第三步是將數組的原型方法指向重寫的原型。對應源碼src/core/observer/index.js的 49 行。if (hasProto) {
protoAugment(value, arrayMethods)
} else {
// ...
}也就是protoAugment方法:
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
第四步進行了兩步操作。首先是對數組的原型方法進行重寫,對應源碼src/core/observer/array.js。/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [ // 這裡列舉的數組的方法是調用後能改變原數組的
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) { // 重寫原型方法
// cache original method
const original = arrayProto[method] // 調用原數組方法
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted) // 進行深度監控
// notify change
ob.dep.notify() // 調用數組方法後,手動通知視圖更新
return result
})
})
為什麼Vue採用異步渲染?我們先來想一個問題:如果Vue不採用異步更新,那麼每次數據更新時是不是都會對當前組件進行重寫渲染呢?
答案是肯定的,為了性能考慮,會在本輪數據更新後,再去異步更新視圖。
通過一張圖來說明Vue異步更新的流程:
第一步調用dep.notify()通知watcher進行更新操作。對應源碼src/core/observer/dep.js中的 37 行。notify () { // 通知依賴更新
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 依賴中的update方法
}
}
第二步其實就是在第一步的notify方法中,遍歷subs,執行subs[i].update()方法,也就是依次調用watcher的update方法。對應源碼src/core/observer/watcher.js的 164 行/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) { // 計算屬性
this.dirty = true
} else if (this.sync) { // 同步watcher
this.run()
} else {
queueWatcher(this) // 當數據發生變化時會將watcher放到一個隊列中批量更新
}
}
第三步是執行update函數中的queueWatcher方法。對應源碼src/core/observer/scheduler.js的 164 行。
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id // 過濾watcher,多個屬性可能會依賴同一個watcher
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher) // 將watcher放到隊列中
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue) // 調用nextTick方法,在下一個tick中刷新watcher隊列
}
}
}
第四步就是執行nextTick(flushSchedulerQueue)方法,在下一個tick中刷新watcher隊列談一下nextTick的實現原理?Vue.js在默認情況下,每次觸發某個數據的 setter 方法後,對應的 Watcher 對象其實會被 push 進一個隊列 queue 中,在下一個 tick 的時候將這個隊列 queue 全部拿出來 run( Watcher 對象的一個方法,用來觸發 patch 操作) 一遍。
因為目前瀏覽器平臺並沒有實現 nextTick 方法,所以 Vue.js 源碼中分別用 Promise、setTimeout、setImmediate 等方式在 microtask(或是task)中創建一個事件,目的是在當前調用棧執行完畢以後(不一定立即)才會去執行這個事件。
nextTick方法主要是使用了宏任務和微任務,定義了一個異步方法.多次調用nextTick 會將方法存入隊列中,通過這個異步方法清空當前隊列。
❝所以這個 nextTick 方法是異步方法。
❞通過一張圖來看下nextTick的實現:
首先會調用nextTick並傳入cb。對應源碼src/core/util/next-tick.js的 87 行。export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
接下來會定義一個callbacks 數組用來存儲 nextTick,在下一個 tick 處理這些回調函數之前,所有的 cb 都會被存在這個 callbacks 數組中。下一步會調用timerFunc函數。對應源碼src/core/util/next-tick.js的 33 行。let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
timerFunc = () => {
// ...
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
timerFunc = () => {
// ...
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}來看下timerFunc的取值邏輯:
1、 我們知道異步任務有兩種,其中 microtask 要優於 macrotask ,所以優先選擇 Promise 。因此這裡先判斷瀏覽器是否支持 Promise。
2、 如果不支持再考慮 macrotask 。對於 macrotask 會先後判斷瀏覽器是否支持 MutationObserver 和 setImmediate 。
3、 如果都不支持就只能使用 setTimeout 。這也從側面展示出了 macrotask 中 setTimeout 的性能是最差的。
❝nextTick中 if (!pending) 語句中 pending 作用顯然是讓 if 語句的邏輯只執行一次,而它其實就代表 callbacks 中是否有事件在等待執行。
❞這裡的flushCallbacks函數的主要邏輯就是將 pending 置為 false 以及清空 callbacks 數組,然後遍歷 callbacks 數組,執行裡面的每一個函數。
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}這裡 if 對應的情況是我們調用 nextTick 函數時沒有傳入回調函數並且瀏覽器支持 Promise ,那麼就會返回一個 Promise 實例,並且將 resolve 賦值給 _resolve。回到nextTick開頭的一段代碼:
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})當我們執行 callbacks 的函數時,發現沒有 cb 而有 _resolve 時就會執行之前返回的 Promise 對象的 resolve 函數。
你知道Vue中computed是怎麼實現的嗎?這裡先給一個結論:計算屬性computed的本質是 computed Watcher,其具有緩存。
一張圖了解下computed的實現:
首先是在組件實例化時會執行initComputed方法。對應源碼src/core/instance/state.js的 169 行。const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}initComputed 函數拿到 computed 對象然後遍歷每一個計算屬性。判斷如果不是服務端渲染就會給計算屬性創建一個 computed Watcher 實例賦值給watchers[key](對應就是vm._computedWatchers[key])。然後遍歷每一個計算屬性調用 defineComputed 方法,將組件原型,計算屬性和對應的值傳入。
defineComputed定義在源碼src/core/instance/state.js210 行。// src/core/instance/state.js
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering();
if (typeof userDef === "function") {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (
process.env.NODE_ENV !== "production" &&
sharedPropertyDefinition.set === noop
) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}首先定義了 shouldCache 表示是否需要緩存值。接著對 userDef 是函數或者對象分別處理。這裡有一個 sharedPropertyDefinition ,我們來看它的定義:
// src/core/instance/state.js
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};sharedPropertyDefinition其實就是一個屬性描述符。
回到 defineComputed 函數。如果 userDef 是函數的話,就會定義 getter 為調用 createComputedGetter(key) 的返回值。
❝因為 shouldCache 是 true
❞而 userDef 是對象的話,非服務端渲染並且沒有指定 cache 為 false 的話,getter 也是調用 createComputedGetter(key) 的返回值,setter 則為 userDef.set 或者為空。
所以 defineComputed 函數的作用就是定義 getter 和 setter ,並且在最後調用 Object.defineProperty 給計算屬性添加 getter/setter ,當我們訪問計算屬性時就會觸發這個 getter。
❝對於計算屬性的 setter 來說,實際上是很少用到的,除非我們在使用 computed 的時候指定了 set 函數。
❞無論是userDef是函數還是對象,最終都會調用createComputedGetter函數,我們來看createComputedGetter的定義:function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
}
};
}
❝我們知道訪問計算屬性時才會觸發這個 getter,對應就是computedGetter函數被執行。
❞computedGetter 函數首先通過 this._computedWatchers[key] 拿到前面實例化組件時創建的 computed Watcher 並賦值給 watcher 。
❝在new Watcher時傳入的第四個參數computedWatcherOptions的lazy為true,對應就是watcher的構造函數中的dirty為true。在computedGetter中,如果dirty為true(即依賴的值沒有發生變化),就不會重新求值。相當於computed被緩存了。
❞接著有兩個 if 判斷,首先調用 evaluate 函數:
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}首先調用 this.get() 將它的返回值賦值給 this.value ,來看 get 函數:
// src/core/observer/watcher.js
/**
* Evaluate the getter, and re-collect dependencies.
*/
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 函數第一步是調用 pushTarget 將 computed Watcher 傳入:
// src/core/observer/dep.js
export function pushTarget(target: ?Watcher) {
targetStack.push(target);
Dep.target = target;
}可以看到 computed Watcher 被 push 到 targetStack 同時將 Dep.target 置為 computed Watcher 。而 Dep.target 原來的值是渲染 Watcher ,因為正處於渲染階段。回到 get 函數,接著就調用了 this.getter 。
回到 evaluate 函數:
evaluate () {
this.value = this.get()
this.dirty = false
}執行完get函數,將dirty置為false。
回到computedGetter函數,接著往下進入另一個if判斷,執行了depend函數:
// src/core/observer/watcher.js
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}這裡的邏輯就是讓 Dep.target 也就是渲染 Watcher 訂閱了 this.dep 也就是前面實例化 computed Watcher 時候創建的 dep 實例,渲染 Watcher 就被保存到 this.dep 的 subs 中。
在執行完 evaluate 和 depend 函數後,computedGetter 函數最後將 evaluate 的返回值返回出去,也就是計算屬性最終計算出來的值,這樣頁面就渲染出來了。