Vue2源码解析 Object变化监听

目录

1 什么是变化监听

2 如何跟踪变化

3  何如收集依赖

4  依赖收集在哪

5  依赖是谁

6  什么是Watcher

7  递归侦测所有key

8  关于Object的问题

9  总结


1 什么是变化监听

 vue会自动通过状态生成dom,并将其输出到页面上显示出来,整个过程叫做渲染;

vue2.0开始,引入了虚拟dom,将粒度调整为中等粒度,即一个状态所绑定的依赖不再是具体dom节点,而是一个组件。这样状态变化后,会通知到组件,组件内部再使用虚拟dom进行对比。这样可以大大降低依赖数量,从而降低依赖跟踪所消耗的内存。

2 如何跟踪变化

 有两种方法可侦测到Object的变化:1.Object.defineProperty;2.es6的Proxy;

vue中使用的是Object.defineProperty,对Object.defineProperty进行封装,具体代码如下:

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            // 每当从data的key中读取数据时,get函数被触发
            return val
        },
        set: function (newVal) {
            // 每当往data的key中设置数据时,set函数被触发
            if (val == newVal) {
                return
            }
            val = newVal
        },
    })
}

3  何如收集依赖

先收集依赖,即把用到数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍;

总结起来就一句话,在getter中收集依赖,在setter中触发依赖。

注意:在vue2.0中,模板使用数据等于组件使用数据,所以当数据发生变化时,会将通知发送给组件,然后组件内部再通知虚拟DOM重新渲染。

4  依赖收集在哪

每个key都有一个数组,用来存储当前key的依赖;新增数组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, l = subs.length; i < l; 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() // 新增
        },
    })
}

5  依赖是谁

收集谁,换句话说,就是当属性发生变化后,通知谁;

我们要通知用到数据数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个。接着,它再负责通知其它地方。这个类叫Watcher实例

6  什么是Watcher

Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其它地方。

关于Watcher,先看一个经典的案例:

vm.$watch('a.b.c', function (newVal, oldVal) {
    // to do
    /**
     * 原理:把watcher实例添加到data.a.b.c属性的Dep中就行了;
     *      然后,当data.a.b.c的值发生改变时,通知Watcher;
     *      接着,Watcher再执行参数中的这个回调函数
     */
})

思考一下怎样实现这个功能的呢?只要把这个Watcher实例添加到data.a.b.c属性的Dep中就行了。然后,当data.a.b.c的值发生变化时,通知Watcher。接着,Watcher再执行参数中的这个回调函数

代码如下:

export class Watcher {
    constructor(vm, expOrFn, cb) {
        this.vm = vm
        // 执行this.getter(),就可以读取data.a.b.c的内容
        // parsePath的实现:解析简单路径,先把path用split进行切割
        //                 然后再循环数组一层一层的读取
        this.getter = parsePath(expOrFn)
        this.cb = cb
        this.value = this.get()
    }
    get() {
        /**
         * 会自己主动添加到data.a.b.c的Dep中去
         * 在get方法中先把window.target设置成了this,也就是当前watch实例,
         * 然后再读一下data.a.b.c的值,这肯定会触发getter;
         * 触发了getter,就会触发收集依赖的逻辑;
         * 依赖收集,会从window.target中读取一个依赖并添加到Dep中;
         */
        window.target = this
        let value = this.getter.call(this.vm, this.vm)
        window.target = undefined
        return value
    }
    update() {
        /**
         * 依赖注入到Dep中后,每当data.a.b.c的值发生变化时,就会让依赖列表中所有的依赖循环
         * 触发update(),也就是Watcher中的update();
         * 而update方法会执行参数中的回调函数,将value和oldValue传到参数中
         */
        const oldVal = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldVal)
    }
}

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()
        },
    })
}

8  关于Object的问题

/**
 * 关于Object的问题:
 * vue通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,
 * 但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性
 * 这也是没办法的事,es6之前,js没提供元编程的能力,无法侦测到属性的新增或删除
 * vue.js提供了两个API——vm.$set和vm.$delete
 */
let vm = new Vue({
    el: '#el',
    methods: {
        action() {
            // this.obj.name = "myName" 或
            delete this.obj.name
            // 以上两种方式都无法侦听到变化,所以不会向依赖发送通知
        },
    },
    data: {
        // obj: {}, 或
        obj: { name: 'myName' },
    },
})

9  总结

 Object可以通过Object.defineProperty将属性转换成getter/setter的形式来追踪变化。读取数据时会触发getter,修改数据时会触发setter。

收集依赖需要为依赖找一个存储依赖的地方,此为我们创建了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、Dep和Watcher之间的关系,如下图所示:

         a.Data通过Observer转换成了getter/setter的形式来追踪变化;

        b.当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中;

        c.当数据发生了变化时,会触发setter,从而向Dep中依赖(Watcher)发送通知;

        d.Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。

注:本文章来自于《深入浅出vue.js》(人民邮电出版社)阅读后的笔记整理

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值