首頁 > 易卦

從原始碼層面解讀16道Vue常考面試題

作者:由 酷扯兒 發表于 易卦日期:2022-04-28

為什麼不能建立鉤子

「來源: |前端迷 ID:love_frontend」

本文透過 16 道 vue 常考題來解讀 vue 部分實現原理,希望讓大家更深層次的理解 vue;

近期自己也實踐了幾個編碼常考題目,希望能夠幫助大家加深理解:

ES5 實現 new

ES5 實現 let/const

ES5 實現 call/apply/bind

ES5 實現 防抖和節流函式

如何實現一個透過 Promise/A+ 規範的 Promise

基於 Proxy 實現簡易版 Vue

題目概覽

new Vue() 都做了什麼?

Vue。use 做了什麼?

vue 的響應式?

vue3 為何用 proxy 替代了 Object。defineProperty?

vue 雙向繫結,model 怎麼改變 view,view 怎麼改變 vue?

vue 如何對陣列方法進行變異?例如 push、pop、slice 等;

computed 如何實現?

computed 和 watch 的區別在哪裡?

計算屬性和普通屬性的區別?

v-if/v-show/v-html 的原理是什麼,它是如何封裝的?

v-for 給每個元素繫結事件需要事件代理嗎?

你知道 key 的作嗎?

說一下 vue 中所有帶$的方法?

你知道 nextTick 嗎?

子元件為什麼不能修改父元件傳遞的 props,如果修改了,vue 是如何監聽到並給出警告的?

父元件和子元件生命週期鉤子的順序?

題目詳解

1。 new Vue() 都做了什麼?

建構函式

這裡我們直接檢視原始碼 src/core/instance/index。js 檢視入口:

首先 new 關鍵字在 JavaScript 中是例項化一個物件;

這裡 Vue 是 function 形式實現的類,new Vue(options) 宣告一個例項物件;

然後執行 Vue 建構函式,this。_init(options) 初始化入參;

import { initMixin } from“。/init”;

import { stateMixin } from“。/state”;

import { renderMixin } from“。/render”;

import { eventsMixin } from“。/events”;

import { lifecycleMixin } from“。/lifecycle”;

import { warn } from“。。/util/index”;

functionVue(options) {

// 建構函式

if (process。env。NODE_ENV !== “production” && !(thisinstanceof Vue)) {

warn(“Vue is a constructor and should be called with the `new` keyword”);

}

// 初始化引數

this。_init(options);

}

// 初始化方法混入

initMixin(Vue);

stateMixin(Vue);

eventsMixin(Vue);

lifecycleMixin(Vue);

renderMixin(Vue);

exportdefault Vue;

_init

深入往下,在 src/core/instance/init。js 中找到 this。_init 的宣告

// 這裡的混入方法入參 Vue

exportfunctioninitMixin(Vue: Class) {

// 增加原型鏈 _init 即上面建構函式中呼叫該方法

Vue。prototype。_init = function (options?: Object) {

// 上下文轉移到 vm

const vm: Component = this;

// a uid

vm。_uid = uid++;

let startTag, endTag;

/* istanbul ignore if */

if (process。env。NODE_ENV !== “production” && config。performance && mark) {

startTag = `vue-perf-start:${vm。_uid}`;

endTag = `vue-perf-end:${vm。_uid}`;

mark(startTag);

}

// a flag to avoid this being observed

vm。_isVue = true;

// 合併配置 options

if (options && options。_isComponent) {

// optimize internal component instantiation

// since dynamic options merging is pretty slow, and none of the

// internal component options needs special treatment。

// 初始化內部元件例項

initInternalComponent(vm, options);

} else {

vm。$options = mergeOptions(

resolveConstructorOptions(vm。constructor),

options || {},

vm

);

}

/* istanbul ignore else */

if (process。env。NODE_ENV !== “production”) {

// 初始化代理 vm

initProxy(vm);

} else {

vm。_renderProxy = vm;

}

// expose real self

vm。_self = vm;

// 初始化生命週期函式

initLifecycle(vm);

// 初始化自定義事件

initEvents(vm);

// 初始化渲染

initRender(vm);

// 執行 beforeCreate 生命週期

callHook(vm, “beforeCreate”);

// 在初始化 state/props 之前初始化注入 inject

initInjections(vm); // resolve injections before data/props

// 初始化 state/props 的資料雙向繫結

initState(vm);

// 在初始化 state/props 之後初始化 provide

initProvide(vm); // resolve provide after data/props

// 執行 created 生命週期

callHook(vm, “created”);

/* istanbul ignore if */

if (process。env。NODE_ENV !== “production” && config。performance && mark) {

vm。_name = formatComponentName(vm, false);

mark(endTag);

measure(`vue ${vm。_name} init`, startTag, endTag);

}

// 掛載到 dom 元素

if (vm。$options。el) {

vm。$mount(vm。$options。el);

}

};

}

小結

綜上,可總結出,new Vue(options) 具體做了如下事情:

執行建構函式;

上下文轉移到 vm;

如果 options。_isComponent 為 true,則初始化內部元件例項;否則合併配置引數,並掛載到 vm。$options 上面;

初始化生命週期函式、初始化事件相關、初始化渲染相關;

執行 beforeCreate 生命週期函式;

在初始化 state/props 之前初始化注入 inject;

初始化 state/props 的資料雙向繫結;

在初始化 state/props 之後初始化 provide;

執行 created 生命週期函式;

掛載到 dom 元素

其實 vue 還在生產環境中記錄了初始化的時間,用於效能分析;

2。 Vue。use 做了什麼?

use

直接檢視 src/core/global-api/use。js, 如下

import { toArray } from“。。/util/index”;

exportfunctioninitUse(Vue: GlobalAPI) {

Vue。use = function (plugin: Function | Object) {

// 外掛快取陣列

const installedPlugins =

this。_installedPlugins || (this。_installedPlugins = []);

// 已註冊則跳出

if (installedPlugins。indexOf(plugin) > -1) {

returnthis;

}

// 附加引數處理,擷取第1個引數之後的引數

const args = toArray(arguments, 1);

// 第一個引數塞入 this 上下文

args。unshift(this);

// 執行 plugin 這裡遵循定義規則

if (typeof plugin。install === “function”) {

// 外掛暴露 install 方法

plugin。install。apply(plugin, args);

} elseif (typeof plugin === “function”) {

// 外掛本身若沒有 install 方法,則直接執行

plugin。apply(null, args);

}

// 新增到快取陣列中

installedPlugins。push(plugin);

returnthis;

};

}

小結

綜上,可以總結 Vue。use 做了如下事情:

檢查外掛是否註冊,若已註冊,則直接跳出;

處理入參,將第一個引數之後的引數歸集,並在首部塞入 this 上下文;

執行註冊方法,呼叫定義好的 install 方法,傳入處理的引數,若沒有 install 方法並且外掛本身為 function 則直接進行註冊;

3。 vue 的響應式?

Observer

上程式碼,直接檢視 src/core/observer/index。js,class Observer,這個方法使得物件/陣列可響應

exportclassObserver{

value: any;

dep: Dep;

vmCount: number; // number of vms that have this object as root $data

constructor(value: any) {

this。value = value;

this。dep = new Dep();

this。vmCount = 0;

def(value, “__ob__”, this);

if (Array。isArray(value)) {

// 陣列則透過擴充套件原生方法形式使其可響應

if (hasProto) {

protoAugment(value, arrayMethods);

} else {

copyAugment(value, arrayMethods, arrayKeys);

}

this。observeArray(value);

} else {

this。walk(value);

}

}

/**

* Walk through all properties and convert them into

* getter/setters。 This method should only be called when

* value type is Object。

*/

walk(obj: Object) {

const keys = Object。keys(obj);

for (let i = 0; i < keys。length; i++) {

defineReactive(obj, keys[i]);

}

}

/**

* Observe a list of Array items。

*/

observeArray(items: Array) {

for (let i = 0, l = items。length; i < l; i++) {

observe(items[i]);

}

}

}

defineReactive

上程式碼,直接檢視 src/core/observer/index。js,核心方法 defineReactive,這個方法使得物件可響應,給物件動態新增 getter 和 setter

// 使物件中的某個屬性可響應

exportfunctiondefineReactive(

obj: Object,

key: string,

val: any,

customSetter?: ?Function,

shallow?: boolean

) {

// 初始化 Dep 物件,用作依賴收集

const dep = new Dep();

const property = Object。getOwnPropertyDescriptor(obj, key);

if (property && property。configurable === false) {

return;

}

// cater for pre-defined getter/setters

const getter = property && property。get;

const setter = property && property。set;

if ((!getter || setter) && arguments。length === 2) {

val = obj[key];

}

let childOb = !shallow && observe(val);

// 響應式物件核心,定義物件某個屬性的 get 和 set 監聽

Object。defineProperty(obj, key, {

enumerable: true,

configurable: true,

get: functionreactiveGetter() {

const value = getter ? getter。call(obj) : val;

// 監測 watcher 是否存在

if (Dep。target) {

// 依賴收集

dep。depend();

if (childOb) {

childOb。dep。depend();

if (Array。isArray(value)) {

dependArray(value);

}

}

}

return value;

},

set: functionreactiveSetter(newVal) {

const value = getter ? getter。call(obj) : val;

/* eslint-disable no-self-compare */

if (newVal === value || (newVal !== newVal && value !== value)) {

return;

}

/* eslint-enable no-self-compare */

if (process。env。NODE_ENV !== “production” && customSetter) {

customSetter();

}

// #7981: for accessor properties without setter

if (getter && !setter) return;

if (setter) {

setter。call(obj, newVal);

} else {

val = newVal;

}

childOb = !shallow && observe(newVal);

// 通知更新

dep。notify();

},

});

}

Dep

依賴收集,我們需要看一下 Dep 的程式碼,它依賴收集的核心,在 src/core/observer/dep。js 中:

import type Watcher from“。/watcher”;

import { remove } from“。。/util/index”;

import config from“。。/config”;

let uid = 0;

/**

* A dep is an observable that can have multiple

* directives subscribing to it。

*/

exportdefaultclassDep{

// 靜態屬性,全域性唯一 Watcher

// 這裡比較巧妙,因為在同一時間只能有一個全域性的 Watcher 被計算

static target: ?Watcher;

id: number;

// watcher 陣列

subs: Array

constructor() {

this。id = uid++;

this。subs = [];

}

addSub(sub: Watcher) {

this。subs。push(sub);

}

removeSub(sub: Watcher) {

remove(this。subs, sub);

}

depend() {

if (Dep。target) {

// Watcher 中收集依賴

Dep。target。addDep(this);

}

}

notify() {

// stabilize the subscriber list first

const subs = this。subs。slice();

if (process。env。NODE_ENV !== “production” && !config。async) {

// subs aren‘t sorted in scheduler if not running async

// we need to sort them now to make sure they fire in correct

// order

subs。sort((a, b) => a。id - b。id);

}

// 遍歷所有的 subs,也就是 Watcher 的例項陣列,然後呼叫每一個 watcher 的 update 方法

for (let i = 0, l = subs。length; i < l; i++) {

subs[i]。update();

}

}

}

// The current target watcher being evaluated。

// This is globally unique because only one watcher

// can be evaluated at a time。

// 全域性唯一的 Watcher

Dep。target = null;

const targetStack = [];

exportfunctionpushTarget(target: ?Watcher) {

targetStack。push(target);

Dep。target = target;

}

exportfunctionpopTarget() {

targetStack。pop();

Dep。target = targetStack[targetStack。length - 1];

}

Watcher

Dep 是對 Watcher 的一種管理,下面我們來看一下 Watcher, 在 src/core/observer/watcher。js 中

let uid = 0;

/**

* 一個 Watcher 分析一個表示式,收集依賴項, 並在表示式值更改時觸發回撥。

* 用於 $watch() api 和指令

*/

exportdefaultclassWatcher{

vm: Component;

expression: string;

cb: Function;

id: number;

deep: boolean;

user: boolean;

lazy: boolean;

sync: boolean;

dirty: boolean;

active: boolean;

deps: Array

newDeps: Array

depIds: SimpleSet;

newDepIds: SimpleSet;

before: ?Function;

getter: Function;

value: any;

constructor(

vm: Component,

expOrFn: string | Function,

cb: Function,

options?: ?Object,

isRenderWatcher?: boolean

) {

this。vm = vm;

if (isRenderWatcher) {

vm。_watcher = this;

}

vm。_watchers。push(this);

// options

if (options) {

this。deep = !!options。deep;

this。user = !!options。user;

this。lazy = !!options。lazy;

this。sync = !!options。sync;

this。before = options。before;

} else {

this。deep = this。user = this。lazy = this。sync = false;

}

this。cb = cb;

this。id = ++uid; // uid for batching

this。active = true;

this。dirty = this。lazy; // for lazy watchers

this。deps = [];

this。newDeps = [];

this。depIds = newSet();

this。newDepIds = newSet();

this。expression =

process。env。NODE_ENV !== “production” ? expOrFn。toString() : “”;

// parse expression for getter

if (typeof expOrFn === “function”) {

this。getter = expOrFn;

} else {

this。getter = parsePath(expOrFn);

if (!this。getter) {

this。getter = noop;

process。env。NODE_ENV !== “production” &&

warn(

`Failed watching path: “${expOrFn}” ` +

“Watcher only accepts simple dot-delimited paths。 ” +

“For full control, use a function instead。”,

vm

);

}

}

this。value = this。lazy ? undefined : this。get();

}

// 評估getter,並重新收集依賴項。

get() {

// 實際上就是把 Dep。target 賦值為當前的渲染 watcher 並壓棧(為了恢復用)。

pushTarget(this);

let value;

const vm = this。vm;

try {

// this。getter 對應就是 updateComponent 函式,這實際上就是在執行:

// 這裡需要追溯 new Watcher 執行的地方,是在

value = this。getter。call(vm, vm);

} catch (e) {

if (this。user) {

handleError(e, vm, `getter for watcher “${this。expression}”`);

} else {

throw e;

}

} finally {

// “touch” every property so they are all tracked as

// dependencies for deep watching

// 遞迴深度遍歷每一個屬性,使其都可以被依賴收集

if (this。deep) {

traverse(value);

}

// 出棧

popTarget();

// 清理依賴收集

this。cleanupDeps();

}

return value;

}

// 新增依賴

// 在 Dep 中會呼叫

addDep(dep: Dep) {

const id = dep。id;

// 避免重複收集

if (!this。newDepIds。has(id)) {

this。newDepIds。add(id);

this。newDeps。push(dep);

if (!this。depIds。has(id)) {

// 把當前的 watcher 訂閱到這個資料持有的 dep 的 subs 中

// 目的是為後續資料變化時候能通知到哪些 subs 做準備

dep。addSub(this);

}

}

}

// 清理依賴

// 每次新增完新的訂閱,會移除掉舊的訂閱,所以不會有任何浪費

cleanupDeps() {

let i = this。deps。length;

// 首先遍歷 deps,移除對 dep。subs 陣列中 Wathcer 的訂閱

while (i——) {

const dep = this。deps[i];

if (!this。newDepIds。has(dep。id)) {

dep。removeSub(this);

}

}

let tmp = this。depIds;

this。depIds = this。newDepIds;

this。newDepIds = tmp;

this。newDepIds。clear();

tmp = this。deps;

this。deps = this。newDeps;

this。newDeps = tmp;

this。newDeps。length = 0;

}

// 釋出介面

// 依賴更新的時候觸發

update() {

/* istanbul ignore else */

if (this。lazy) {

// computed 資料

this。dirty = true;

} elseif (this。sync) {

// 同步資料更新

this。run();

} else {

// 正常資料會經過這裡

// 派發更新

queueWatcher(this);

}

}

// 排程介面,用於執行更新

run() {

if (this。active) {

const value = this。get();

if (

value !== this。value ||

// Deep watchers and watchers on Object/Arrays should fire even

// when the value is the same, because the value may

// have mutated。

isObject(value) ||

this。deep

) {

// 設定新的值

const oldValue = this。value;

this。value = value;

if (this。user) {

try {

this。cb。call(this。vm, value, oldValue);

} catch (e) {

handleError(

e,

this。vm,

`callback for watcher “${this。expression}”`

);

}

} else {

this。cb。call(this。vm, value, oldValue);

}

}

}

}

/**

* Evaluate the value of the watcher。

* This only gets called for lazy watchers。

*/

evaluate() {

this。value = this。get();

this。dirty = false;

}

/**

* Depend on all deps collected by this watcher。

*/

depend() {

let i = this。deps。length;

while (i——) {

this。deps[i]。depend();

}

}

/**

* Remove self from all dependencies’ subscriber list。

*/

teardown() {

if (this。active) {

// remove self from vm‘s watcher list

// this is a somewhat expensive operation so we skip it

// if the vm is being destroyed。

if (!this。vm。_isBeingDestroyed) {

remove(this。vm。_watchers, this);

}

let i = this。deps。length;

while (i——) {

this。deps[i]。removeSub(this);

}

this。active = false;

}

}

}

小結

綜上響應式核心程式碼,我們可以描述響應式的執行過程:

根據資料型別來做不同處理,如果是物件則 Object。defineProperty() 監聽資料屬性的 get 來進行資料依賴收集,再透過 get 來完成資料更新的派發;如果是陣列如果是陣列則透過覆蓋該陣列原型的法,擴充套件它的 7 個變更法(push/pop/shift/unshift/splice/reverse/sort),透過監聽這些方法可以做到依賴收集和派發更新;

Dep 是主要做依賴收集,收集的是當前上下文作為 Watcher,全域性有且僅有一個 Dep。target,透過 Dep 可以做到控制當前上下文的依賴收集和通知 Watcher 派發更新;

Watcher 連線表示式和值,說白了就是 watcher 連線檢視層的依賴,並可以觸發檢視層的更新,與 Dep 緊密結合,透過 Dep 來控制其對檢視層的監聽

4。 vue3 為何用 proxy 替代了 Object。defineProperty?

traverse

擷取上面 Watcher 中部分程式碼

if (this。deep) {

// 這裡其實遞迴遍歷屬性用作依賴收集

traverse(value);

}

再檢視 src/core/observer/traverse。js 中 traverse 的實現,如下:

const seenObjects = newSet();

// 遞迴遍歷物件,將所有屬性轉換為 getter

// 使每個物件內巢狀屬性作為依賴收集項

exportfunctiontraverse(val: any) {

_traverse(val, seenObjects);

seenObjects。clear();

}

function_traverse(val: any, seen: SimpleSet) {

let i, keys;

const isA = Array。isArray(val);

if (

(!isA && !isObject(val)) ||

Object。isFrozen(val) ||

val instanceof VNode

) {

return;

}

if (val。__ob__) {

const depId = val。__ob__。dep。id;

if (seen。has(depId)) {

return;

}

seen。add(depId);

}

if (isA) {

i = val。length;

while (i——) _traverse(val[i], seen);

} else {

keys = Object。keys(val);

i = keys。length;

while (i——) _traverse(val[keys[i]], seen);

}

}

小結

再綜上一題程式碼實際瞭解,其實我們看到一些弊端:

Watcher 監聽 對屬性做了遞迴遍歷,這裡可能會造成效能損失;

defineReactive 遍歷屬性對當前存在的屬性 Object。defineProperty() 作依賴收集,但是對於不存在,或者刪除屬性,則監聽不到;從而會造成 對新增或者刪除的屬性無法做到響應式,只能透過 Vue。set/delete 這類 api 才可以做到;

對於 es6 中新產的 Map、Set 這些資料結構不持

5。 vue 雙向繫結,Model 怎麼改變 View,View 怎麼改變 Model?

其實這個問題需要承接上述第三題,再結合下圖

從原始碼層面解讀16道Vue常考面試題

響應式原理

Model 改變 View:

defineReactive 中透過 Object。defineProperty 使 data 可響應;

Dep 在 getter 中作依賴收集,在 setter 中作派發更新;

dep。notify() 通知 Watcher 更新,最終呼叫 vm。_render() 更新 UI;

View 改變 Model:其實同上理,View 與 data 的資料關聯在了一起,View 透過事件觸發 data 的變化,從而觸發了 setter,這就構成了一個雙向迴圈綁定了;

6。 vue 如何對陣列方法進行變異?例如 push、pop、slice 等;

這個問題,我們直接從原始碼找答案,這裡我們擷取上面 Observer 部分原始碼,先來追溯一下,Vue 怎麼實現陣列的響應:

constructor(value: any) {

this。value = value;

this。dep = new Dep();

this。vmCount = 0;

def(value, “__ob__”, this);

if (Array。isArray(value)) {

// 陣列則透過擴充套件原生方法形式使其可響應

if (hasProto) {

protoAugment(value, arrayMethods);

} else {

copyAugment(value, arrayMethods, arrayKeys);

}

this。observeArray(value);

} else {

this。walk(value);

}

}

arrayMethods

這裡需要檢視一下 arrayMethods 這個物件,在 src/core/observer/array。js 中

import { def } from“。。/util/index”;

const arrayProto = Array。prototype;

// 複製陣列原型鏈,並建立一個空物件

// 這裡使用 Object。create 是為了不汙染 Array 的原型

exportconst arrayMethods = Object。create(arrayProto);

const methodsToPatch = [

“push”,

“pop”,

“shift”,

“unshift”,

“splice”,

“sort”,

“reverse”,

];

// 攔截突變方法併發出事件

// 攔截了陣列的 7 個方法

methodsToPatch。forEach(function (method) {

// cache original method

const original = arrayProto[method];

// 使其可響應

def(arrayMethods, method, functionmutator(。。。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);

// notify change

// 派發更新

ob。dep。notify();

return result;

});

});

def

def 使物件可響應,在 src/core/util/lang。js

exportfunctiondef(obj: Object, key: string, val: any, enumerable?: boolean) {

Object。defineProperty(obj, key, {

value: val,

enumerable: !!enumerable,

writable: true,

configurable: true,

});

}

小結

Object。create(Array。prototype) 複製 Array 原型鏈為新的物件;

攔截了陣列的 7 個方法的執行,並使其可響應,7 個方法分別為:push, pop, shift, unshift, splice, sort, reverse;

當陣列呼叫到這 7 個方法的時候,執行 ob。dep。notify() 進行派發通知 Watcher 更新;

附加思考

不過,vue 對陣列的監聽還是有限制的,如下:

陣列透過索引改變值的時候監聽不到,比如:array[2] = newObj

陣列長度變化無法監聽

這些操作都需要透過 Vue。set/del 去操作才行;

7。 computed 如何實現?

initComputed

這個方法用於初始化 options。computed 物件, 這裡還是上原始碼,在 src/core/instance/state。js 中,這個方法是在 initState 中呼叫的

const computedWatcherOptions = { lazy: true };

functioninitComputed(vm: Component, computed: Object) {

// $flow-disable-line

// 建立一個空物件

const watchers = (vm。_computedWatchers = Object。create(null));

// computed properties are just getters during SSR

const isSSR = isServerRendering();

for (const key in computed) {

// 遍歷拿到每個定義的 userDef

const userDef = computed[key];

const getter = typeof userDef === “function” ? userDef : userDef。get;

// 沒有 getter 則 warn

if (process。env。NODE_ENV !== “production” && getter == null) {

warn(`Getter is missing for computed property “${key}”。`, vm);

}

if (!isSSR) {

// 為每個 computed 屬性建立 watcher

// create internal watcher for the computed property。

watchers[key] = new Watcher(

vm,

getter || noop,

noop,

computedWatcherOptions // {lazy: true}

);

}

// component-defined computed properties are already defined on the

// component prototype。 We only need to define computed properties defined

// at instantiation here。

if (!(key in vm)) {

// 定義 vm 中未定義的計算屬性

defineComputed(vm, key, userDef);

} elseif (process。env。NODE_ENV !== “production”) {

if (key in vm。$data) {

// 判斷 key 是不是在 data

warn(`The computed property “${key}” is already defined in data。`, vm);

} elseif (vm。$options。props && key in vm。$options。props) {

// 判斷 key 是不是在 props 中

warn(

`The computed property “${key}” is already defined as a prop。`,

vm

);

}

}

}

}

defineComputed

這個方法用作定義 computed 中的屬性,繼續看程式碼:

exportfunctiondefineComputed(

target: any,

key: string,

userDef: Object | Function

) {

const shouldCache = !isServerRendering();

if (typeof userDef === “function”) {

sharedPropertyDefinition。get = shouldCache

? createComputedGetter(key)

: createGetterInvoker(userDef);

sharedPropertyDefinition。set = noop;

} else {

sharedPropertyDefinition。get = userDef。get

? shouldCache && userDef。cache !== false

? createComputedGetter(key)

: createGetterInvoker(userDef。get)

: noop;

sharedPropertyDefinition。set = userDef。set || noop;

}

if (

process。env。NODE_ENV !== “production” &&

sharedPropertyDefinition。set === noop

) {

sharedPropertyDefinition。set = function () {

warn(

`Computed property “${key}” was assigned to but it has no setter。`,

this

);

};

}

// 定義計算屬性的 get / set

Object。defineProperty(target, key, sharedPropertyDefinition);

}

// 返回計算屬性對應的 getter

functioncreateComputedGetter(key) {

returnfunctioncomputedGetter() {

const watcher = this。_computedWatchers && this。_computedWatchers[key];

if (watcher) {

if (watcher。dirty) {

// watcher 檢查是 computed 屬性的時候 會標記 dirty 為 true

// 這裡是 computed 的取值邏輯, 執行 evaluate 之後 則 dirty false,直至下次觸發

// 其實這裡就可以說明 computed 屬性其實是觸發了 getter 屬性之後才進行計算的,而觸發的媒介便是 computed 引用的其他屬性觸發 getter,再觸發 dep。update(), 繼而 觸發 watcher 的 update

watcher。evaluate();

// ——————————————- Watcher ————————————————

// 這裡擷取部分 Watcher 的定義

// update 定義

// update () {

// /* istanbul ignore else */

// if (this。lazy) {

// // 觸發更新的時候標記計算屬性

// this。dirty = true

// } else if (this。sync) {

// this。run()

// } else {

// queueWatcher(this)

// }

// }

// evaluate 定義

// evaluate () {

// this。value = this。get()

// // 取值後標記 取消

// this。dirty = false

// }

// ————————————- Watcher ——————————————————

}

if (Dep。target) {

// 收集依賴

watcher。depend();

}

return watcher。value;

}

};

}

functioncreateGetterInvoker(fn) {

returnfunctioncomputedGetter() {

return fn。call(this, this);

};

}

小結

綜上程式碼分析過程,總結 computed 屬性的實現過程如下(以下分析過程均忽略了 ssr 情況):

Object。create(null) 建立一個空物件用作快取 computed 屬性的 watchers,並快取在 vm。_computedWatchers 中;

遍歷計算屬性,拿到使用者定義的 userDef,為每個屬性定義 Watcher,標記 Watcher 屬性 lazy: true;

定義 vm 中未定義過的 computed 屬性,defineComputed(vm, key, userDef),已存在則判斷是在 data 或者 props 中已定義並相應警告;

接下來就是定義 computed 屬性的 getter 和 setter,這裡主要是看 createComputedGetter 裡面的定義:當觸發更新則檢測 watcher 的 dirty 標記,則執行 watcher。evaluate() 方法執行計算,然後依賴收集;

這裡再追溯 watcher。dirty 屬性邏輯,在 watcher。update 中 當遇到 computed 屬性時候被標記為 dirty:false,這裡其實可以看出 computed 屬性的計算前提必須是引用的正常屬性的更新觸發了 Dep。update(),繼而觸發對應 watcher。update 進行標記 dirty:true,繼而在計算屬性 getter 的時候才會觸發更新,否則不更新;

以上便是計算屬性的實現邏輯,部分程式碼邏輯需要追溯上面第三題響應式的部分 Dep/Watcher 的觸發邏輯;

8。 computed 和 watch 的區別在哪裡?

initWatch

這裡還是老樣子,上程式碼,在 src/core/instance/state。js 中:

functioninitWatch(vm: Component, watch: Object) {

// 遍歷 watch 物件屬性

for (const key in watch) {

const handler = watch[key];

// 陣列則進行遍歷建立 watcher

if (Array。isArray(handler)) {

for (let i = 0; i < handler。length; i++) {

createWatcher(vm, key, handler[i]);

}

} else {

createWatcher(vm, key, handler);

}

}

}

// 建立 watcher 監聽

functioncreateWatcher(

vm: Component,

expOrFn: string | Function,

handler: any,

options?: Object

) {

if (isPlainObject(handler)) {

options = handler;

handler = handler。handler;

}

// handler 傳入字串,則直接從 vm 中獲取函式方法

if (typeof handler === “string”) {

handler = vm[handler];

}

// 建立 watcher 監聽

return vm。$watch(expOrFn, handler, options);

}

$watch

我們還需要看一下 $watch 的邏輯,在 src/core/instance/state。js 中:

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

// 建立 watch 屬性的 Watcher 例項

const watcher = new Watcher(vm, expOrFn, cb, options)

if (options。immediate) {

try {

cb。call(vm, watcher。value)

} catch (error) {

handleError(error, vm, `callback for immediate watcher “${watcher。expression}”`)

}

}

// 用作銷燬

returnfunctionunwatchFn () {

// 移除 watcher 的依賴

watcher。teardown()

}

}

}

小結

綜上程式碼分析,先看來看一下 watch 屬性的實現邏輯:

遍歷 watch 屬性分別建立屬性的 Watcher 監聽,這裡可以看出其實該屬性並未被 Dep 收集依賴;

可以分析 watch 監聽的屬性 必然是已經被 Dep 收集依賴的屬性了(data/props 中的屬性),進行對應屬性觸發更新的時候才會觸發 watch 屬性的監聽回撥;

這裡就可以分析 computed 與 watch 的異同:

computed 屬性的更新需要依賴於其引用屬性的更新觸發標記 dirty: true,進而觸發 computed 屬性 getter 的時候才會觸發其本身的更新,否則其不更新;

watch 屬性則是依賴於本身已被 Dep 收集依賴的部分屬性,即作為 data/props 中的某個屬性的尾隨 watcher,在監聽屬性更新時觸發 watcher 的回撥;否則監聽則無意義;

這裡再引申一下使用場景:

如果一個數據依賴於其他資料,那麼就使用 computed 屬性;

如果你需要在某個資料變化時做一些事情,使用 watch 來觀察這個資料變化;

9。 計算屬性和普通屬性的區別?

這個題目跟上題類似,區別如下:

普通屬性都是基於 getter 和 setter 的正常取值和更新;

computed 屬性是依賴於內部引用普通屬性的 setter 變更從而標記 watcher 中 dirty 標記為 true,此時才會觸發更新;

10。 v-if/v-show/v-html 的原理是什麼,它是如何封裝的?

v-if

先來看一下 v-if 的實現,首先 vue 編譯 template 模板的時候會先生成 ast 靜態語法樹,然後進行標記靜態節點,再之後生成對應的 render 函式,這裡就直接看下 genIf 的程式碼,在src/compiler/codegen/index。js中:

exportfunctiongenIf(

el: any,

state: CodegenState,

altGen?: Function,

altEmpty?: string

): string{

el。ifProcessed = true; // 標記避免遞迴,標記已經處理過

return genIfConditions(el。ifConditions。slice(), state, altGen, altEmpty);

}

functiongenIfConditions(

conditions: ASTIfConditions,

state: CodegenState,

altGen?: Function,

altEmpty?: string

): string{

if (!conditions。length) {

return altEmpty || “_e()”;

}

const condition = conditions。shift();

// 這裡返回的是一個三元表示式

if (condition。exp) {

return`(${condition。exp})?${genTernaryExp(

condition。block

)}:${genIfConditions(conditions, state, altGen, altEmpty)}`;

} else {

return`${genTernaryExp(condition。block)}`;

}

// v-if with v-once should generate code like (a)?_m(0):_m(1)

functiongenTernaryExp(el) {

return altGen

? altGen(el, state)

: el。once

? genOnce(el, state)

: genElement(el, state);

}

}

v-if 在 template 生成 ast 之後 genIf 返回三元表示式,在渲染的時候僅渲染表示式生效部分;

v-show

這裡擷取 v-show 指令的實現邏輯,在 src/platforms/web/runtime/directives/show。js 中:

exportdefault {

bind(el: any, { value }: VNodeDirective, vnode: VNodeWithData) {

vnode = locateNode(vnode);

const transition = vnode。data && vnode。data。transition;

const originalDisplay = (el。__vOriginalDisplay =

el。style。display === “none” ? “” : el。style。display);

if (value && transition) {

vnode。data。show = true;

enter(vnode, () => {

el。style。display = originalDisplay;

});

} else {

el。style。display = value ? originalDisplay : “none”;

}

},

update(el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) {

/* istanbul ignore if */

if (!value === !oldValue) return;

vnode = locateNode(vnode);

const transition = vnode。data && vnode。data。transition;

if (transition) {

vnode。data。show = true;

if (value) {

enter(vnode, () => {

el。style。display = el。__vOriginalDisplay;

});

} else {

leave(vnode, () => {

el。style。display = “none”;

});

}

} else {

el。style。display = value ? el。__vOriginalDisplay : “none”;

}

},

unbind(

el: any,

binding: VNodeDirective,

vnode: VNodeWithData,

oldVnode: VNodeWithData,

isDestroy: boolean

) {

if (!isDestroy) {

el。style。display = el。__vOriginalDisplay;

}

},

};

這裡其實比較明顯了,v-show 根據表示式的值最終操作的是 style。display

v-html

v-html 比較簡單,最終操作的是 innerHTML,我們還是看程式碼,在 src/platforms/compiler/directives/html。js 中:

import { addProp } from“compiler/helpers”;

exportdefaultfunctionhtml(el: ASTElement, dir: ASTDirective) {

if (dir。value) {

addProp(el, “innerHTML”, `_s(${dir。value})`, dir);

}

}

小結

綜上程式碼證明:

v-if 在 template 生成 ast 之後 genIf 返回三元表示式,在渲染的時候僅渲染表示式生效部分;

v-show 根據表示式的值最終操作的是 style。display,並標記當前 vnode。data。show 屬性;

v-html 最終操作的是 innerHTML,將當前值 innerHTML 到當前標籤;

11。 v-for 給每個元素繫結事件需要事件代理嗎?

首先,我們先來看一下 v-for 的實現,同上面 v-if,在模板渲染過程中由genFor 處理,在 src/compiler/codegen/index。js 中:

exportfunctiongenFor(

el: any,

state: CodegenState,

altGen?: Function,

altHelper?: string

): string{

const exp = el。for;

const alias = el。alias;

const iterator1 = el。iterator1 ? `,${el。iterator1}` : “”;

const iterator2 = el。iterator2 ? `,${el。iterator2}` : “”;

if (

process。env。NODE_ENV !== “production” &&

state。maybeComponent(el) &&

el。tag !== “slot” &&

el。tag !== “template” &&

!el。key

) {

state。warn(

`<${el。tag} v-for=“${alias} in ${exp}”>: component lists rendered with ` +

`v-for should have explicit keys。 ` +

`See https://vuejs。org/guide/list。html#key for more info。`,

el。rawAttrsMap[“v-for”],

true/* tip */

);

}

el。forProcessed = true; // 標記避免遞迴,標記已經處理過

return (

`${altHelper || “_l”}((${exp}),` +

`function(${alias}${iterator1}${iterator2}){` +

`return ${(altGen || genElement)(el, state)}` +

“})”

);

// 虛擬碼解析後大致如下

// _l(data, function (item, index) {

// return genElement(el, state);

// });

}

這裡其實可以看出,genFor 最終返回了一串虛擬碼(見註釋)最終每個迴圈返回 genElement(el, state),其實這裡可以大膽推測,vue 並沒有單獨在 v-for 對事件做委託處理,只是單獨處理了每次迴圈的處理;

可以確認的是,vue 在 v-for 中並沒有處理事件委託,處於效能考慮,最好自己加上事件委託,這裡有個帖子有分析對比,第 94 題:vue 在 v-for 時給每項元素繫結事件需要用事件代理嗎?為什麼?

12。 你知道 key 的作嗎?

key 可預想的是 vue 拿來給 vnode 作唯一標識的,下面我們先來看下 key 到底被拿來做啥事,在 src/core/vdom/patch。js 中:

updateChildren

functionupdateChildren(

parentElm,

oldCh,

newCh,

insertedVnodeQueue,

removeOnly

) {

let oldStartIdx = 0;

let newStartIdx = 0;

let oldEndIdx = oldCh。length - 1;

let oldStartVnode = oldCh[0];

let oldEndVnode = oldCh[oldEndIdx];

let newEndIdx = newCh。length - 1;

let newStartVnode = newCh[0];

let newEndVnode = newCh[newEndIdx];

let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

// removeOnly is a special flag used only by

// to ensure removed elements stay in correct relative positions

// during leaving transitions

const canMove = !removeOnly;

if (process。env。NODE_ENV !== “production”) {

checkDuplicateKeys(newCh);

}

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

if (isUndef(oldStartVnode)) {

oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left

} elseif (isUndef(oldEndVnode)) {

oldEndVnode = oldCh[——oldEndIdx];

} elseif (sameVnode(oldStartVnode, newStartVnode)) {

patchVnode(

oldStartVnode,

newStartVnode,

insertedVnodeQueue,

newCh,

newStartIdx

);

oldStartVnode = oldCh[++oldStartIdx];

newStartVnode = newCh[++newStartIdx];

} elseif (sameVnode(oldEndVnode, newEndVnode)) {

patchVnode(

oldEndVnode,

newEndVnode,

insertedVnodeQueue,

newCh,

newEndIdx

);

oldEndVnode = oldCh[——oldEndIdx];

newEndVnode = newCh[——newEndIdx];

} elseif (sameVnode(oldStartVnode, newEndVnode)) {

// Vnode moved right

patchVnode(

oldStartVnode,

newEndVnode,

insertedVnodeQueue,

newCh,

newEndIdx

);

canMove &&

nodeOps。insertBefore(

parentElm,

oldStartVnode。elm,

nodeOps。nextSibling(oldEndVnode。elm)

);

oldStartVnode = oldCh[++oldStartIdx];

newEndVnode = newCh[——newEndIdx];

} elseif (sameVnode(oldEndVnode, newStartVnode)) {

// Vnode moved left

patchVnode(

oldEndVnode,

newStartVnode,

insertedVnodeQueue,

newCh,

newStartIdx

);

canMove &&

nodeOps。insertBefore(parentElm, oldEndVnode。elm, oldStartVnode。elm);

oldEndVnode = oldCh[——oldEndIdx];

newStartVnode = newCh[++newStartIdx];

} else {

if (isUndef(oldKeyToIdx))

oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);

idxInOld = isDef(newStartVnode。key)

? oldKeyToIdx[newStartVnode。key]

: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);

if (isUndef(idxInOld)) {

// New element

createElm(

newStartVnode,

insertedVnodeQueue,

parentElm,

oldStartVnode。elm,

false,

newCh,

newStartIdx

);

} else {

vnodeToMove = oldCh[idxInOld];

if (sameVnode(vnodeToMove, newStartVnode)) {

patchVnode(

vnodeToMove,

newStartVnode,

insertedVnodeQueue,

newCh,

newStartIdx

);

oldCh[idxInOld] = undefined;

canMove &&

nodeOps。insertBefore(parentElm, vnodeToMove。elm, oldStartVnode。elm);

} else {

// same key but different element。 treat as new element

createElm(

newStartVnode,

insertedVnodeQueue,

parentElm,

oldStartVnode。elm,

false,

newCh,

newStartIdx

);

}

}

newStartVnode = newCh[++newStartIdx];

}

}

if (oldStartIdx > oldEndIdx) {

refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1]。elm;

addVnodes(

parentElm,

refElm,

newCh,

newStartIdx,

newEndIdx,

insertedVnodeQueue

);

} elseif (newStartIdx > newEndIdx) {

removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);

}

}

這段程式碼是 vue diff 演算法的核心程式碼了,用作比較同級節點是否相同,批次更新的,可謂是效能核心了,以上可以看下 sameVnode 比較節點被用了多次,下面我們來看下是怎麼比較兩個相同節點的

sameVnode

functionsameVnode(a, b) {

return (

// 首先就是比較 key,key 相同是必要條件

a。key === b。key &&

((a。tag === b。tag &&

a。isComment === b。isComment &&

isDef(a。data) === isDef(b。data) &&

sameInputType(a, b)) ||

(isTrue(a。isAsyncPlaceholder) &&

a。asyncFactory === b。asyncFactory &&

isUndef(b。asyncFactory。error)))

);

}

可以看到 key 是 diff 演算法用來比較節點的必要條件,可想而知 key 的重要性;

小結

以上,我們瞭解到 key 的關鍵性,這裡可以總結下:

key 在 diff 演算法比較中用作比較兩個節點是否相同的重要標識,相同則複用,不相同則刪除舊的建立新的;

相同上下文的 key 最好是唯一的;

別用 index 來作為 key,index 相對於列表元素來說是可變的,無法標記原有節點,比如我新增和插入一個元素,index 對於原來節點就發生了位移,就無法 diff 了;

13。 說一下 vue 中所有帶$的方法?

例項 property

vm。$data: Vue 例項觀察的資料物件。Vue 例項代理了對其 data 物件 property 的訪問。

vm。$props: 當前元件接收到的 props 物件。Vue 例項代理了對其 props 物件 property 的訪問。

vm。$el: Vue 例項使用的根 DOM 元素。

vm。$options: 用於當前 Vue 例項的初始化選項。

vm。$parent: 父例項,如果當前例項有的話。

vm。$root: 當前元件樹的根 Vue 例項。如果當前例項沒有父例項,此例項將會是其自己。

vm。$children: 當前例項的直接子元件。需要注意 $children 並不保證順序,也不是響應式的。如果你發現自己正在嘗試使用 $children 來進行資料繫結,考慮使用一個數組配合 v-for 來生成子元件,並且使用 Array 作為真正的來源。

vm。$slots: 用來訪問被插槽分發的內容。每個具名插槽有其相應的 property (例如:v-slot:foo 中的內容將會在 vm。$slots。foo 中被找到)。default property 包括了所有沒有被包含在具名插槽中的節點,或 v-slot:default 的內容。

vm。$scopedSlots: 用來訪問作用域插槽。對於包括 預設 slot 在內的每一個插槽,該物件都包含一個返回相應 VNode 的函式。

vm。$refs: 一個物件,持有註冊過 ref attribute 的所有 DOM 元素和元件例項。

vm。$isServer: 當前 Vue 例項是否運行於伺服器。

vm。$attrs: 包含了父作用域中不作為 prop 被識別 (且獲取) 的 attribute 繫結 (class 和 style 除外)。當一個元件沒有宣告任何 prop 時,這裡會包含所有父作用域的繫結 (class 和 style 除外),並且可以透過 v-bind=“$attrs” 傳入內部元件——在建立高級別的元件時非常有用。

vm。$listeners: 包含了父作用域中的 (不含 。native 修飾器的) v-on 事件監聽器。它可以透過 v-on=“$listeners” 傳入內部元件——在建立更高層次的元件時非常有用。

例項方法 / 資料

vm。$watch( expOrFn, callback, [options] ): 觀察 Vue 例項上的一個表示式或者一個函式計算結果的變化。回撥函式得到的引數為新值和舊值。表示式只接受監督的鍵路徑。對於更復雜的表示式,用一個函式取代。

vm。$set( target, propertyName/index, value ): 這是全域性 Vue。set 的別名。

vm。$delete( target, propertyName/index ): 這是全域性 Vue。delete 的別名。

例項方法 / 事件

vm。$on( event, callback ): 監聽當前例項上的自定義事件。事件可以由 vm。$emit 觸發。回撥函式會接收所有傳入事件觸發函式的額外引數。

vm。$once( event, callback ): 監聽一個自定義事件,但是隻觸發一次。一旦觸發之後,監聽器就會被移除。

vm。$off( [event, callback] ): 移除自定義事件監聽器。

如果沒有提供引數,則移除所有的事件監聽器;

如果只提供了事件,則移除該事件所有的監聽器;

如果同時提供了事件與回撥,則只移除這個回撥的監聽器。

vm。$emit( eventName, […args] ): 觸發當前例項上的事件。附加引數都會傳給監聽器回撥。

例項方法 / 生命週期

vm。$mount( [elementOrSelector] )

如果 Vue 例項在例項化時沒有收到 el 選項,則它處於“未掛載”狀態,沒有關聯的 DOM 元素。可以使用 vm。$mount() 手動地掛載一個未掛載的例項。

如果沒有提供 elementOrSelector 引數,模板將被渲染為文件之外的的元素,並且你必須使用原生 DOM API 把它插入文件中。

這個方法返回例項自身,因而可以鏈式呼叫其它例項方法。

vm。$forceUpdate(): 迫使 Vue 例項重新渲染。注意它僅僅影響例項本身和插入插槽內容的子元件,而不是所有子元件。

vm。$nextTick( [callback] ): 將回調延遲到下次 DOM 更新迴圈之後執行。在修改資料之後立即使用它,然後等待 DOM 更新。它跟全域性方法 Vue。nextTick 一樣,不同的是回撥的 this 自動繫結到呼叫它的例項上。

vm。$destroy(): 完全銷燬一個例項。清理它與其它例項的連線,解綁它的全部指令及事件監聽器。

觸發 beforeDestroy 和 destroyed 的鉤子。

14。 你知道 nextTick 嗎?

直接上程式碼,在 src/core/util/next-tick。js 中:

import { noop } from“shared/util”;

import { handleError } from“。/error”;

import { isIE, isIOS, isNative } from“。/env”;

exportlet isUsingMicroTask = false;

const callbacks = [];

let pending = false;

functionflushCallbacks() {

pending = false;

const copies = callbacks。slice(0);

callbacks。length = 0;

for (let i = 0; i < copies。length; i++) {

copies[i]();

}

}

//這裡我們使用微任務使用非同步延遲包裝器。

//在2。5中,我們使用(宏)任務(與微任務結合使用)。

//但是,當狀態在重新繪製之前被更改時,它會有一些微妙的問題

//(例如#6813,輸出轉換)。

// 此外,在事件處理程式中使用(宏)任務會導致一些奇怪的行為

//不能規避(例如#7109、#7153、#7546、#7834、#8109)。

//因此,我們現在再次在任何地方使用微任務。

//這種權衡的一個主要缺點是存在一些場景

//微任務的優先順序過高,並在兩者之間被觸發

//順序事件(例如#4521、#6690,它們有解決方案)

//甚至在同一事件的冒泡(#6566)之間。

let timerFunc;

// nextTick行為利用了可以訪問的微任務佇列

//透過任何一個原生承諾。然後或MutationObserver。

// MutationObserver獲得了更廣泛的支援,但它受到了嚴重的干擾

// UIWebView在iOS >= 9。3。3時觸發的觸控事件處理程式。它

//觸發幾次後完全停止工作…所以,如果本地

// Promise可用,我們將使用:

if (typeofPromise !== “undefined” && isNative(Promise)) {

const p = Promise。resolve();

timerFunc = () => {

p。then(flushCallbacks);

//在有問題的UIWebViews中,承諾。然後不完全打破,但是

//它可能陷入一種奇怪的狀態,即回撥被推入

// 但是佇列不會被重新整理,直到瀏覽器重新整理

//需要做一些其他的工作,例如處理定時器。因此,我們可以

//透過新增空計時器來“強制”重新整理微任務佇列。

if (isIOS) setTimeout(noop);

};

isUsingMicroTask = true;

} elseif (

!isIE &&

typeof MutationObserver !== “undefined” &&

(isNative(MutationObserver) ||

// PhantomJS and iOS 7。x

MutationObserver。toString() === “[object MutationObserverConstructor]”)

) {

//在原生 Promise 不可用的情況下使用MutationObserver,

//例如PhantomJS, iOS7, android4。4

// (#6466 MutationObserver在IE11中不可靠)

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;

} elseif (typeof setImmediate !== “undefined” && isNative(setImmediate)) {

//退回到setimmediation。

//技術上它利用了(宏)任務佇列,

//但它仍然是比setTimeout更好的選擇。

timerFunc = () => {

setImmediate(flushCallbacks);

};

} else {

// Fallback to setTimeout。

timerFunc = () => {

setTimeout(flushCallbacks, 0);

};

}

exportfunctionnextTick(cb?: Function, ctx?: Object) {

let _resolve;

// 入佇列

callbacks。push(() => {

if (cb) {

try {

cb。call(ctx);

} catch (e) {

handleError(e, ctx, “nextTick”);

}

} elseif (_resolve) {

_resolve(ctx);

}

});

if (!pending) {

pending = true;

timerFunc();

}

// 這是當 nextTick 不傳 cb 引數的時候,提供一個 Promise 化的呼叫

if (!cb && typeofPromise !== “undefined”) {

returnnewPromise((resolve) => {

_resolve = resolve;

});

}

}

小結

結合以上程式碼,總結如下:

回撥函式先入佇列,等待;

執行 timerFunc,Promise 支援則使用 Promise 微佇列形式,否則,再非 IE 情況下,若支援 MutationObserver,則使用 MutationObserver 同樣以 微佇列的形式,再不支援則使用 setImmediate,再不濟就使用 setTimeout;

執行 flushCallbacks,標記 pending 完成,然後先複製 callback,再清理 callback;

以上便是 vue 非同步佇列的一個實現,主要是優先以(promise/MutationObserver)微任務的形式去實現(其次才是(setImmediate、setTimeout)宏任務去實現),等待當前宏任務完成後,便執行當下所有的微任務

15。 子元件為什麼不能修改父元件傳遞的 props,如果修改了,vue 是如何監聽到並給出警告的?

initProps

這裡可以看一下 initProps 的實現邏輯,先看一下 props 的初始化流程:

functioninitProps(vm: Component, propsOptions: Object) {

const propsData = vm。$options。propsData || {};

const props = (vm。_props = {});

// cache prop keys so that future props updates can iterate using Array

// instead of dynamic object key enumeration。

const keys = (vm。$options。_propKeys = []);

const isRoot = !vm。$parent;

// root instance props should be converted

if (!isRoot) {

toggleObserving(false);

}

// props 屬性遍歷監聽

for (const key in propsOptions) {

keys。push(key);

const value = validateProp(key, propsOptions, propsData, vm);

/* istanbul ignore else */

if (process。env。NODE_ENV !== “production”) {

const hyphenatedKey = hyphenate(key);

if (

isReservedAttribute(hyphenatedKey) ||

config。isReservedAttr(hyphenatedKey)

) {

warn(

`“${hyphenatedKey}” is a reserved attribute and cannot be used as component prop。`,

vm

);

}

// props 資料繫結監聽

defineReactive(props, key, value, () => {

// 開發環境下會提示 warn

if (!isRoot && !isUpdatingChildComponent) {

warn(

`Avoid mutating a prop directly since the value will be ` +

`overwritten whenever the parent component re-renders。 ` +

`Instead, use a data or computed property based on the prop’s ` +

`value。 Prop being mutated: “${key}”`,

vm

);

}

});

} else {

// props 資料繫結監聽

defineReactive(props, key, value);

}

// static props are already proxied on the component‘s prototype

// during Vue。extend()。 We only need to proxy props defined at

// instantiation here。

if (!(key in vm)) {

proxy(vm, `_props`, key);

}

}

toggleObserving(true);

}

分析程式碼發現 props 單純做了資料淺繫結監聽,提示是在開發環境中做的校驗

小結

如上可知,props 初始化時對 props 屬性遍歷 defineReactive(props, key, value) 做了資料淺繫結監聽:

如果 value 為基本屬性(開發環境中),當更改 props 的時候則會 warn,但是這裡修改並不會改變父級的屬性,因為這裡的基礎資料是值複製;

如果 value 為物件或者陣列時,則更改父級物件值的時候也會 warn(但是不會影響父級 props),但是當修改其 屬性的時候則不會 warn,並且會直接修改父級的 props 對應屬性值;

注意這裡父級的 props 在元件建立時是資料複製過來的;

繼續分析,如果 vue 允許子元件修改父元件的情況下,這裡 props 將需要在父元件以及子元件中都進行資料繫結,這樣講導致多次監聽,而且不利於維護,並且可想而知,容易邏輯交叉,不容易維護;

所以 vue 在父子元件的資料中是以單向資料流來做的處理,這樣父子的業務資料邏輯不易交叉,並且易於定位問題源頭;

16。 父元件和子元件生命週期鉤子的順序?

渲染過程

從父到子,再由子到父;(由外到內再由內到外)

父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted

子元件更新過程

父 beforeUpdate->子 beforeUpdate->子 updated->父 updated

父元件更新過程

父 beforeUpdate->父 updated

銷燬過程

父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

展望

謝謝大家的閱讀,希望對大家有所幫助,後續打算:

解讀 vuex 原始碼常考題;

解讀 react-router 原始碼常考題;

實現自己的 vue/vuex/react-router 系列;

歡迎關注,敬請期待。