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

2.1 变化侦测是干什么的?

vue在渲染页面时,会根据数据的变化不停的进行状态的更替,也需要不停的对页面进行渲染,变化侦测就是用来监测状态的变化。

2.1.1 三大框架是如何监测状态的?

Angular&&React:当状态发生变化时,发送信号告诉框架,框架接到信号后,通过暴力比对找到需要重新渲染的DOM节点。Angular中使用脏检查,React中使用虚拟DOM。
Vue:状态变化时,vue立刻知道是谁发生了变化,(数据变化时主动推送给框架)从而进行dom 的更新。

2.2 如何追踪变化?

(本节暂只探讨如何追踪一个对象的变化)

JS中有两种方式可以追踪到变化:Object.defineProperty和ES6的Proxy。

vue中使用了Object.defineProperty来追踪一个对象的变化:定义一个响应式数据,每当从data的key中读取数据时,get函数被触发;每当往data的key中设置数据是,set函数被触发。具体原理是:

function defineReactive(data,key,val){
  Object.defineProperty(data,key,{
    enumerable:true,
    configurable:true,
    get:function(){
      return val
    },
    set:function(newVal){
      if(val===newVal){
        return
      }
      val=newVal
    }
  })
}

2.3 依赖是什么?

vue的变化侦测具体是怎么发生的呢?具体的说,一个状态绑定着多个依赖,每个依赖代表着一个具体的DOM节点,当这个状态发生变化时,通过向这个状态的所有依赖发送通知,让他们来更新DOM。

那么依赖是什么呢?依赖就是当属性发生变化时,我们要通知的“用到数据的地方”。

真实情况是,使用这个数据的地方有很多,类型也不一样(有可能是一个模板,也有可能是用户写的一个watch),为了方便处理,我们需要抽象出一个能集中处理这些问题的类,然后我们在收集依赖阶段只收集这个封装好的类的实例,通知也只通知它一个,让它负责通知其他地方,这里的“它”就被称为“依赖”——Watcher

2.4 如何收集依赖?

收集依赖是什么意思?收集依赖就是把用到某一数据的地方收集起来的过程,等数据的属性发生变化时,把之前收集的依赖循环触发一遍就好了。
具体的说,在getter中收集依赖,在setter中触发依赖。

2.5 依赖收集在哪里?

最简单的思路是:收集的依赖存在一个数组里,在getter中将数据都push入这个数组,然后在setter中循环这个数组来触发所有依赖。记这个数据为dep,那么:

function defineReactive(data,key,val){
  let dep=[]  //新增
  Object.defineProperty(data,key,{
    enumerable:true,
    configurable:true,
    get:function(){
      dep.push(window.target)  //新增
      return val
    },
    set:function(newVal){
      if(val===newVal){
        return
      }
    //新增
    for(let i=0;i<dep.length,i++){
      dep[i](val,newVal)
      }
      val=newVal
    }
  })
}

这样的代码耦合度过高,现在我们把dep封装成一个类:

export default class Dep(
      constructor(){
        this.subs=[]
      }
      addSub(sub){
        this.subs.push(sub)
      }
      removeSub(sub){
        remove(this.subs, sub)
      }
      depend(){
        if(window.target){
          this.addSub(window.target)
        }
      }
      notify(){
        const subs=this.subs.slice()
        for(let i=0;i<subs.length;i++){
          subs[i].update()
        }
      }
    )
    function remove(arr, item){
      if(arr.length){
        const index=arr.indexOf(item)
        if(index > -1){
          return arr.splice(index, 1)
        }
      }
    },
    function defineReactive(data,key,val){
      let dep=new Dep()  //新增
      Object.defineProperty(data,key,{
         enumerable:true,
         configurable:true,
         get:function(){
           dep.depend()  //新增
           return val
         },
         set:function(newVal){
           if(val===newVal){
              return
            }
           val=newVal
           dep.notify()//新增
         }
     })
 }

2.6 重新介绍watcher 是什么?

白话:Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。

Watcher的原理是把自己设置到全局唯一的指定位置(例如window.target),然后读取数据——>触发数据的getter,在getter中就从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个Watcher收集到Dep,这样,Watcher就可以订阅任意一个数据的变化。

Watcher的经典使用方法是:

 // keypath
    vm.$watch('a.b.c',function(newVal,oldVal){
      // 做点什么
    })

当data.a.b.c发生变化时,触发第二个参数中的函数

如何实现这个功能呢:把这个watcher实例添加到data.a.b.c属性的Dep中,当data.a.b.c的值发生变化时,通知Watcher,然后Watcher执行参数中的这个回调函数。代码如下:

export default class Watcher{
      constructor (vm,expOrFn,cb){
        this.vm=vm
        // 执行this.getter(),就可以读取data.a.b.c的内容
        this.getter=parsePath(expOrFn)
        this.cb=cb
        this.value=this.get()
      }
      get(){
        window.target=this
        let value=this.getter.call(this.vm,this.vm)
        window.target=underfined
        return value
      }
      update(){
        const oldValue=this.value
        this.value=this.get()
        this.cb.call(this.vm,this.value,oldValue)
      }
    }

代码解析

  • 在get()中,this指向当前watcher实例,触发了getter就会触发收集依赖的机制,即从window.target中读取一个依赖并且添加到Dep中。
  • 依赖注入到Dep之后,每当data.a.b.c的值发生变化时,就会让依赖列表中所有的依赖循环触发update方法,而update方法会执行参数中的回调函数将value和oldValue传到参数中。

总结:不管是用户执行vm.$watch('a.b.c',(value,oldValue)=>{}),还是模板中用到的data,都是通过Watcher来通知自己是否需要发生变化。

补充介绍1:parsePath是怎么读取一个字符串的keypath的?

//解析简单路径
    const bailRE=/[^\w.$]/
    export function parsePath(path){
      if(bailRE.test(path)){
        return
      }
      const segments=path.split('.')
      return function(obj){
        for(let i=0;i<segments.length;i++){
          if(!obj)return
          obj=obj[segments[i]]
        }
        return obj
      }
    }

代码解析

  • 使用.分割keypath为一个数组,然后循环数组一层一层去读数据,最后拿到的obj就是keypath中想要读的数据。

补充介绍2:keypath是什么?

  • keypath(键路径)是一个由 . 作为分隔符的键组成的字符串,用于支撑一个连接在一起的对象性质序列。第一个键的性质由先前的性质决定,接下来每个键的值也是相对于其前面的性质。
  • keypath用于键值额观察,当另一个对象的属性发生变化时,可以直接通知对象。
  • 通过keypath找到这个属性的值,然后去检查这个值是否有变化,从而得知该对象是否发生了变化。

关于keypath的详情介绍请点击:https://www.jianshu.com/p/e008f73a35ba

2.7 递归侦测所有key

前面介绍的代码只能侦测数据中的某一个属性,可以封装一个?observer类来侦测数据中的所有属性。

    export class Observer(){
      constructor (value){
        this.value=value
        if(!Array.isArray(value)){
          this.walk(value)
        }
      }
      walk(obj){
        const keys=Object.keys(obj)
        for(let i=0;i<keys.length;i++){
          defineReactive(obj,keys[i],obj[keys[i]])
        }
      }
    }
    function defineReactive(data,key,val){
      // 新增,递归子属性
      let dep=new Dep()  //新增
      Object.defineProperty(data,key,{
        enumerable:true,
        configurable:true,
        get:function(){
          dep.depend()  //新增
          return val
        },
        set:function(newVal){
          if(val===newVal){
              return
            }
          val=newVal
          dep.notify()//新增
        }
    })
}

代码解析:
1 Objecter类将一个正常的object转换成被侦测的object,即将一个数据内的所有属性都转换成getter/setter的形式,然后去追踪它们的变化。
2 判断数据的类型,只有Object类型的数据才会调用walk将每一个属性转换成getter/setter的形式来侦测变化。
3 早defineReactive中新增new Observer(val)来递归子属性,这样就完成了侦测所有属性的功能。当data中的属性发生变化时,与这个属性相对应的依赖就会接收到通知。

2.8 关于Object的问题

Vue.js通过Object.defineProoerty将对象的key转换成getter/setter的形式来追踪变化,但是getter/setter只能追踪一个数据是否是修改,不能追踪到数据是新增还是删除,为了解决这个问题,Vue.js提供了两个API:vm.set和vm.delete,后文会介绍。
像对象中新增属性:

var vm=new Vue(){
      el:'#el',
      template:'#demo-template',
      methods:{
        action(){
          //新增属性值
          this.obj.name='xiaoming'
        }
      },
      data:{
        obj:{}
      }
    }

在对象中删除属性:

var vm=new Vue(){
      el:'#el',
      template:'#demo-template',
      methods:{
        action(){
          //删除一个属性
          delete this.obj.name
        }
      },
      data:{
        obj:{
          name:'xiaoming'
        }
      }
    }
image.png

声明:本文参考自:刘博文-深入浅出Vue.j

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

推荐阅读更多精彩内容