本文經過 16 道 vue 常考題來解讀 vue 部分實現原理,但願讓你們更深層次的理解 vue;javascript
近期本身也實踐了幾個編碼常考題目,但願可以幫助你們加深理解:html
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
是如何監聽到並給出警告的?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";
function Vue(options) {
// 構造函數
if (process.env.NODE_ENV !== "production" && !(this instanceof 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);
export default Vue;
複製代碼
_init
深刻往下,在
src/core/instance/init.js
中找到this._init
的聲明vue
// 這裏的混入方法入參 Vue
export function initMixin(Vue: Class<Component>) {
// 增長原型鏈 _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)
具體作了以下事情:java
- 執行構造函數;
- 上下文轉移到 vm;
- 若是
options._isComponent
爲 true,則初始化內部組件實例;不然合併配置參數,並掛載到vm.$options
上面;- 初始化生命週期函數、初始化事件相關、初始化渲染相關;
- 執行
beforeCreate
生命週期函數;- 在初始化
state/props
以前初始化注入inject
;- 初始化
state/props
的數據雙向綁定;- 在初始化
state/props
以後初始化provide
;- 執行
created
生命週期函數;- 掛載到
dom
元素其實
vue
還在生產環境中記錄了初始化的時間,用於性能分析;node
Vue.use
作了什麼?直接查看
src/core/global-api/use.js
, 以下react
import { toArray } from "../util/index";
export function initUse(Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// 插件緩存數組
const installedPlugins =
this._installedPlugins || (this._installedPlugins = []);
// 已註冊則跳出
if (installedPlugins.indexOf(plugin) > -1) {
return this;
}
// 附加參數處理,截取第1個參數以後的參數
const args = toArray(arguments, 1);
// 第一個參數塞入 this 上下文
args.unshift(this);
// 執行 plugin 這裏遵循定義規則
if (typeof plugin.install === "function") {
// 插件暴露 install 方法
plugin.install.apply(plugin, args);
} else if (typeof plugin === "function") {
// 插件自己若沒有 install 方法,則直接執行
plugin.apply(null, args);
}
// 添加到緩存數組中
installedPlugins.push(plugin);
return this;
};
}
複製代碼
綜上,能夠總結
Vue.use
作了以下事情:android
- 檢查插件是否註冊,若已註冊,則直接跳出;
- 處理入參,將第一個參數以後的參數歸集,並在首部塞入 this 上下文;
- 執行註冊方法,調用定義好的 install 方法,傳入處理的參數,若沒有 install 方法而且插件自己爲 function 則直接進行註冊;
vue
的響應式?上代碼,直接查看
src/core/observer/index.js
,classObserver
,這個方法使得對象/數組可響應git
export class Observer {
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<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}
複製代碼
上代碼,直接查看
src/core/observer/index.js
,核心方法defineReactive
,這個方法使得對象可響應,給對象動態添加 getter 和 setteres6
// 使對象中的某個屬性可響應
export function defineReactive( 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: function reactiveGetter() {
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: function reactiveSetter(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
的代碼,它依賴收集的核心,在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. */
export default class Dep {
// 靜態屬性,全局惟一 Watcher
// 這裏比較巧妙,由於在同一時間只能有一個全局的 Watcher 被計算
static target: ?Watcher;
id: number;
// watcher 數組
subs: Array<Watcher>;
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 = [];
export function pushTarget(target: ?Watcher) {
targetStack.push(target);
Dep.target = target;
}
export function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
複製代碼
Dep 是對 Watcher 的一種管理,下面咱們來看一下 Watcher, 在
src/core/observer/watcher.js
中
let uid = 0;
/** * 一個 Watcher 分析一個表達式,收集依賴項, 並在表達式值更改時觸發回調。 * 用於 $watch() api 和指令 */
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
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 = new Set();
this.newDepIds = new Set();
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;
} else if (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
來控制其對視圖層的監聽
vue3
爲什麼用 proxy
替代了 Object.defineProperty
?截取上面 Watcher 中部分代碼
if (this.deep) {
// 這裏其實遞歸遍歷屬性用做依賴收集
traverse(value);
}
複製代碼
再查看
src/core/observer/traverse.js
中traverse
的實現,以下:
const seenObjects = new Set();
// 遞歸遍歷對象,將全部屬性轉換爲 getter
// 使每一個對象內嵌套屬性做爲依賴收集項
export function traverse(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
這些數據結構不⽀持
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,這就構成了一個雙向循環綁定了;
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 這個對象,在
src/core/observer/array.js
中
import { def } from "../util/index";
const arrayProto = Array.prototype;
// 複製數組原型鏈,並建立一個空對象
// 這裏使用 Object.create 是爲了避免污染 Array 的原型
export const 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, function mutator(...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 使對象可響應,在
src/core/util/lang.js
export function def(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
去操做才行;
computed
如何實現?這個方法用於初始化
options.computed
對象, 這裏仍是上源碼,在src/core/instance/state.js
中,這個方法是在initState
中調用的
const computedWatcherOptions = { lazy: true };
function initComputed(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);
} else if (process.env.NODE_ENV !== "production") {
if (key in vm.$data) {
// 判斷 key 是否是在 data
warn(`The computed property "${key}" is already defined in data.`, vm);
} else if (vm.$options.props && key in vm.$options.props) {
// 判斷 key 是否是在 props 中
warn(
`The computed property "${key}" is already defined as a prop.`,
vm
);
}
}
}
}
複製代碼
這個方法用做定義 computed 中的屬性,繼續看代碼:
export function defineComputed( 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
function createComputedGetter(key) {
return function computedGetter() {
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;
}
};
}
function createGetterInvoker(fn) {
return function computedGetter() {
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
的觸發邏輯;
computed
和 watch
的區別在哪裏?initWatch
這裏仍是老樣子,上代碼,在
src/core/instance/state.js
中:
function initWatch(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 監聽
function createWatcher( 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}"`)
}
}
// 用做銷燬
return function unwatchFn () {
// 移除 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
來觀察這個數據變化;
這個題目跟上題相似,區別以下:
- 普通屬性都是基於
getter
和setter
的正常取值和更新;computed
屬性是依賴於內部引用普通屬性的setter
變動從而標記watcher
中dirty
標記爲true
,此時纔會觸發更新;
v-if/v-show/v-html
的原理是什麼,它是如何封裝的?先來看一下
v-if
的實現,首先 vue 編譯 template 模板的時候會先生成 ast 靜態語法樹,而後進行標記靜態節點,再以後生成對應的 render 函數,這裏就直接看下genIf
的代碼,在src/compiler/codegen/index.js
中:
export function genIf( el: any, state: CodegenState, altGen?: Function, altEmpty?: string ): string {
el.ifProcessed = true; // 標記避免遞歸,標記已經處理過
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty);
}
function genIfConditions( 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)
function genTernaryExp(el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state);
}
}
複製代碼
v-if 在 template 生成 ast 以後 genIf 返回三元表達式,在渲染的時候僅渲染表達式生效部分;
這裏截取 v-show 指令的實現邏輯,在
src/platforms/web/runtime/directives/show.js
中:
export default {
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 比較簡單,最終操做的是
innerHTML
,咱們仍是看代碼,在src/platforms/compiler/directives/html.js
中:
import { addProp } from "compiler/helpers";
export default function html(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 到當前標籤;
v-for
給每一個元素綁定事件須要事件代理嗎?首先,咱們先來看一下
v-for
的實現,同上面v-if
,在模板渲染過程當中由genFor
處理,在src/compiler/codegen/index.js
中:
export function genFor( 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 時給每項元素綁定事件須要用事件代理嗎?爲何?
key
的做⽤嗎?
key
可預想的是vue
拿來給vnode
做惟一標識的,下面咱們先來看下 key 到底被拿來作啥事,在src/core/vdom/patch.js
中:
function updateChildren( 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 <transition-group>
// 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
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (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];
} else if (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
);
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
複製代碼
這段代碼是 vue diff 算法的核心代碼了,用做比較同級節點是否相同,批量更新的,可謂是性能核心了,以上能夠看下
sameVnode
比較節點被用了屢次,下面咱們來看下是怎麼比較兩個相同節點的
function sameVnode(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 了;
vue
中全部帶$
的方法?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] )
vm.$mount()
手動地掛載一個未掛載的實例。vm.$forceUpdate()
: 迫使 Vue 實例從新渲染。注意它僅僅影響實例自己和插入插槽內容的子組件,而不是全部子組件。
vm.$nextTick( [callback] )
: 將回調延遲到下次 DOM 更新循環以後執行。在修改數據以後當即使用它,而後等待 DOM 更新。它跟全局方法 Vue.nextTick 同樣,不一樣的是回調的 this 自動綁定到調用它的實例上。
vm.$destroy()
: 徹底銷燬一個實例。清理它與其它實例的鏈接,解綁它的所有指令及事件監聽器。
nextTick
嗎?直接上代碼,在
src/core/util/next-tick.js
中:
import { noop } from "shared/util";
import { handleError } from "./error";
import { isIE, isIOS, isNative } from "./env";
export let isUsingMicroTask = false;
const callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
//這裏咱們使用微任務使用異步延遲包裝器。
//在2.5中,咱們使用(宏)任務(與微任務結合使用)。
//可是,當狀態在從新繪製以前被更改時,它會有一些微妙的問題
//(例如#6813,輸出轉換)。
// 此外,在事件處理程序中使用(宏)任務會致使一些奇怪的行爲
//不能規避(例如#710九、#715三、#754六、#783四、#8109)。
//所以,咱們如今再次在任何地方使用微任務。
//這種權衡的一個主要缺點是存在一些場景
//微任務的優先級太高,並在二者之間被觸發
//順序事件(例如#452一、#6690,它們有解決方案)
//甚至在同一事件的冒泡(#6566)之間。
let timerFunc;
// nextTick行爲利用了能夠訪問的微任務隊列
//經過任何一個原生承諾。而後或MutationObserver。
// MutationObserver得到了更普遍的支持,但它受到了嚴重的干擾
// UIWebView在iOS >= 9.3.3時觸發的觸摸事件處理程序。它
//觸發幾回後徹底中止工做…因此,若是本地
// Promise可用,咱們將使用:
if (typeof Promise !== "undefined" && isNative(Promise)) {
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
//在有問題的UIWebViews中,承諾。而後不徹底打破,可是
//它可能陷入一種奇怪的狀態,即回調被推入
// 可是隊列不會被刷新,直到瀏覽器刷新
//須要作一些其餘的工做,例如處理定時器。所以,咱們能夠
//經過添加空計時器來「強制」刷新微任務隊列。
if (isIOS) setTimeout(noop);
};
isUsingMicroTask = true;
} else if (
!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;
} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
//退回到setimmediation。
//技術上它利用了(宏)任務隊列,
//但它仍然是比setTimeout更好的選擇。
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
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();
}
// 這是當 nextTick 不傳 cb 參數的時候,提供一個 Promise 化的調用
if (!cb && typeof Promise !== "undefined") {
return new Promise((resolve) => {
_resolve = resolve;
});
}
}
複製代碼
結合以上代碼,總結以下:
- 回調函數先入隊列,等待;
- 執行 timerFunc,Promise 支持則使用 Promise 微隊列形式,不然,再非 IE 狀況下,若支持 MutationObserver,則使用 MutationObserver 一樣以 微隊列的形式,再不支持則使用 setImmediate,再不濟就使用 setTimeout;
- 執行 flushCallbacks,標記 pending 完成,而後先複製 callback,再清理 callback;
以上即是 vue 異步隊列的一個實現,主要是優先以(promise/MutationObserver)微任務的形式去實現(其次纔是(setImmediate、setTimeout)宏任務去實現),等待當前宏任務完成後,便執行當下全部的微任務
props
,若是修改了,vue
是如何監聽到並給出警告的?這裏能夠看一下
initProps
的實現邏輯,先看一下 props 的初始化流程:
function initProps(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 在父子組件的數據中是以單向數據流來作的處理,這樣父子的業務數據邏輯不易交叉,而且易於定位問題源頭;
從父到子,再由子到父;(由外到內再由內到外)
感謝閱讀,但願對你們有所幫助,後續打算:
- 解讀 vuex 源碼常考題;
- 解讀 react-router 源碼常考題;
- 實現本身的 vue/vuex/react-router 系列;
卑微求個贊,謝謝各位大佬。