rwson

rwson

一个前端开发

Vue中computed计算属性和watch观察者原理

在用Vue做开发中, 我们多多少少都会用到里面两个比较重要的东西: computedwatch, 接下来我们一起分析并简单实现下这两个属性。

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这个函数中, 我们做了下面几步:

  1. 缓存当前属性值
  2. 调用Object.defineProperty方法在obj上定义属性key
  3. 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);