1.談一下你對MVVM原理的理解
- 傳統的 MVC 指的是,用戶操作會請求服務端路由,路由會調用對應的控制器來處理,控制器會獲取數
據。將結果返回給前端,頁面重新渲染 - MVVM :傳統的前端會將數據手動渲染到頁面上, MVVM 模式不需要用戶收到操作 dom 元素,將數據綁
定到 viewModel 層上,會自動將數據渲染到頁面中,視圖變化會通知 viewModel層 更新數據。
ViewModel 就是我們 MVVM 模式中的橋樑.
2.請說一下響應式數據的原理?
理解:
- 1.核心點: Object.defineProperty
- 2.默認 Vue 在初始化數據時,會給 data 中的屬性使用 Object.defineProperty 重新定義所有屬
性,當頁面取到對應屬性時。會進行依賴收集(收集當前組件的watcher) 如果屬性發生變化會通
知相關依賴進行更新操作。
原理:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend() // ** 收集依賴 ** /
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
val = newVal
childOb = !shallow && observe(newVal)
dep.notify() /**通知相關依賴進行更新**/
}
})
3.Vue中是如何檢測數組變化?
理解:
- 使用函數劫持的方式,重寫了數組的方法
- Vue 將 data 中的數組,進行了原型鏈重寫。指向了自己定義的數組原型方法,這樣當調用數組
api 時,可以通知依賴更新.如果數組中包含着引用類型。會對數組中的引用類型再次進行監控。
原理:
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (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
})
})
this.observeArray(value) // 進行深度監控
4.爲何Vue採用異步渲染?
理解:
- 因爲如果不採用異步更新,那麼每次更新數據都會對當前組件進行重新渲染.所以爲了性能考慮。 Vue
會在本輪數據更新後,再去異步更新視圖!
原理:
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this); // 當數據發生變化時會將watcher放到一個隊列中批量更新
}
}
export function queueWatcher(watcher: Watcher) {
const id = watcher.id // 會對相同的watcher進行過濾
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
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方法 批量的進行更新
}
}
}
5.nextTick實現原理?
理解:(宏任務和微任務) 異步方法
nextTick
方法主要是使用了宏任務和微任務,定義了一個異步方法.多次調用nextTick
會將方法存入
隊列中,通過這個異步方法清空當前隊列。 所以這個nextTick
方法就是異步方法
原理:
let timerFunc // 會定義一個異步方法
if (typeof Promise !== 'undefined' && isNative(Promise)) { // promise
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && ( //
MutationObserver
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined') { // setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => { // setTimeout
setTimeout(flushCallbacks, 0)
}
}
// nextTick實現
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()
}
}
6.Vue中Computed的特點
理解:
- 默認 computed 也是一個 watcher 是具備緩存的,只要當依賴的屬性發生變化時纔會更新視圖
原理:
function initComputed(vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
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)
}
}
}
}
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
}
}
}
7.Watch中的deep:true 是如何實現的
理解:
- 當用戶指定了 watch 中的deep屬性爲 true 時,如果當前監控的值是數組類型。會對對象中的每
一項進行求值,此時會將當前 watcher 存入到對應屬性的依賴中,這樣數組中對象發生變化時也
會通知數據更新
原理:
get() {
pushTarget(this) // 先將當前依賴放到 Dep.target上
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 {
if (this.deep) { // 如果需要深度監控
traverse(value) // 會對對象中的每一項取值,取值時會執行對應的get方法
}
popTarget()
}
return value
}
function _traverse(val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
8.Vue組件的生命週期
理解:
要掌握每個生命週期什麼時候被調用
- beforeCreate 在實例初始化之後,數據觀測(data observer) 之前被調用。
- created 實例已經創建完成之後被調用。在這一步,實例已完成以下的配置:數據觀測(data
observer),屬性和方法的運算, watch/event 事件回調。這裏沒有$el - beforeMount 在掛載開始之前被調用:相關的 render 函數首次被調用。
- mounted el 被新創建的 vm.$el 替換,並掛載到實例上去之後調用該鉤子。
- beforeUpdate 數據更新時調用,發生在虛擬 DOM 重新渲染和打補丁之前。
- updated 由於數據更改導致的虛擬 DOM 重新渲染和打補丁,在這之後會調用該鉤子。
- beforeDestroy 實例銷燬之前調用。在這一步,實例仍然完全可用。
- destroyed Vue 實例銷燬後調用。調用後, Vue 實例指示的所有東西都會解綁定,所有的事件
監聽器會被移除,所有的子實例也會被銷燬。 該鉤子在服務器端渲染期間不被調用。
要掌握每個生命週期內部可以做什麼事
- created 實例已經創建完成,因爲它是最早觸發的原因可以進行一些數據,資源的請求。
- mounted 實例已經掛載完成,可以進行一些DOM操作
- beforeUpdate 可以在這個鉤子中進一步地更改狀態,這不會觸發附加的重渲染過程。
- updated 可以執行依賴於 DOM 的操作。然而在大多數情況下,你應該避免在此期間更改狀態,
因爲這可能會導致更新無限循環。 該鉤子在服務器端渲染期間不被調用。 - destroyed 可以執行一些優化操作,清空定時器,解除綁定事件
原理:
9.ajax請求放在哪個生命週期中
理解:
- 在created的時候,視圖中的 dom 並沒有渲染出來,所以此時如果直接去操 dom 節點,無法找到相
關的元素 - 在mounted中,由於此時 dom 已經渲染出來了,所以可以直接操作 dom 節點
一般情況下都放到 mounted 中,保證邏輯的統一性,因爲生命週期是同步執行的, ajax 是異步執行的
服務端渲染不支持mounted方法,所以在服務端渲染的情況下統一放到created中
10.何時需要使用beforeDestroy
理解:
- 可能在當前頁面中使用了 $on 方法,那需要在組件銷燬前解綁。
- 清除自己定義的定時器
- 解除事件的綁定 scroll mousemove …