实现数据的响应式,从而达到数据驱动视图
UI = render(state)
状态 state 是输入,页面 UI 输出,状态输入一旦变化了,页面输出也随之而变化。我们把这种特性称之为数据驱动视图。(数据驱动视图简单来说就是数据变化引起视图变化)
- Object.defineProperty(obj, prop, descriptor),js为我们提供了这个API---(静态方法)
- obj: 要定义属性的对象。
- prop: 要定义或修改的属性的名称或 Symbol 。
- descriptor: 要定义或修改的属性描述符。定义属性行为的元数据信息。
- 描述符:
- 数据描述符:是一个具有值的属性,该值可以是可写的,也可以是不可写的。
- 存取描述符:是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。 在 getter 中收集依赖,在 setter 中通知依赖更新。
案例 1:创建属性
const Obj={}
// 向对象Obj中添加一个属性a,并设置数据描述符
Object.defineProperty(Obj,'a',{
value:100, //默认值
writable:true, //可写
enumerable:true, //可枚举
configurable:true, //可以再次修改属性描述符
})
// 对象obj有了属性a,其结果为100
console.log(Obj.a);
案例 2:get、set 存取描述符
const Obj = {};
let name = null;
Object.defineProperty(Obj,'a',{
get:function(){
return name
},
set:function(newValue){
name = newValue;
}
})
Obj.a='张三';
console.log(Obj.a);//这里访问Obj.a,会执行get这个方法
let obj = {}
Object.defineProperty(obj, 'name', {
configurable: true,
enumerable: true,
set: function (val) {
console.log(`设置obj.name的值为:${val}`)
}
})
obj.name = '李四'
总结:vue2 的响应式原理
-
响应式的原理是通过 Object.defineProperty 为对象的每个 key 设置 getter、setter,从而拦截对数据的访问和设置。 当对数据进行更新操作时,比如 obj.key = 'new val' 就会触发 setter 的拦截,从而检测新值和旧值是否相等,如果相等什么也不做,如果不相等,则更新值,然后由 dep 通知 watcher 进行更新。所以,异步更新 的入口点就是 setter 中最后调用的 dep.notify() 方法。
-
Object.defineProperty 的作用就是用于定义对象的属性的值,它接收三个参数:obj、prop、descriptor; 分别表示添加属性的那个对象、要定义的属性名以及属性配置描述,其中最重要的就是配置描述。 分两种:数据描述符和存取描述符,通过存取描述符里的 get 和 set 监控数据变化,当数据发生变化时,可通知页面做 update 等操作!
-
响应式的数据分为两类:
-
1.对象,循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依旧为对象,则递归为属性值上的每个 key 设置 getter、setter Object.defineProperty: 负责数据的拦截。getter 时进行依赖收集,setter 时让 dep 通知 watcher 去更新
-
2.数组,增强数组的那 7 个可以更改自身的原型方法,然后拦截对这些方法的操作 添加新数据时进行响应式处理,然后由 dep 通知 watcher 去更新 删除数据时,也要由 dep 通知 watcher 去更新
object.defineProperty()不能监听对数组的改变。
不足指出
- Object.defineProperty 方法只能观测到 object 数据的取值及设置值,当我们向 object 数据里添加一对新的 key/value 或删除一对已有的 key/value 时,它是无法观测到的,导致当我们对 object 数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。 想要响应式的修改对象中的属性,比如添加或者删除,需要用到 Vue.set()和 Vue.delete()
受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。 Vue 不允许动态添加根级响应式属性
Vue.set( target, propertyName/index, value )
- target : 要更改的数据源(可以是对象或者数组);
- propertyName/index : 是指向我们传入第一个参数 object/array 的 property 或 key(要更改的具体数据);
- value : 重新赋的值。
- 返回值:设置的值。
Vue.set 和 this.$set 的区别
- vue.set 和 this.$.set 的区别不大,主要都是用于更新数组。
- 不过 Vue.set 可以添加属性,this.$set 则是修改属性。
- Vue.set() 是将 set 函数绑定在 Vue 构造函数上,this.$set() 是将 set 函数绑定在 Vue 原型上。
observer 类 (响应式布局)
-
源码位于 src/core/observer/index.js
-
在源码中,定义了 observer 类,它用来将一个正常的 object 转换成可观测的 object;并给 value 新增一个__ob__属性,相当于为 value 打上标记,表示它已经被转化成响应式了,避免重复操作.
-
然后判断类型:分为数组和对象
- 为对象: 如果是对象,直接调用 walk, walk 将每一个属性转换成 getter/setter 的形式来侦测变化。
- 为数组: 判断对象是否存在 __proto__ 属性,通过 obj.__proto__ 可以访问对象的原型链,覆盖数组默认的七个原型方法,然后设置响应式
Dep 类 (依赖管理器)- 用于存储收集到的依赖。
-
源码位置:src/core/observer/dep.js
在 getter 中收集依赖,在 setter 中通知依赖更新。
在 dep 里,先初始化了一个 subs 数组,用来存放依赖,并且定义了几个实例方法用来对依赖进行添加,删除,通知等操作。
有了依赖管理器后,我们就可以在 Object.defineProperty()里通过 getter 收集依赖,在 setter 中通知依赖更新了,我们在 getter 中调用了 dep.depend()方法收集依赖,在 setter 中调用 dep.notify()方法通知所有依赖更新。
- removeSub : 删除依赖
- depend : 添加依赖
- notify : 通知所以依赖更新
Watcher 类
-
源码位置:src/core/observer/watcher.js
-
为每一个依赖都创建了一个 Watcher 实例,当数据发生变化时,通知 Watcher 实例,由 Watcher 实例去做真实的更新操作(去通知真正的依赖)。
-
依赖的是谁?而 Watcher 类的实例就是"谁"。换句话说就是:谁用到了数据,谁就是依赖,为"谁"创建一个 Watcher 实例。在之后数据变化时,我们不直接去通知依赖更新,而是通知依赖对应的 Watch 实例,由 Watcher 实例去通知真正的视图。
总结 Watcher: Watcher 先把自己设置到全局唯一的指定位置(window.target),然后读取数据。因为读取了数据,所以会触发这个数据的 getter。接着,在 getter 中就会从全局唯一的那个位置读取当前正在读取数据的 Watcher,并把这个 watcher 收集到 Dep 中去。收集好之后,当数据发生变化时,会向 Dep 中的每个 Watcher 发送通知。通过这样的方式,Watcher 可以主动去订阅任意一个数据的变化。
关系图
以上,就彻底完成了对 Object 数据的侦测,依赖收集,依赖的更新等所有操作。
整个流程大致如下:
- Data 通过 observer 转换成了 getter/setter 的形式来追踪变化。
- 当外界通过 Watcher 读取数据时,会触发 getter 从而将 Watcher 添加到依赖中。
- 当数据发生了变化时,会触发 setter,从而向 Dep 中的依赖(即 Watcher)发送通知。
- Watcher 接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
不足指出
- Object.defineProperty 方法只能观测到 object 数据的取值及设置值,当我们向 object 数据里添加一对新的 key/value 或删除一对已有的 key/value 时,它是无法观测到的,导致当我们对 object 数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。 想要响应式的修改对象中的属性,比如添加或者删除,需要用到 Vue.set()和 Vue.delete()
受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。 Vue 不允许动态添加根级响应式属性
Vue.set( target, propertyName/index, value )
- target : 要更改的数据源(可以是对象或者数组);
- propertyName/index : 是指向我们传入第一个参数 object/array 的 property 或 key(要更改的具体数据);
- value : 重新赋的值。
- 返回值:设置的值。
Vue.set 和 this.$set 的区别
- vue.set 和 this.$.set 的区别不大,主要都是用于更新数组。
- 不过 Vue.set 可以添加属性,this.$set 则是修改属性。
- Vue.set() 是将 set 函数绑定在 Vue 构造函数上,this.$set() 是将 set 函数绑定在 Vue 原型上。
初次写文章,如有不对,请多包涵~