尝鲜Vue3之六:响应式原理的革新 - Vue2、Vue3实现对比

4,223 阅读4分钟

目录

响应式是什么

首先我们说说什么是响应式。通过某种方法可以达到数据变了可以自由定义对应的响应就叫响应式。

具体到我们MVVM中 ViewModel的需要就是数据变了需要视图作出响应。 如果用Jest用例便表示就是这样

    it('测试数据改变时 是否被响应', () => {
        const data = reactive({
            name: 'abc',
            age: {
                n: 5
            }
        })
        // Mock一个响应函数
        const fn = jest.fn()
        const result = fn()

        // 设置响应函数
        effect(fn)

        // 改变数据
        data.name = 'efg'

        // 确认fn生效
        expect(fn).toBeCalled()
    })

假定我们需要的是数据data变化时可以触发fn函数也就是作出相应,当然相应一般是触发视图更新当然也可以不是。我们这里面用jest做了一个Mock函数来检测是否作出相应。

最后代码expect(fn).toBeCalled()有效即代表测试通过也就是作出了相应

Vue2的解决方案

下面展示的是vue2的实现方式是通过Object.defineProperty来重新定义getter,setter方法实现的。

let effective
function effect(fun) {
    effective = fun
}

function reactive(data) {
    if (typeof data !== 'object' || data === null) {
        return data
    }


    Object.keys(data).forEach(function (key) {
        let value = data[key]
        Object.defineProperty(data, key, {
            emumerable: false,
            configurable: true,
            get: () => {
                return value
            },
            set: newVal => {
                if (newVal !== value) {
                    effective()
                    value = newVal
                }
            }
        })

    })
    return data
}

module.exports = {
    effect, reactive
}

当然还有两个重要的问题需要处理 第一个就是这样做只能做浅层响应 也就是如果是第二层就不行了。

it('测试多层数据中改变时 是否被响应', () => {
        const data = reactive({
            age: {
                n: 5
            }
        })
        // Mock一个响应函数
        const fn = jest.fn()

        // 设置响应函数
        effect(fn)

        // 改变多层数据
        data.age.n = 1

        // 确认fn生效
        expect(fn).toBeCalled()
    })

比如以下用例 就过不去了 当然解决的办法是有的 递归调用就好了

当然这样也递归也带来了性能上的极大损失 这个大家先记住。

然后是数组问题 数组问题我们可以通过函数劫持的方式解决

const oldArrayPrototype = Array.prototype
const proto = Object.create(oldArrayPrototype);

['push','pop','shift','unshift','splice','sort','reverse'].forEach(method => {
    
    // 函数劫持
    proto[method] = function(){
        effective()
        oldArrayPrototype[method].call(this,...arguments)
    }
})
// 数组通过数据劫持提供响应式
if(Array.isArray(data)){
    data.__proto__ = proto
}

Vue3

新版的Vue3使用ES6的Proxy方式来解决这个问题。之前遇到的两个问题就简单的多了。首先Proxy是支持数组的也就是数组是不需要做特别的代码的。对于深层监听也不不必要使用递归的方式解决。当get是判断值为对象时将对象做响应式处理返回就可以了。大家想想这个并不不是发生在初始化的时候而是设置值得时候当然性能上得到很大的提升。

function reactive(data) {
    if (typeof data !== 'object' || data === null) {
        return data
    }
    const observed = new Proxy(data, {
        get(target, key, receiver) {
            // Reflect有返回值不报错
            let result = Reflect.get(target, key, receiver)

            // 多层代理
            return typeof result !== 'object' ? result : reactive(result) 
        },
        set(target, key, value, receiver) {
            effective()
            // proxy + reflect
            const ret = Reflect.set(target, key, value, receiver)
            return ret
        },

        deleteProperty(target,key){
            const ret = Reflect.deleteProperty(target,key)
            return ret
        }

    })
    return observed
}

当然目前还是优缺点的缺点,比如兼容性问题目前IE11就不支持Proxy。不过相信ES6的全面支持已经是不可逆转的趋势了,这都不是事。

为了对比理解Vue2、3的响应式实现的不同我把两种实现都写了一下,并且配上了jest测试。大家可以参考一下 github.com/su37josephx…

// clone代码
yarn
npx jest reactivity-demo

目录