原文首地址 掘金
三連哦 更多好文 github
大家好夭问,我是林一一圈膏,這是一篇關(guān)于 vue 的原理面試題芳肌,如果能夠完全弄懂相信對大家很有幫助肛搬。
面試題篇
1.老生常談之党觅, MPA/SPA 的理解谅海,優(yōu)缺點是什么?
MPA
多頁面應(yīng)用嫁乘。
- 構(gòu)成:有多個頁面
html
構(gòu)成昆婿, - 跳轉(zhuǎn)方式:頁面的跳轉(zhuǎn)是從一個頁面到另一個頁面
- 刷新的方式:全頁面刷新
- 頁面數(shù)據(jù)跳轉(zhuǎn):依賴
URL/cookie/localStorage
- 跳轉(zhuǎn)后的資源
會重新加載
- 優(yōu)點:對 SEO 比較友好,開發(fā)難度低一點蜓斧。
SPA
單頁面應(yīng)用 - 頁面組成:由一個外殼頁面包裹仓蛆,多個頁面(組件)片段組成
- 跳轉(zhuǎn)方式:在外殼頁面中跳轉(zhuǎn),將片段頁面(組件)顯示或隱藏
- 刷新方式:頁面片段的局部刷新
- 頁面的數(shù)據(jù)跳轉(zhuǎn):組件間的傳值比較容易
- 跳轉(zhuǎn)后的資源
不會重新加載
- 缺點:對 SEO 搜索不太友好需要單獨做配置挎春,開發(fā)難度高一點需要專門的開發(fā)框架
iframe 實際上是
MPA
看疙,但是可以實現(xiàn)SPA
的一些效果豆拨,但是本身由不少問題。
2.老生常談之能庆,為什么需要有這些 MVC/MVVM 模式施禾?談?wù)勀銓?MVC,MVVM 模式的區(qū)別搁胆,
目的:借鑒后端的思想弥搞,職責(zé)劃分和分層
- Vue, React 不是真正意義上的 MVVM 更不是 MVC,兩者核心只處理視圖層
view
渠旁。
MVC模式
單向的數(shù)據(jù)攀例,用戶的每一步操作都需要重新請求數(shù)據(jù)庫來修改視圖層的渲染,形成一個單向的閉環(huán)顾腊。比如
jQuery+underscore+backbone
粤铭。
- M:
model
數(shù)據(jù)存放層 - V:
view
:視圖層 頁面 - C:
controller
:控制器 js 邏輯層。
controller
控制層將數(shù)據(jù)層model層
的數(shù)據(jù)處理后顯示在視圖層view層
杂靶,同樣視圖層view層
接收用戶的指令也可以通過控制層controller
梆惯,作用到數(shù)據(jù)層model
。所以MVC的缺點是視圖層不能和數(shù)據(jù)層直接交互吗垮。
MVVM模式
隱藏了
controller
控制層加袋,直接操控View
視圖層和Model
數(shù)據(jù)層。
- M:model 數(shù)據(jù)模型
- V: view 視圖模板
- VM:view-model 視圖數(shù)據(jù)模板(vue處理的層抱既,vue 中的definedProperty 就是處理 VM 層的邏輯)
雙向的數(shù)據(jù)綁定:
model
數(shù)據(jù)模型層通過數(shù)據(jù)綁定Data Bindings
直接影響視圖層View
,同時視圖層view
通過監(jiān)聽Dom Listener
也可以改變數(shù)據(jù)模型層model
扁誓。
- 數(shù)據(jù)綁定和DOM事件監(jiān)聽就是
viewModel
層Vue
主要做的事防泵。也就是說:只要將數(shù)據(jù)模型層Model
的數(shù)據(jù)掛載到ViewModel
層Vue
就可以實現(xiàn)雙向的數(shù)據(jù)綁定。 - 加上
vuex/redux
可以作為vue和react
的model
數(shù)據(jù)層蝗敢。
var vm = new Vue()
vm 就是
view-model
數(shù)據(jù)模型層捷泞,data:就是vmview-model
層所代理的數(shù)據(jù)。
- 綜上兩者的區(qū)別:MVC 的視圖層和數(shù)據(jù)層交互需要通過控制層
controller
屬于單向鏈接寿谴。MVVM 隱藏了控制層controller
锁右,讓視圖層和數(shù)據(jù)層可以直接交互 屬于雙向連接。
3. 說一下對 Vue 中響應(yīng)式數(shù)據(jù)的理解
小tip:響應(yīng)式數(shù)據(jù)指的是數(shù)據(jù)發(fā)生了變化讶泰,視圖可以更新就是響應(yīng)式的數(shù)據(jù)
-
vue
中實現(xiàn)了一個definedReactive
方法咏瑟,方法內(nèi)部借用Object.definedProperty()
給每一個屬性都添加了get/set
的屬性。 -
definedReactive
只能監(jiān)控到最外層的對象痪署,對于內(nèi)層的對象需要遞歸劫持?jǐn)?shù)據(jù)码泞。 - 數(shù)組則是重寫的7個
push pop shift unshift reverse sort splice
來給數(shù)組做數(shù)據(jù)攔截,因為這幾個方法會改變原數(shù)組 - 擴展:
// src\core\observer\index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 準(zhǔn)備給屬性添加一個 dep 來依賴收集 Watcher 用于更新視圖狼犯。
const dep = new Dep()
// some code
// observe() 用來觀察值的類型余寥,如果是屬性也是對象就遞歸领铐,為每個屬性都加上`get/set`
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 這里取數(shù)據(jù)時依賴收集
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
// childOb 是對對像進(jìn)行收集依賴
if (childOb) {
childOb.dep.depend()
//這里對數(shù)組和內(nèi)部的數(shù)組進(jìn)行遞歸收集依賴,這里數(shù)組的 key 和 value 都有dep宋舷。
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 屬性發(fā)生改變绪撵,這里會通知 watcher 更新視圖
}
})
}
上面的 Dep(類) 是用來干嘛的?答:用來收集渲染的
Watcher
祝蝠,Watcher
又是一個啥東西音诈?答:watcher
是一個類,用于更新視圖的
4. Vue 是怎么檢測數(shù)組的變化的续膳?
- vue 沒有對數(shù)組的每一項用
definedProperty()
來數(shù)據(jù)攔截改艇,而是通過重寫數(shù)組的方法push pop shift unshift reverse sort splice
。 - 手動調(diào)用 notify,通知 render watcher,執(zhí)行 update
- 數(shù)組中如果有對象類型(
對象和數(shù)組
)的話會進(jìn)行數(shù)據(jù)攔截坟岔。 - 所以通過修改數(shù)組下標(biāo)和數(shù)組長度是不會進(jìn)行數(shù)據(jù)攔截的谒兄,也就不會有響應(yīng)式變化。例如
arr[0] = 1, arr.length = 2
都不會有響應(yīng)式 - 擴展:
// src\core\observer\array.js
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)
// 手動調(diào)用 notify 派發(fā)更新
ob.dep.notify()
return result
})
})
5.Vue 是怎樣依賴收集的社付?(dep 和 Watcher 是什么關(guān)系)
tip:
Dep
是一個用來負(fù)責(zé)收集Watcher
的類承疲,Watcher
是一個封裝了渲染視圖邏輯的類,用于派發(fā)更新的鸥咖。需要注意的是Watcher 是不能直接更新視圖的還需要結(jié)合Vnode經(jīng)過patch()中的diff算法才可以生成真正的DOM
- 每一個屬性都有自己的
dep
屬性燕鸽,來存放依賴的Watcher
,屬性發(fā)生變化后會通知Watcher
去更新啼辣。 - 在用戶獲取(
getter
) 數(shù)據(jù)時 Vue 給每一個屬性都添加了dep
屬性來(collect as Dependency)收集Watcher
啊研。在用戶setting
設(shè)置屬性值時dep.notify()
通知收集的Watcher
重新渲染。詳情見上面的defineReactive()
-
Dep依賴收集類
其和Watcher類
是多對多雙向存儲的關(guān)系 - 每一個屬性都可以有多個
Watcher 類
鸥拧,因為屬性可能在不同的組件中被使用党远。 - 同時一個
Watcher 類
也可以對應(yīng)多個屬性。
6. Vue 中的模板編譯
<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc2f78cda8514103b4d0a29acb1a4c9a~tplv-k3u1fbpfcp-watermark.image" width="80%" height="360px"/>
Vue中模板編譯:其實就是將
template
轉(zhuǎn)化成render
函數(shù)富弦。說白了就是將真實的DOM(模板)
編譯成虛擬dom(Vnode)
- 第一步是將
template 模板
字符串轉(zhuǎn)換成ast 語法樹
(parser 解析器)沟娱,這里使用了大量的正則來匹配標(biāo)簽的名稱,屬性腕柜,文本等济似。 - 第二步是對 AST 進(jìn)行靜態(tài)節(jié)點
static
標(biāo)記,主要用來做虛擬 DOM 的渲染優(yōu)化(optimize優(yōu)化器)盏缤,這里會遍歷出所有的子節(jié)點也做靜態(tài)標(biāo)記 - 第三步是 使用
ast語法樹
重新生成render 函數(shù)
代碼字符串 code砰蠢。(codeGen 代碼生成器)
為什么要靜態(tài)標(biāo)記節(jié)點,如果是靜態(tài)節(jié)點(沒有綁定數(shù)據(jù)蛾找,前后不需要發(fā)生變化的節(jié)點)那么后續(xù)就不需要 diff 算法來作比較娩脾。
7. 生命周期鉤子實現(xiàn)原理
- vue 中的生命周期鉤子只是一個回調(diào)函數(shù),在創(chuàng)建組件實例化的過程中會調(diào)用對應(yīng)的鉤子執(zhí)行打毛。
- 使用
Vue.mixin({})
混入的鉤子或生命周期中定義了多個函數(shù)柿赊,vue 內(nèi)部會調(diào)用mergeHook()
對鉤子進(jìn)行合并放入到隊列中依次執(zhí)行 - 擴展
// src\core\util\options.js
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal) // 合并
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
8.老生常談之 vue 生命周期有哪些俩功,一般在哪里發(fā)送請求?
-
beforeCreate
: 剛開始初始化 vue 實例碰声,在數(shù)據(jù)觀測observer
之前調(diào)用诡蜓,還沒有創(chuàng)建data/methods
等屬性 -
created
: vue 實例初始化結(jié)束,所有的屬性已經(jīng)創(chuàng)建胰挑。 -
beforeMount
: 在 vue 掛載數(shù)據(jù)到頁面上之前蔓罚,觸發(fā)這個鉤子,render 函數(shù)此時被觸發(fā)瞻颂。 -
mounted
: el 被 創(chuàng)建的vm.$el
替換豺谈,vue 初始化的數(shù)據(jù)已經(jīng)掛載到頁面之上,這里可以訪問到真實的 DOM贡这。一般會在這里請求數(shù)據(jù)茬末。 -
beforeUpdate
: 數(shù)據(jù)更新時調(diào)用,也就是在虛擬 dom 重新渲染之前盖矫。 -
updated
: 數(shù)據(jù)變化導(dǎo)致虛擬 dom 發(fā)生重新渲染之后發(fā)生丽惭。 -
beforeDestroy
: 實例銷毀之前調(diào)用該鉤子,此時實例還在辈双。vm.$destroy
觸發(fā)兩個方法责掏。 -
destroyed
: Vue 實例銷毀之后調(diào)用。所有的事件監(jiān)聽都會被接觸湃望。
請求數(shù)據(jù)要看具體的業(yè)務(wù)需求決定在哪里發(fā)送
ajax
9.Vue.mixin({})的使用場景和原理
- 使用場景:用于抽離一個公共的業(yè)務(wù)邏輯實現(xiàn)復(fù)用换衬。
- 實現(xiàn)原理:調(diào)用
mergeOptions()
方法采用策略模式針對不同的屬性合并≈ぐ牛混入的數(shù)據(jù)和組件的數(shù)據(jù)有沖突就采用組件本身的冗疮。 -
Vue.mixin({})
缺陷,1.可能會導(dǎo)致混入的屬性名和組件屬性名發(fā)生命名沖突檩帐;2. 數(shù)據(jù)依賴的來源問題 - 擴展
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// some code
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)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
10.老生常談之 vue 組件中的data 為什么必須是一個函數(shù)?
- 這和 js 本身機制相關(guān)另萤,
data
函數(shù)中返回的對象引用地址不同湃密,就能保證不同組件之間的數(shù)據(jù)不相互污染。 -
Vue.mixin()
中如果混入data
屬性四敞,那么data
也必須是一個函數(shù)泛源。因為Vue.mixin()
也可以多處使用。 - 實例中
data
可以是一個對象也可以是一個函數(shù)忿危,因為我們一個頁面一般只初始化一個Vue實例(單例)
11. 老生常談之 vue 中 vm.$nextTick(cb)實現(xiàn)原理和場景
- 場景:
在 dom 更新循環(huán)結(jié)束后調(diào)用达箍,用于獲取更新后的 dom 數(shù)據(jù)
- 實現(xiàn)原理:
vm.$nextTick(cb)
是一個異步的方法為了兼容性做了很多降級處理依次有promise.then,MutationObserver,setImmediate铺厨,setTimeout
缎玫。在數(shù)據(jù)修改后不會馬上更新視圖硬纤,而是經(jīng)過set
方法 notify 通知Watcher
更新,將需要更新的Watcher
放入到一個異步隊列中赃磨,nexTick
的回調(diào)函數(shù)就放在Watcher
的后面筝家,等待主線程中同步代碼執(zhí)行借宿然后依次清空隊列中,所以vm.nextTick(callback)
是在dom
更新結(jié)束后執(zhí)行的邻辉。
上面將對列中
Watcher
依次清空就是vue 異步批量更新的原理
溪王。提一個小思考:為什么不直接使用setTimeout
代替?因為setTimeout
是一個宏任務(wù)值骇,宏任務(wù)多性能也會差莹菱。關(guān)于事件循環(huán)可以看看 JS 事件循環(huán)
12.老生常談之 watch 和 computed 區(qū)別
-
computed
內(nèi)部就是根據(jù)Object.definedProperty()
實現(xiàn)的 -
computed
具備緩存功能,依賴的值不發(fā)生變化吱瘩,就不會重新計算道伟。 -
watch
是監(jiān)控值的變化,值發(fā)生變化時會執(zhí)行對應(yīng)的回調(diào)函數(shù)搅裙。 -
computed
和watch
都是基于Watcher類
來執(zhí)行的皱卓。
computed
緩存功能依靠一個變量dirty
,表示值是不是臟的默認(rèn)是true
部逮,取值后是false
娜汁,再次取值時dirty
還是false
直接將還是上一次的取值返回。
// src\core\instance\state.js computed 取值函數(shù)
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) { // 判斷值是不是臟 dirty
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
// src\core\instance\state.js watch 實現(xiàn)
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
// 實例化 watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}
參考
結(jié)束
謝謝大家閱讀到這里兄朋,如果覺得寫的還可以掐禁,歡迎三連呀,我是林一一颅和,下次見傅事。