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

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是拦截不到的,比如修改数组中某一个元素的值或者直接修改数组的长度。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容