天天看點

【面試題】面試官:vue的這些原理你了解嗎?

【面試題】面試官:vue的這些原理你了解嗎?

前言

在之前面試的時候我自己也經常會遇到一些vue原理的問題, 我也總結了下自己的經常的用到的,友善自己學習,今天也給大家分享出來, 歡迎大家一起學習交流, 有更好的方法歡迎評論區指出, 後序我也将持續整理總結~

描述 Vue 與 React 差別

說明概念: vue:是一套用于建構使用者界面的漸進式架構,Vue 的核心庫隻關注視圖層 react:用于建構使用者界面的 JavaScript 庫 聲明式, 元件化

  1. 定位
  • vue 漸進式 響應式
  • React 單向資料流
  1. 寫法 vue:template,jsx react: jsx
  2. Hooks:vue3 和 react16 支援 hook
  3. UI 更新
  4. 文化 vue 官方提供 React 第三方提供,自己選擇

整個 new Vue 階段做了什麼?

  1. vue.prototype._init(option)
  2. initState(vm)
  3. Observer(vm.data)
  4. new Observer(data)
  5. 調用 walk 方法,周遊 data 中的每個屬性,監聽資料的變化
  6. 執行 defineProperty 監聽資料讀取和設定

資料描述符綁定完成後,我們就能得到以下的流程圖

【面試題】面試官:vue的這些原理你了解嗎?
  • 圖中我們可以看出,vue 初始化的時候,進行了資料的 get\set 綁定,并建立了一個
  • dep 對象就是用來依賴收集, 他實作了一個釋出訂閱模式,完後了資料 data 的渲染視圖 watcher 的訂閱
class Dep {
  // 根據 ts 類型提示,我們可以得出 Dep.target 是一個 Watcher 類型。
  static target: ?Watcher;
  // subs 存放搜集到的 Watcher 對象集合
  subs: Array<Watcher>;
  constructor() {
    this.subs = [];
  }
  addSub(sub: Watcher) {
    // 搜集所有使用到這個 data 的 Watcher 對象。
    this.subs.push(sub);
  }
  depend() {
    if (Dep.target) {
      // 搜集依賴,最終會調用上面的 addSub 方法
      Dep.target.addDep(this);
    }
  }
  notify() {
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      // 調用對應的 Watcher,更新視圖
      subs[i].update();
    }
  }
}
複制代碼      

描述 vue 的響應式原理

【面試題】面試官:vue的這些原理你了解嗎?

Vue 的三個核心類

  1. ​Observer​

    ​ :給對象的屬性添加 getter 和 setter ,用于依賴收集和派發更新
  2. ​Dep​

    ​​ :用于收集目前響應式對象的依賴關系,每個響應式對象都有 dep 執行個體,​

    ​dep.subs = watcher[]​

    ​​,當資料發生變更的時候,會通過​

    ​dep.notify()​

    ​通知各個 watcher
  3. ​watcher​

    ​:是一個中介,資料發生變化時通過 watcher 中轉,通知元件 觀察者對象,render watcher,computed watcher, user watcher
  • 依賴收集
  • 需要用到資料的地方,稱為依賴
  • 在​

    ​getter​

    ​​中收集依賴,在​

    ​setter​

    ​中觸發依賴
  1. ​initState​

    ​​, 對 computed 屬性初始化時,會觸發​

    ​computed​

    ​​

    ​watcher​

    ​ 依賴收集
  2. ​initState​

    ​​, 對監聽屬性初始化的時候,觸發​

    ​user​

    ​​

    ​watcher​

    ​ 依賴收集
  3. ​render​

    ​​,觸發​

    ​render​

    ​​

    ​watcher​

    ​ 依賴收集
  • 派發更新​

    ​Object.defindeProperty​

  1. 元件中對響應式的資料進行了修改,會觸發 setter 邏輯
  2. ​dep.notify()​

  3. 周遊所有 subs,調用每一個 watcher 的 update 方法 總結: 當建立一個 vue 執行個體時, vue 會周遊 data 裡的屬性, Objeect.defineProperty 為屬性添加 getter 和 setter 對資料的讀取進行劫持 getter:依賴收集 setter:派發更新 每個元件的執行個體都有對應的 watcher 執行個體

計算屬性的原理

computed watcher 計算屬性的監聽器,格式化轉換,求值等操作

computed watcher 持有一個 dep 執行個體,通過 dirty 屬性标記計算屬性是否需要重新求值 當 computed 依賴值改變後,就會通知訂閱的 watcher 進行更新,對于 computed watcher 會将 dirty 屬性設定為 true,并且進行計算屬性方法的調用,

注意

  1. 計算屬性是基于他的響應式依賴進行緩存的,隻有依賴發生改變的時候才會重新求值
  2. 意義:比如計算屬性方法内部操作非常頻繁時,周遊一個極大的數組,計算一次可能要耗時 1s,如果依賴值沒有變化的時候就不會重新計算

nextTick 原理

概念

nextTick 的作用是在下一次 DOM 更新循環結束後,執行延遲回調,nextTick 就是建立一個異步任務,要他等到同步任務執行完後才執行

使用

在資料變化後要執行某個操作,而這個操作依賴因資料的改變而改變 dom,這個操作應該放到 nextTick 中

vue2 中的實作

<template>
  <div>{{ name }}</div>
</template>
<script>
export default {
  data() {
    return {
      name: ""
    }
  },
  mounted() {
    console.log(this.$el.clientHeight) // 0
    this.name = "better"
    console.log(this.$el.clientHeight) // 0
    this.$nextTick(() => {
      console.log(this.$el.clientHeight) // 18
    });
  }
};
</script>
複制代碼      

我們發現直接擷取最新的 DOM 相關的資訊是拿不到的,隻有在 nextTick 中才能擷取罪行的 DOM 資訊

原理分析

在執行 this.name = 'better' 會觸發 Watcher 更新, Watcher 會把自己放到一個隊列,然後調用 nextTick()函數

使用隊列的原因: 比如多個資料變更更新視圖多次的話,性能上就不好了, 是以對視圖更新做一個異步更新的隊列,避免重複計算和不必要的 DOM 操作,在下一輪時間循環的時候重新整理隊列,并執行已去重的任務(nextTick 的回調函數),更新視圖
export function queueWatcher (watcher: Watcher) {
  ...
  // 因為每次派發更新都會引起渲染,是以把所有 watcher 都放到 nextTick 裡調用
  nextTick(flushSchedulerQueue)
}

複制代碼      

這裡的參數​

​flushSchedulerQueue​

​​方法就會被放入事件循環中,主線程任務執行完後就會執行這個函數,對 watcher 隊列排序,周遊,執行 watcher 對應的 run 方法,然後 render,更新視圖 也就是在執行 this.name = 'better'的時候,任務隊列可以了解為[flushSchedulerQueue],然後在下一行的 console.log,由于會更新視圖任務​

​flushSchedulerQueue​

​在任務隊列中沒有執行,是以無法拿到更後的視圖 然後在執行 this.$nextTick(fn)的時候,添加一個異步任務,這時的任務隊列可以了解為[flushSchedulerQueue, fn], 然後同步任務執行完了,接着按順序執行任務隊列裡的任務, 第一個任務執行就會更新視圖,後面自然能得到更新後的視圖了

nextTick 源碼

源碼分為兩個部分:一個是判斷目前環境能使用的最合适的 API 并儲存異步函數,二是調用異步函數執行回調隊列 1 環境判斷 主要是判斷用哪個宏任務或者微任務,因為宏任務的消耗時間是大于微任務的,是以先使用微任務, 用以下的判斷順序

  • promise
  • MutationObserver
  • setImmediate
  • setTimeout
export let isUsingMicroTask = false; // 是否啟用微任務開關
const callbacks = []; // 回調隊列
let pending = false; // 異步控制開關,标記是否正在執行回調函數

// 該方法負責執行隊列中的全部回調
function flushCallbacks() {
  // 重置異步開關
  pending = false;
  // 防止nextTick裡有nextTick出現的問題
  // 是以執行之前先備份并清空回調隊列
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  // 執行任務隊列
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}
let timerFunc; // 用來儲存調用異步任務方法
// 判斷目前環境是否支援原生 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 儲存一個異步任務
  const p = Promise.resolve();
  timerFunc = () => {
    // 執行回調函數
    p.then(flushCallbacks);
    // ios 中可能會出現一個回調被推入微任務隊列,但是隊列沒有重新整理的情況
    // 是以用一個空的計時器來強制重新整理任務隊列
    if (isIOS) setTimeout(noop);
  };
  isUsingMicroTask = true;
} else if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  // 不支援 Promise 的話,在支援MutationObserver的非 IE 環境下
  // 如 PhantomJS, iOS7, Android 4.4
  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' && isNative(setImmediate)) {
  // 使用setImmediate,雖然也是宏任務,但是比setTimeout更好
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // 以上都不支援的情況下,使用 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}
複制代碼      

環境判斷結束就會得到一個延遲回調函數​

​timerFunc​

​ 然後進入核心的 nextTick

2 nextTick()函數源碼 在使用的時候就是調用 nextTick()這個方法

  • 把傳入的回調函數放進回調隊列 callbacks
  • 執行儲存的異步任務 timerFunc,就會周遊 callbacks 執行相應的回調函數了
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();
  }
  // 如果沒有提供回調,并且支援 Promise,就傳回一個 Promise
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}
複制代碼      

可以看到最後有傳回一個 Promise,是可以讓我們在不傳參的時候用

this.$nextTick().then(()=>{ ... })
複制代碼      

vue3 中分析

點選按鈕更新 DOM 内容, 并擷取最新的 DOM 内容

<template>
     <div ref="test">{{name}}</div>
     <el-button @click="handleClick">按鈕</el-button>
 </template>
 <script setup>
     import { ref, nextTick } from 'vue'
     const name = ref("better")
     const test = ref(null)
     async function handleClick(){
         name.value = '掘金'
         console.log(test.value.innerText) // better
         await nextTick()
         console.log(test.value.innerText) // 掘金
     }
     return { name, test, handleClick }
 </script>

複制代碼      

在使用方式上面有了一些變化,事件循環的原理還是一樣的,隻是加了幾個專門維護隊列的方法,以及關聯到 effect

vue3 nextTick 源碼剖析

const resolvedPromise: Promise<any> = Promise.resolve();
let currentFlushPromise: Promise<void> | null = null;

export function nextTick<T = void>(
  this: T,
  fn?: (this: T) => void,
): Promise<void> {
  const p = currentFlushPromise || resolvedPromise;
  return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
複制代碼      

簡單來看就是一個 Promise nextTick 接受一個函數為參數,同時會建立一個微任務,在我們頁面調用 nextTick 的時候,會執行該函數,把我們的參數 fn 指派給 p.then(fn) ,在隊列的任務完成後, fn 就執行了 由于加了幾個維護隊列的方法,是以執行順序是 ​

​queueJob​

​​ -> ​

​queueFlush​

​​ -> ​

​flushJobs​

​ -> nextTick 參數的 fn

flushJobs 該方法主要負責處理隊列任務,主要邏輯如下

  • 先處理前置任務隊列
  • 根據 Id 排列隊列
  • 周遊執行隊列任務
  • 執行完畢後清空并重置隊列
  • 執行後置隊列任務
  • 如果還有就遞歸繼續執行

vue Router

路由就是一組 key-value 的對應關系,在前端項目中說的路由可以了解為 url-視圖之間的映射關系,這種映射是單向的,url 變化不會走 http 請求,但是會更新切換前端 UI 視圖,像 vue 這種單頁面應用 就是這樣的規則.

路由守衛

  1. 全局路由守衛
  • 前置路由守衛:​

    ​beforeEach​

    ​ 路由切換之前被調用
  • 全局解析守衛:​

    ​beforeResolve​

    ​ 在每次導航時就會觸發,但是確定在導航被确認之前,同時在所有元件内守衛和異步路由元件被解析之後 2,解析守衛就被正确調用,如確定使用者可以通路自定義 meta 屬性​

    ​requiresCamera​

    ​  的路由:
router.beforeResolve(async (to) => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission();
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 處理錯誤,然後取消導航
        return false;
      } else {
        // 意料之外的錯誤,取消導航并把錯誤傳給全局處理器
        throw error;
      }
    }
  }
});
複制代碼      

​router.beforeResolve​

​  是擷取資料或執行任何其他操作(如果使用者無法進入頁面時你希望避免執行的操作)的理想位置。

  • 後置路由守衛 :​

    ​afterEach​

    ​​ 路由切換之後被調用​

    ​requiresCamera​

    ​  的路由:
  1. 獨享路由守衛
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      },
    },
  ],
});
複制代碼      
  1. 元件內路由守衛 可以在元件内使用者兩個鈎子
  • 通過路由規則,進入該元件時被調用
beforeRouteEnter (to, from, next) {

}
複制代碼      
  • 通過路由規則,離開該元件時調用
beforeRouteLeave (to, from, next) {

}
複制代碼      

完整的導航解析過程

  1. 導航被觸發。
  2. 在失活的元件裡調用​

    ​beforeRouteLeave​

    ​  守衛。
  3. 調用全局的​

    ​beforeEach​

    ​  守衛。
  4. 在重用的元件裡調用​

    ​beforeRouteUpdate​

    ​  守衛(2.2+)。
  5. 在路由配置裡調用​

    ​beforeEnter​

    ​。
  6. 解析異步路由元件。
  7. 在被激活的元件裡調用​

    ​beforeRouteEnter​

    ​。
  8. 調用全局的​

    ​beforeResolve​

    ​  守衛(2.5+)。
  9. 導航被确認。
  10. 調用全局的​

    ​afterEach​

    ​  鈎子。
  11. 觸發 DOM 更新。
  12. 調用​

    ​beforeRouteEnter​

    ​​  守衛中傳給​

    ​next​

    ​  的回調函數,建立好的元件執行個體會作為回調函數的參數傳入。

路由模式

  1. history 模式​

    ​/​

    ​: 使用​

    ​pushState​

    ​和​

    ​replaceState​

    ​,通過這兩個 API 可以改變 url 位址不發生請求,​

    ​popState​

    ​事件
  2. hash 模式​

    ​#​

    ​ :

    hash 是 URL 中 hash(#)及後面的那部分,常用作錨點在頁面内進行導航,改變 hash 值不會随着 http 請求發送給伺服器,通過​

    ​hashChange​

    ​事件監聽 URL 的變化,可以用他來實作更新頁面部分内容的操作

vueRouter 的實作

剖析 VueRouter 本質

通過使用 vueRouter 可以知道

  1. 通過 new Router() 獲得一個 router 執行個體,我門引入的 VueRouter 其實就是一個類
class VueRouter {}
複制代碼      
  1. 使用 Vue.use(),而 Vue.use 的一個原則就是執行對象的 install 這個方法,所有,我們可以再一步假設 VueRouter 有 install 這個方法 是以得出
//myVueRouter.js
class VueRouter {}
VueRouter.install = function () {};

export default VueRouter;
複制代碼      

分析 Vue.use

Vue.use(plugin) 用法: 用于安裝 vue.js 插件,如果插件是一個對象,必須提供 install 方法,如果插件是一個函數,它會被作為 install 方法,調用 install 方法的時候,會将 vue 作為參數傳入,install 方法被同一個插件多次調用時,插件也隻會被安裝一次

作用: 注冊插件,此時隻需要調用 install 方法并将 Vue 作為參數傳入 1. 插件的類型,可以是 install 方法,也可以是一個包含 install 方法的對象 2. 插件隻能被安裝一次,保證插件清單中不能有重複的插件

需要将 Vue 作為 install 方法第一個參數傳入,先将 Vue 儲存起來,将傳進來的 Vue 建立兩個元件 router-link 和 router-view

//myVueRouter.js
let Vue = null;
class VueRouter {}
VueRouter.install = function (v) {
  Vue = v;
  console.log(v);

  //新增代碼
  Vue.component('router-link', {
    render(h) {
      return h('a', {}, '首頁');
    },
  });
  Vue.component('router-view', {
    render(h) {
      return h('h1', {}, '首頁視圖');
    },
  });
};

export default VueRouter;
複制代碼      

​install​

​​ 一般是給每個 vue 執行個體添加東西的,路由中就是添加​

​$router​

​​和​

​$route​

​​,注意:每個元件添加的​

​$router​

​​是同一個和​

​$route​

​ 是同一個,避免隻是根元件有這個 router 值,使用代理的思想

//myVueRouter.js
let Vue = null;
class VueRouter {}
VueRouter.install = function (v) {
  Vue = v;
  // 新增代碼
  Vue.mixin({
    beforeCreate() {
      if (this.$options && this.$options.router) {
        // 如果是根元件
        this._root = this; //把目前執行個體挂載到_root上
        this._router = this.$options.router;
      } else {
        //如果是子元件
        this._root = this.$parent && this.$parent._root;
      }
      Object.defineProperty(this, '$router', {
        get() {
          return this._root._router;
        },
      });
    },
  });

  Vue.component('router-link', {
    render(h) {
      return h('a', {}, '首頁');
    },
  });
  Vue.component('router-view', {
    render(h) {
      return h('h1', {}, '首頁視圖');
    },
  });
};

export default VueRouter;
複制代碼      

完善 VueRouter 類 首先明确下是執行個體化的時候傳了 v 的參數為 mode(路由模式), routes(路由表),在類的構造器中傳參

class VueRouter {
  constructor(options) {
    this.mode = options.mode || 'hash';
    this.routes = options.routes || []; //你傳遞的這個路由是一個數組表
  }
}
複制代碼      

但是我們直接處理 routes 的十分不友善的,是以我們先要轉換成 key:value 的格式

createMap(routes) {
    return routes.reduce((pre,current) => {
        pre[current.path] = current.component
        return pre
    },{})
}
複制代碼      

vue 模闆編譯的原理

  • 解析階段: 使用大量的正規表達式對 template 字元串進行解析,将标簽,指令,屬性等轉化為抽象文法樹 AST
  • 優化階段: 周遊 AST,找打其中的一些靜态節點進行标記, 友善在頁面重渲染的時候進行 diff 比較時,直接跳過這些靜态節點,優化 runtime 的性能
  • 生成階段: 将最終的 AST 轉化為 render 函數字元串

繼續閱讀