1. 程式人生 > 其它 >前端vue面試題(持續更新中)

前端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-linkrouter-view,分別起到導航作用和內容渲染作用,但是回答如何生效還真有一定難度

回答範例

  1. vue-router中兩個重要元件router-linkrouter-view,分別起到路由導航作用和元件內容渲染作用
  2. 使用中router-link預設生成一個a標籤,設定to屬性定義跳轉path。實際上也可以通過custom和插槽自定義最終的展現形式。router-view是要顯示元件的佔位元件,可以巢狀,對應路由配置的巢狀關係,配合name可以顯示具名元件,起到更強的佈局作用。
  3. 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中的雙向繫結流程是什麼的

  1. new Vue()首先執行初始化,對data執行響應化處理,這個過程發生Observe
  2. 同時對模板執行編譯,找到其中動態繫結的資料,從data中獲取並初始化檢視,這個過程發生在Compile
  3. 同時定義⼀個更新函式和Watcher,將來對應資料變化時Watcher會呼叫更新函式
  4. 由於data的某個key在⼀個檢視中可能出現多次,所以每個key都需要⼀個管家Dep來管理多個Watcher
  5. 將來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統⼀通知

實現思路

  1. defineReactive時為每⼀個key建立⼀個Dep例項
  2. 初始化檢視時讀取某個key,例如name1,建立⼀個watcher1
  3. 由於觸發name1getter方法,便將watcher1新增到name1對應的Dep
  4. 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要做許可權管理該怎麼做?控制到按鈕級別的許可權怎麼做?

分析

  • 綜合實踐題目,實際開發中經常需要面臨許可權管理的需求,考查實際應用能力。
  • 許可權管理一般需求是兩個:頁面許可權和按鈕許可權,從這兩個方面論述即可。

思路

  • 許可權管理需求分析:頁面和按鈕許可權
  • 許可權管理的實現方案:分後端方案和前端方案闡述
  • 說說各自的優缺點

回答範例

  1. 許可權管理一般需求是頁面許可權和按鈕許可權的管理

  2. 具體實現的時候分後端和前端兩種方案:

  • 前端方案 會把所有路由資訊在前端配置,通過路由守衛要求使用者登入,使用者登入後根據角色過濾出路由表。比如我會配置一個asyncRoutes陣列,需要認證的頁面在其路由的meta中新增一個roles欄位,等獲取使用者角色之後取兩者的交集,若結果不為空則說明可以訪問。此過濾過程結束,剩下的路由就是該使用者能訪問的頁面,最後通過router.addRoutes(accessRoutes)方式動態新增路由即可

  • 後端方案 會把所有頁面路由資訊存在資料庫中,使用者登入的時候根據其角色查詢得到其能訪問的所有頁面路由資訊返回給前端,前端再通過addRoutes動態新增路由資訊

  • 按鈕許可權的控制通常會實現一個指令,例如v-permission,將按鈕要求角色通過值傳給v-permission指令,在指令的moutned鉤子中可以判斷當前使用者角色和按鈕是否存在交集,有則保留按鈕,無則移除按鈕

  1. 純前端方案的優點是實現簡單,不需要額外許可權管理頁面,但是維護起來問題比較大,有新的頁面和角色需求就要修改前端程式碼重新打包部署;服務端方案就不存在這個問題,通過專門的角色和許可權管理頁面,配置頁面和按鈕許可權資訊到資料庫,應用每次登陸時獲取的都是最新的路由資訊,可謂一勞永逸!

可能的追問

  1. 類似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>
  1. 服務端返回的路由資訊如何新增到路由器中?
// 前端元件名和元件對映表
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 的情形 :

  1. 這個 prop 用來傳遞一個初始值;這個子元件接下來希望將其作為一個本地的 prop 資料來使用。 在這種情況下,最好定義一個本地的 data 屬性並將這個 prop用作其初始值
props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}
  1. 這個 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:nonedom元素依舊還在。v-if顯示隱藏是將dom元素整個新增或刪除
  • 編譯過程:v-if切換有一個區域性編譯/解除安裝的過程,切換過程中合適地銷燬和重建內部的事件監聽和子元件;v-show只是簡單的基於css切換
  • 編譯條件:v-if是真正的條件渲染,它會確保在切換過程中條件塊內的事件監聽器和子元件適當地被銷燬和重建。只有渲染條件為假時,並不做操作,直到為真才渲染
  • v-showfalse變為true的時候不會觸發元件的生命週期
  • v-iffalse變為true的時候,觸發元件的beforeCreatecreatebeforeMountmounted鉤子,由true變為false的時候觸發元件的beforeDestorydestoryed方法
  • 效能消耗:v-if有更高的切換消耗;v-show有更高的初始渲染消耗

v-show與v-if的使用場景

  • v-ifv-show 都能控制dom元素在頁面的顯示
  • v-if 相比 v-show 開銷更大的(直接操作dom節點增加與刪除)
  • 如果需要非常頻繁地切換,則使用 v-show 較好
  • 如果在執行時條件很少改變,則使用 v-if 較好

v-show與v-if原理分析

  1. 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)
  }
}
  1. 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是“路由資訊物件”,包括pathparamshashqueryfullPathmatchedname等路由資訊引數。
  • $router是“路由例項”物件包括了路由的跳轉方法,鉤子函式等

v-if和v-for哪個優先順序更高

  • 實踐中不應該把v-forv-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 移動至容器元素上 (比如 ulol)或者外面包一層template即可
  • 文件中明確指出永遠不要把 v-ifv-for 同時用在同一個元素上,顯然這是一個重要的注意事項
  • 原始碼裡面關於程式碼生成的部分,能夠清晰的看到是先處理v-if還是v-for,順序上vue2vue3正好相反,因此產生了一些症狀的不同,但是不管怎樣都是不能把它們寫在一起的

vue2.x原始碼分析

在vue模板編譯的時候,會將指令系統轉化成可執行的render函式

編寫一個p標籤,同時使用v-ifv-for

<div id="app">
  <p v-if="isShow" v-for="item in items">
    {{ item.title }}
  </p>
</div>

建立vue例項,存放isShowitems資料

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) }
}
  • _lvue的列表渲染函式,函式內部都會進行一次if判斷
  • 初步得到結論:v-for優先順序是比v-if高
  • 再將v-forv-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-forv-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

什麼是遞迴元件?舉個例子說明下?

分析

遞迴元件我們用的比較少,但是在TreeMenu這類元件中會被用到。

體驗

元件通過元件名稱引用它自己,這種情況就是遞迴元件

<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>

回答範例

  1. 如果某個元件通過元件名稱引用它自己,這種情況就是遞迴元件。
  2. 實際開發中類似TreeMenu這類元件,它們的節點往往包含子節點,子節點結構和父節點往往是相同的。這類元件的資料往往也是樹形結構,這種都是使用遞迴元件的典型場景。
  3. 使用遞迴元件時,由於我們並未也不能在元件內部匯入它自己,所以設定元件name屬性,用來查詢元件定義,如果使用SFC,則可以通過SFC檔名推斷。元件內部通常也要有遞迴結束條件,比如model.children這樣的判斷。
  4. 檢視生成渲染函式可知,遞迴元件查詢時會傳遞一個布林值給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
}

回答範例

分析

這是一道特別常見的問題,主要考查大家對虛擬DOMpatch細節的掌握程度,能夠反映面試者理解層次

思路分析:

  • 給出結論,key的作用是用於優化patch效能
  • key的必要性
  • 實際使用方式
  • 總結:可從原始碼層面描述一下vue如何判斷兩個節點是否相同

回答範例:

  1. key的作用主要是為了更高效的更新虛擬DOM
  2. vuepatch過程中 判斷兩個節點是否是相同節點是key是一個必要條件 ,渲染一組列表時,key往往是唯一標識,所以如果不定義key的話,vue只能認為比較的兩個節點是同一個,哪怕它們實際上不是,這導致了頻繁更新元素,使得整個patch過程比較低效,影響效能
  3. 實際使用中在渲染一組列表時key必須設定,而且必須是唯一標識,應該避免使用陣列索引作為key,這可能導致一些隱蔽的bugvue中在使用相同標籤元素過渡切換時,也會使用key屬性,其目的也是為了讓vue可以區分它們,否則vue只會替換其內部屬性而不會觸發過渡效果
  4. 從原始碼中可以知道,vue判斷兩個節點是否相同時主要判斷兩者的key標籤型別(如div)等,因此如果不設定key,它的值就是undefined,則可能永遠認為這是兩個相同節點,只能去做更新操作,這造成了大量的dom更新操作,明顯是不可取的

如果不使用 keyVue 會使用一種最大限度減少動態元素並且儘可能的嘗試就地修改/複用相同型別元素的演算法。key 是為 Vuevnode 的唯一標記,通過這個 key,我們的 diff 操作可以更準確、更快速

diff程可以概括為:oldChnewCh各有兩個頭尾的變數StartIdxEndIdx,它們的2個變數相互比較,一共有4種比較方式。如果4種比較都沒匹配,如果設定了key,就會用key進行比較,在比較的過程中,變數會往中間靠,一旦StartIdx>EndIdx表明oldChnewCh至少有一個已經遍歷完了,就會結束比較,這四種比較方式就是舊尾新頭舊頭新尾

相關程式碼如下

// 判斷兩個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 實現

  • 建立與其他屬性(如:dataStore)的聯絡;
  • 屬性改變後,通知計算屬性重新計算

實現時,主要如下

  • 初始化 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);
  }
};