美文网首页
【一起读】深入浅出Vue.js——Array的变化侦测

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

作者: 小妍妍说 | 来源:发表于2021-06-07 09:29 被阅读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
          })
        })
    

    代码解析

    • arrayMethods继承自Array.prototype,具备其所有功能,我们用arrayMethods来覆盖Array.prototype。
    • 在arrayMethods上使用Object.defineProperty方法封装数组的那七种原型方法。
    • 使用Array原型方法时,实际调用的是mutator方法。
    • mutator方法执行原型方法来完成工作。

    比如,要使用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)
            }
          }
        }
    

    代码解析

    • 新增的代码将拦截器赋值给value._proto_,通过_proto_巧妙的实现覆盖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])
          }
        }
    

    代码解析

    • 使用hasProto变量来判断当前浏览器是否支持_proto_。如果支持,使用protoAugment方法覆盖原型;如果不支持,调用copyAugment方法将拦截器中的方法挂载到value上。
    • 使用copyAugment方法用于将已经加工了拦截操作的原型方法直接添加到value的属性中。

    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
        }
    

    代码解析

    • 尝试为value创建一个Observer实例,如果创建成功,直接返回该实例;如果value已经存在一个Observer实例,则直接返回它。这样可以避免重复侦测value变化的问题。
    • 在defineReactive函数中调用了observe,它把val当做参数传进去并且拿到一个返回值,那就是Observer实例。
    • 通过observe我们得到数组的Observer的实例(childOb),最后通过childOb的dep执行depend方法📱依赖。

    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)
            }
          }
        }
    

    代码解析

    • 在value上新增一个不可枚举的属性ob,这个属性的值就是当前Observer的实例。
    • ob可以在拦截器中访问Observer实例(value.ob),还可以标记当前value是否已经被Observer转换成响应式数据。

    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])
            }
          }
          ......
        }
    

    代码解析

    • 新增的observeArray函数循环Array中的每一项,执行observe函数来侦测变化。observe函数是将数组中的每个元素执行一遍new Observer。so这里是递归。

    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
          })
        })
    

    代码解析

    • 从this._ob_上拿到Observer实例后,如果有新增元素,则使用ob.observeArray来侦测这些新增元素的变化。

    3.10 关于Array的问题

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

    相关文章

      网友评论

          本文标题:【一起读】深入浅出Vue.js——Array的变化侦测

          本文链接:https://www.haomeiwen.com/subject/yrygsltx.html