Object的变化侦测

1.1 什么是变化侦测

vue.js会自动通过状态生成DOM,并将其输出到页面显示,这个过程叫渲染。vue.js的渲染过程是生命式的,我们通过模板来描述状态和DOM之间的映射关系。
通常,在运行时应用内部的状态会不断发生变化,此时需要不停的重新渲染,这时候如何确定状态发生了变化?它分为两种类型:一种是“推”(push),另一种是“拉”(pull)
Angular和React中的变化侦测都属于“拉”,这就是说当状态发生变化时,它不知道那个状态变了,只知道状态有可能变了,然后会发送一个信号给框架,框架内部收到信号后,会进行暴力对比来找出哪些DOM节点需要重新渲染。这在Angular中是脏检查的流程,在React中使用的是虚拟DOM,
而Vue.js的变化测试属于“推”,当状态发生变化时候,Vue.js立刻就知道,而且在一定程度上知道哪些状态变了。因此,它知道的信息更多,也就可以进行更细粒度的更新。
所谓更细粒度的更新,就是说:假如有一个状态绑定了好多个依赖,每个依赖表示一个具体的DOM节点,那么当这个状态发生变化时,像这个状态的所有依赖发送通知,让他们进行DOM更新操作,相比较而言,“拉”的粒度时最粗的。
但是它也有一定的代价,因为粒度越细,每个状态绑定的依赖也就越多,依赖追踪在内存上的消耗也就越大,因此,在Vue.js2.0中,它引入了虚拟DOM,将粒度调整为中等粒度,即一个状态所绑定的依赖不再是具体的某个DOM节点,而是一个组件,这样状态变化后,会通知到组件,组件内部在使用虚拟DOM进行对比,这样可以降低依赖的数量,从而降低内存消耗。

1.2 如何追踪变化

1.Object.defineProperty
2.ES6的Proxy

知道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;
        }
    })
}

这里的函数defineDirective用来对Object.defineProperty进行封装,从函数的名字可以看书,其作用是定义一个响应式数据,也就是在这个函数中进行变化追踪,封装后只需要传递data,key,val即可。
封装好之后每当从data的key中读取数据的时候,get被触发,每当往data的key中设置数据,set函数触发。

1.3 如何收集依赖?

之所以要观察数据,其目的是当数据的属性发生变化是,可以通知那些曾经使用了该数据的地方。
举个例子

<template>
    <h1>{{name}}</h1>
</template>

该模板中使用了数据name,所以当它发生变化时,要向使用它的地方发送通知
注意:在Vue.js 2.0中,模板使用数据等于组件使用数据,所以当数据发生变化时,会通知发送到组件,然后组件内部在通过虚拟DOM重新渲染。
对于上面的问题,我的回答是,先收集依赖,即把用到数据name的地方收集起来,然后等属性发生变化时候,把收集好的依赖循环触发一下。
总结起来就是: 在getter中收集依赖,在setter中触发依赖。

1.4 依赖收集在哪里

现在我们已经有了很明确的目标,就是在getter中收集依赖,那么要把依赖收集到哪里去?
思考一下,说先想到的是每个key都有一个数组,用来存储当前key的依赖,假设依赖时一个函数,保存在window.target上,现在就可以吧defindReactive函数稍微该着一下:

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[i](newVal, val)
            }
            val = newVal;
        }
    })
}

这里我们新增了数组dep,用来存储被收集的依赖。
然后在set被触发时,循环dep以触发收集到的依赖。
这样写有点耦合,我们把依赖收集的代码封装成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++) {
            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 val;
        },
        set: function (newVal) {
            if (val === newVal){
                return;
            }
            val = newVal;
            dep.notify(); // 修改
        }
    })
}

此时,代码收集到Dep中!

1.5 依赖是谁

从上面的代码中,我们收集的依赖时window.target,那么它到底是什么?我们究竟要收集谁呢?
收集谁?换句话说,就是当属性发生变化后,通知谁。
我们要通知到用到数据的地方,而是用这个数据的地方有很多,而且类型还不一样,即有可能是模板,也有可能是用户䘏的一个watch,这是需要抽象出一个能集中处理这些情况的类,然后我们在依赖收集阶段只收集封装好的类的实例进来,通知也只通知他一个,接着,他在负责通知其他地方。所以,我们要抽象这个动作需要掀起一个名字,就是Watcher。
总结:收集谁?Watcher!

1.6 什么是Watcher

Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
关于Watcher,先看一个经典的使用方式:

// keypath
vm.$watch('a.b.c', function(newVal, oldVal){
    // todo
})

这段代码表示当data.a.b.c属性发生变化时,触发第二个参数中的函数
怎么实现这个功能?好像只要把这个watcher实例添加到data.a.b.c属性的Dep中就行,然后当data.a.b.c的值发生变化时,通知Watcher。接着,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 = this.get()
    }

    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设置成了this,也就是当前watcher实例,然后在读一下data.a.b.c的值,这肯定会触发getter。
触发了getter,就会触发收集依赖的逻辑,上面已经介绍了,会从window.target中读取一个依赖并添加到Dep中。
这就导致,只要现在window.target赋一个this,然后再读一下值,去触发getter,就可以把this主动添加到keypath中的Dep。
依赖注入到Dep中后,每当data.a.b.c的值发生变化时,就会让依赖列表中所欲的依赖循环触发update方法,也就是Watcher中的update方法,而update方法会执行参数中回调函数,将value和oldValue传到参数中。
所以,其实不管是用户执行vm.$watch('a.b.c',(value, oldValue) => {}),还是模板中用到的data,都是通过Watcher来通知自己是否需要发生变化。

/**
 * 解析简单路径
 * */
const bailRe = /[^\w.$]/
export 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]];
        }
        return obj;
    }
}

可以看到,这其实并不复杂,现将keypath,用.分割成数组,然后循环数组一层一层去读取数据,最后拿到的obj就是keypath中想要的数据。

1.7 递归侦测所有key

现在其实已经可以实现变化侦测的功能了,但是前面介绍的代码只能侦测数据的某一个属性,我们希望吧数据中的所有属性包括子属性都侦测到,所以要封装一个Observer类,这个类的作用是将一个数据内的所有属性包括子属性都转化成getter/setter的形式,然后去追踪他们的变化:

/**
 * Observer类会附加到每一个被侦测的object上
 * 一旦被附加上,Observer会将object的所有属性转化为getter/setter的形式
 * 来收集属性的依赖,并且当属性发生变化时会通知这些依赖
 * */
export class Observer{
    constructor(value) {
        this.value = value;
        if (!Array.isArray(value)){
            this.walk(value)
        }
    }
    
    /**
     * walk会将每一个属性都转化为getter/setter的形式来侦测变化
     * 这个方法只有在数据类型为Object时候被调用
     * */
    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(); // 修改
        }
    })
}

从上面代码中,我们定义了Observer类,让他用来讲一个正常的object转化为被侦测的object,然后去判断你数据的类型,只有object数据类型才会调用getter/setter的形式来侦测变化,最后在defineReactive中新增new Observer(val)来递归子属性,这样我们就可以把data中的所有属性(包括子属性)都转化为getter/setter的形式来侦测变化
当data中的属性发生变化时,与这个属性对应得依赖就会收到通知,也就是说我们将一个object传到Observer中,那么这个object就会变成响应式的object。

1.8 关于Object的问题

前面介绍了object类型的变化侦测原理,了解了数据变化时通过getter/setter来追踪变化的,也正是由于这种追踪方式,有些语法即便发生了变化,Vue.js也追踪不到,比如像object添加属性:

var vm = new Vue({
    el: '#el',
    template: '#demo',
    methods: {
      action() {
          this.obj.name = 'test';
      }  
    },
    data: {
        obj: {}
    }
})

在action方法中,我们在obj上面新增了name属性,Vue.js无法侦测到这个变化,所以不会向依赖发送通知

var vm = new Vue({
    el: '#el',
    template: '#demo',
    methods: {
        action() {
            delete this.obj.name
        }
    },
    data: {
        obj: {
            name: 'test'
        }
    }
})

在action方法中,我们在obj上面删除了name属性,Vue.js无法侦测到这个变化,所以不会向依赖发送通知
Vue.js 通过Object.defineProperty来将对象的key转化成getter/setter的形式追踪变化,只能追踪到是否被修改,无法追踪到新属性和删除属性,所以vue.js提供了两个api,vm.set和vm.delete。

1.9总结

1.变化侦测就是侦测数据的变化,当数据发生变化时,要能侦测到并发出通知。
2.Object可以通过Object.defineProperty将属性转化为getter/setter的形式来追踪变化,读取数据时用getter,修改数据时用setter。
3.我们需要在getter中收集哪些依赖使用了数据,当setter被触发时,就去通知getter中收集的依赖数据发生变化。
4.收集依赖需要为依赖找一个存储的地方,为此我们创建了Dep,它用来收集依赖,删除依赖和向依赖发送通知。
5.所谓的依赖,其实就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍.
Watcher的原理就是先把自己设置到全局唯一的指定位置,例如window.target。然后读取数据,因为读取了数据,所以会触发数据的getter,在getter中就会从全局唯一的那个位置读当前正在读取数据的Watcher,并把这个Watcher收集到Dep中去,通过这样的方式,Watcher可以主动的去订阅任意一个数据的变化。
6.此外,我们创建了Observer类,它的作用是吧一个object中的数据,包括子数据都转化成响应式的,也就是他会侦测object的所有数据的变化。

文章出自vue.js深入浅出一书

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,076评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,658评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,732评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,493评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,591评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,598评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,601评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,348评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,797评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,114评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,278评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,953评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,585评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,202评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,180评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,139评论 2 352

推荐阅读更多精彩内容

  • 这方面的文章很多,但是我感觉很多写的比较抽象,本文会通过举例更详细的解释。(此文面向的Vue新手们,如果你是个大牛...
    Ivy_2016阅读 15,385评论 8 64
  • vue 简介 渐进式框架:就是把框架分层。最核心的是视图层渲染,然后往外是组件机制,在这个基础上加入路由机制,再加...
    zx_lau阅读 715评论 0 3
  • 摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归...
    Fundebug阅读 5,491评论 0 9
  • 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你...
    浪里行舟阅读 1,995评论 0 16
  • 2019.3.18 星期一 晴 今天是孩子分班第一天,也是校内课后托管第一天。接她回家路上和她聊了会,我...
    厦门路小学邵艺馨妈妈阅读 76评论 0 0