Vue的Object变化侦测原理

本文所有内容来自《深入浅出Vue.js》的读书笔记

  • 什么是变换侦测
  • 如何追踪变化
  • 如何搜集依赖
  • 搜集依赖存储在哪里
  • 依赖是谁
  • 什么是watcher
  • 递归侦测所有key
  • Object的问题
  • 总结

什么是变化侦测

vue.js会通过状态生成DOM,并将其输出到页面上显示出来,这个过程称为渲染。

vue.js的渲染过程是声明式的,通过模板来描述状态和DOM节点之间的映射关系

如何追踪变化

js中有两种方法可以检测到变化:

  • 基于Object.defineProperty
  • 基于ES6的proxy
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函数将被触发。

如何搜集依赖

该功能的需求为,当我们通过模板的方式绑定数据后,当数据的属性发生了变化后,我们期望通知所有依赖该数据的地方进行重新渲染,因此,可以在getter中搜集依赖,在setter中触发依赖。

搜集的依赖存储在哪里

思考每个对象的属性key,对于一个key,可以有很多的依赖,此时我们可以使用数组来存储关于某key的依赖:

function defineReactive(data, key, val) {
    let dep = [] // 新增
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            dep.push(widnow.target) // 新增
            return val
        },
        set: function(newVal) {
            if (val === newVal) {
                return
            }

            // 循环dep以触发收集到的依赖
            for(let i = 0; i < dep.length; i++) {
                dep[i](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, 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进行改造:

import Dep from "./dep"

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的对象来保存当前所有的依赖;
那么接下来,既然已经搜集到这些依赖了,当key的属性值发生了变化,如何感知呢?也就是需要一个类似“哨兵”的角色去接收这些变化通知。

然而,用到这个数据的地方有很多,我们需要抽象出来一个类,来负责集中处理,再由这个类去通知到其他的地方,这就是Watcher

什么是watcher

watcher是一个中介的角色,当数据发生了变化,通知给它,然后它再继续通知到其他的地方。有一个经典的使用方式:

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

上述的代码的含义为,当属性a.b.c的值发生了变化就执行第二参数中的回调函数。
那么这个功能应该如何实现?

只要把这个watcher实例添加到data.a.b.c的Dep中就可以了。

export default class Watcher {
    constructor(vm, expOrFn, cb) {
        this.vm = vm // 当前实例
        this.getter = parsePath(expOrFn)  // 解析表达式返回获取key的值的函数
        this.cb = cb // 回调函数
        this.value = this.get() // 调用get() 触发get函数,将本依赖绑定到Dep对象上
    }

    get() {
        window.target = this // 先把window.target 指向了当前实例

        // 其次基于call调用getter函数
        let value = this.getter.call(this.vm, this.vm) 

        window.target = undefined // 重新把window.target 设置为undefined
        return value // 返回当前的值
    }

    update() {
        const oldValue = this.value
        this.value = this.get() // 重新调用get()函数触发增加依赖的逻辑!
        this.cb.call(this.vm, this.value, oldValue) // 利用回调函数来通知值发生了变化!
    }
}

const bailRE = /[^\w.$]/
export function parsePath(path) {
    // 校验是否符合a.b.c的格式
    if(bailRE.test(path)) {
        return
    }
    const segments = path.split(".")
    return function(obj) {
        for(let i = 0; l < segments.length; i++) {
            if(!obj) {
                return
            }
            obj = obj[segments[i]]
        }
        return obj
    }
}

上面的代码有一处设计的比较巧妙,在get()方法中,通过读取a.b.c属性的值,触发了getter函数中的逻辑(增加数据依赖,添加到Dep中)

递归侦测所有key

前面的代码已经能够实现变化侦测的功能了,但是只能侦测数据中某一个属性,而实际的开发需求中需要侦测所有的属性,还包括子属性。此时需要对Observer加以改造:


export 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() // 修改
        }
    })
}

defineReactive中新增new Observer(val)来递归子属性

Object的问题

关于Object类型的数据变化,是通过getter/setter来追踪的,但是由于这种方式,有些语法中即便是数据发生了变化,vue.js是无法追踪到的。
例如向object中添加属性:

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

又例如从object中删除一个属性:

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

在ES6之前,JavaScript没有提供元编程的能力,无法监听到一个新属性被添加到了对象中,同时无法监听到一个属性被移除。基于此,vue提供了两个APIvm.$setvm.$delete来实现。

总结

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值