首先Vue的最大特点是数据驱动视图。什么是数据驱动试图呢?我可以用一条公式来描述
UI= reader(state)
在这条公式中,UI是页面输出,state是状态/数据,render就是vue,vue当检测到数据变化,就会触发改变UI。
那么问题来了,Vue怎么根据数据变化而更改视图呢?下面我们总结一下变化侦测。并且从零基础开始手写Vue的变化侦测
什么是变化侦测
变化侦测是数据变化了,就要更新视图。
目前的前端三大框架中均有涉及。在Angular中是通过脏值检查流程来实现变化侦测;在React是通过对比虚拟DOM来实现变化侦测,而在Vue中也有自己的一套变化侦测实现机制。
这里我们分两篇文章分别描述vue变化侦测,分别是Object类测试和Array类侦测。
Object的变化侦测
首先我们先了解JS语言一个Object.defineProperty 方法,该方法会直接在一个对象上定义一个新属性,或者修改一个对象现有的属性,并且返回此对象。
这个方法就是Vue变化侦测的核心,我们在这里可以成为上帝的钥匙。
在这里我们可以看defineProperty几个重要的属性和方法
label | 类别 | 备注 | 默认值 |
---|---|---|---|
configurable | 属性 | 当属性该属性为true时,该属性的描述符能够被改变,同事该属性可以能从对象上删除 | false |
enumerable | 属性 | 当该属性为true时,该属性可以出现在对象的枚举属性中 | false |
writable | 属性 | 当该属性为true时,可以任意更改属性的值 | false |
get | 方法 | 属性的 getter 函数,当访问属性的时候,就会调用此函数,一般我们成为“getter” | undefined |
set | 方法 | 属性的 setter函数,当修改属性的时候,就会调用此函数,一般我们成为“setter” | undefined |
Object数据变得可观性
数据客观性是随时可以知道数据什么时候被读写。当属性被读和写的时候,会触发 get 和 set 属性。
请看以下例子
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="./index.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
</body>
<script type="text/javascript">
let student ={
name :"Tony"
}
let val ="GoGO"
Object.defineProperty(student,"name",{
enumerable: true,
configurable: true,
get(){
console.log('name 属性被读取了');
return val
},
set(newVal){
console.log("name 属性被修改:%s",newVal);
val = newVal;
// return val
}
})
console.log(student.name)
student.name ="Janny"
</script>
</html>
在网页控制台显示
name 属性被读取了
index.html:28 GoGO
index.html:23 name 属性被修改:Janny
上面demo说明每次读数据时候会调用get,修改属性时候调用set
有了以上基础,我们开始写Object数据变化侦测。
首先我们先看一个流程图,我的学习经验是先把框架以流程图形式记录下来,方便理解。
下面开始手把手从零基础写Vue变化侦测。
定义一个Observer class类
请看代码
index.js
function defineReactive(obj,key,val){
console.log("我是defineReactive",obj,key);
if(arguments.length == 2){
val = obj[key];
}
Object.defineProperty(obj,key,{
enumerable: true,
configurable: true,
get(){
console.log('%s 属性被读取了',key);
return val;
},
set(newVal){
console.log('%s 属性被写入 %d',key,newVal);
if(val === newVal){
return ;
}
val = newVal;
}
})
}
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="./index.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
</body>
<script type="text/javascript">
let student ={
name :"Tony"
}
let val ="GoGO"
defineReactive(student,"name");
student.name=1000;
console.log(student.name)
</script>
</html>
执行以上代码结果是
我是defineReactive {name: "Tony"} name
index.js:15 name 属性被写入 1000
index.js:11 name 属性被读取了
index.html:17 1000
这里我们把Object.defineProperty封装在defineReactive函数里面。
定义一个Observer 类
这里我们定义一个Observer 类,该类是功能是给每个object添加 __ob__ 属性。
什么是 __ob__ 属性?
value.__ob__={
value,
enumerable,
writable:true,
configurable:true
}
意思是给每个object的 value修改属性,让其可以读写删除,可以枚举,赋予初值。
所以我们在 index.js定义一个def函数
function def(obj,key,value,enumerable){
Object.defineProperty(obj,key,{
value,
enumerable,
writable:true,
configurable:true
})
}
流程图,分别写Observer 类,obverse函数,请看以下demo
function def(obj, key, value, enumerable) {//第一步
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}
class Observer {//第二步
constructor(value) {
console.log('我是Observer constructor', value);
def(value, '__ob__', this, false)//添加__ob__属性
this.walk(value);
}
walk(value){//读取object里面每个属性
for(let k in value){
defineReactive(value,k);
}
}
}
function observe(value) {//第三步
console.log("准备调用observe", value)
if (typeof value !== 'object')
return;
console.log("调用observe 啦", value);
var ob;
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
}
function defineReactive(obj, key, val) {
console.log("我是defineReactive", obj, key);
if (arguments.length == 2) {
val = obj[key];
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
console.log('%s 属性被读取了', key);
return val;
},
set(newVal) {
console.log('%s 属性被写入', key, newVal);
if (val === newVal) {
return;
}
val = newVal;
}
})
};
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="./index.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
</body>
<script type="text/javascript">
var Student ={
name:"Tony",
year:20,
grade:{
math:100,
chinese:45
}
}
// defineReactive(Student,"name");
observe(Student);
console.log(Student);
Student.name ="Janny";
console.log(Student.name);
Student.grade.math =20;
</script>
</html>
在Observer类中,在 constructor 中添加 __ob__ 属性,然后在每个属性调用defineReactive方法,通过Object.defineProperty修改对象每个属性的值。此时可以监听到getter 和setter。
在 observe 方法中,把value传输Observer类,Observer类作用是使Object第一个层级属性改为可响应式。
于是以上程序运行结果是:
我们展开Student对象,可以发现,在Ojbect第一层存在 __ob__ 属性,但是在grade对象中,发现没有 __ob__ 属性。说明我们必须递归添加 __ob__ 属性。
问题来了,怎么在每一层 Object 条件 添加 __ob__ 属性呢?
在流程图上,我们把 defineReactive 的 val 再次指向observe,所以我们添加ChildOB,形成闭环。所以我们修改defineReactive 方法如下:
function defineReactive(obj, key, val) {
console.log("我是defineReactive",obj,key);
if (arguments.length == 2) {
val = obj[key];
}
var childOb = observe(val);//递归
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log('%s 属性被读取了',key);
return val;
},
set(newVal) {
console.log('%s 属性被写入了 %d:',key,newVal);
if(val === newVal){
return ;
}
val = newVal;
childOb = observe(newVal);//添加新属性时要observe一下
}
})
};
上面是修改后得代码,我们添加两行,但是会有小伙伴会问,这个流程图是一直闭环,那会不会一直无限循环下去?终点在哪里?
在observe方法里面,判断了当输入的值不是object类型,就退出该方法。
if (typeof value !== 'object')
return;
另外还有一个小细节,其实这里有一个闭包
function defineReactive(obj, key, val) 第三个参数val,其实是个闭包。
查看执行结果,我们发现Object对象每一层都存在一个 __ob__ 属性
然后每次修改Student里面属性值,都会调用get和set属性,此时说明编写数据可观性成功。
Student.name ="Janny";
console.log(Student.name);
Student.grade.math =20;
总结:
1.学习Object对象数据客观性要先了解整体流程图
2.然后要熟悉 Object.defineProperty 方法,知道get和set属性。
3.善用chrome浏览器console控制台每个数据进行浏览分析,加深理解。
要查看源码,请看此链接