Vue2 源碼從零詳解系列文章, 還沒有看過的同學可能需要看一下之前的,vue.windliang.wang/
場景
import { observe } from "./reactive";
import { initComputed } from "./state";
import Watcher from "./watcher";
const options = {
data: {
firstName: "wind",
secondName: "liang",
title: "标題",
},
computed: {
name() {
console.log("name我執行啦!");
return this.firstName + this.secondName;
},
name2: {
get() {
console.log("name2我執行啦!");
return "name2" + this.firstName + this.secondName;
},
set() {
console.log("name2的我執行啦!set執行啦!");
},
},
},
};
observe(options.data);
initComputed(options.data, options.computed);
複制
Vue
中肯定少不了
computed
屬性的使用,
computed
的最大的作用就是惰性求值,同時它也是響應式資料。
這篇文章主要就來實作上邊的
initComputed
方法。
實作思路
主要做三件事情
- 惰性的響應式資料
- 處理
的值computed
-
屬性的響應式computed
惰性的響應式資料
回想一下我們之前的
Watcher
。
如果我們調用
new Watcher
const options = {
data: {
firstName: "wind",
secondName: "liang",
},
computed: {
name() {
console.log("name我執行啦!");
return this.firstName + this.secondName;
},
},
};
observe(options.data);
new Watcher(options.data, options.computed.name);
複制
在
Watcher
内部我們會立即執行一次
options.computed.name
并将傳回的值儲存起來。
export default class Watcher {
constructor(data, expOrFn, cb, options) {
this.data = data;
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
...
this.value = this.get();
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
pushTarget(this); // 儲存包裝了目前正在執行的函數的 Watcher
let value;
try {
value = this.getter.call(this.data, this.data);
} catch (e) {
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;
}
複制
為了實作惰性求值,我們可以增加一個
lazy
屬性,構造函數裡我們不去直接執行。
同時增加一個
dirty
屬性,
dirty
為
true
表示
Watcher
依賴的屬性發生了變化,需要重新求值。
dirty
為
false
表示
Watcher
依賴的屬性沒有發生變化,無需重新求值。
export default class Watcher {
constructor(data, expOrFn, cb, options) {
this.data = data;
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
this.depIds = new Set(); // 擁有 has 函數可以判斷是否存在某個 id
this.deps = [];
this.newDeps = []; // 記錄新一次的依賴
this.newDepIds = new Set();
this.id = ++uid; // uid for batching
this.cb = cb;
// options
if (options) {
this.deep = !!options.deep;
this.sync = !!options.sync;
/******新增 *************************/
this.lazy = !!options.lazy;
/************************************/
}
/******新增 *************************/
this.dirty = this.lazy;
this.value = this.lazy ? undefined : this.get();
/************************************/
}
複制
我們把
computed
的函數傳給
Watcher
的時候可以增加一個
lazy
屬性,
cb
參數是為了
watch
使用,這裡就傳一個空函數。
const options = {
data: {
firstName: "wind",
secondName: "liang",
},
computed: {
name() {
console.log("name我執行啦!");
return this.firstName + this.secondName;
},
},
};
observe(options.data);
const noop = () => {}
const watcher = new Watcher(options.data, options.computed.name, noop, {
lazy: true,
});
console.log(watcher.value);
複制
此時
wacher.value
就是
undefined
了,沒有拿到值。
我們還需要在
Wacher
類中提供一個
evaluate
方法,供使用者手動執行
Watcher
所儲存的
computed
函數。
export default class Watcher {
constructor(data, expOrFn, cb, options) {
this.data = data;
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
...
// options
if (options) {
this.deep = !!options.deep;
this.sync = !!options.sync;
this.lazy = !!options.lazy;
}
this.dirty = this.lazy;
this.value = this.lazy ? undefined : this.get();
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
pushTarget(this); // 儲存包裝了目前正在執行的函數的 Watcher
let value;
try {
value = this.getter.call(this.data, this.data);
} catch (e) {
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;
}
...
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
/******新增 *************************/
evaluate() {
this.value = this.get();
this.dirty = false; // dirty 為 false 表示目前值已經是最新
}
/**********************************/
}
複制
輸出
value
之前我們可以先執行一次
evaluate
。
const options = {
data: {
firstName: "wind",
secondName: "liang",
},
computed: {
name() {
console.log("name我執行啦!");
return this.firstName + this.secondName;
},
},
};
observe(options.data);
const noop = () => {};
const watcher = new Watcher(options.data, options.computed.name, noop, {
lazy: true,
});
console.log(watcher.value);
watcher.evaluate();
console.log(watcher.value);
複制
輸出結果如下:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAjM2EzLcd3LcJzLcJzdllmVldWYtl2Pn5GcuETY1kjN4ITOwE2N5UTZihjZxgDMkFWZ4AjZxUzYwMzNvwlM3kTN3MjMtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.png)
image-20220420084245239
我們解決了初始時候的惰性,但如果去修改
firstName
的值,
Watcher
還會立即執行,如下所示:
const options = {
data: {
firstName: "wind",
secondName: "liang",
},
computed: {
name() {
console.log("name我執行啦!");
return this.firstName + this.secondName;
},
},
};
observe(options.data);
const noop = () => {};
const watcher = new Watcher(options.data, options.computed.name, noop, {
lazy: true,
});
console.log(watcher.value);
watcher.evaluate();
console.log(watcher.value);
console.log("修改 firstName 的值");
options.data.firstName = "wind2";
setTimeout(() => {
console.log(watcher.value);
}); // 為什麼用 setTimeout 參考 https://vue.windliang.wang/posts/Vue2%E5%89%A5%E4%B8%9D%E6%8A%BD%E8%8C%A7-%E5%93%8D%E5%BA%94%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B9%8BnextTick.html
複制
輸出如下:
image-20220420085931210
是以,當觸發
Watcher
執行的時候,我們應該隻将
dirty
置為
true
而不去執行。
export default class Watcher {
constructor(data, expOrFn, cb, options) {
this.data = data;
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
...
this.dirty = this.lazy;
this.value = this.lazy ? undefined : this.get();
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
pushTarget(this); // 儲存包裝了目前正在執行的函數的 Watcher
let value;
try {
value = this.getter.call(this.data, this.data);
} catch (e) {
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;
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
/******新增 *************************/
if (this.lazy) {
this.dirty = true;
/************************************/
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
複制
這樣,在使用
name
值前,我們先判斷下
dirty
,如果
dirty
為
true
,先手動調用
evaluate
方法進行求值,然後再使用。
const options = {
data: {
firstName: "wind",
secondName: "liang",
},
computed: {
name() {
console.log("name我執行啦!");
return this.firstName + this.secondName;
},
},
};
observe(options.data);
const noop = () => {};
const watcher = new Watcher(options.data, options.computed.name, noop, {
lazy: true,
});
console.log(watcher.value);
watcher.evaluate();
console.log(watcher.value);
console.log("修改 firstName 的值");
options.data.firstName = "wind2";
setTimeout(() => {
if (watcher.dirty) {
watcher.evaluate();
}
console.log(watcher.value);
}); // 為什麼用 setTimeout 參考 https://vue.windliang.wang/posts/Vue2%E5%89%A5%E4%B8%9D%E6%8A%BD%E8%8C%A7-%E5%93%8D%E5%BA%94%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B9%8BnextTick.html
複制
處理 computed
的值
computed
接下來就是
initComputed
的邏輯,主要就是結合上邊所講的,将傳進來的
computed
轉為惰性的響應式資料。
export function noop(a, b, c) {}
const computedWatcherOptions = { lazy: true };
// computed properties are just getters during SSR
export function initComputed(data, computed) {
const watchers = (data._computedWatchers = Object.create(null)); // 儲存目前所有的 watcher,并且挂在 data 上供後邊使用
for (const key in computed) {
const userDef = computed[key];
const getter = typeof userDef === "function" ? userDef : userDef.get; // 如果是對象就取 get 的值
// create internal watcher for the computed property.
watchers[key] = new Watcher(
data,
getter || noop,
noop,
computedWatcherOptions
);
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
defineComputed(data, key, userDef);
}
}
複制
上邊的
defineComputed
主要就是将
computed
函數定義為
data
的屬性,這樣就可以像正常屬性一樣去使用
computed
。
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};
export function defineComputed(target, key, userDef) {
// 初始化 get 和 set
if (typeof userDef === "function") {
sharedPropertyDefinition.get = createComputedGetter(key);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? createComputedGetter(key)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
// 将目前屬性挂到 data 上
Object.defineProperty(target, key, sharedPropertyDefinition);
}
複制
其中
createComputedGetter
中去完成我們手動更新
Watcher
值的邏輯。
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]; // 拿到相應的 watcher
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
return watcher.value;
}
};
}
複制
讓我們測試一下
initComputed
。
import { observe } from "./reactive";
import { initComputed } from "./state";
import Watcher from "./watcher";
const options = {
data: {
firstName: "wind",
secondName: "liang",
title: "标題",
},
computed: {
name() {
console.log("name我執行啦!");
return this.firstName + this.secondName;
},
name2: {
get() {
console.log("name2我執行啦!");
return "name2" + this.firstName + this.secondName;
},
set() {
console.log("name2的我執行啦!set執行啦!");
},
},
},
};
observe(options.data);
initComputed(options.data, options.computed);
const updateComponent = () => {
console.log("updateComponent執行啦!");
console.log("我使用了 name2", options.data.name2);
document.getElementById("root").innerText =
options.data.name + options.data.title;
};
new Watcher(options.data, updateComponent);
複制
分析一下
updateComponent
函數執行的邏輯:
-
console.log("我使用了 name2", options.data.name2);
:
讀取了
的值,是以會觸發我們定義好的name2
,觸發get
中computed
函數的執行。name2.get
-
document.getElementById("root").innerText = options.data.name + options.data.title;
:
讀取了
的值,會觸發name
中computed
name
函數的執行。
讀取了
的值,title
會收集目前data.title
,未來Watcher
改變的時候,會觸發data.title
函數的執行。updateComponent
下邊是輸出結果:
image-20220420091914961
此時我們如果修改
title
的值,
updateComponent
函數會重新執行,但因為
name
和
name2
依賴的屬性值并沒有發生變化,是以他們相應的函數就不會執行了。
image-20220420092134386
computed 屬性的響應式
思考下邊的場景:
import { observe } from "./reactive";
import { initComputed } from "./state";
import Watcher from "./watcher";
const options = {
data: {
firstName: "wind",
secondName: "liang",
title: "标題",
},
computed: {
name() {
console.log("name我執行啦!");
return this.firstName + this.secondName;
},
},
};
observe(options.data);
initComputed(options.data, options.computed);
const updateComponent = () => {
console.log("updateComponent執行啦!");
document.getElementById("root").innerText =
options.data.name + options.data.title;
};
new Watcher(options.data, updateComponent);
setTimeout(() => {
options.data.firstName = "wind2";
}, 1000); // 為什麼用 setTimeout 參考 https://vue.windliang.wang/posts/Vue2%E5%89%A5%E4%B8%9D%E6%8A%BD%E8%8C%A7-%E5%93%8D%E5%BA%94%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B9%8BnextTick.html
複制
當我們修改了
firstName
的值,毫無疑問,
name
和
name2
的值肯定也會變化,使用了
name
和
name2
的函數
updateComponent
此時也應該執行。
但事實上隻在第一次的時候執行了,并沒有二次觸發。
image-20220420093442962
讓我們看一下目前的收集依賴圖:
image-20220420094032444
title
屬性收集了包含
updateComponent
函數的
Watcher
,
firstName
和
secondName
屬性都收集了包含
computed.name()
函數的
Watcher
。
name
屬性是我們後邊定義的,沒有
Dep
類,什麼都沒有收集。
我們現在想要實作改變
firstName
或者
secondName
值的時候,觸發
updateComponent
函數的執行。
我們隻需要讀取
name
的時候,讓
firstName
和
secondName
收集一下目前的
Watcher
,因為讀取
name
的值是在
updateComponent
中執行的,是以目前
Watcher
就是包含了
updateComponent
函數的
Watcher
。
怎麼讓
firstName
和
secondName
收集目前的
Watcher
呢?
在
name
的
get
中,我們能拿到
computed.name()
對應的
Watcher
,而在
Watcher
執行個體中,我們把它所有的依賴都儲存起來了,也就是這裡的
firstName
和
secondName
,如下圖:
image-20220420100828571
是以我們隻需在
Watcher
中提供一個
depend
方法, 周遊所有的依賴收集目前
Watcher
。
export default class Watcher {
constructor(data, expOrFn, cb, options) {
this.data = data;
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
this.depIds = new Set(); // 擁有 has 函數可以判斷是否存在某個 id
this.deps = [];
this.newDeps = []; // 記錄新一次的依賴
this.newDepIds = new Set();
...
this.dirty = this.lazy;
this.value = this.lazy ? undefined : this.get();
}
/**
* Add a dependency to this directive.
*/
addDep(dep) {
const id = dep.id;
// 新的依賴已經存在的話,同樣不需要繼續儲存
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
}
/**
* 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();
}
}
/************************************/
}
複制
然後在之前定義的計算屬性的
get
中觸發收集依賴即可。
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
}
};
}
複制
回到開頭的場景:
import { observe } from "./reactive";
import { initComputed } from "./state";
import Watcher from "./watcher";
const options = {
data: {
firstName: "wind",
secondName: "liang",
title: "标題",
},
computed: {
name() {
console.log("name我執行啦!");
return this.firstName + this.secondName;
},
},
};
observe(options.data);
initComputed(options.data, options.computed);
const updateComponent = () => {
console.log("updateComponent執行啦!");
document.getElementById("root").innerText =
options.data.name + options.data.title;
};
new Watcher(options.data, updateComponent);
setTimeout(() => {
options.data.firstName = "wind2";
}, 1000);
複制
此時修改
firstName
的值就會觸發
updateComponent
函數的執行了。
image-20220420101247642
此時的依賴圖如下:
image-20220420101629653
總
computed
對應的函數作為了一個
Watcher
,使用計算屬性的函數也是一個
Watcher
,
computed
函數中使用的屬性會将這兩個
Watcher
都收集上。
此外
Watcher
增加了
lazy
屬性,如果
lazy
為
true
,當觸發
Watcher
執行的時候不執行内部的函數,将函數的執行讓渡給外部管理。
需要注意的一點是,我們是将
computed
所有的
watcher
都挂在了
data
上,實際上
Vue
中是挂在目前的元件執行個體上的。