Vue中computed计算属性和watch观察者原理
在用Vue
做开发中, 我们多多少少都会用到里面两个比较重要的东西: computed
和watch
, 接下来我们一起分析并简单实现下这两个属性。
computed
computed
名为计算属性, 目的就是让我们在模板里面只关注简单的绑定, 不做复杂操作, 拿官网的代码做例子,下面就是一个相对复杂的操作:
<div id="example">
{{ message.split('').reverse().join('') }}
</div>
先来看下用法:
new Vue({
data() {
return {
firstName: "rw",
lastName: "son",
age: 25
};
},
computed: {
// 指定计算属性的getter
info() {
return `info content: my name is ${this.firstName}${this.lastName}, I'm ${this.age} years old`;
},
// 同时提供getter和setter, setter中可以操作其他数据
fullName: {
get() {
return `my fullName is: ${this.firstName}${this.lastName}`;
},
set(value) {
value = value.split(" ");
this.firstName = value[0];
this.lastName = value[value.length - 1];
}
},
text: "just a test case"
},
created() {
console.log(this.info);
// info content: my name is rwson, I'm 25 years old
this.fullName = "son rw";
console.log(this.info);
// info content: my name is sonrw, I'm 25 years old
console.log(this.fullName);
// my fullName is: sonrw
}
});
computed
的用法就是上面例子中的两种, 下面我们一起来模拟实现下:
首先我们模拟封装一个Vm
函数作为构造器, 然后去调用它,
const vm = new Vm({
data() {
return {
firstName: "rw",
lastName: "son",
age: 25
};
},
computed: {
info() {
return `info content: my name is ${this.firstName}${this.lastName}, I'm ${this.age} years old`;
},
fullName: {
get() {
return `my fullName is: ${this.firstName}${this.lastName}`;
},
set(value) {
value = value.split(" ");
this.firstName = value[0];
this.lastName = value[value.length - 1];
}
},
text: "just a test case"
}
});
console.log(vm.text);
// just a test case
console.log(vm.info);
// info content: my name is rwson, I'm 25 years old
vm.fullName = "song rw";
console.log(vm.info);
// info content: my name is songrw, I'm 25 years old
console.log(vm.firstName);
// song
上面是运行结果和调用, 下面我们一起实现下Vm
这个函数:
// vm构造函数
function Vm(obj) {
const data = obj.data();
// 处理data执行的返回结果
handleData(this, data);
// 处理compoted对象
handleComputed(this, obj.computed);
}
// hasOwnProperty简写
const hasOwnProp = (obj, prop) => obj.hasOwnProperty(prop);
// 获取prototype类名
const typeOf = (obj) => {
return {}.toString.call(obj).slice(8, -1).toLowerCase();
};
// 枚举对象
function loopObj(obj, callback) {
Object.keys(obj).forEach((key) => {
callback(key, obj[key], obj);
});
}
// 给data用Object.defineProperty绑定
function handleData(context, data) {
loopObj(data, (key, val, data) => {
defineProp(context, key, val);
});
}
// 处理computed属性
function handleComputed(context, obj) {
let type;
loopObj(obj, (key, val, obj) => {
defineProp(context, key, val);
});
}
function defineProp(obj, key, val) {
let _val = val, type;
return Object.defineProperty(obj, key, {
enumerable: true,
configable: true,
get: function() {
type = typeOf(_val);
switch (type) {
/**
* 属性值对应一个函数, 也就是computed里面只声明了getter
* eg.
* ...
* computed: {
* info() {
* return `info content: my name is ${this.firstName}${this.lastName}, I'm ${this.age} years old`;
* }
* ...
* }
* ...
*/
case "function":
return _val.call(obj);
break;
/**
* 属性值对应一个对象, 也就是computed里面同时声明了getter/setter
* eg.
* ...
* computed: {
* fullName: {
* get() {
* return `my fullName is: ${this.firstName}${this.lastName}`;
* },
* set() {
* value = value.split(" ");
* this.firstName = value[0];
* this.lastName = value[value.length - 1];
* }
* }
* }
* ...
*/
case "object":
return _val.get.call(obj);
break;
// 其他类型默认
default:
return _val;
break;
}
},
set: function(newV) {
type = typeOf(_val);
switch (type) {
// 属性值对应一个对象, 也就是computed里面同时声明了getter/setter
case "object":
_val.set.call(obj, newV);
break;
// 其他类型的默认
default:
if (newV !== _val) {
_val = newV;
}
break;
}
}
});
}
上面就是我们一个实现了, 其中defineProp
这个函数中, 我们做了下面几步:
- 缓存当前属性值
- 调用Object.defineProperty方法在obj上定义属性key
- 在
descriptor
中的getter
/setter
里面判断属性值类型(如果是函数或者对象, 那该属性就是在computed
[先不谈watch
]里面声明的,getter
里面返回该函数的执行结果、setter
里面执行computed
中指定的set
函数[如果有的话], 否则作为普通的data
处理)
watch
watch
名为观察者, 通过监听组件内某个属性来执行某些操作, 最常用的就是组件中某个属性值发生变化之后, 向后端发送一个异步请求, 比如一些商品网站上有顶部tab栏, 每个tab都是一个类目, 这时候我们可以给这些tab绑定click
事件, 然后在事件处理中处理顶部tab栏绑定的属性值, watch中去监听它的变化来发送异步请求等等, 比如下面的例子:
先来看模板部分:
...
<ul>
<li @click="setFilter('1')">男装</li>
<li @click="setFilter('2')">女装</li>
<li @click="setFilter('3')">童装</li>
<li @click="setFilter('4')">美食</li>
</ul>
...
然后我们看看js部分实现:
export default() {
data() {
return {
page: 1,
filter: "1",
goodsList: []
};
},
watch: {
async filter() {
const { filter, page } = this,
res = await this.queryGoods(filter, page);
if (res.success) {
this.goodsList = res.goods;
}
}
},
methods: {
setFilter(filter) {
this.filter = filter;
this.page = 1;
this.goodsList = [];
},
async queryGoods(filter, page) {
// ...
}
},
// ...
}
当然这个例子可能有点极端, 因为queryGoods
完全可以放在setFilter
里面去调用, 但是为了演示效果, 只能这么做了😂
下面一起看看看看watch
部分的实现, 有了刚才的分析, 我们知道watch
中声明的肯定是在setter
部分进行调用的, 所以我们需要在处理data
的时候就把watch
对应的加上:
// 首先我们需要在构造函数里面, 改下handleData这个函数的调用, 给它把obj.watch也传进去
// handleData(this, data);
handleData(this, data, obj.watch);
// 接下来我们改写handleData这个函数
function handleData(context, data, watch) {
let inWatch, watchVal;
loopObj(data, (key, val, data) => {
// 判断在watch中是否也声明了该属性值
inWatch = hasOwnProp(watch, key);
// 如果有取出来, 作为defineProp的第五个参数给传进去
watchVal = inWatch ? watch[key] : null;
defineProp(context, key, val, inWatch, watchVal);
});
}
// 下面就是我们说到的在setter部分调用声明的watch观察者
function defineProp(obj, key, val, inWatch = false, watchVal = () => {}) {
let _val = val, type;
return Object.defineProperty(obj, key, {
enumerable: true,
configable: true,
get: function() {
type = typeOf(_val);
switch (type) {
/**
* 属性值对应一个函数, 也就是computed里面只声明了getter
* eg.
* ...
* computed: {
* info() {
* return `info content: my name is ${this.firstName}${this.lastName}, I'm ${this.age} years old`;
* }
* ...
* }
* ...
*/
case "function":
return _val.call(obj);
break;
/**
* 属性值对应一个对象, 也就是computed里面同时声明了getter/setter
* eg.
* ...
* computed: {
* fullName: {
* get() {
* return `my fullName is: ${this.firstName}${this.lastName}`;
* },
* set() {
* value = value.split(" ");
* this.firstName = value[0];
* this.lastName = value[value.length - 1];
* }
* }
* }
* ...
*/
case "object":
return _val.get.call(obj);
break;
// 其他类型默认
default:
return _val;
break;
}
},
set: function(newV) {
type = typeOf(_val);
switch (type) {
// 属性值对应一个对象, 也就是computed里面同时声明了getter/setter
case "object":
_val.set.call(obj, newV);
break;
// 其他类型的默认
default:
if (newV !== _val) {
_val = newV;
}
break;
}
// 如果watch观察者存在, 并且对应一个回调函数, 在值发生改变的时候调用该回调, 并修改this指向到当前vm实例
if (inWatch && typeOf(watchVal) === "function") {
watchVal.call(obj);
}
}
});
}
再来看下用法:
const vm = new Vm({
data() {
return {
firstName: "rw",
lastName: "son",
age: 25
};
},
computed: {
info() {
return `info content: my name is ${this.firstName}${this.lastName}, I'm ${this.age} years old`;
},
fullName: {
get() {
return `my fullName is: ${this.firstName}${this.lastName}`;
},
set(value) {
value = value.split(" ");
this.firstName = value[0];
this.lastName = value[value.length - 1];
}
},
text: "just a test case"
},
watch: {
firstName() {
console.log(`%c now firstName's value is: ${this.firstName}`, "color: red");
// 走到vm.fullName = "song rw";就会输出
},
lastName() {
console.log(`%c now lastName's value is: ${this.lastName}`, "color: green");
// 走到vm.fullName = "song rw";就会输出
},
age() {
console.log(`%c now age's value is: ${this.age}`, "color: yellow");
// 走到vm.fullName = "song rw";就会输出
}
}
});
console.log(vm.text);
console.log(vm.info);
vm.fullName = "song rw";
console.log(vm.info);
console.log(vm.firstName);