前言:好久沒寫博客了,今年確實有點小忙,學習跟工作都很充實,身邊也有小夥伴也經常說:技術是學不完的,能不能專一搞點自己喜歡的東西呢?學完新技術然後過幾年又沒了,豈不是白學了呢? 哈哈~ 說的確定有一點道理啊,但是沒辦法啊,新技術還是得去了解啊,至少要去看看別人的文檔跟設計思想吧,也不怕小夥伴笑哈,最近還在補js跟css基礎知識,學無止境,加油吧~騷年!
前面有寫過兩篇vue的源碼的文章,有興趣的童鞋可以去看看,Vue源碼解析(一)、Vue源碼解析二(render&mount)接觸vue也有蠻長一段時間了,不得不說,vue的設計思想跟整體架構還是很牛逼的,所以帶着崇拜跟學習的心態我們一起看看vue的computed跟watch屬性。
一、觀察者模式
在說computed跟watch之前,我們先來認識一種設計模式 “觀察者模式”,觀察者模式分爲“Subject主題”、"Observer觀察者 ",在此種模式中,一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主動發出通知。這通常透過呼叫各觀察者所提供的方法來實現。此種模式通常被用來實現事件處理系統。
字面意思比較抽象,我們直接上代碼,還記得在大學的時候老師就講過“觀察者模式”,雖然舉的例子不很貼切,但是還算是比較好的詮釋,老師跟我們講了一個 “小美與她的兩個男盆友” 的故事。
第一步:定義subject
export default class Subject {
constructor(name,desc) {
this.name = name;
this.desc = desc;
}
add() {
throw new Error("this method should be overrided!!");
}
notify() {
throw new Error("this method should be overrided!!");
}
cry() {
throw new Error("this method should be overrided!!");
}
}
第二步:定義Observer
export default class Observer {
constructor(name,desc) {
this.name = name;
this.desc = desc;
}
say() {
throw new Error("this method should be overrided!!");
}
}
第三步:定義具體的subject對象(girl女孩)
import ISubject from './Subject';
export default class Girl extends ISubject {
constructor(name, desc) {
super(name, desc);
this.boys = [];
}
add(boy) {
this.boys.push(boy);
}
notify() {
this.boys.forEach((boy) => {
boy.say();
});
}
cry() {
console.log(this.name + " : " + this.desc);
this.notify();
}
}
第四步:定義具體的observer對象(boy男孩)
import Observer from './Observer';
export default class Boy extends Observer {
constructor(name, desc) {
super(name, desc);
}
say() {
console.log(this.name + ": " + this.desc);
}
}
測試:
import Girl from './Girl';
import Boy from './Boy';
const girl1 = new Girl("小美", "今天心情很不好,哄不好的那種~");
const boy1 = new Boy("小黑", "親愛的,你購物車需要清一下了!");
const boy2 = new Boy("小白", "Darling 逛街去!");
girl1.add(boy1);
girl1.add(boy2);
girl1.cry();
結果:
小美心情不好就給兩個男盆友打電話,兩個男盆友都聽到了然後各種安慰。
好啦! 故事也完了,我們進入今天的主題“vue的computed跟watch”
二、computed&watch
做過vue的小夥伴應該都知道computed的用法,沒用過的可以去看看vue的官網,我就不囉嗦了,先定義一下我們最終需要實現的效果:
import MyVue from './computed/MyVue';
const myVue = new MyVue({
data() {
return {
firstName: "Yasin",
lastName: "Yin"
};
},
computed: {
fullName() {
const fullName = this.firstName + "-" + this.lastName;
console.log("fullName", fullName);
return fullName;
}
},
watch: {
firstName(oldValue, newValue) {
console.log("firstName值改變了 oldValue=> " + oldValue + " newValue=> " + newValue);
}
}
});
setTimeout(() => {
myVue.firstName = "Tom";
}, 100);
小夥伴應該很熟悉這個代碼吧,沒錯! 這也是官網對computed的例子,現在我們就簡單的來實現一下,小夥伴跟緊了哦~~
第一步:定義一個Dep
Dep裏面應該有什麼內容呢?小夥伴猜應該能猜到,我們demo是這樣定義的:
fullName() {
const fullName = this.firstName + "-" + this.lastName;
console.log("fullName", fullName);
return fullName;
}
所以當firstName跟lastName變化的時候,我們的fullName方法需要執行,所以這裏的Dep就相當於我們上面所說的“女孩”,只不過這裏面有兩個女孩~~ (哈哈,現在變成男孩腳踏兩隻船了)
Dep.js:
export default class Dep {
constructor() {
this.subs = [];
}
/**
* 添加觀察者
* @param sub
*/
addSub(sub) {
if (!this.subs.includes(sub)) {
this.subs.push(sub);
}
}
/**
* 與觀察者建立關係
*/
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
/**
* 通知所有的觀察者
*/
notify() {
for (let i = 0, l = this.subs.length; i < l; i++) {
this.subs[i].update();
}
}
}
//當前待申請的觀察者
Dep.target = null;
//設置當前觀察者(成爲觀察者是需要申請的))
export function pushTarget(target) {
Dep.target = target;
}
第二步:定義一個Watcher對象
Watcher相當於我們前面說的男孩的概念,會有自己的名字信息,跟具體的表達方式等等。
/**
* @author YASIN
* @version [React-Native V01, 2019/10/11]
* @date 2019/10/11
* @description Watcher
*/
import Dep, {pushTarget} from './Dep';
export default class Watcher {
constructor(vm, expOrFn, cb, lazy = false) {
this.vm = vm; //當前vue實例
const watchers = vm._watchers = []; //vue實例中所有的觀察者
watchers.push(this);
this.cb = cb; //被觀察的事物發生變化時候的回調(男孩的say方法)
this.deps = []; //觀察的事物的集合(女孩)
this.getter = expOrFn; //獲取觀察者的信息(男孩的名字跟表達)
if (!lazy) { //是否立馬申請成爲觀察者(還說是等到女孩子哭的時候去撿漏)
this.value = this.get();
}
}
/**
* 申請成爲觀察者
*/
get() {
pushTarget(this);
let value;
const vm = this.vm;
value = this.getter.call(vm, vm);
return value;
}
/**
* 建立與被觀察對象的關係
*/
addDep(dep) {
if (!this.deps.includes(dep)) {
this.deps.push(dep);
dep.addSub(this);
}
}
/**
* 展示觀察者具體動作
*/
update() {
this.run();
}
/**
* 獲取觀察者的具體動作
*/
run() {
const value = this.get();
if (
value !== this.value) {
const oldValue = this.value;
this.value = value;
this.cb.call(this.vm, oldValue, value);
}
}
/**
* 列出自己所觀察的所有被觀察的事物.
*/
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
}
第三步:定義響應式可以觀察的對象Observer
/**
* @author YASIN
* @version [React-Native V01, 2019/10/11]
* @date 2019/10/11
* @description observer
*/
import Dep from './Dep';
/**
* Define a reactive property on an Object.
*/
export function defineReactive(obj, key, val) {
//創建一個可觀察的對象
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = val;
//是否有需要申請的觀察者
if (Dep.target) {
//建立與觀察者的聯繫
dep.depend();
}
return value;
},
set: function reactiveSetter(newVal) {
const value = val;
// eslint-disable-next-line no-self-compare
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
val = newVal;
//通知所有的觀察者
dep.notify();
}
});
}
export class Observer {
constructor(value) {
this.walk(value);
}
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]]);
}
}
}
第四步:封裝Vue類
爲了跟vue不衝突我們創建一個叫MyVue的類,然後創建構造方法:
export default class MyVue {
constructor(opts) {
if (opts.data) {
this.initData(opts.data);
}
if (opts.computed) {
this.initComputed(opts.computed);
}
if (opts.watch) {
this.initWatch(opts.watch);
}
}
/**
* 初始化data
* @param value
*/
initData(value) {
}
/**
* 初始化computed
* @param computed
*/
initComputed(computed) {
}
/**
* 初始化watch
* @param watch
*/
initWatch(watch) {
}
}
我們在vue中都知道,當我們在data中聲明對象的時候,vue會把data對象的所有屬性值都映射到vm上,所以在vue中我們可以直接使用this.xxxx就可以訪問data的屬性:
/**
* 初始化data
* @param value
*/
initData(value) {
this._data = value(); //獲取data的值付給vm的_data
const keys = Object.keys(this._data);
let i = keys.length;
while (i--) { //遍歷所有的data屬性
const key = keys[i];
//代理到vm上,
this.proxy(this, `_data`, key);
}
new Observer(this._data);
}
/**
* 代理
* @param target vm
* @param sourceKey 目標值
* @param key 屬性值
*/
proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
//定義響應式屬性
Object.defineProperty(target, key, sharedPropertyDefinition);
}
這樣我們就可以把data函數中返回的對象映射到vm的實例上了~
然後我們初始化computed:
/**
* 初始化computed
* @param computed
*/
initComputed(computed) {
const watchers = this._computedWatchers = Object.create(null);
//遍歷所有的computed
for (const key in computed) {
//對每個computed創建一個監聽者
watchers[key] = new Watcher(
this,
computed[key],
noop,
false
);
//把computed的屬性也映射到vm實例上
if (!(key in this)) {
this.defineComputed(this, key);
}
}
}
/**
* 定義computed
* @param target vm
* @param key 屬性
*/
defineComputed(target, key) {
sharedPropertyDefinition.set = noop;
sharedPropertyDefinition.get = this.createComputedGetter(key);
Object.defineProperty(target, key, sharedPropertyDefinition);
}
/**
* 當使用computed的內容的時候,我們需要知道有誰需要成爲觀察者(誰關注了我),
* 然後建立與觀察者的聯繫
* 把computed的"所有女朋友"介紹給"關注computed"的人
* @param key 男孩名字
* @returns {computedGetter}
*/
createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
//當使用computed的內容的時候,我們需要知道有誰需要成爲觀察者(誰關注了我)
if (Dep.target) {
//然後建立與觀察者的聯繫
//把computed的"所有女朋友"介紹給"關注computed"的人
watcher.depend();
}
//返回自己的值
return watcher.value;
}
};
}
這裏強調一下“當有人也觀察了computed”的時候,computed需要把自己所觀察的事物都介紹給自己的粉絲(另外一個computed)。
最後實現我們的watch:
watch是最簡單的部分了,也就是創建一個觀察者,然後關注某個人就可以了:
/**
* 初始化watch
* @param watch
*/
initWatch(watch) {
//遍歷所有的watcher
for (const key in watch) {
//創建一個watcher
const watcher = new Watcher(this, () => {
return this[key]; //需要觀察的事物名稱
}, watch[key] //回調地址
);
watch[key].call(this, watcher.value); //立馬執行一次
}
}
好啦!! 寫了一大堆東西,現在我們來測試一下我們的代碼~~
import MyVue from './computed/MyVue';
const myVue = new MyVue({
data() {
return {
firstName: "Yasin",
lastName: "Yin"
};
},
computed: {
fullName() {
const fullName = this.firstName + "-" + this.lastName;
console.log("fullName", fullName);
return fullName;
}
},
watch: {
firstName(oldValue, newValue) {
console.log("firstName值改變了 oldValue=> " + oldValue + " newValue=> " + newValue);
}
}
});
做過vue的小夥伴應該是可以知道答案的吧:
然後我們模擬一下當firstName改變的時候fullName會不會變:
setTimeout(() => {
myVue.firstName = "Tom";
}, 100);
結果:
好啦~~整個過程結合demo就算是講完了,下面看一下整個流程。
當有computed的時候—>爲每個computed的屬性(fullName)創建成爲監聽者,同時申請
自己成爲監聽者—>被觀察的事物(firstName、lastName)看到有人需要成爲監聽者就立馬建
立與他的關係—>fullName成功的成爲了監聽者—>當被觀察的事物變化的時候firstName–>
通知監聽者fullName—>監聽者fullName執行回調方法獲取最新值
好啦~~ computed跟watch就可以講完了,歡迎指正,大牛勿噴!!