前言:好久没写博客了,今年确实有点小忙,学习跟工作都很充实,身边也有小伙伴也经常说:技术是学不完的,能不能专一搞点自己喜欢的东西呢?学完新技术然后过几年又没了,岂不是白学了呢? 哈哈~ 说的确定有一点道理啊,但是没办法啊,新技术还是得去了解啊,至少要去看看别人的文档跟设计思想吧,也不怕小伙伴笑哈,最近还在补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就可以讲完了,欢迎指正,大牛勿喷!!