Object变化侦测

什么是变化侦测

运行时,内部状态可能会发生变化,相应页面要重新渲染,变化侦测就是弄清楚是哪里发生了变化。
从实现的方案上分为两种:react(虚拟DOM)和angular(脏检查)使用的是“拉”的方案,vue使用的是“推”的方案。
拉的方案实际上不知道是哪里发生的变化,要通过暴力对比的方法来查找。推的方案就不同了,它实际是知道哪里发生了变化,可以进行更“细”的更新,缺点就是随着粒度的变细,每个状态所绑定的依赖就越多,依赖追踪的内存开销就越大。
Vue2.0开始引入了虚拟DOM,变成了一个折中的方案,状态绑定的不再是DOM节点而是组件。当状态发生变化通知到组件一级,组件内再进行虚拟DOM对比。

如何追踪变化
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;
        }
    })
}

用Object.defineProperty就可以解决这个问题,每当data中的状态被读取,get函数就会触发,每当状态被修改就触发set函数。
由于浏览器对ES6的支持不理想,暂时选择了这样的方案。下一个版本会使用Proxy来重写这里。

如何收集依赖
<template>
    <h1>{{name}}</h1>
</template>

对于这样一个对data.name的引用当如何处理呢?
Vue2.0中,模板使用数据等同组件使用数据,所以当数据发生变化只会通知到组件一级。
看看上面Object.defineProperty的部分,在getter收集依赖,在setter触发依赖就好了

依赖收集在哪里

我们给每一个key分配一个数组存放依赖就好了,假设依赖是一个函数,就存在window.target上,要怎么丰满我们的defineReactive呢?

function defineReactive(data, key, val){
    let dep = [];
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function(){
            dep.push(window.target) // 新增
            return val;
        },
        set: function(newVal){
            if(val === newVal){
                return;
            }
            // 新增
            for(let i = 0;i < dep.length;i++){
                dep[8i](newVal, val);
            }
            val = newVal;
        }
    })
}

但是这样写有点耦合,我们可以把收集依赖的部分封装成一个Dep类,为我们管理依赖。

export.default = class Dep{
    constructor(){
        this.subs = [];
    }
    addSub(sub){
        this.subs.push(sub);
    }
    removeSub(sub){
        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++){
            sub[i].update();
        }
    }
}
function remove(arr, item){
    if(arr.length){
        const index = arr.indexOf(item);
        if(index > -1){
            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 val;
        },
        set: function(newVal){
            if(val === newVal){
                return;
            }
            dep.notify(); // 修改
            val = newVal;
        }
    })
}
依赖是谁

依赖就是window.target,window.target是啥?核心问题就是当状态发生变化的时候通知谁?
要通知用到这个状态的地方,可能是模板也可能是一个用户写的watch。所以可以抽象出一个类,在状态改变的时候通知到收集到的类实例,由它们再通知到确实的地方。给这个类起一个名字,Watcher

什么是Watcher

先看一个经典的使用场景。

// keypath
vm.$watch('a.b.c', function(newVal, oldVal){
    // 做点什么
})

像上面这样写了之后,我们希望data.a.b.c发生变化触发回调函数,如何实现这个功能呢?
将这个依赖实例Watcher添加到data.a.b.c的Dep中,当状态变化时,通知这个实例,它再触发回调函数。
开始实现Watcher吧。

export default class Watcher{
    constructor(vm, expOrFn, cb){
        this.vm = vm;
        // 执行this.getter(),就可以获取到data.a.b.c的内容
        this.getter = parsePath(expOrFn);
        this.cb = cb;
        this.value = value;
    }
    get(){
        window.target = this;
        let value = this.getter.call(this.vm, this.vm);
        window.target = undefined;
        return value;
    }
    update(){
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this.value, oldValue);
    }
}

实例化一个这样的类,它就把自身主动添加到data.a.b.c的Dep中,神奇吧!关注一下get方法的过程,先将实例赋值给window.target,然后用getter方法读取一次data.a.b.c,就把实例主动添加到data.a.b.c的Dep中了,最后window.target赋undefined。
parsePath是怎样生成读取指定状态的函数的呢?

const bailRE = /[^\w.$]/
export function parsePath(path){
    if(bailRE.test(path)){
        return;
    }
    const segments = path.split('.');
    return function(obj){
        if(!obj)return;
        for(let i = 0;i < segments.length;i++){
            obj = obj[segments[i]];
        }
        return obj;
    }
}

就是以.分割返回的函数会从实例里一层层取值。

递归侦测所有的key

前面实现了一个属性的变化侦测,我们希望用一个类,将所有属性(包括子属性)都转换成getter/setter形式,然后侦测变化,叫它Observer好奇查了一下英文,惊喜,观察者。。。。。(模式两个字没脱口)

export class Observer{
    constructor(value){
        this.value = value;
        if(!Array.isArray(value)){
            this.walk(value);
        }
    }
    walk(obj){
        const keys = obj.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;
            }
            dep.notify();
            val = newVal;
        }
    })
}

在原有的defineReactive加入了递归生成Observer实例的过程。这样给Observer传入一个obj,obj就变成响应式的了。

关于Object的问题

对于对象(不讲数组)添加属性和用delete字段删除属性,将成为漏网之鱼,并不会添加和移除这些属性的侦测。也就是说多数情况下,响应式的内容开始就定下来了,除非调用特定方法(vm.set和vm.delete)。
这种局面也是没有办法的事,引入proxy可能会改观吧。

总结
Data、Observer、Dep和Watcher之间的关系.png

本文是参照《深入浅出Vue.js》,老实说本白能力有限,拜读第二遍才清楚作者要表达的意思,第一遍模模糊糊。
关于看源码方面,引用左少的话‘收起你野马般的思绪’,忍不住会像如果参数是那样的就不对了,不公开的方法设计的人当然按设计意图小心使用,而且会报错的工具就不是好工具了吗?

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 回忆 Observe观察者(建立响应式对象) 概括:它给数据通过defineProperty进行响应式化。...
    LoveBugs_King阅读 5,878评论 1 1
  • 前言 Vue.js 的核心包括一套“响应式系统”。 “响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码...
    NARUTO_86阅读 37,693评论 8 86
  • 这方面的文章很多,但是我感觉很多写的比较抽象,本文会通过举例更详细的解释。(此文面向的Vue新手们,如果你是个大牛...
    Ivy_2016阅读 15,465评论 8 64
  • 前言 vuex作为vue官方出品的状态管理框架,以及其简单API设计、便捷的开发工具支持,在中大型的vue项目中得...
    Kaku_fe阅读 112,159评论 22 147
  • 自我介绍: 姓名:金雨荷️ 性别:和妈妈一样 外貌:双眼皮,有两颗“兔子牙”,鼻子不大不小 ...
    金雨荷阅读 641评论 1 6