
前言
在之前面試的時候我自己也經常會遇到一些vue原理的問題, 我也總結了下自己的經常的用到的,友善自己學習,今天也給大家分享出來, 歡迎大家一起學習交流, 有更好的方法歡迎評論區指出, 後序我也将持續整理總結~
描述 Vue 與 React 差別
說明概念: vue:是一套用于建構使用者界面的漸進式架構,Vue 的核心庫隻關注視圖層 react:用于建構使用者界面的 JavaScript 庫 聲明式, 元件化
- 定位
- vue 漸進式 響應式
- React 單向資料流
- 寫法 vue:template,jsx react: jsx
- Hooks:vue3 和 react16 支援 hook
- UI 更新
- 文化 vue 官方提供 React 第三方提供,自己選擇
整個 new Vue 階段做了什麼?
- vue.prototype._init(option)
- initState(vm)
- Observer(vm.data)
- new Observer(data)
- 調用 walk 方法,周遊 data 中的每個屬性,監聽資料的變化
- 執行 defineProperty 監聽資料讀取和設定
資料描述符綁定完成後,我們就能得到以下的流程圖
- 圖中我們可以看出,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 的三個核心類
-
:給對象的屬性添加 getter 和 setter ,用于依賴收集和派發更新Observer
-
:用于收集目前響應式對象的依賴關系,每個響應式對象都有 dep 執行個體,Dep
,當資料發生變更的時候,會通過dep.subs = watcher[]
通知各個 watcherdep.notify()
-
:是一個中介,資料發生變化時通過 watcher 中轉,通知元件 觀察者對象,render watcher,computed watcher, user watcherwatcher
- 依賴收集
- 需要用到資料的地方,稱為依賴
- 在
中收集依賴,在getter
中觸發依賴setter
-
, 對 computed 屬性初始化時,會觸發initState
computed
依賴收集watcher
-
, 對監聽屬性初始化的時候,觸發initState
user
依賴收集watcher
-
,觸發render
render
依賴收集watcher
- 派發更新
Object.defindeProperty
- 元件中對響應式的資料進行了修改,會觸發 setter 邏輯
-
dep.notify()
- 周遊所有 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,并且進行計算屬性方法的調用,
注意
- 計算屬性是基于他的響應式依賴進行緩存的,隻有依賴發生改變的時候才會重新求值
- 意義:比如計算屬性方法内部操作非常頻繁時,周遊一個極大的數組,計算一次可能要耗時 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 這種單頁面應用 就是這樣的規則.
路由守衛
- 全局路由守衛
- 前置路由守衛:
路由切換之前被調用beforeEach
- 全局解析守衛:
在每次導航時就會觸發,但是確定在導航被确認之前,同時在所有元件内守衛和異步路由元件被解析之後 2,解析守衛就被正确調用,如確定使用者可以通路自定義 meta 屬性beforeResolve
的路由: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
- 獨享路由守衛
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
},
},
],
});
複制代碼
- 元件內路由守衛 可以在元件内使用者兩個鈎子
- 通過路由規則,進入該元件時被調用
beforeRouteEnter (to, from, next) {
}
複制代碼
- 通過路由規則,離開該元件時調用
beforeRouteLeave (to, from, next) {
}
複制代碼
完整的導航解析過程
- 導航被觸發。
- 在失活的元件裡調用
守衛。beforeRouteLeave
- 調用全局的
守衛。beforeEach
- 在重用的元件裡調用
守衛(2.2+)。beforeRouteUpdate
- 在路由配置裡調用
。beforeEnter
- 解析異步路由元件。
- 在被激活的元件裡調用
。beforeRouteEnter
- 調用全局的
守衛(2.5+)。beforeResolve
- 導航被确認。
- 調用全局的
鈎子。afterEach
- 觸發 DOM 更新。
- 調用
守衛中傳給beforeRouteEnter
的回調函數,建立好的元件執行個體會作為回調函數的參數傳入。next
路由模式
- history 模式
: 使用/
和pushState
,通過這兩個 API 可以改變 url 位址不發生請求,replaceState
事件popState
- hash 模式
#
:
hash 是 URL 中 hash(#)及後面的那部分,常用作錨點在頁面内進行導航,改變 hash 值不會随着 http 請求發送給伺服器,通過
事件監聽 URL 的變化,可以用他來實作更新頁面部分内容的操作hashChange
vueRouter 的實作
剖析 VueRouter 本質
通過使用 vueRouter 可以知道
- 通過 new Router() 獲得一個 router 執行個體,我門引入的 VueRouter 其實就是一個類
class VueRouter {}
複制代碼
- 使用 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 函數字元串