前言
最近在看 Vue3 源码,发现在响应式对象调用数组办法时,Vue 做了非凡的解决,回想起之前在原生 Proxy 调用push 办法时输入的异样——读写操作各拦挡了两次
而后又调用了一些其余办法,尝试依据代理输入的后果,了解并实现了数组办法的底层逻辑
代理数组
就是用 Proxy 代理数组,监听其读写操作,不懂 Proxy 上 MDN 学习
监听数组的代码如下
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 },})
数组的办法
接下来咱们在代理身上调用数组办法,探索并实现其底层原理
push
先从最常见的 push 办法开始
console.log('res:', p.push(5))// 输入如下// get push// get length// set 4 5// set length 5// res: 5
先获取数组的 push 办法调用,而后读取了 length 属性,之后赋值,设置 length,最初返回新的数组长度
咱们晓得调用push办法时也能够一次传入多个参数,后果如下,多了一次赋值
console.log('res:', p.push(5, 6))// 输入如下// get push// get length// set 4 5// set 5 6// set length 6// res: 6
push 的作用不难理解,实现起来也很简略
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 // 返回新长度}
因为咱们是定义函数实现的,相比之前的输入少了数组办法的读取 'get push'
,之后的代码实现中不再赘述
pop
聊完 push 紧跟的必定就是 pop
console.log('res:', p.pop())// 输入如下// get pop// get length// get 3// set length 3// res: 4
pop 的流程也很简略,读 length,依据长度读开端元素,而后通过设置长度实现元素的删除,最初返回删除的开端元素
上代码实现
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}
须要留神的是,必须要判断长度大于 0 才能够为后果赋值,因为有时数组会存在 '-1'
这个属性
shift
而后是头部删除 shift,这个办法会导致所有元素前移
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
实现也很简略,从前往后顺次赋值,而后设置长度删除元素
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}
因为原生数组每次 shift 会将都会将所有元素赋值一遍,间接当作队列使用性能并不好,在这里安利一篇[实现 JS
队列的文章](https://segmentfault.com/a/11...)
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 + argLengthreturn 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]}
而后也是赋值新增元素……
for (let i = 0; i < argLength; i++) { arr[start + i] = args[i]}
残缺代码
至此三种状况剖析结束,咱们发现,任一状况都会赋值新元素,区别是在新增元素与删除元素数目不同时,要先解决原数组调整空位,所以实现代码如下
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}
其余
数组的办法还有很多,就不一一实现了,感兴趣的能够依据代理的输入自行尝试
- indexOf、includes、forEach、join、every、reduce 等办法只波及读取
- map、slice、fliter 还调用了结构器
- reverse 是取值与赋值交替进行、sort 是读取所有值排序后一次性赋值
总结
ES6 推出的 Proxy 让咱们有了了解原生函数的另一种形式,而不必去看编译器的 C++ 源码
本文咱们借助 Proxy 拦挡数组的读写,仿照代理的输入了解并实现了批改数组的 5 个办法,其中 splice 较为简单,须要分状况探讨。
还要说一点,本文只思考了失常的状况。一些违规操作:比方参数类型谬误、数组长度达到最大值(2^32-1)或是在密封/解冻数组上调用。因为在咱们理论应用不会遇到,也就没有去探索与实现。
如果感觉文章的内容有所帮忙,心愿能点赞关注,激励一下作者。