深入浅出Vue.js阅读——变化侦测——Object

Object的变化侦测

1. Vue.jsReactAngular对变化侦测的区别

  变化侦测分为两种类型:1.推(push),2.拉(pull)

  • AngularReact中的变化侦测属于"拉"
      当状态发生变化的时候,不知道是哪个状态发生变化了,只知道状态可能发生变化了,然后发送一个信号告诉框架,框架内部收到信号后,会进行一个暴力对比来找出哪些DOM节点需要重新渲染。在Angular中是脏检查的流程,在React中使用的是虚拟DOM。
  • Vue.js变化侦测属于“推”
    当状态发生变化时,立马就就知道了,在一定程度上知道哪些状态发生变化了,由于它知道的更多,就可以进行更细粒度的更新。
      从Vue.js 2.0开始,引入了虚拟DOM,将粒度调整为中等粒度。也就是说,一个状态所绑定的依赖不再是具体的DOM节点,而是一个组件。当状态变化后,会通知到组件,组件内部再使用虚拟DOM进行对比。

2. 在JavaScript中如何侦测一个对象的变化

  有两种方法可以侦测到变化,分别是:Object.definePropertyES6中的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函数被触发;向datakey中设置数据时,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的值发生变化时,通知WatcherWatcher再执行参数中的这个回调函数

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类型数据的变化侦测原理,了解到数据的变化是通过gettersetter来追踪的,有些语法中即使数据发生了变化,也追踪不到。

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.$setvm.$delete

总结

  1. 变化侦测就是侦测数据的变化。当数据发生变化时,要能侦测到并发出通知。
  2. Object可以通过Object.defineProperty来属性转换成getter/setter的形式来追踪变化,读取数据时会触发getter,修改数据时会触发setter
  3. 我们需要在getter中收集有哪些依赖使用了数据,当setter被触发时,去通知getter中收集的依赖数据发生了变化。
  4. 收集依赖需要为依赖寻找一个存储依赖的地方,为此创建了Dep,它用来收集依赖,删除依赖和向依赖发送消息等。
  5. 所谓的依赖就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
  6. Watcher的原理就是先把自己设置到全局唯一的指定位置(例如:window.target),然后读取数据。因为读取数据,所以会触发这个数据的getter。在getter中就会从全局唯一的那个位置读取当前正在读取的Watcher,并把这个Watcher几种收集到Dep中。通过这样的方式,Watcher可以主动去订阅任意一个数据的变化。
  7. Observer的作用是把一个object中的所有数据(包括子数据)都转换成响应式的,也就是它会侦测object中所有数据的变化(包括子数据).
  8. 在ES6之前的js没有提供元编程的能力,所以对象上新增和删除属性都是无法追踪到的
  9. Data通过Observer转换成getter/setter的形式来追踪变化.
  10. 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
  11. 当数据发生变化时,会触发setter,从而向Dep中的依赖(Watcher)发送通知。
  12. Watcher收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。

第一章Vue.js简介阅读补充

操作DOM的两种方式:

  1. 命令式操作DOM
  2. 声明式操作DOM

  所谓渐进式框架,就是把框架分层。最核心的部分是视图层渲染,然后往外是组件机制,再在这个基础上加上路由机制,再加入状态管理,最外层是构建工具。
  Vue.js 2.0引入了虚拟DOM,其渲染过程变得更快了。这个说法是错误的,事实上,并不是引入虚拟DOM后,渲染速度变快了。准确的说,应该是80%的场景下变快了,而剩下的20%反而变慢了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值