Vue响应式原理解析(三)

本文深入探讨Vue中数组响应式的工作原理,通过重写数组的push、pop等方法,利用Object.create()和Object.setPrototypeOf()实现数组的响应式更新,并解决数组嵌套监听的问题,为后续的视图更新和依赖收集奠定基础。
摘要由CSDN通过智能技术生成

Vue中数据响应式原理——重写数组方法实现数组响应式

此篇是在 Vue响应式原理解析(二) 的基础之上展开

情景准备

上篇提到,我们只是实现了对Object类型的响应式,而数组却没有做到响应式,本篇就在此基础上针对数组来扒一扒Vue如何实现Array的响应式

开搞~

其实去网上稍微查一查就会知道Vue他实现对数组的响应式是通过了重写原有的数组方法而实现的,但是他是怎么重写的,我这里按照自己理解的流程来写。

上图~~

在这里插入图片描述
既然如此,那么最最重要的环节岂不就是如何去备份一份方法并且重写了么?大神用了一个方法:Object.create() 创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
根据思路图,去控制台尝试,我们得到个这个东西:
在这里插入图片描述
虽然这个对象身上可以找到了数组的原有方法,但是不是我们想要的:

  1. 我们得让他身上有我们想要的方法
  2. vue重写了七个方法:push, pop, shift, unshift, splice, sort, reverse
  3. 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类,最复杂的放最后来搞。如有错误支持欢迎指出,互相交流学习。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值