為什麼不能建立鉤子
「來源: |前端迷 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?
其實這個問題需要承接上述第三題,再結合下圖
響應式原理
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 系列;
歡迎關注,敬請期待。