Analyze the underlying logic of array methods with Proxy | Implement the most native splice method

foreword

Recently, I was looking at the Vue3 source code and found that when the responsive object calls the array method, Vue does special processing. I recall the exception that was output when the native Proxy called the push method before - the read and write operations were intercepted twice each

Then some other methods are called, trying to understand and implement the underlying logic of the array method according to the result output by the agent

proxy array

It is to use the proxy array to monitor its read and write operations. If you don't understand proxy, you can read this article or learn on MDN .

The code to monitor the array is as follows

const arr = [1, 2, 3, 4]
const p = new Proxy(arr, {
    get(target, key) {
        console.log(`get ${key}`)
        return target[key]
    },
    set(target, key, value) {
        console.log(`set ${String(key)} ${value}`)
        target[key] = value
        return true
    },
})
复制代码

array method

Next, we call the array method on the agent to explore and implement its underlying principles

push

Start with the most common push method

console.log('res:', p.push(5))
// 输出如下
// get push
// get length
// set 4 5
// set length 5
// res: 5
复制代码

First get the push method call of the array, then read the length property, then assign a value, set the length, and finally return the new array length

We know that multiple parameters can also be passed in when calling the push method. The result is as follows, one more assignment

console.log('res:', p.push(5, 6))
// 输出如下
// get push
// get length
// set 4 5
// set 5 6
// set length 6
// res: 6
复制代码

The role of push is not difficult to understand, and it is very simple to implement

function push(arr, ...args) {
    const arrLength = arr.length // 读取数组长度
    const argLength = args.length // 记录参数长度
    for (let i = 0; i < argLength; i++) {
        arr[arrLength + i] = args[i] // 遍历赋值
    }
    arr.length = arrLength + argLength // 设置新长度
    return arrLength + argLength // 返回新长度
}
复制代码

Because we implement it by defining a function, the reading of the array method is less than the previous output 'get push', and will not be repeated in the subsequent code implementation.

pop

After chatting about push, the next thing to follow is pop.

console.log('res:', p.pop())
// 输出如下
// get pop
// get length
// get 3
// set length 3
// res: 4
复制代码

The process of pop is also very simple, read the length, read the end element according to the length, then delete the element by setting the length, and finally return the deleted end element

Implementation of the above code

function pop(arr) {
    const arrLength = arr.length
    let res // 定义结果
    if (arrLength > 0) {
        res = arr[arrLength - 1] // 读末尾元素
    }
    arr.length = Math.max(0, arrLength - 1) // 删除元素 长度最小为0
    return res
}
复制代码

It should be noted that it is necessary to judge that the length is greater than 0 before assigning a value to the result, because sometimes the array will have '-1'this attribute

shift

Then the head delete shift, this method will cause all elements to move forward

console.log('res:', p.shift())
// 输出如下
// get shift
// get length
// get 0
// get 1
// set 0 2
// get 2
// set 1 3
// get 3
// set 2 4
// set length 3
// res: 1
复制代码

The implementation is also very simple, assign values ​​from front to back , and then set the length to delete the element

function shift(arr) {
    const arrLength = arr.length
    let res
    if (arrLength > 0) {
        res = arr[0]
        for (let i = 0; i < arrLength - 1; i++) {
            arr[i] = arr[i + 1] // 从前往后依次赋值
        }
    }
    arr.length = Math.max(0, arrLength - 1)
    return res
}
复制代码

Because the native array will assign all elements once every shift, it is not good to use it directly as a queue. Here is an article on the implementation of JS queue by Amway

unshift

头部删除之后是头部插入,这个方法会导致所有元素后移

console.log('res:', p.unshift(-1, 0))
// 输出如下
// get unshift
// get length
// get 3
// set 5 4
// get 2
// set 4 3
// get 1
// set 3 2
// get 0
// set 2 1
// set 0 -1
// set 1 0
// set length 6
// res: 6
复制代码

从输出的结果可以看出,所有元素后移,从后往前依次赋值,然后再设置新元素

function unshift(arr, ...args) {
    const arrLength = arr.length
    const argLength = args.length
    for (let i = arrLength + argLength - 1; i >= argLength; i--) {
        arr[i] = args[i - argLength] // 从后往前依次赋值
    }
    for (let i = 0; i < argLength; i++) {
        arr[i] = args[i] // 从前往后设置新元素
    }
    arr.length = arrLength + argLength
    return arrLength + argLength
}
复制代码

splice

splice 是最复杂的一个方法了,它分很多种情况,让我们一点点实现

处理参数

splice 方法的参数分 3 部分,起始位置,删除元素数目,添加的元素

array.splice(start[, deleteCount[, item1[, item2[, ...]]]] )
复制代码

搭个简单框架

function splice(arr, start, deleteCount, ...args) {}
复制代码

先处理起始位置,他可能为负数,表示从数组末位开始的第几位

if (start < 0) {
    start = arrLength + start
}
复制代码

并且还要限制起始位置在数组范围内

if (start < 0) {
    start = Math.max(0, arrLength + start)
} else {
    start = Math.min(start, arrLength)
}
复制代码

然后是删除元素数目,抛去起始位置之前的元素后,不能比剩余元素还多

而且实际删除元素的数目,也就是函数返回数组的长度

const resLength = Math.min(deleteCount, arrLength - start)
复制代码

删除数目与新值数目相等

处理完参数,咱先分析最简单的,删除数目与新值数目相等的情况

看看代理输出的结果

console.log('res:', p.splice(1, 2, ...[5, 6]))
// get splice
// get length
// get constructor
// get 1
// get 2
// set 1 5
// set 2 6
// set length 4
// res: [ 2, 3 ]
console.log('arr:', p)
// arr: [ 1, 5, 6, 4 ]
复制代码

发现读取了一个特殊的属性,构造器 constructor

考虑到 splice 返回的也是一个数组,莫非是调用构造器创建的?

定义一个新类测试一下,发现 splice 返回的类型与调用函数对象的类型相同

class MyArray extends Array {}
const myArr = new MyArray()
const res = myArr.splice()
console.log(res instanceof MyArray) // true
复制代码

所以在我们的代码中,也调用一下构造器来创建结果

const res = arr.constructor(resLength) // 也可以不初始化数组长度
复制代码

创建数组之后,读取要删除的元素,赋值给结果数组

for (let i = 0; i < resLength; i++) {
    res[i] = arr[start + i]
}
复制代码

然后用新增元素,覆盖原来的数据

for (let i = 0; i < argLength; i++) {
    arr[start + i] = args[i]
}
复制代码

设置一下长度,返回结果

arr.length = arrLength - resLength + argLength
return res
复制代码

新增元素比删除元素多

接下来分析新增元素比删除元素多的情况

console.log('res:', p.splice(1, 1, ...[5, 6]))
// get splice
// get length
// get constructor
// get 1   构建要返回的数组
// get 3
// set 4 4
// get 2
// set 3 3   剩余元素后移
// set 1 5
// set 2 6   设置新增的元素
// set length 5
// res: [ 2 ]
console.log('arr:', p)
// arr: [ 1, 5, 6, 3, 4 ]
复制代码

还是先构建要返回的数组,然后将删除元素之后的元素后移,后移位数为新增数目与删除数目之差:argLength - resLength

需要注意的是,要从后向前处理,所以循环是从数组末尾起始位置加删除数目

for (let i = arrLength - 1; i >= start + resLength; i--) {
    arr[i + argLength - resLength] = arr[i]
}
复制代码

然后赋值新增元素……

for (let i = 0; i < argLength; i++) {
    arr[start + i] = args[i]
}
复制代码

新增元素比删除元素少

再接着分析新增元素比删除元素少的情况

console.log('res:', p.splice(0, 2, ...[5]))
// get splice
// get length
// get constructor
// get 0
// get 1   构建要返回的数组
// get 2
// set 1 3
// get 3
// set 2 4   剩余元素前移
// set 0 5   设置新增的元素
// set length 3
// res: [ 1, 2 ]
console.log('arr:', p)
// arr: [ 5, 3, 4 ]
复制代码

这次是要将删除元素之后的元素前移,前移位数也还是新增数目与删除数目之差(负数):argLength - resLength

需要注意的是,这次是从前向后处理,所以循环是从起始位置加删除数目数组末尾

for (let i = start + resLength; i < arrLength; i++) {
    arr[i + argLength - resLength] = arr[i]
}
复制代码

Then it is also assigning new elements...

for (let i = 0; i < argLength; i++) {
    arr[start + i] = args[i]
}
复制代码

full code

So far, after analyzing the three cases, we find that new elements will be assigned in any case. The difference is that when the number of new elements and deleted elements is different, the original array must be adjusted first, so the implementation code is as follows

function splice(arr, start, deleteCount, ...args) {
    const arrLength = arr.length
    const argLength = args.length
    // 处理起始索引
    if (start < 0) {
        start = Math.max(0, arrLength + start)
    } else {
        start = Math.min(start, arrLength)
    }
    // 返回数组长度
    const resLength = Math.min(deleteCount, arrLength - start)
    // 调用构造函数,生成数组或继承数组的类实例
    const res = arr.constructor(resLength)
    // 先处理好要作为函数结果返回的数组
    for (let i = 0; i < resLength; i++) {
        res[i] = arr[start + i]
    }
    // 如果新增元素与删除元素数目不同,要处理原数组,调整空位
    if (argLength > resLength) {
        // 新增元素比删除元素多 原始元素要后移 从后向前处理
        for (let i = arrLength - 1; i >= start + resLength; i--) {
            arr[i + argLength - resLength] = arr[i]
        }
    } else if (argLength < resLength) {
        // 新增元素比删除元素少,后面的元素前移 从前向后处理
        for (let i = start + resLength; i < arrLength; i++) {
            arr[i + argLength - resLength] = arr[i]
        }
    }
    // 将新增的数据填入空位
    for (let i = 0; i < argLength; i++) {
        arr[start + i] = args[i]
    }
    // 设置长度,返回删除元素的数组
    arr.length = arrLength - resLength + argLength
    return res
}
复制代码

other

There are many methods for arrays, so I will not implement them one by one. Those who are interested can try it themselves according to the output of the agent.

  • methods such as indexOf, includes, forEach, join, every, reduce only involve reading
  • map, slice, fliter also call the constructor
  • reverse is to alternate between values ​​and assignments, sort is to read all values ​​and then assign them at one time

Summarize

The Proxy introduced by ES6 gives us another way to understand native functions without having to look at the C++ source code of the compiler

In this paper, we use Proxy to intercept the reading and writing of the array, and follow the output of the proxy to understand and implement five methods of modifying the array. Among them, splice is more complicated and needs to be discussed on a case-by-case basis.

It should also be said that this article only considers the normal situation. Some illegal operations: such as the wrong parameter type, the maximum length of the array (2^32-1), or calling on a sealed/frozen array. Since we will not encounter it in our actual use, we have not explored and implemented it.

If you think the content of the article is helpful, I hope to like and follow, and encourage the author.

Guess you like

Origin juejin.im/post/7119742046499241991