前端vue面試題(持續更新中)
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() }
什麼是作用域插槽
插槽
- 建立元件虛擬節點時,會將元件兒子的虛擬節點儲存起來。當初始化元件時,通過插槽屬性將兒子進行分類
{a:[vnode],b[vnode]}
- 渲染元件時會拿對應的
slot
屬性的節點進行替換操作。(插槽的作用域為父元件)
<app>
<div slot="a">xxxx</div>
<div slot="b">xxxx</div>
</app>
slot name="a"
slot name="b"
作用域插槽
- 作用域插槽在解析的時候不會作為元件的孩子節點。會解析成函式,當子元件渲染時,會呼叫此函式進行渲染。(插槽的作用域為子元件)
- 普通插槽渲染的作用域是父元件,作用域插槽的渲染作用域是當前子元件。
// 插槽 const VueTemplateCompiler = require('vue-template-compiler'); let ele = VueTemplateCompiler.compile(` <my-component> <div slot="header">node</div> <div>react</div> <div slot="footer">vue</div> </my-component> ` ) // with(this) { // return _c('my-component', [_c('div', { // attrs: { "slot": "header" }, // slot: "header" // }, [_v("node")] // _文字及誒點 ) // , _v(" "), // _c('div', [_v("react")]), _v(" "), _c('div', { // attrs: { "slot": "footer" }, // slot: "footer" }, [_v("vue")])]) // } const VueTemplateCompiler = require('vue-template-compiler'); let ele = VueTemplateCompiler.compile(` <div> <slot name="header"></slot> <slot name="footer"></slot> <slot></slot> </div> ` ); with(this) { return _c('div', [_v("node"), _v(" "), _t(_v("vue")])]), _v(" "), _t("default")], 2) } // _t定義在 core/instance/render-helpers/index.js
// 作用域插槽:
let ele = VueTemplateCompiler.compile(` <app>
<div slot-scope="msg" slot="footer">{{msg.a}}</div>
</app> `
);
// with(this) {
// return _c('app', { scopedSlots: _u([{
// // 作用域插槽的內容會被渲染成一個函式
// key: "footer",
// fn: function (msg) {
// return _c('div', {}, [_v(_s(msg.a))]) } }])
// })
// }
// }
const VueTemplateCompiler = require('vue-template-compiler');
VueTemplateCompiler.compile(` <div><slot name="footer" a="1" b="2"></slot> </div> `);
// with(this) { return _c('div', [_t("footer", null, { "a": "1", "b": "2" })], 2) }
router-link和router-view是如何起作用的
分析
vue-router
中兩個重要元件router-link
和router-view
,分別起到導航作用和內容渲染作用,但是回答如何生效還真有一定難度
回答範例
-
vue-router
中兩個重要元件router-link
和router-view
,分別起到路由導航作用和元件內容渲染作用 - 使用中
router-link
預設生成一個a
標籤,設定to
屬性定義跳轉path
。實際上也可以通過custom
和插槽自定義最終的展現形式。router-view
是要顯示元件的佔位元件,可以巢狀,對應路由配置的巢狀關係,配合name
可以顯示具名元件,起到更強的佈局作用。 -
router-link
元件內部根據custom
屬性判斷如何渲染最終生成節點,內部提供導航方法navigate
,使用者點選之後實際呼叫的是該方法,此方法最終會修改響應式的路由變數,然後重新去routes
匹配出陣列結果,router-view
則根據其所處深度deep
在匹配陣列結果中找到對應的路由並獲取元件,最終將其渲染出來。
雙向繫結的原理是什麼
我們都知道 Vue
是資料雙向繫結的框架,雙向繫結由三個重要部分構成
- 資料層(Model):應用的資料及業務邏輯
- 檢視層(View):應用的展示效果,各類UI元件
- 業務邏輯層(ViewModel):框架封裝的核心,它負責將資料與檢視關聯起來
而上面的這個分層的架構方案,可以用一個專業術語進行稱呼:MVVM
這裡的控制層的核心功能便是 “資料雙向繫結” 。自然,我們只需弄懂它是什麼,便可以進一步瞭解資料繫結的原理
理解ViewModel
它的主要職責就是:
- 資料變化後更新檢視
- 檢視變化後更新資料
當然,它還有兩個主要部分組成
- 監聽器(
Observer
):對所有資料的屬性進行監聽 - 解析器(
Compiler
):對每個元素節點的指令進行掃描跟解析,根據指令模板替換資料,以及繫結相應的更新函式
Vue中修飾符.sync與v-model的區別
sync
的作用
-
.sync
修飾符可以實現父子元件之間的雙向繫結,並且可以實現子元件同步修改父元件的值,相比較與v-model
來說,sync
修飾符就簡單很多了 - 一個元件上可以有多個
.sync
修飾符
<!-- 正常父傳子 -->
<Son :a="num" :b="num2" />
<!-- 加上sync之後的父傳子 -->
<Son :a.sync="num" :b.sync="num2" />
<!-- 它等價於 -->
<Son
:a="num"
:b="num2"
@update:a="val=>num=val"
@update:b="val=>num2=val"
/>
<!-- 相當於多了一個事件監聽,事件名是update:a, -->
<!-- 回撥函式中,會把接收到的值賦值給屬性繫結的資料項中。 -->
v-model
的工作原理
<com1 v-model="num"></com1>
<!-- 等價於 -->
<com1 :value="num" @input="(val)=>num=val"></com1>
- 相同點
- 都是語法糖,都可以實現父子元件中的資料的雙向通訊
- 區別點
- 格式不同:
v-model="num"
,:num.sync="num"
-
v-model
:@input + value
-
:num.sync
:@update:num
-
v-model
只能用一次;.sync
可以有多個
- 格式不同:
實現雙向繫結
我們還是以Vue
為例,先來看看Vue
中的雙向繫結流程是什麼的
-
new Vue()
首先執行初始化,對data
執行響應化處理,這個過程發生Observe
中 - 同時對模板執行編譯,找到其中動態繫結的資料,從
data
中獲取並初始化檢視,這個過程發生在Compile
中 - 同時定義⼀個更新函式和
Watcher
,將來對應資料變化時Watcher
會呼叫更新函式 - 由於
data
的某個key
在⼀個檢視中可能出現多次,所以每個key
都需要⼀個管家Dep
來管理多個Watcher
- 將來data中資料⼀旦發生變化,會首先找到對應的
Dep
,通知所有Watcher
執行更新函式
流程圖如下:
先來一個建構函式:執行初始化,對data
執行響應化處理
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 對data選項做響應式處理
observe(this.$data);
// 代理data到vm上
proxy(this);
// 執行編譯
new Compile(options.el, this);
}
}
對data
選項執行響應化具體操作
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return;
}
new Observer(obj);
}
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
編譯Compile
對每個元素節點的指令進行掃描跟解析,根據指令模板替換資料,以及繫結相應的更新函式
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 獲取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍歷子元素
if (this.isElement(node)) { // 判斷是否為節點
console.log("編譯元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("編譯插值⽂本" + node.textContent); // 判斷是否為插值文字 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判斷是否有子元素
this.compile(node); // 對子元素進行遞迴遍歷
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
依賴收集
檢視中會用到data
中某key
,這稱為依賴。同⼀個key
可能出現多次,每次都需要收集出來用⼀個Watcher
來維護它們,此過程稱為依賴收集多個Watcher
需要⼀個Dep
來管理,需要更新時由Dep
統⼀通知
實現思路
-
defineReactive
時為每⼀個key
建立⼀個Dep
例項 - 初始化檢視時讀取某個
key
,例如name1
,建立⼀個watcher1
- 由於觸發
name1
的getter
方法,便將watcher1
新增到name1
對應的Dep
中 - 當
name1
更新,setter
觸發時,便可通過對應Dep
通知其管理所有Watcher
更新
// 負責更新檢視
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 建立例項時,把當前例項指定到Dep.target靜態屬性上
Dep.target = this
// 讀一下key,觸發get
vm[key]
// 置空
Dep.target = null
}
// 未來執行dom更新函式,由dep呼叫的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
宣告Dep
class Dep {
constructor() {
this.deps = []; // 依賴管理
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
建立watcher
時觸發getter
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
依賴收集,建立Dep
例項
function defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher例項
return val;
},
set(newVal) {
if (newVal === val) return;
dep.notify(); // 通知dep執行更新方法
},
});
}
參考 前端進階面試題詳細解答
Vue要做許可權管理該怎麼做?控制到按鈕級別的許可權怎麼做?
分析
- 綜合實踐題目,實際開發中經常需要面臨許可權管理的需求,考查實際應用能力。
- 許可權管理一般需求是兩個:頁面許可權和按鈕許可權,從這兩個方面論述即可。
思路
- 許可權管理需求分析:頁面和按鈕許可權
- 許可權管理的實現方案:分後端方案和前端方案闡述
- 說說各自的優缺點
回答範例
-
許可權管理一般需求是頁面許可權和按鈕許可權的管理
-
具體實現的時候分後端和前端兩種方案:
-
前端方案 會把所有路由資訊在前端配置,通過路由守衛要求使用者登入,使用者登入後根據角色過濾出路由表。比如我會配置一個
asyncRoutes
陣列,需要認證的頁面在其路由的meta
中新增一個roles
欄位,等獲取使用者角色之後取兩者的交集,若結果不為空則說明可以訪問。此過濾過程結束,剩下的路由就是該使用者能訪問的頁面,最後通過router.addRoutes(accessRoutes)
方式動態新增路由即可 -
後端方案 會把所有頁面路由資訊存在資料庫中,使用者登入的時候根據其角色查詢得到其能訪問的所有頁面路由資訊返回給前端,前端再通過
addRoutes
動態新增路由資訊 -
按鈕許可權的控制通常會
實現一個指令
,例如v-permission
,將按鈕要求角色通過值傳給v-permission
指令,在指令的moutned
鉤子中可以判斷當前使用者角色和按鈕是否存在交集,有則保留按鈕,無則移除按鈕
- 純前端方案的優點是實現簡單,不需要額外許可權管理頁面,但是維護起來問題比較大,有新的頁面和角色需求就要修改前端程式碼重新打包部署;服務端方案就不存在這個問題,通過專門的角色和許可權管理頁面,配置頁面和按鈕許可權資訊到資料庫,應用每次登陸時獲取的都是最新的路由資訊,可謂一勞永逸!
可能的追問
- 類似
Tabs
這類元件能不能使用v-permission
指令實現按鈕許可權控制?
<el-tabs>
<el-tab-pane label="⽤戶管理" name="first">⽤戶管理</el-tab-pane>
<el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane>
</el-tabs>
- 服務端返回的路由資訊如何新增到路由器中?
// 前端元件名和元件對映表
const map = {
//xx: require('@/views/xx.vue').default // 同步的⽅式
xx: () => import('@/views/xx.vue') // 非同步的⽅式
}
// 服務端返回的asyncRoutes
const asyncRoutes = [
{ path: '/xx', component: 'xx',... }
]
// 遍歷asyncRoutes,將component替換為map[component]
function mapComponent(asyncRoutes) {
asyncRoutes.forEach(route => {
route.component = map[route.component];
if(route.children) {
route.children.map(child => mapComponent(child))
}
})
}
mapComponent(asyncRoutes)
怎樣理解 Vue 的單向資料流
資料總是從父元件傳到子元件,子元件沒有權利修改父元件傳過來的資料,只能請求父元件對原始資料進行修改。這樣會 防止從子元件意外改變父級元件的狀態 ,從而導致你的應用的資料流向難以理解
注意 :在子元件直接用 v-model
繫結父元件傳過來的 prop
這樣是不規範的寫法 開發環境會報警告
如果實在要改變父元件的 prop
值,可以在 data
裡面定義一個變數 並用 prop
的值初始化它 之後用$emit
通知父元件去修改
有兩種常見的試圖改變一個 prop 的情形 :
- 這個
prop
用來傳遞一個初始值;這個子元件接下來希望將其作為一個本地的prop
資料來使用。 在這種情況下,最好定義一個本地的data
屬性並將這個prop
用作其初始值
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
- 這個
prop
以一種原始的值傳入且需要進行轉換。 在這種情況下,最好使用這個prop
的值來定義一個計算屬性
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
瞭解history有哪些方法嗎?說下它們的區別
history
這個物件在html5
的時候新加入兩個api
history.pushState()
和history.repalceState()
這兩個API
可以在不進行重新整理的情況下,操作瀏覽器的歷史紀錄。唯一不同的是,前者是新增一個歷史記錄,後者是直接替換當前的歷史記錄。
從引數上來說:
window.history.pushState(state,title,url)
//state:需要儲存的資料,這個資料在觸發popstate事件時,可以在event.state裡獲取
//title:標題,基本沒用,一般傳null
//url:設定新的歷史紀錄的url。新的url與當前url的origin必須是一樣的,否則會丟擲錯誤。url可以時絕對路徑,也可以是相對路徑。
//如 當前url是 https://www.baidu.com/a/,執行history.pushState(null, null, './qq/'),則變成 https://www.baidu.com/a/qq/,
//執行history.pushState(null, null, '/qq/'),則變成 https://www.baidu.com/qq/
window.history.replaceState(state,title,url)
//與pushState 基本相同,但她是修改當前歷史紀錄,而 pushState 是建立新的歷史紀錄
另外還有:
-
window.history.back()
後退 -
window.history.forward()
前進 -
window.history.go(1)
前進或者後退幾步
從觸發事件的監聽上來說:
-
pushState()
和replaceState()
不能被popstate
事件所監聽 - 而後面三者可以,且使用者點選瀏覽器前進後退鍵時也可以
Vue元件渲染和更新過程
渲染元件時,會通過
Vue.extend
方法構建子元件的建構函式,並進行例項化。最終手動呼叫$mount()
進行掛載。更新元件時會進行patchVnode
流程,核心就是diff
演算法
v-if和v-show區別
-
v-show
隱藏則是為該元素新增css--display:none
,dom
元素依舊還在。v-if
顯示隱藏是將dom
元素整個新增或刪除 - 編譯過程:
v-if
切換有一個區域性編譯/解除安裝的過程,切換過程中合適地銷燬和重建內部的事件監聽和子元件;v-show
只是簡單的基於css
切換 - 編譯條件:
v-if
是真正的條件渲染,它會確保在切換過程中條件塊內的事件監聽器和子元件適當地被銷燬和重建。只有渲染條件為假時,並不做操作,直到為真才渲染 -
v-show
由false
變為true
的時候不會觸發元件的生命週期 -
v-if
由false
變為true
的時候,觸發元件的beforeCreate
、create
、beforeMount
、mounted
鉤子,由true
變為false
的時候觸發元件的beforeDestory
、destoryed
方法 - 效能消耗:
v-if
有更高的切換消耗;v-show
有更高的初始渲染消耗
v-show與v-if的使用場景
-
v-if
與v-show
都能控制dom
元素在頁面的顯示 -
v-if
相比v-show
開銷更大的(直接操作dom節
點增加與刪除) - 如果需要非常頻繁地切換,則使用 v-show 較好
- 如果在執行時條件很少改變,則使用
v-if
較好
v-show與v-if原理分析
-
v-show
原理
不管初始條件是什麼,元素總是會被渲染
我們看一下在vue中是如何實現的
程式碼很好理解,有transition
就執行transition
,沒有就直接設定display
屬性
// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display
if (transition && value) {
transition.beforeEnter(el)
} else {
setDisplay(el, value)
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el)
}
},
updated(el, { value, oldValue }, { transition }) {
// ...
},
beforeUnmount(el, { value }) {
setDisplay(el, value)
}
}
-
v-if
原理
v-if
在實現上比v-show
要複雜的多,因為還有else
else-if
等條件需要處理,這裡我們也只摘抄原始碼中處理 v-if
的一小部分
返回一個node
節點,render
函式通過表示式的值來決定是否生成DOM
// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// ...
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
})
}
)
說說 vue 內建指令
$route
和$router
的區別
-
$route
是“路由資訊物件”,包括path
,params
,hash
,query
,fullPath
,matched
,name
等路由資訊引數。 - 而
$router
是“路由例項”物件包括了路由的跳轉方法,鉤子函式等
v-if和v-for哪個優先順序更高
- 實踐中不應該把
v-for
和v-if
放一起 - 在
vue2
中,v-for
的優先順序是高於v-if
,把它們放在一起,輸出的渲染函式中可以看出會先執行迴圈再判斷條件,哪怕我們只渲染列表中一小部分元素,也得在每次重渲染的時候遍歷整個列表,這會比較浪費;另外需要注意的是在vue3
中則完全相反,v-if
的優先順序高於v-for
,所以v-if
執行時,它呼叫的變數還不存在,就會導致異常 - 通常有兩種情況下導致我們這樣做:
- 為了過濾列表中的專案 (比如
v-for="user in users" v-if="user.isActive"
)。此時定義一個計算屬性 (比如activeUsers
),讓其返回過濾後的列表即可(比如users.filter(u=>u.isActive)
) - 為了避免渲染本應該被隱藏的列表 (比如
v-for="user in users" v-if="shouldShowUsers"
)。此時把v-if
移動至容器元素上 (比如ul
、ol
)或者外面包一層template
即可
- 為了過濾列表中的專案 (比如
- 文件中明確指出永遠不要把
v-if
和v-for
同時用在同一個元素上,顯然這是一個重要的注意事項 - 原始碼裡面關於程式碼生成的部分,能夠清晰的看到是先處理
v-if
還是v-for
,順序上vue2
和vue3
正好相反,因此產生了一些症狀的不同,但是不管怎樣都是不能把它們寫在一起的
vue2.x原始碼分析
在vue模板編譯的時候,會將指令系統轉化成可執行的
render
函式
編寫一個p
標籤,同時使用v-if
與 v-for
<div id="app">
<p v-if="isShow" v-for="item in items">
{{ item.title }}
</p>
</div>
建立vue
例項,存放isShow
與items
資料
const app = new Vue({
el: "#app",
data() {
return {
items: [
{ title: "foo" },
{ title: "baz" }]
}
},
computed: {
isShow() {
return this.items && this.items.length > 0
}
}
})
模板指令的程式碼都會生成在render
函式中,通過app.$options.render
就能得到渲染函式
ƒ anonymous() {
with (this) { return
_c('div', { attrs: { "id": "app" } },
_l((items), function (item)
{ return (isShow) ? _c('p', [_v("\n" + _s(item.title) + "\n")]) : _e() }), 0) }
}
-
_l
是vue
的列表渲染函式,函式內部都會進行一次if
判斷 - 初步得到結論:
v-for
優先順序是比v-i
f高 - 再將
v-for
與v-if
置於不同標籤
<div id="app">
<template v-if="isShow">
<p v-for="item in items">{{item.title}}</p>
</template>
</div>
再輸出下render
函式
ƒ anonymous() {
with(this){return
_c('div',{attrs:{"id":"app"}},
[(isShow)?[_v("\n"),
_l((items),function(item){return _c('p',[_v(_s(item.title))])})]:_e()],2)}
}
這時候我們可以看到,v-for
與v-if
作用在不同標籤時候,是先進行判斷,再進行列表的渲染
我們再在檢視下vue原始碼
原始碼位置:\vue-dev\src\compiler\codegen\index.js
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
...
}
在進行if
判斷的時候,v-for
是比v-if
先進行判斷
最終結論:v-for
優先順序比v-if
高
什麼是遞迴元件?舉個例子說明下?
分析
遞迴元件我們用的比較少,但是在Tree
、Menu
這類元件中會被用到。
體驗
元件通過元件名稱引用它自己,這種情況就是遞迴元件
<template>
<li>
<div> {{ model.name }}</div>
<ul v-show="isOpen" v-if="isFolder">
<!-- 注意這裡:元件遞迴渲染了它自己 -->
<TreeItem
class="item"
v-for="model in model.children"
:model="model">
</TreeItem>
</ul>
</li>
<script>
export default {
name: 'TreeItem',
// ...
}
</script>
回答範例
- 如果某個元件通過元件名稱引用它自己,這種情況就是遞迴元件。
- 實際開發中類似
Tree
、Menu
這類元件,它們的節點往往包含子節點,子節點結構和父節點往往是相同的。這類元件的資料往往也是樹形結構,這種都是使用遞迴元件的典型場景。 - 使用遞迴元件時,由於我們並未也不能在元件內部匯入它自己,所以設定元件
name
屬性,用來查詢元件定義,如果使用SFC
,則可以通過SFC
檔名推斷。元件內部通常也要有遞迴結束條件,比如model.children
這樣的判斷。 - 檢視生成渲染函式可知,遞迴元件查詢時會傳遞一個布林值給
resolveComponent
,這樣實際獲取的元件就是當前元件本身
原理
遞迴元件編譯結果中,獲取元件時會傳遞一個識別符號 _resolveComponent("Comp", true)
const _component_Comp = _resolveComponent("Comp", true)
就是在傳遞maybeSelfReference
export function resolveComponent(
name: string,
maybeSelfReference?: boolean
): ConcreteComponent | string {
return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
}
resolveAsset
中最終返回的是元件自身:
if (!res && maybeSelfReference) {
// fallback to implicit self-reference
return Component
}
說說Vue的生命週期吧
什麼時候被呼叫?
- beforeCreate :例項初始化之後,資料觀測之前呼叫
- created:例項建立萬之後呼叫。例項完成:資料觀測、屬性和方法的運算、
watch/event
事件回撥。無$el
. - beforeMount:在掛載之前呼叫,相關
render
函式首次被呼叫 - mounted:了被新建立的
vm.$el
替換,並掛載到例項上去之後呼叫改鉤子。 - beforeUpdate:資料更新前呼叫,發生在虛擬DOM重新渲染和打補丁,在這之後會呼叫改鉤子。
- updated:由於資料更改導致的虛擬DOM重新渲染和打補丁,在這之後會呼叫改鉤子。
- beforeDestroy:例項銷燬前呼叫,例項仍然可用。
- destroyed:例項銷燬之後呼叫,呼叫後,Vue例項指示的所有東西都會解綁,所有事件監聽器和所有子例項都會被移除
每個生命週期內部可以做什麼?
- created:例項已經建立完成,因為他是最早觸發的,所以可以進行一些資料、資源的請求。
- mounted:例項已經掛載完成,可以進行一些DOM操作。
- beforeUpdate:可以在這個鉤子中進一步的更改狀態,不會觸發重渲染。
- updated:可以執行依賴於DOM的操作,但是要避免更改狀態,可能會導致更新無線迴圈。
- destroyed:可以執行一些優化操作,清空計時器,解除繫結事件。
ajax放在哪個生命週期?:一般放在 mounted
中,保證邏輯統一性,因為生命週期是同步執行的, ajax
是非同步執行的。單數服務端渲染 ssr
同一放在 created
中,因為服務端渲染不支援 mounted
方法。 什麼時候使用beforeDestroy?:當前頁面使用 $on
,需要解綁事件。清楚定時器。解除事件繫結, scroll mousemove
。
請說明Vue中key的作用和原理,談談你對它的理解
-
key
是為Vue
中的VNode
標記的唯一id
,在patch
過程中通過key
可以判斷兩個虛擬節點是否是相同節點,通過這個key
,我們的diff
操作可以更準確、更快速 -
diff
演算法的過程中,先會進行新舊節點的首尾交叉對比,當無法匹配的時候會用新節點的key
與舊節點進行比對,然後檢出差異 - 儘量不要採用索引作為
key
- 如果不加
key
,那麼vue
會選擇複用節點(Vue的就地更新策略),導致之前節點的狀態被保留下來,會產生一系列的bug
-
更準確 :因為帶
key
就不是就地複用了,在sameNode
函式a.key === b.key
對比中可以避免就地複用的情況。所以會更加準確。 -
更快速 :
key
的唯一性可以被Map
資料結構充分利用,相比於遍歷查詢的時間複雜度O(n)
,Map
的時間複雜度僅僅為O(1)
,比遍歷方式更快。
原始碼如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
回答範例
分析
這是一道特別常見的問題,主要考查大家對虛擬DOM
和patch
細節的掌握程度,能夠反映面試者理解層次
思路分析:
- 給出結論,
key
的作用是用於優化patch
效能 -
key
的必要性 - 實際使用方式
- 總結:可從原始碼層面描述一下
vue
如何判斷兩個節點是否相同
回答範例:
-
key
的作用主要是為了更高效的更新虛擬DOM
-
vue
在patch
過程中 判斷兩個節點是否是相同節點是key
是一個必要條件 ,渲染一組列表時,key
往往是唯一標識,所以如果不定義key
的話,vue
只能認為比較的兩個節點是同一個,哪怕它們實際上不是,這導致了頻繁更新元素,使得整個patch
過程比較低效,影響效能 - 實際使用中在渲染一組列表時
key
必須設定,而且必須是唯一標識,應該避免使用陣列索引作為key
,這可能導致一些隱蔽的bug
;vue
中在使用相同標籤元素過渡切換時,也會使用key
屬性,其目的也是為了讓vue
可以區分它們,否則vue
只會替換其內部屬性而不會觸發過渡效果 - 從原始碼中可以知道,
vue
判斷兩個節點是否相同時主要判斷兩者的key
和標籤型別(如div)
等,因此如果不設定key
,它的值就是undefined
,則可能永遠認為這是兩個相同節點,只能去做更新操作,這造成了大量的dom
更新操作,明顯是不可取的
如果不使用
key
,Vue
會使用一種最大限度減少動態元素並且儘可能的嘗試就地修改/複用相同型別元素的演算法。key
是為Vue
中vnode
的唯一標記,通過這個key
,我們的diff
操作可以更準確、更快速
diff程可以概括為:
oldCh
和newCh
各有兩個頭尾的變數StartIdx
和EndIdx
,它們的2
個變數相互比較,一共有4
種比較方式。如果4
種比較都沒匹配,如果設定了key
,就會用key
進行比較,在比較的過程中,變數會往中間靠,一旦StartIdx>EndIdx
表明oldCh
和newCh
至少有一個已經遍歷完了,就會結束比較,這四種比較方式就是首
、尾
、舊尾新頭
、舊頭新尾
相關程式碼如下
// 判斷兩個vnode的標籤和key是否相同 如果相同 就可以認為是同一節點就地複用
function isSameVnode(oldVnode, newVnode) {
return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}
// 根據key來建立老的兒子的index對映表 類似 {'a':0,'b':1} 代表key為'a'的節點在第一個位置 key為'b'的節點在第二個位置
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index;
});
return map;
}
// 生成的對映表
let map = makeIndexByKey(oldCh);
Vue computed 實現
- 建立與其他屬性(如:
data
、Store
)的聯絡; - 屬性改變後,通知計算屬性重新計算
實現時,主要如下
- 初始化
data
, 使用Object.defineProperty
把這些屬性全部轉為getter/setter
。 - 初始化
computed
, 遍歷computed
裡的每個屬性,每個computed
屬性都是一個watch
例項。每個屬性提供的函式作為屬性的getter
,使用Object.defineProperty
轉化。 -
Object.defineProperty getter
依賴收集。用於依賴發生變化時,觸發屬性重新計算。 - 若出現當前
computed
計算屬性巢狀其他computed
計算屬性時,先進行其他的依賴收集
既然Vue通過資料劫持可以精準探測資料變化,為什麼還需要虛擬DOM進行diff檢測差異
- 響應式資料變化,
Vue
確實可以在資料變化時,響應式系統可以立刻得知。但是如果給每個屬性都新增watcher
用於更新的話,會產生大量的watcher
從而降低效能 - 而且粒度過細也得導致更新不準確的問題,所以
vue
採用了元件級的watcher
配合diff
來檢測差異
生命週期鉤子是如何實現的
Vue 的生命週期鉤子核心實現是利用釋出訂閱模式先把使用者傳入的的生命週期鉤子訂閱好(內部採用陣列的方式儲存)然後在建立元件例項的過程中會一次執行對應的鉤子方法(釋出)
相關程式碼如下
export function callHook(vm, hook) {
// 依次執行生命週期對應的方法
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm); //生命週期裡面的this指向當前例項
}
}
}
// 呼叫的時候
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, "beforeCreate"); //初始化資料之前
// 初始化狀態
initState(vm);
callHook(vm, "created"); //初始化資料之後
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};