vue进阶测试——数据双向绑定原理探究

   本课题研究承接浏览器的渲染机制一文,写文章前感谢一下趣链大佬的面试点播,虽然不知道听来了什么,哈哈~

   依旧是放上详细的参考链接

   用了vue几个月后,写前端的思维方式逐渐从操作dom的方式中解脱出来,现在思考问题的时候,几乎都是带着如何以数据驱动模板的方式思考,因此,了解一下数据是如何驱动模板是一件十分必要的事情。

  关于双向绑定一词(mvvm),可以分开来解读,即:如何监听数据改变,和如何监听dom事件。监听dom事件,如input的输入事件,你可以用原生的addEventListener,或者在dom节点上绑定oninput事件。那么如何监听数据的变化呢?如果从代码的角度出发,大概可以有这样一种猜想:

   第一步:记录每一个数据的旧值

   第二步:用一个定时器不断地监听这个值的新值(不管数据有没有更新),并和旧值进行比较。

   第三步:当值发生改变,操作对应的dom节点。

   这三步实现起来并不复杂,就是有点蠢。

   扯淡到此为止,进入正题,vue中是如何实现数据双向绑定的 —— defineProperty 

   Object.defineProperty(obj, prop, descriptor)

   obj:要在其上定义属性的对象。

   prop:要定义或修改的属性的名称。

   descriptor:将被定义或修改的属性描述符。

  没有用过的程序猿到这里已经是一脸懵逼了,下面我举个例子分别解释下这三个参数

  首先是obj,顾名思义,这个参数是个对象,可以是: 身份信息 = { 性别:'女',姓名:'晓甜甜',住址:‘XXX公寓‘ }

  然后是prop,这是你要关注的属性,可以是 '性别'

  最后这个descriptor比较复杂,你可以理解为,你关注这个对象的某个属性后,要对这个属性做什么事情,最常见的有,你要获取这个对象的属性的值(get),或者你想要修改这个对象的属性的值(set),来看一段代码

  let person = {
    'sex':'girl',
    'name':'晓甜甜',
    'address':'XXX公寓'
  }
  function observe(obj,prop,val){
    Object.defineProperty(obj,prop,{
      enumerable: true,
      configurable: true,
      get:function(){
        return '此人住在'+val
      },
      set:function(newVal){
        val = newVal
        console.log('此人换新家了,现在在' + newVal)
      }
    })
  }
  observe(person,'address',person['address'])
  console.log(person.address)
  person.address = 'YYY小区'
 
 //控制台打印
 //此人住在XXX公寓
 //此人换新家了,现在在YYY小区

    结果如上面说的,当你想要监听person的address属性,并且重写了他的get和set方法后,控制台会打印和理论上不同的结果,你这种方法称为数据劫持,这是数据驱动模板的核心部分,也就是你已经可以成功监听到数据的变化了,并且在数据变化触发了set后,你可以重写里面的方法,干一些你想干的事情,比如更新dom。

  注意,这里我用了observe封装了这部分代码,来看如果不用observe会发生什么事情(写代码,尽量不要偷懒,除非你很熟)

  let person = {
    'sex':'girl',
    'name':'晓甜甜',
    'address':'XXX公寓'
  }
  Object.defineProperty(person,'address',{
    enumerable: true,
    configurable: true,
    get:function(){
      return '此人住在'+ this.address
    },
    set:function(newVal){
      val = newVal
      console.log('此人换新家了,现在在' + newVal)
    }
  })

  observe(person,'address',person['address'])
  console.log(person.address)
  person.address = 'YYY小区'
  
 //控制台直接报错了!!!!

   这里我偷了个懒,没错我一开始为了做演示就是这样写的,我利用了get回调中的this指向person,想要通过this.address去访问person的address,这里就造成了一个死循环,你在访问当前属性的时候的访问回调里又访问了当前属性,就卡死了,当然浏览器比较厉害,他会帮你发现这个错误,直接给你报错,同时这个错误也验证了,一旦你要访问该对象的某个属性,不管什么情况下他都会触发get回调,同理,当你要改变这个值的时候,他就会触发set,你可以在set回调里去改变这个属性的值,同样会发生死循环。

  最后输出一下person对象,可以看到,address的get和set已经被劫持了。

  

 数据双向绑定的数据监听已经搞定了,下面来了解一下如何实现一个简单的双向绑定。先上一张图,看一下整体设计思路。

   先来解释一下上面这张图,理一下整体设计思路。

   第一步:我们需要一个observer,他可以帮我们监听所有数据。我们还需要一个compile,他可以帮我们解析虚拟dom的模板,如v-on,v-if,{{}}等,你可以用正则的方式去匹配这些特殊字符。

   第二步:我们已经监听到了数据,我们也有了模板解析器,这时候我们需要将数据和模板联动起来,因此我们需要一个订阅者watcher,在数据和模板初始化的他能将这两者联系起来,具体如何联系,下面会详细说明。

   确立了大致的设计思路后,来一个个实现这些组件,并实现一个简易的mvvm。

1.实现observer

  首先,observer的功能应该是监听所有数据,实现所借助的工具是Object.defineProperty(obj, prop, descriptor),在刚才的基础上,用递归的方法,来实现一个对象所有属性的监听。

 let person = {
    'sex':'girl',
    'name':'晓甜甜',
    'address':'XXX公寓',
    'parent':{
      'father':'小明',
      'mother':'李红'
    }
  }
 //监听数据对象的所有属性值
  function observe(data){
    if(!data||typeof(data)!=='object'){
      return
    }
    // Object.keys(data) 和 for...in的遍历差不多,该函数返回一个对象包含的所有属性的数组
    Object.keys(data).forEach((key)=>{
      dataHijacking(data,key,data[key]) //用数据劫持改写get和set方法
    })
  }

  function dataHijacking(obj,prop,val){
    observe(val) //递归监听
    Object.defineProperty(obj,prop,{
      enumerable: true,
      configurable: true,
      get:function(){
        console.log('get val:'+val)
        return val
      },
      set:function(newVal){
        if(val === newVal){
          return
        }
        val = newVal
        console.log('set newVal')
      }
    })
  }

  observe(person)
  let receive = person.parent.father //触发两次get
  person.parent.mother = '小红' //触发一次get,和一次set

   2.实现dependence

   在思路说明中,watcher用于连接数据更新和模板更新。而Dep(dependence的缩写)则用于watcher和数据更新的关联。为什么需要Dep这个媒介呢?先不说原理,先来看个场景说明:

  observer在一家研究所工作,他负责所有研究数据的监管工作,一旦有一项研究数据发生了变更,他就需要通知所有关注这项研究的订阅者(watcher),数据发生了变更,具体怎么办你们自己看着办。那么,订阅者如何加入这项研究呢,或者说,observer观测到数据变更后应该通知哪些watcher呢?这个时候就需要一个管理员(dep),统一管理订阅者,订阅者通过加入管理员的名单,来和数据源的更新进行沟通。一旦有数据发生变更,只需要通知管理员(dep),管理员负责通知所有订阅者。

 下面是dependence的实现思路

  function dataHijacking(obj,prop,val){
    observe(val) //递归监听
    let dep = new dependence()
    Object.defineProperty(obj,prop,{
      enumerable: true,
      configurable: true,
      get:function(){
        dep.addwatcher(watcher) //通过触发get回调添加订阅者watcher
        return val
      },
      set:function(newVal){
        if(val === newVal){
          return
        }
        val = newVal
        dep.notify() //数据更新时通过依赖函数通知所有订阅者
      }
    })
  }

  function dependence(){
    this.watchers = [] //用一个数组用于存储watcher
  }
  dependence.prototype = {
    addwatcher:function(watcher){
      this.watchers.push(watcher)
    },
    notify:function(){
      this.watchers.forEach((watcher)=>{
        watcher.update() //通知所有订阅者触发更新函数
      })
    }
  }

    在上面的代码中,dependence包含一个用于存储订阅者的数组,订阅者需要通过触发数据的get回调将自己添加到某个数据的管理员(dep)中去,至于如何触发,会在watcher的实现中说明,当数据更新时,会通过dep通知订阅该数据的所有wacher,需要调用watcher的更新函数。

3.实现watcher

   我们知道watcher可以收到属性的变化通知并执行相应的函数,从而更新视图。在这之前,我们应当将watcher加入到对应的dep维护的数组中去,才能收到通知,在dep的实现思路中,我们是通过触发data的get回调来实现添加订阅者的操作的,来看一下具体代码和注释。

  function dataHijacking(obj,prop,val){
    observe(val) //递归监听
    let dep = new dependence()
    Object.defineProperty(obj,prop,{
      enumerable: true,
      configurable: true,
      get:function(){
        if(dep.target){
          dep.addwatcher(dep.target) //通过触发get回调添加订阅者watcher
        }
        return val
      },
      set:function(newVal){
        if(val === newVal){
          return
        }
        val = newVal
        dep.notify() //数据更新时通过依赖函数通知所有订阅者
      }
    })
  }

 function watcher(vm,key,callback){
    this.callback = callback //执行回调,这里可以动态操作相关dom
    this.vm = vm  //这个参数一般会保留全局的vm,用于访问data
    this.key = key //确定自己是哪个数据的订阅者
    this.value = this.get() //通过调用这个方法将自己添加到依赖数组中去,同时缓存旧值
  }
  watcher.prototype = {
    update:function(){
      let value = this.vm.data[this.key] //一般我们都会操作实例vue上挂载的data
      let oldVal = this.value //初始化时候的旧值
      if(oldVal!==value){
        //如果这两个值不相等,才触发回调,也就是dom操作,这有利于优化性能
        this.value = value
        this.callback.call(this.vm, value, oldVal)
      }
    },
    get:function(){
      Dep.target = this //将自己缓存,准备添加到依赖中去
      let value = this.vm.data[this.key] //这个操作会触发observe的get
      Dep.target = null // 释放自己
      return value
    }
  }

  哇~这个坑挖的有点大~什么时候能更新完,随缘随缘

猜你喜欢

转载自blog.csdn.net/dkr380205984/article/details/81204880
今日推荐