【笔记】怎样像vue2.x中让对象和数组变成响应式

93 阅读5分钟

说道vue我们先来说一说MVVM,一句话总结,数据驱动视图。

image.png

那MVVM里面的第一个字母M就是model,数据。也就是vue中的data。第二个字母V就是View,视图,就是vue中的template。后面的VM比较抽象,可以理解为视图与数据的连接线,比如一些事件点击触发之类。

说到数据驱动视图,那数据怎样驱动视图呢?数据改变了页面怎样才会跟着变化呢?下面我们就用代码举例:

这里主要讲怎样实现数据响应式以至于改变数据触发视图更新,并不会具体讲解视图收到数据改变响应后怎样去更新。

在这里,就不提原始数据类型了。

首先我们写串基础代码:

const data = {
    count: 1
}
function updateView() {
    console.log('视图更新了')
}
data.count = 2

上面这串代码想要达到的目的就是data.count=2后,因为data的值发生了改变,然后给予数据驱动视图这个概念那么久应该触发updateView这个方法。但实际上,你在编辑器中运行一下会发现并没有什么反应。那么,我们就对上面的代码再完善一下:

const data = {
    count: 1
}
function updateView() {
    console.log('视图更新了')
}
function bind(target, key, value) {
    // 实现响应式的核心
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            // 判断是否设置为了相同的值
            if(newValue !== value) {
                value = newValue
                updateView()
            }
        }
    })
}
function watch(target) {
    // 监听对象属性 target就是具体的对象
    // 做一下判断 如果传进来的数据不是对象或者说是null 就直接返回
    if (typeof target !== 'object' || target === null) {
        return target
    }
    // 遍历对象,使其每个属性变成响应式
    for (let key in target) {
        bind(target, key, target[key])
    }
    
}
watch(data)
data.count = 2

然后再运行一下:

image.png 就可以看到视图更新打印出来了。

上面这只是最基础的情况,如果我修改一下data的值:

const data = {
    count: 1
}
function updateView() {
    console.log('视图更新了')
}
data.count = 2

上面这串代码想要达到的目的就是data.count=2后,因为data的值发生了改变,然后给予数据驱动视图这个概念那么久应该触发updateView这个方法。但实际上,你在编辑器中运行一下会发现并没有什么反应。那么,我们就对上面的代码再完善一下:

const data = {
    count: 1,
    info: {
        name: 'zky'
    }
}
function updateView() {
    console.log('视图更新了')
}
function bind(target, key, value) {
    // 实现响应式的核心
    watch(value)
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            // 判断是否设置为了相同的值
            if(newValue !== value) {
                value = newValue
                updateView()
            }
        }
    })
}
function watch(target) {
    // 监听对象属性 target就是具体的对象
    // 做一下判断 如果传进来的数据不是对象或者说是null 就直接返回
    if (typeof target !== 'object' || target === null) {
        return target
    }
    // 遍历对象,使其每个属性变成响应式
    for (let key in target) {
        bind(target, key, target[key])
    }
    
}
watch(data)
data.info.name = 'zzz'

这样把data改为了嵌套对象,就发现又没有视图更新的打印了。其实这里也很简单,就跟你遍历多层级数据一样,递归。

    count: 1,
    info: {
        name: 'zky',
        place: {
            address: 'xx'
        }
    }
}
function updateView() {
    console.log('视图更新了')
}
function bind(target, key, value) {
    // 实现响应式的核心
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            // 判断是否设置为了相同的值
            if(newValue !== value) {
                value = newValue
                updateView()
            }
        }
    })
}
function watch(target) {
    // 监听对象属性 target就是具体的对象
    // 做一下判断 如果传进来的数据不是对象或者说是null 就直接返回
    if (typeof target !== 'object' || target === null) {
        return target
    }
    // 遍历对象,使其每个属性变成响应式
    for (let key in target) {
        bind(target, key, target[key])
    }
    
}
watch(data)
data.info.name = 'zzz'
data.info.place.address = 'oo'

我们修改了两次data中的数据,那么就应该打印两次视图更新。

image.png

这样多层级的对象就完成了响应式。当然这样使用的时候也需要思考一些问题,如果这个初始对象层级很深,会不会有性能影响。

接下来就是数组实现响应式,首先我们还是用上面的例子看下会不会有打印出结果:

const data = {
    count: 1
    arr: [1,2,3]
}
function updateView() {
    console.log('视图更新了')
}
function bind(target, key, value) {
    // 实现响应式的核心
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            // 判断是否设置为了相同的值
            if(newValue !== value) {
                value = newValue
                updateView()
            }
        }
    })
}
function watch(target) {
    // 监听对象属性 target就是具体的对象
    // 做一下判断 如果传进来的数据不是对象或者说是null 就直接返回
    if (typeof target !== 'object' || target === null) {
        return target
    }
    // 遍历对象,使其每个属性变成响应式
    for (let key in target) {
        bind(target, key, target[key])
    }
    
}
watch(data)
data.arr.push(4)

很明显,视图更新又没有触发。监听数组的话会用到一个叫重新定义数组原型的概念,请看代码:

const data = {
    count: 1,
    arr: [1,2,3]
}
function updateView() {
    console.log('视图更新了')
}

const arrayPrototype = Array.prototype
// Object.create创建新对象,原型指向传入的对象,然后可以添加方法并且不会覆盖原型中的方法。
const arrProto = Object.create(arrayPrototype)

// 模拟部分数组中的方法
const methods = ['push', 'pop', 'shift', 'unshift'];
methods.forEach(method => {
    // 重写上面列出的方法对应的函数
    arrProto[method] = function() {
        // 执行array原型中对应方法的操作 然后更新视图
        arrayPrototype[method].call(this, ...arguments)
        updateView()
    }
})

function bind(target, key, value) {
    // 实现响应式的核心
    watch(value)
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            // 判断是否设置为了相同的值
            if(newValue !== value) {
                value = newValue
                updateView()
            }
        }
    })
}
function watch(target) {
    // 监听对象属性 target就是具体的对象
    // 做一下判断 如果传进来的数据不是对象或者说是null 就直接返回
    if (typeof target !== 'object' || target === null) {
        return target
    }
    // 判断是否为数组
    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }
    // 遍历对象,使其每个属性变成响应式
    for (let key in target) {
        bind(target, key, target[key])
    }
    
}
watch(data)
data.arr.push(5)
console.log(data.arr)

这样就大功告成了。