Object
的变化侦测
1. Vue.js
和React
、Angular
对变化侦测的区别
变化侦测分为两种类型:1.推(push
),2.拉(pull
)
Angular
和React
中的变化侦测属于"拉"
当状态发生变化的时候,不知道是哪个状态发生变化了,只知道状态可能发生变化了,然后发送一个信号告诉框架,框架内部收到信号后,会进行一个暴力对比来找出哪些DOM节点需要重新渲染。在Angular
中是脏检查的流程,在React
中使用的是虚拟DOM。Vue.js
变化侦测属于“推”
当状态发生变化时,立马就就知道了,在一定程度上知道哪些状态发生变化了,由于它知道的更多,就可以进行更细粒度的更新。
从Vue.js 2.0
开始,引入了虚拟DOM,将粒度调整为中等粒度。也就是说,一个状态所绑定的依赖不再是具体的DOM节点,而是一个组件。当状态变化后,会通知到组件,组件内部再使用虚拟DOM进行对比。
2. 在JavaScript
中如何侦测一个对象的变化
有两种方法可以侦测到变化,分别是:Object.defineProperty
和ES6
中的Proxy
。可能是作者在写这本书的时候Vue.js
是2.0版本,所以本书中主要讲述的是Object.defineProperty
,而在最新的3.0版本中使用了Proxy
,有兴趣的朋友可以到我的博文Proxy靠什么干掉了Object.defineProperty在Vue3.0中成功上位 去一看究竟。
Object.defineProperty
可以侦测到对象的变化,可以写出如下代码:
function defineReactive(data,key,val){
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
return val;
},
set:function(newVal){
if(val ==newVal){
return
}
val = newVal
}
})
}
当从data
中的key
中读取数据时,get
函数被触发;向data
的key
中设置数据时,set
函数会被触发。
3. 如何收集依赖
在v ue.js 2.0
中,模块使用数据等同于组件使用数据,当数据发生变化时,会将通知发送到组件,组件内部通过虚拟DOM重新渲染。总结:在getter
中收集依赖,setter
中触发依赖
4. 依赖收集到哪里
首先想到的是每一个数组都有一个数组,用来存储当前key的依赖。假设依赖是一个函数,保存在window.target上。
function defineReactive(data,key,val){
let dep=[];
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
dep.push(window.target)
},
set:function(newVal){
if(val == newVal){
return
}
for(let i = 0;i<dep.lenght;i++){
dep[i](newVal,val)
}
val = newVal
}
})
}
新增了数组dep
,用来存储被收集的依赖。在set
被触发时,循环dep以触发收集到的依赖。这么写的代码耦合度太高,我们把它重新封装成一个Dep类,让它专门管理依赖。使用这个类,我们可以收集依赖、删除依赖和向依赖发送通知。
class Dep{
constructor(){
this.subs = []
}
addSub(){
this.subs.push(sub)
}
removeSub(){
remove(this.subs,sub)
}
depend(){
if(window.target){
this.addSub(window.target)
}
}
notify(){
const subs = this.subs.slice()
for(let i = 0; i<subs.length;i++){
subs[i] .update()
}
}
}
function remove(arr,item){
if(arr.length){
const index = arr.indexOf(item)
if(index >-1){
return arr.splice(index,1)
}
}
}
改造defineReactive
函数:
function defineReactive(data,key,val){
let dep = new Dep()
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
dep.depend();
return;
},
set:function(newVal){
if(val == newVal){
return;
}
val = newVal;
dep.notify()
}
})
}
5. Watcher
Watcher
是一个中介角色,数据发生变化时先通知它,它再通知其他地方。
关于Watcher
,先看一个使用方式:
vm.$watch('a.b.c',function(newVal,oldVal){
//do something
})
这段代码表示当data.a.b.c
属性发生变化的时候,触发第二个参数中的函数。
如何实现此功能,把watcher
实例添加到data.a.b.c
属性的Dep
中就行了,当data.a.b.c
的值发生变化时,通知Watcher
,Watcher
再执行参数中的这个回调函数
class Watcher{
constructor(vm,expOrFn,cb){
this.vm = vm;
this.getter() = parsePath(expOrFn);
this.cb = cb;
this.value = this.get()
}
get(){
window.target = this;
let value = this.getter.call(this.vm,this,vm);
window.target = undefined;
console.log(value);
return value;
}
update(){
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm,this.value,oldValue)
}
}
在get
方法中先把window.target
设置成了this
,也就是watcher
的实例,然后再读一下data.a.b.c
的值,这肯定会触发getter
。
对于上面代码中的parsePath
是怎么读取一个字符串的keypath
的解释:
const bailRE = /[^\w.$]/;
function parsePath(path){
if(bailRE.test(path)){
return;
}
const segments = path.split(".");
return function (obj){
for(let i = 0;i<segments.length;i++){
if(!obj){
return
}
obj = obj[segments[i]]
}
}
}
6. 递归侦测所有的key
根据以上完成的代码,已经可以实现变化侦测的功能了,但是只能侦测数据的某一项属性,我们希望可以侦测到数据的所有属性(包括子属性)。所以,我们需要封装一个类,把数据内所有的属性都转换成getter/setter
的形式去追踪它的变化。
export default class Observer{
constructor(value){
this.value = value;
if(!Array.isArray(value)){
this.walk(value)
}
}
walk(obj){
const keys = Object.keys(obj);
for(let i = 0;i<keys.length;i++){
defineReactive(obj,keys[i],obj[keys[i]])
}
}
}
function defineReactive(data,key,val){
// 新增递归属性
if(typeof val === 'object'){
new Observer(val);
}
let dep = new Dep();
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
dep.depend();
return val;
},
set:function(newVal){
if(val === newVal){
return;
}
val = newVal;
dep.notify()
}
})
}
只要将一个object传到Observer中,这个object就会变成响应式的object
7. Object不能被侦测到的问题
上面的代码解释了object类型数据的变化侦测原理,了解到数据的变化是通过getter
和setter
来追踪的,有些语法中即使数据发生了变化,也追踪不到。
var vm = new Vue({
el:"#App",
methods: {
action(){
this.obj.name = "mgd"
console.log(this.obj);
}
},
data:{
obj:{
age:23,
name:""
}
}
})
在action方法中,我们在obj上面新增了name属性,Vue.js
无法侦测到这个变化,所以不会向依赖发送通知
再看这个例子:
var vm = new Vue({
el:"#App",
methods: {
action(){
// this.obj.name = "mgd"
delete this.obj.age;
console.log(this.obj);
}
},
data:{
obj:{
age:23,
name:""
}
}
})
在action方法中,我们删除了obj中的age属性,而Vue.js
无法侦测到这个变化,所以不会向依赖发送通知
Vue.js
通过Object.defineProperty
来将对象的key
转换成getter/setter
的形式来追踪变化,但是getter/setter
只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。为了解决这个问题,Vue.js
提供了两个API
——vm.$set
和vm.$delete
。
总结
- 变化侦测就是侦测数据的变化。当数据发生变化时,要能侦测到并发出通知。
Object
可以通过Object.defineProperty
来属性转换成getter/setter
的形式来追踪变化,读取数据时会触发getter
,修改数据时会触发setter
。- 我们需要在
getter
中收集有哪些依赖使用了数据,当setter
被触发时,去通知getter
中收集的依赖数据发生了变化。 - 收集依赖需要为依赖寻找一个存储依赖的地方,为此创建了Dep,它用来收集依赖,删除依赖和向依赖发送消息等。
- 所谓的依赖就是
Watcher
。只有Watcher
触发的getter才会收集依赖,哪个Watcher
触发了getter
,就把哪个Watcher
收集到Dep
中。当数据发生变化时,会循环依赖列表,把所有的Watcher
都通知一遍。 Watcher
的原理就是先把自己设置到全局唯一的指定位置(例如:window.target),然后读取数据。因为读取数据,所以会触发这个数据的getter
。在getter
中就会从全局唯一的那个位置读取当前正在读取的Watcher
,并把这个Watcher
几种收集到Dep
中。通过这样的方式,Watcher
可以主动去订阅任意一个数据的变化。Observer
的作用是把一个object
中的所有数据(包括子数据)都转换成响应式的,也就是它会侦测object
中所有数据的变化(包括子数据).- 在ES6之前的js没有提供元编程的能力,所以对象上新增和删除属性都是无法追踪到的
Data
通过Observer
转换成getter/setter
的形式来追踪变化.- 当外界通过
Watcher
读取数据时,会触发getter
从而将Watcher
添加到依赖中。 - 当数据发生变化时,会触发
setter
,从而向Dep
中的依赖(Watcher
)发送通知。 Watcher
收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
第一章Vue.js
简介阅读补充
操作DOM的两种方式:
- 命令式操作DOM
- 声明式操作DOM
所谓渐进式框架,就是把框架分层。最核心的部分是视图层渲染,然后往外是组件机制,再在这个基础上加上路由机制,再加入状态管理,最外层是构建工具。
Vue.js 2.0
引入了虚拟DOM
,其渲染过程变得更快了。这个说法是错误的,事实上,并不是引入虚拟DOM
后,渲染速度变快了。准确的说,应该是80%的场景下变快了,而剩下的20%反而变慢了。