vue响应式与双向绑定原理

本文深入探讨Vue2中的数据响应式原理,通过Object.defineProperty实现数据劫持和观察者模式的双向绑定。详细介绍了observer、watcher和dependency的角色,以及如何通过getter和setter触发视图更新。同时,对比Vue3中使用Proxy替代defineProperty,实现更灵活的数据响应。
摘要由CSDN通过智能技术生成

vue2

使用Object.defineProperty进行数据劫持,再结合观察者模式实现双向绑定。
主要是observer、watcher、dependency。
入口是observer,dependency负责收集wather和通知watcher进行视图更新,主要方法就是add和notify。watcher是负责订阅属性,执行更新视图的回调函数。
入口是observer主要是使用defineProperty给对象属性添加get和set方法。并在
get中执行dep的add,在set中执行dep的update。

  1. 为每个属性添加getter和setter,一开始temp肯定不存在,因为watch的构造函数还没有执行,不会添加到访问者数组subscribers。
  2. 添加了以后会执行compile创建一个缓冲区,把document全部剪切到一个空白页面fragment,然后执行fragment_compile修改缓冲区页面。
  3. 在fragment中,先识别模板字符串,把值替换上去,再订阅,这样值改变就能实现响应式。
  4. new订阅的时候得告诉发布者把自己加到订阅者数组啊,不然你后面发通知我不在群里接收不到啊,所以订阅者的constructor中temp暂存一下我这个watcher(this),然后故意触发一下get,get这时候发现temp不为空了,有信号了,那就添加到订阅者数组。
  5. 然后让数据改变的时候发布者在群里发通知就行了,这样订阅者知道数据改变了也就相应的去执行更新的回调就行了。
  6. 同理,数据修改时触发set也要让发布者通知订阅者执行更新回调。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id='app'>
    <span>姓名:{{name}}</span>
    <input  type="text" v-model='name'/>
    <span>爱好:{{more.hobby}}</span>
    <input type="text" v-model='more.hobby' />
  </div>
</body>
  <script>
    class Vue{
      constructor(obj_instance){
          this.$data=obj_instance.data
          //1.数据劫持
          Observer(this.$data)
          Compile(obj_instance.el,this)
      }
    }

    function Observer(data_instance){
      //递归出口
      if(!data_instance || typeof data_instance !=='object')
          return
      const dependency=new Dependency()
      Object.keys(data_instance).forEach((key)=>{
          //使用Object.defineProperty后属性就被修改,如果直接在get里调用值为undefined,所以先保存value
          let value=data_instance[key]
          //递归的对属性嵌套的对象添加监听
          Observer(value)
          Object.defineProperty(data_instance,key,{
              enumerable:true,
              configurable:true,
              get(){
                  console.log(`访问了属性:${key} ---> 值:${value}`)
                  //8.发布者收集订阅者
                  Dependency.temp &&dependency.addSub(Dependency.temp)
                  return value
              },
              set(newValue){
                  console.log(`修改了属性:${key} ---> 值:${newValue}`)
                  value=newValue
                  //如果对象属性被修改为新的对象,也要给新的对象属性们进行监听
                  Observer(newValue)
                  //9.属性修改赶紧告诉发布者,让发布者通知订阅者执行update操作
                  dependency.notify()
              }
          })
      })
    }
    /*
    *3.创建缓冲区将所有数据更新后再渲染页面
    */
    function Compile(element,vm){
      vm.$el=document.querySelector(element)
      // console.log(vm.$el.childNodes)
      //创建文档碎片
      const fragment=document.createDocumentFragment()
      let child
      while((child=vm.$el.firstChild)){
        // 将所有子节点摘下来放入文档碎片中,页面会变成空白因为createDocumentFragment是剪切过去的
          fragment.appendChild(child)
      }
      // console.log(fragment)
      // 4.替换文档碎片的值
      fragment_compile(fragment)

      /**
       * 替换文档碎片内容
      */
      function fragment_compile(node){
        // console.log(node)
          const pattern=/\{\{\s*(\S+)\s*\}\}/
          // 如果结点是文本,修改其中插值表达式的内容
          if(node.nodeType === 3){
            // console.log(node)
              //保存模板字符串中的属性名称
              const xxx=node.nodeValue
              const result_regex=pattern.exec(node.nodeValue)
              // console.log(node.nodeValue)
              // console.log(result_regex)
              if(result_regex){
                //more.like搞成['more','like']
                  const arr=result_regex[1].split('.')
                  // console.log(arr)
                  //叠加为vm.$data[more][like]
                  const value=arr.reduce((total,cur)=>total[cur],vm.$data)
                  //实现插值表达式显示data内容,value还是初始没变的旧值
                  node.nodeValue=xxx.replace(pattern,value)
                  //7.创建订阅者,如果改变了值以后如何更新
                  new Watcher(vm,result_regex[1],newValue=>{
                      node.nodeValue=xxx.replace(pattern,newValue)
                  })
              }
              return
          }
          // 10.处理input框的双向绑定
          if(node.nodeType===1 && node.nodeName ==='INPUT'){
            // console.log(node)
              const attr=Array.from(node.attributes);
              console.log(attr)
              attr.forEach(i=>{
                  if(i.nodeName === 'v-model'){
                      const value=i.nodeValue.split('.').reduce((total,cur)=>total[cur],vm.$data)
                      // Input赋值
                      node.value=value
                      //创建订阅者
                      new Watcher(vm,i.nodeValue,newValue=>{
                          node.value=newValue
                      })
                      // 11.监听input框的改变,同时改变数据
                      node.addEventListener('input',e=>{
                          const arr1=i.nodeValue.split('.')  //['more','like']
                          const arr2=arr1.slice(0,arr1.length-1) //['more']
                          const final=arr2.reduce((total,cur)=>total[cur],vm.$data)
                          final[arr1[arr1.length-1]]=e.target.value //vm.$data[]
                      })
                  }
              })
          }
          //递归修改结点的子节点
          node.childNodes.forEach(child=>{
              fragment_compile(child)
          })
      }
      vm.$el.appendChild(fragment)
    }

         /**
   * 5.实现响应式更新值 创建发布者:收集和响应订阅者,让订阅者更新
   */
   class Dependency{
        constructor(){
            this.subscribers=[]
        }
        addSub(sub){
            this.subscribers.push(sub)
        }
        notify(){
            this.subscribers.forEach(sub=>sub.update())
        }
    }

       /**
     * 6.创建订阅者类
     * key属性名
     * callback更新执行的回调函数
     */
     class Watcher {
        constructor(vm,key,callback){
            this.vm=vm
            this.key=key
            this.callback=callback
            // 临时属性
            Dependency.temp=this
            //访问属性能触发getter,例如more[like],为啥要触发是想要在get里执行dependy里添加订阅者,进行第8步
            key.split('.').reduce((total,cur)=>total[cur],vm.$data)
            // 因为getter会多次触发而一个属性只用订阅一次所以触发getter后让temp等于空,这样不会再add了
            Dependency.temp=null
        }
        update(){
          // console.log(this),this是触发的那个watcher
          console.log(this.key)
          const value=this.key.split('.').reduce((total,cur)=>total[cur],this.vm.$data)
          this.callback(value)
        }
    }

    const vm=new Vue(
      {
        el:'#app',
          data:{
            name:'tom',
            more:{
              hobby:'唱歌'
            }
          }
      }
    )
    </script>
</html>

vue3 proxy

不用defineProperty而是用proxy代理整个对象,对象属性的增加删除,数组等都可以。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值