【一起读】深入浅出Vue.js——Array的变化侦测

2021-06-07  本文已影响0人  小妍妍说

3.1 如何追踪变化

Object通过触发getter/setter来实现变化侦测,在Array中,使用push等方法来改变数据,并没有触发getter/setter,所以Object的侦测方式不适用于Array。

为了达到追踪变化的目的,vue使用了自定义的方法覆盖原生的原型的方法。具体的说,是用一个拦截器覆盖Array.prototype。每次使用Array原型上的方法操作数组时,其实执行的都是拦截器中提供的方法,比如push方法,然后在拦截器中使用原生Array的原型方法来操作数组。通过这个拦截器,我们追踪到了Array的方法。

3.2 拦截器

拦截器是在Array.prototype的基础上添加自定义方法的一个Object。

Array中原型方法有7个:push、pop、shift、unshift、splice、sort和reverse。

const arrayProto=Array.prototype
export const arrayMethods=Object.create(arrayProto)
    ;[
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ].forEach(function(method){
      const original=arrayProto[method]
      Object.defineProperty(arrayMethods,method,{
        value:function mutator(...args){
          return original.apply(this,args)
        },
        enumerable:false,
        writable:true,
        configurable:true
      })
    })

代码解析

比如,要使用push方法,实际调用的是arrayMethods.push,而arrayMethods.push是函数mutator,在mutator中调用原生的Array.prototype上的push方法来完成工作。这样,为了实现array的追踪变化,我们在mutator上编写“发送变化通知”的功能就好了。

3.3 使用拦截器覆盖Array原型的具体操作

使用拦截器直接覆盖Array.prototype会污染全局的Array,这不是我们想要的。

我们的目的是侦测到Array中变化了的数据,因此,希望拦截器只覆盖那些响应式数组的原型就好了。
第二章介绍过,在Observer中的数据是响应式的,因此,我们只需要在Observer中使用拦截器覆盖即将被转换成响应式Array类型数据的原型就好了:

export class Observer{
      constructor(value){
        this.value=value
        if(Array.isArray(value)){
          value._proto_=arrayMethods   //新增
        }else{
          this.walk(value)
        }
      }
    }

代码解析

补充说明:_proto_其实是Object.getPrototypeOf和Object.setPrototypeOf的早期实现,使用ES6中的Object.setPrototypeOf来代替_proto_可以实现同样的效果,但是,目前ES6在浏览器中的支持度还不够理想。

3.4 将拦截器挂载到数组的属性上

大部分浏览器都支持3.3的方法,但是只是大部分哦,不是100%哦,所以,还需要处理不能使用_proto_的情况。

不支持_proto_方法时,vue是怎么做的呢?

vue简单粗暴的将arrayMethods身上的这些方法设置到被侦测的数组上:


image.png
    import { arrayMethods } from './array'

    // _proto_是否可用
    const hasProto='_proto_' in {}
    const arrayKeys=Object.getOwnPropertyNames(arrayMethods)

    export class Observer{
      constructor (value){
        this.value=value
        if(Array.isArray(value)){
          //修改
          const augment=hasProto?protoAugment:copyAugment
          augment(value,arrayMethods,arrayKeys)
        }else{
          this.walk(value)
        }
      }
      .....
    }
    function protoAugment(target,src,keys){
      target._proto_=src
    }
    function copyAugment(target,src,keys){
      for(let i=0,l=keys.length;i<l;i++){
        const key=keys[i]
        def(target,key,src[key])
      }
    }

代码解析

3.5 如何收集依赖

使用拦截器实现了发送变化通知的能力,但是通知给谁呢?

在Object中,变化的通知发送给了依赖(Watcher),在getter中使用Dep收集依赖,每个key都有一个对应的Dep列表来存储依赖。

在Array中,同样是在getter中收集依赖,但是是在拦截器中触发依赖。为了保证依赖在getter和拦截器中都可以访问到,我们将依赖保存在Observer的实例上,因为无论在getter还是拦截器,都可以访问到Observer实例。

function defineReactive(data,key,val){
      let childOb=observe(val)   //修改
      let dep=new Dep()  
      Object.defineProperty(data,key,{
        enumerable:true,
        configurable:true,
        get:function(){
          dep.depend()  
         //新增
          if(childOb){
              childOb.dep.depend()
          }
          return val
        },
        set:function(newVal){
          if(val===newVal){
              return
            }
          val=newVal
          dep.notify()
        }
    })
}
export function observe(value,asRootData){
      if(!isObject(vlaue)){
        return
      }
      let ob
      if(hasOwn(value,'_ob_')&&value._ob_ instanceof Observer){
        ob=value._ob_
      }else{
        ob=new Observer(value)
      }
      return ob
    }

代码解析

3.6 在拦截器中获取Observer实例

Array拦截器是对原型的一种封装,所以可以在拦截器中访问到this(当前被操作的数组)。而dep保存在Observer中,所以需要再this上读到Observer的实例。

function dep(obj,key,val,enumerable){
      Object.defineProperty(obj,key,{
        value:val,
        enumerable:!!enumerable,
        writable:true,
        configurable:true
      })
    }
    export class Observer{
      constructor(value){
        this.value=value
        this.dep=new Dep()
        def(value,'_ob_',this)  //新增

        if(Array.isArray(value)){
          const augment=hasProto?protoAugment:copyaugment
          augment(value,arrayMethods,arrayKeys)
        }else{
          this.walk(value)
        }
      }
    }

代码解析

3.7 向数组的依赖发送通知

;[
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ].forEach(function(method){
      // 缓存原始方法
      const original=arrayProto[method]
      def(arrayMethods,method,function mutator(...args){
        const result=original.apply(this, args)
          const ob=this._ob_
          ob.dep.notify()   // 向依赖发送消息
          return result
      })
    })

ob.dep.notify()通知依赖(Watcher)数据发生了变化

3.8 侦测数组中元素的变化

除了要判断数组自身发生的变化(比如增减元素),还要侦测数组中元素的变化(比如数组中object身上某一属性的值发生了变化)

export class Observer{
      constructor(value){
        this.value=value
        def(value,'_ob_',this)
        //新增
        if(Array.isArray(value)){
          this.observeArray(value)
        }else{
          this.walk(value)
        }
      }
      // 侦测数组中的每一个元素
      observeArray(items){
        for(let i=0;i<items.length;i++){
          observe(items[i])
        }
      }
      ......
    }

代码解析

3.9 侦测新增元素的变化

数组的push、unshift和splice三种方法可以新增数组,因此,特殊处理这三种原型方法即可。

Observer会将自身的实例附加到value的_ob_属性上,所有被侦测了变化。

;[
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ].forEach(function(method){
      // 缓存原始方法
      const original=arrayProto[method]
      def(arrayMethods,method,function mutator(...args){
        const result=original.apply(this, args)
          const ob=this._ob_
          let inserted
          switch(method){
            case 'push'
            case 'shift'
              inserted =args
              break
            case 'splice'
              inserted=args.slice(2)
              break
          }
          if(inserted) ob.observeArray(inserted)  //新增
          ob.dep.notify()  
          return result
      })
    })

代码解析

3.10 关于Array的问题

关于Array的变化侦测是通过拦截原型的方式实现的,so有些数组的操作vue.js是拦截不到的,比如修改数组中某一个元素的值或者直接修改数组的长度。

上一篇 下一篇

猜你喜欢

热点阅读