最近负责了公司的小程序项目,前端部分都由本人来完成,花了几天时间看了下小程序的文档,然后开始动手写,小程序项目开始时间是在十一月初,离写这篇博客已经过去了两个月左右,之前一直由于没时间,今天就把写小程序的一些东西给记录下来,方便以后阅读或已经给一些同行如果刷到这篇文章需要用到的提供一些方便
虽说是在小程序里封装的,但是同样适用于web端的属性监听,两者是通用的,好了,接下来开始上正文
1.何为watch监听事件
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过
watch
选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。摘自vue文档
简单的来说,就是响应数据的变化,追踪一个属性数据的变化
2.watch监听事件实现的原理
有过JS基础的都知道,js是一门弱类型语言,不是一门真正的面向对象语言,ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是自己定义的
在写原理之前,我们先搞懂几个东西,把它们看懂以后,原理也就自然懂了。
(1)属性类型
ECMA-262第5版在定义只有内部才用的特性(attribute)时,描述了属性(property)的各种特征。定义这些特性是为了实现JavaScript引擎用的,因此在JavaScript中不能直接访问它们。为了表示特性是内部值,该规范把它们放在了俩对儿方括号里,例如[[Enumerable]]。
ECMAScript中有两种属性:数据属性和访问器属性。
数据属性:数据属性包含一个数据值的位置,在这个位置可以读取和写入值。
[[Configurable]] : 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。为true表示可以通过delete删除属性从而重新定义属性。
[[Enumerable]] : 表示能否通过for...in循环返回属性,通常所说的是否可枚举。为true表示可以枚举
[[Writable]] : 表示能否修改属性的值。为true表示可以修改当前描述属性的的值
[[Value]] : 包含这个属性的数据值。
访问器属性:访问器属性不包含数据值;访问器属性包含一对getter和setter函数(这两个函数都不是必需的)。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器有如下4个特性:
[[Configurable]] : 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
[[Enumerable]] : 表示能否通过for...in循环返回属性。
[[Get]]:在读取属性时调用的函数。默认值为undefined。此函数的作用就是
[[Set]]:在写入属性时调用的函数。默认值为undefined。
看完这些后,想必脑子里就有了一个构思,难道实现watch监听事件就是在访问器属性中的Set()方法里,当每一次写入值的时候,它会自动执行set方法,然后在set方法里将你定义的那个方法在调用出来?没错就是这样的。
然而,我们创建的对象最多的方式就是通过对象字面量或者通过自定义构造函数创建对象,但是这两种创建的对象,其描述对象属性的属性描述符默认就是数据属性,且默认都是为true,这与我们在访问器属性里利用set方法实现功能可是大相径庭了,别急,后面会有写。你们可以通过getOwnPropertyDescriptor()方法打印看属性描述符,它是在Object原型上个一个方法,所以用法Object.getOwnPropertyDescriptor(obj,"name")。
前面有说过,我们这样创建出来的对象,其属性描述符都是数据属性,所以我们如果要将其修改为访问器属性,就要用到ES5中的一个方法,Object.defineProperty(obj,prop,descriptor)。
为了举例,我们先通过对象字面量定义一个对象。
let person={ name:"张三" , age:"20" }
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
从括号里可以看出,这个方法接收三个参数。
第一个为要在其上定义属性的对象,也就是说要操作的对象,也就是上面所举例子的person变量。
第二个为要定义或修改的属性的名称,也就是上面所举例子的name或者age属性。
第三个为将被定义或修改的属性描述符,这个是一个对象,这个对象里就是描述该属性的一些特征的属性,也就是上面提到的那几种特性。
具体操作方法如下:
let person={ name:"张三" , age:"20" }
Object.defineProperty(person,"name",{
configurable:false,
value:"李四",
enumerable:false,
writable:false
})
上面的代码表示修改person对象里的name属性的描述符为不能通过delete删除属性,将值变为李四,不可枚举,不可修改name属性的值,在下一次修改时如果你person.name="王五"时,就会报错,因为你修改了writable为false。同样,你delete name
那么,我们如何将数据属性变为访问器属性呢?看代码
let person={ _name:"张三" , age:"20" }
Object.defineProperty(person,"name",{
get:function(){
return this._name;
},
set(newval){
this._name=newval
}
})
对,就是这样就可以了,至于name属性前面为什么要加_,那是因为加了这个,就只有内部方法能够调用,也就是说只有写了get方法,才能将属性拿到,否则会为undefined。
至于set方法,就是你修改时候,它就会执行这个方法从而将你新设置的值映射进去,如果不定义此方法,那么这个name属性就只能读,不能写。
具体看代码:
Object.defineProperty(person,"name",{
get:function(){
return this._name;
},
set(newval){
this._name=newval
}
})
person.name="李四"
上述代码就是将name属性的值修改为李四
那么换个角度想,既然修改的时候可以调用set方法,将新值设置进去,那我们就在set方法里调用由该这个属性名创建的一个方法,当我们将这个属性值改变时,就会调用这个set方法,然后在set方法里将这个同名属性调出来,这就是watch实现监听属性的原理。
所以,我们创建一个js文件,文件命名为watch.js。
然后在文件里定义一个方法,setWatch(),用来封装这段代码。
function setWatch(_this, watch){}
这个方法接收两个参数,第一个参数_this为你定义的最大对象,小程序这边传页面的this过去,web端可以传window,第二个参数watch为你要监听的那个对象,这个参数就相当于vue中watch对象了。
接下来,在实现逻辑。
function setWatch(_this, watch){
Object.keys(watch).forEach(v => {
let key = v.split('.'); //以点号分割key
let setdata = _this.data; // 这个赋值是针对小程序结构而言来赋值的,就是把data对象赋值给setdata
for (let i = 0; i < key.length - 1; i++) {
setdata = setdata[key[i]]; //遍历key数组,将要监听的属性的直接父对象赋给setData
}
let lastKey = key[key.length - 1]; //得到要遍历的属性
let setVal = setdata[lastKey]; //得到这个属性的旧值,好返回出去
//这个方法就是我上面提及到的那个方法,这个方法还不懂的,可以看我上面解释的或者百度
Object.defineProperty(setdata, lastKey, {
configurable: true,
enumerable: true,
get: function () {
return setVal; //因为已经将要监听的属性的描述符改为了访问器属性,所以要定义get方法才能拿到这个属性的值
},
set: function (val) {
_this.watch[v].call(_this, val, setVal); //属性改变时,调用与该属性同名的方法,这样就实现了watch监听原理,其次改变this的指向,这样在你调用的那个方法里,上下文还是你当前的this,另外两个参数就是改变之前的值和旧值,跟vue的一样
setVal = val; // 改变时将新值映射进去
}
})
})
}
module.exports={
setWatch
}
这段代码首先借鉴的是网上一位大牛的代码,然后自己在着手封装的一段代码。
接下来,异一步一步解读。
首先通过Object.keys()对watch对象进行遍历,这个不用讲,不懂的可以百度,该方法返回的是一个数组,该数组的组成就是这个对象的各个key值,就比如我传入的watch对象为let watch={ name(){ }, age(){ } },其中为什么要以name和age为名字命名方法,就假如我在页面中有
let name; let age两个属性,这个是针对web端而言,小程序是在data里data:{ name:"" ,age:"" },然后我想监听这个变量,所以就在watch对象里用name 和age分别命名方法,这也就是vue中watch监听事件的基本用法。
接下来为什么要用点号来分割所遍历出来的属性呢? 那是因为我监听的可能是data对象中的对象中的一个属性(针对小程序而言),即结构可能是这样的data:{ person:{name:"", age:"" } } ,所以它在watch对象里监听的写法就是 watch:{ "person.name" (){ } , "person.age"(){} }, 所以当我们变量watch对象的时候,出来的数组结构是["person.name" , "person.age" ],我们的目的就是监听person对象里的name属性和age属性,所以这就是我们为什么要用点号来分隔数组。
至于后面的步骤,我已经在代码里标注了。
好了,到这里这个功能基本上已经封装完成了,当然,功能肯定不如vue中的齐全,但是基本的需求已经可以满足了。
让我们看下用法(小程序)
文件所在目录如上
然后在页面js里引入:
const watch=require("../../../../utils/view/watch.js");
接下来在onLoad里调用
设置watch对象
上面箭头所指的就是我要监听的属性。
然后,看下web端的:
HTML:
js:
let obj={
data:{
name:"111"
},
watch:{
name(){
console.log("我改变了")
}
}
}
setWatch(obj,obj.watch);
function setWatch(_this, watch){
Object.keys(watch).forEach(v => {
let key = v.split('.');
let setdata = _this.data;
for (let i = 0; i < key.length - 1; i++) {
setdata = setdata[key[i]];
}
let lastKey = key[key.length - 1];
let setVal = setdata[lastKey];
Object.defineProperty(setdata, lastKey, {
configurable: true,
enumerable: true,
get: function () {
return setVal
},
set: function (val) {
_this.watch[v].call(_this, val, setVal);
setVal = val;
}
})
})
}
document.getElementsByTagName("button")[0].onclick=function(){
obj.data.name=222
}
每当person对象中的name属性改变时,打印出“ 我改变了 ”。