Vue 2.X版本对数组数据原型方法劫持的原理实践

2,458 阅读3分钟

前言

虽然本篇只是简单实践数组原型方法的劫持,但是想要完全理解为什么这么做,还是希望你可以对vue响应式的依赖收集有一定的理解。不太理解的小伙伴们可以看这边298行代码带你理解Vue响应式原理和next-Tick原理,最后手写一个自己的小demo

建议同学们在看的时候可以边写代码边理解

1.AOP

Vue对对象的劫持是通过Object.defineProperty方法。而对数组的处理则是通过面向切面编程的思想,在用户调用Array的原型方法和原型方法之间切了一刀,在这一刀切开的中间,实现对数组依赖的追踪。相信用过后端的中间件的同学们对这个概念并不陌生,为了照顾不了解这个概念的同学让我们先来个图。

来个图

就像中间件一样,每次用户使用Array的方法,都不能绕过切片函数切片函数处理完毕之后,会根据用户的输入调用对应的原型函数并返回。在用户态是感觉不到这个切片函数的存在的

2.实现切片

talk is cheep,直接上代码吧

//即将要被劫持的数组
let arr = [1,2,3];

//既然要劫持原型,就要先把原型拿过来
let arrayProto = Array.prototype;
//创建一个我们自己的原型方法
let arrayMethods = Object.create(Array.prototype);
//比如我们要劫持原型上的push方法
arrayMethods.push = function(...args){
    //首先调用真正的原型方法获得正常的返回值
    const result = arrayProto.push.apply(this,args)
    //正常的push执行了之后,插入我们切片的逻辑代码
    console.log('监听到了push方法的调用')
    //返回正常的返回值
    return result
}

arr.__proto__ = arrayMethods

console.log(arr.push(4))

运行之后控制台输出

可以看到我们的切片代码已经执行成功了,并且并没有对原来的方法产生影响

这时候就有同学们有疑问了,难道我们要把数组原型上的所有方法都切片一次吗?答案显然是否定的。
vue对数组方法做切片是为了可以进行依赖收集,说明白一点就是希望可以知道数组数据什么时候发生了变化,然后渲染视图
既然如此,那么我们就只需要对那些可以只对那些可以改变数组数据的原型方法做切片就可以啦。

切片所有可以改变数组数据的原型方法

//即将要被劫持的数组
let arr = [1,2,3];

//先把要劫持的方法列出来
let methods = [
    'push',
    'pop',
    'shift',
    'unshift',
    'reverse',
    'sort',
    'splice'
]

//既然要劫持原型,就要先把原型拿过来
let arrayProto = Array.prototype;
//创建一个我们自己的原型方法
let arrayMethods = Object.create(Array.prototype);

//遍历方法
methods.forEach(method=>{
    //给每个方法做切片
    arrayMethods[method] = function (...args){
        //还是先调用真正的原型方法获得正常的返回值
        const result = arrayProto[method].apply(this,args)
        //模拟插入切片
        console.log(`调用了${method}方法`)
        
        return result
    }
})
arr.__proto__ = arrayMethods

console.log(arr.push(4))
console.log(arr.splice(0,1))

运行后的输出

到此为止,我们已经在不影响原来功能的前提下实现对改变数组值的方法进行切片了。
基于这个能力,我们做一个模拟vue的小东西,代码尽量精简。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <button id="button">push</button>
</body>
<script>
    let list = [1,2,3]
    let i = 4;
    //先把要劫持的方法列出来
    let methods = [
        'push',
        'pop',
        'shift',
        'unshift',
        'reverse',
        'sort',
        'splice'
    ]

    //既然要劫持原型,就要先把原型拿过来
    let arrayProto = Array.prototype;
    //创建一个我们自己的原型方法
    let arrayMethods = Object.create(Array.prototype);

    //遍历方法
    methods.forEach(method=>{
        //给每个方法做切片
        arrayMethods[method] = function (...args){
            //还是先调用真正的原型方法获得正常的返回值
            const result = arrayProto[method].apply(this,args)
            //数据改变了,我们要通知视图更新
            console.log(`调用了${method}方法`)
            update()
            
            return result
        }
    })

    //改变list的原型
    list.__proto__ = arrayMethods

    //模拟更新视图
    function update(){
        document.getElementById('app').innerHTML = `${list}`
    }

    //模拟用户业务代码,更改数据
    function clickFn(){
        list.push(i);
        i++
    }
    document.getElementById('button').addEventListener('click',clickFn)

    //刚开始数据还没修改,第一次算是渲染视图
    update()
</script>
</html>

页面很简单,但是效果很令人惊喜,点击按钮,模拟用户对数据进行操作,但是最新的数据就自动被更新到了视图上

小结

东西很小,但是思想很重要,因为是对数组方法进行切片,所以vue2.x是无法监听数组下标级的更新的啦,3.x原理更换了就可以了。

笔者很,能给大家带来一些收获就很开心,不能的话也希望口下留情啦。