Vue中数据响应式原理——重写数组方法实现数组响应式
此篇是在 Vue响应式原理解析(二) 的基础之上展开
情景准备
上篇提到,我们只是实现了对
Object
类型的响应式,而数组却没有做到响应式,本篇就在此基础上针对数组来扒一扒Vue
如何实现Array
的响应式
开搞~
其实去网上稍微查一查就会知道Vue他实现对数组的响应式是通过了
重写原有的数组方法
而实现的,但是他是怎么重写的,我这里按照自己理解的流程来写。
上图~~
既然如此,那么最最重要的环节岂不就是如何去备份一份方法并且重写了么?大神用了一个方法:Object.create() 创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
根据思路图,去控制台尝试,我们得到个这个东西:
虽然这个对象身上可以找到了数组的原有方法,但是不是我们想要的:
- 我们得让他身上有我们想要的方法
- vue重写了七个方法:
push
,pop
,shift
,unshift
,splice
,sort
,reverse
- 让
arr.方法
调用时指向这个arrMethods
重写的方法
这也就是我们的目的所在,那么怎么才可以让
arr.方法
调用我们自己重写的方法而不是原有方法呢?Vue里用了:Object.setPrototypeOf() 方法,此方法就可以让arr.方法
强制性指向我们重写的方法。
阶段性尝试
为了规划代码条理性,我们分出去一个文件:
array.js
,这里使用了def.js
方法,将重写的方法放到arrayMethods
上面并且不可被枚举
import def from './def.js'
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
let methodsNeedChange = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
methodsNeedChange.forEach(methodsName => {
// 将新的方法写入到以Array的prototype为原型的对象身上
def(arrayMethods,methodsName,function(){
console.log(11111)
},false)
})
console.log(arrayMethods)
在
Observer.js
类中引入并且调试下,注意:这里Object.setPrototypeOf(value , arrayMethods)
是关键,让代码在多次循环中遇到Array
类型时指向arrayMethods
import def from './def.js'
import defineReactive from './defineReactive.js'
import { arrayMethods } from './array.js'
// observer 将一个正常的obj转换成一个每个层级的属性都是响应式的obj
export default class Observer {
constructor(value) {
// 这里的this代表的是Observer的实例,而他的实例本身则是一层一层的对象,其实就是为了确保不重复去创建Observe实例(单例模式)
def(value , '__ob__' , this ,false)
// console.log('我是Observer类',value)
if(Array.isArray(value)){
Object.setPrototypeOf(value , arrayMethods)
}else{
this.walk(value)
}
}
walk(value){
for(let key in value){
defineReactive(value , key)
}
}
}
调试结果:我们在index.js
调用obj.a.push(3)
(成功指向我们改写后的方法
)
改写成功,接下来就是让改写的方法可用
到目前为止,方法改写成功,但是我们使用push之类的方法,原数组并没有改变。想要让方法可用也很简单,我们备份一下原方法,然后在调用时改变他的
this
指向为目标数组即可。还是array.js
import def from './def.js'
const arrayPrototype = Array.prototype
// 以Array的prototype为原型创建对象
export const arrayMethods = Object.create(arrayPrototype)
// 准备要改写的方法名
let methodsNeedChange = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
methodsNeedChange.forEach(methodsName => {
// 根据方法名备份原方法
const original = arrayPrototype[methodsName]
// 将新的方法写入到以Array的prototype为原型的对象身上
def(arrayMethods,methodsName,function(){
console.log(this,arguments)
original.apply(this,arguments)
},false)
})
index.js
import observe from './observe.js'
let obj = {
m:{
n:{
b:10
}
},
a: [1,2,3,4,5]
}
observe(obj)
obj.a.push(3)
console.log(obj.a)
方法调用成功 引出新问题
此时我们发现一个新问题,跟当初
Object
类型一样,如果出现了数组多层嵌套的话,那么内层的数组则不会被监听到了…但是呢,数组的无法被监听(方法使用过后没有效果)与对象稍有区别,看代码.
import def from './def.js'
import defineReactive from './defineReactive.js'
import observe from './observe.js'
import { arrayMethods } from './array.js'
// observer 将一个正常的obj转换成一个每个层级的属性都是响应式的obj
export default class Observer {
constructor(value) {
// 这里的this代表的是Observer的实例,而他的实例本身则是一层一层的对象,其实就是为了确保不重复去创建Observe实例(单例模式)
def(value , '__ob__' , this ,false)
// console.log('我是Observer类',value)
if(Array.isArray(value)){
Object.setPrototypeOf(value , arrayMethods)
this.observeArr(value)
}else{
this.walk(value)
}
}
walk(value){
for(let key in value){
defineReactive(value , key)
}
}
observeArr(preArr){
// 考虑到数组长度会发生变化,所以提前定义他的length存储一下
for(let i = 0 , l = preArr.length ; i < l ; i++){
observe(preArr[i])
}
}
}
很简单,我们将数组遍历一下,遇到内层是数组类型则再次进行一下observe
方法,走一下指向问题即可。
但是但是但是~~~~又有一个问题,因为数组里有些方法比较特殊:
push
,unshift
,splice
, 他们三个可以给原数组新增项,如果他们新增的也是个数组我们就需要让新增的也为可被监听的。
import def from './def.js'
const arrayPrototype = Array.prototype
// 以Array的prototype为原型创建对象
export const arrayMethods = Object.create(arrayPrototype)
let methodsNeedChange = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
methodsNeedChange.forEach(methodsName => {
// 根据方法名备份原方法
const original = arrayPrototype[methodsName]
// 将新的方法写入到以Array的prototype为原型的对象身上
def(arrayMethods,methodsName,function(){
const result = original.apply(this,arguments)
const ob = this.__ob__ // 为了调用它身上的observeArr方法
let inserted = []
switch(methodsName){
// 首尾新增没有太多绕绕,直接让inserted 等于插入传进来的值即可
case 'push':
case 'unshift':
inserted = arguments
break;
case 'splice':
// splice 的格式是splice(下表,数量,插入新项) 而我们需要的只是插入的新项,所以需要裁切下
console.log(arguments)
inserted = [].slice.call(arguments, 2)
break;
}
// 让新插入的也成为响应的 再次调用observeArr方法
if(inserted.length !== 0){
ob.observeArr(inserted)
}
// 因为pop,shift,reverse,sort他们会返回新数组,所以咱这里也要返回一下
return result
},false)
})
完美收工
至此,数组我们也给他实现了响应式更新,但是有人或许会发现,在调用数组方法时,只触发了
defineReactive
里的get
方法,而没有触发set
方法,那是怎么监听它发生改变了呢,其实很简单,监听数组发生改变是在array.js
里面监听的。
最后还会有一篇就有关视图更新了 依赖收集 、 Watcher类和Dep类,最复杂的放最后来搞。如有错误支持欢迎指出,互相交流学习。