看完这个我不信你还不理解Vue2的数据响应式如何实现的!!(对象篇)

87 阅读7分钟

building_town_historic_europe-99049.jpg

前言

我在学习Vue2时,学到数据响应式时,一直很疑惑底层具体到底怎么实现的,我知道是用到了Object.definedProperty(),但是也只是知道的迷迷糊糊的,所以我觉得很多刚刚接触Vue的小伙伴也一定有这样的疑惑,所以我决定写下这篇文章,一是让自己印象更深刻,二是希望对小伙伴们有点帮助,最后也希望看到这篇文章的小伙伴,觉得我说的不对的,可以帮我纠正。

Object.definedProperty()

Vue2的数据响应式原理一定离不开Object.definedProperty(),所以,我们先来看一下 Object.definedProperty() 到底是个什么东西。

其实从翻译角度看也可以看出来这是在为一个对象定义一个属性,具体怎么使用,我们来举个栗子

       const data = {}
        Object.defineProperty(data, 'name', {
            value: 'RachelLenyan'
        })

在这里,我们定义了一个对象,里面没有任何属性,然后我们使用Object.definedProperty()给data对象设置了一个name属性,值为value的后面的值,可以看到控制台可以打印出对应的属性值

那设置属性直接**data.name = ‘RachelLenya’**不行吗,干嘛费老大劲去整这出,这个Object.defineProperty肯定不止就设置个属性值,那我们再看看还可以这么配置,其实这个部分看MDN也很快也很简单的

        const data = {
            age: 19,
            major: 'student'
        }
        Object.defineProperty(data, 'name', {
            value: 'RachelLenyan',
            enumerable: false
        })

在这里我们只加了一个enumerable : false,就可以看到刚刚给data添加的属性无法被枚举,在这里我也不赘述了,直接一次性贴出来吧, 或者MDN上也说的很清楚

::: block-1

配置的属性

configurable 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为 false。

enumerable 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。 默认为 false。 数据描述符还具有以下可选键值:

value 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为 undefined。

writable 当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。 默认为 false。 ::: 当然这不是特别重点的地方,主要是我们可以实现对对象属性的数据劫持

数据劫持

那什么是数据劫持呢,我们可以简单想象一下,你去超市买个个东西,本来要送回家的,结果半路被你的冤种闺蜜劫走了,本来要送回家的东西,送到了他的肚子里,这不就是劫持吗,那数据劫持不就是对数据进行抢劫吗。

说是说明白了,直接上代码瞅一下吧

        const data = {
            name: 'xxx'
        }
        Object.defineProperty(data, 'name', {
            get () {
                return '你的名字被我抢了'
            }
        })

无论我原先的name属性是什么,在我访问属性时,这个‘xxx’就快要给我了,结果被get(){} 方法劫持了,然后返回一个新的属性值

与之对应的还有一个 set(newVal){} 方法,当你修改对应的属性的属性值时,set(newVal){} 就会被调用,它会拿到你修改的那个属性值,然后内部代码怎么写就是看你自己了。

在这里是不是有一点思路了,我要是在set(){}里面把新拿到的值给这属性,然后在get(){} 里面返回这个属性的属性值不就好了吗,哇,原来响应式这么简单哇,于是我高高兴兴的试了一下

        const data = {
            name: 'xxx'
        }
        Object.defineProperty(data, 'name', {
            get () {
                return data.name
            },
            set (newVal) {
                data.name = newVal
            }
        })

好家伙,这一片红看的是心烦意乱啊,这是怎么回事呢,来,我们好好分析一下哈。我在get里面返回了data.name,那data.name的意思不就是要这个属性值吗,于是又跑到get函数里,再返回再调用...无限递归最后爆栈。set函数也是一样的,一直在修改data.name属性就一直访问set,最后爆栈(执行栈的概念可以去了解一下),这个可咋解决啊。

于是冥思苦想,诶!,我搞个中转变量不就好了吗,于是乎...

        const data = {
            name: 'xxx'
        }
        let tmp = data.name
        Object.defineProperty(data, 'name', {
            get () {
                return tmp
            },
            set (newVal) {
                tmp = newVal
            }
        })

欸!成了!第一步成功了,可这Vue也不是一个一个给对象属性来个Object.defineProperty啊,那肯定得封装一个函数吧,来吧,试一下

        const data = {
            name: 'xxx'
        }
        function defineReactive (target, key, val) {
            if (arguments.length === 2) val = target[key]
            Object.defineProperty(target, key, {
                get () {
                    console.log('get')
                    return val
                },
                set (newVal) {
                    console.log('set')
                    if (newVal === val) return
                    val = newVal
                }
            })
        }

        defineReactive(data, 'name')

咱把刚刚的中间变量变成函数的val形参,到这好像还是这么回事哈,我确实实现了数据的监测吧。

递归实现数据监测

可Vue肯定也不能一个一个给对象属性都调一次defineReactive吧,那再封装一个函数可不好使了,也没有代码可以封装了,这可咋整,欸!那这对象的属性我全拿出来再遍历给他们一个一个都加个defineReactive,这可不就解决了吗,开干!

        const data = {
            name: 'xxx',
            age: 19,
            major: 'student'
        }
        function defineReactive (target, key, val) {
            if (arguments.length === 2) val = target[key]
            Object.defineProperty(target, key, {
                get () {
                    console.log('get')
                    return val
                },
                set (newVal) {
                    console.log('set')
                    if (newVal === val) return
                    val = newVal
                }
            })
        }
        function Observer (value) {
            const keys = Object.keys(value)
            keys.forEach(el => {
                defineReactive(value, el)
            })
        }

        Observer(data)

一看结果,这可太棒了啊,我这每一个属性都有get和set为它们服务,这不就成功了吗,可这Vue可以实现多层的数据监测啊,我这好像只能最外面一层实现呢,那这我擅长啊,递归呗,直接完活啊

      const data = {
            name: 'xxx',
            age: 19,
            major: 'student',
            friends: {
                f1: {
                    name: 'lz',
                    age: 20
                },
                f2: {
                    name: 'zl',
                    age: 21
                }
            }
        }
        function defineReactive (target, key, val) {
            if (arguments.length === 2) val = target[key]
            Object.defineProperty(target, key, {
                get () {
                    console.log('get')
                    return val
                },
                set (newVal) {
                    console.log('set')
                    if (newVal === val) return
                    val = newVal
                }
            })
        }
        function Observer (value) {
            const keys = Object.keys(value)
            keys.forEach(el => {
                if (value[el].constructor === Object) Observer(value[el])
                defineReactive(value, el)
            })
        }

        Observer(data)

一看这截图,这不是妥妥的响应式数据了吗,虽然我们现阶段只考虑对象哈,但是效果好像确实达到了啊,思维应该没啥错,但是我们得写的高级点啊,来吧,干活!

最后的代码优化

刚刚我们实现的雏形是不是很像在每个数据身上安个监控,那既然这样就单独给它一个监控的属性 ob,所以我们把Observer写成一个类。那么要添加一个属性,不就又是我们的老朋友了吗,那么这个属性我们肯定不希望它可以被枚举到,所以封装一个def函数,看代码!

function def (obj, key, value, enumerable) {
            Object.defineProperty(obj, key, {
                value,
                enumerable,
                writable: true,
                configurable: true
            })
        }
        
class Observer {
            constructor(value) {
                def(value, '__ob__', this, false)
                this.walk(value)
            }
            walk (value) {
                const keys = Object.keys(value)
                keys.forEach(el => {
                    observe(value[el])
                    defineReactive(value, el)
                })
            }
        }

或许你还是不太懂,没有关系,我们仔仔细细来捋一遍,之前写的Observer()函数就是用来给每个属性添加一个监控对吧,那么我们现在单独给一个__ob__用来放置这个监控(看到后面会知道为什么给个__ob__属性,现在不纠结),那么我们把这个函数重新写成一个类,这个类里面无非就是多了一块def()函数的调用(先不管下面的observe函数),这个def函数就是给这个属性加个监控,值为这个类的实例。然后就执行刚才一样的操作了,遍历对象让每个数据都是响应式的。

那么这个observe函数其实也是很简单的,你一看就能懂!

function observe (value) {
            if (value.constructor !== Object) return
            let ob
            if (typeof value.__ob__ !== 'undefined') {
                ob = value.__ob__
            } else {
                new Observer(value)
            }

            return ob
        }

如果某个属性身上已经有监控了,就不必再安一个监控了,直接赋值返回一顿操作,如果没有就去调用Observer对吧,给每个属性都安一个监控。

结语

因为我也还是学习阶段,希望我的理解可以帮助到刚刚接触vue的小伙伴们,也希望大家可以指出我理解中的错误,让我更上一层楼啦!😊😊😊😊