• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
社会优先于个人
博客园    首页    新随笔    联系   管理    订阅  订阅
vue 2的数据双向绑定

参考文章

https://baijiahao.baidu.com/s?id=1678787660555400574&wfr=spider&for=pc

https://blog.csdn.net/weixin_40712618/article/details/126232040

vue双向绑定的核心

  • Object.defineProperty()。通过这个方法定义一个值。
  • 调用,使用了get方法
  • 赋值,使用了set方法

自己实现一个数据双向绑定

  • 界面影响数据,通过事件监听,改变数据,触发set函数
  • 数据影响界面,改变数据,触发set函数,操纵dom节点
<body>
  <!-- 自己手动实现一个js双向绑定 -->
   <div id="app">
      <input type="text" id="aa">
      <span id="bb"></span>
   </div>
</body>
<script>

  let obj={};
  let value='hello';
  Object.defineProperty(obj,'hello',{
    get: function(){
      console.log('get value:'+ value)
      return value
    },
    set: function(newvalue){
      value=newvalue
      console.log('set value:'+value)
      document.getElementById('aa').value=value;
      document.getElementById('bb').innerHTML=value
    }
  })
  document.addEventListener('keyup',function(e){
    obj.hello=e.target.value
  })
</script>

vue实现数据双向绑定,第一步:data数据绑定到dom节点中(内容绑定)

  • 自己手动实现代码
<body>
   <div id="app">
      <input type="text" v-model="text">
      {{text}}
   </div>
</body>
<script>
<!-- 定义模版编译函数,传入节点,和vue实例对象,将vm的data替换进去 -->
  function compile(node,vm){
    let reg=/\{\{(.*)\}\}/;
    if(node.nodeType===1){
      let attr=node.attributes;
      for(let i=0;i<attr.length;i++){
        if(attr[i].nodeName=='v-model'){
          let name=attr[i].nodeValue;
          node.value=vm.data[name];
          node.removeAttribute('v-model')
        }
      }
    }
    if(node.nodeType===3){
      if(reg.test(node.nodeValue)){
        let name=RegExp.$1;
        name=name.trim();
        node.nodeValue=vm.data[name]
      }
    }
  }
<!-- 定义碎片化文档函数,传入节点和它的所有子节点,和vue实例对象,遍历每个节点,模版编译每个节点,再装入碎片化文档这个容器-->
  function nodeToFragment(node,vm){
    let fragment=document.createDocumentFragment();
    let child;
    while(child=node.firstChild){
      compile(child,vm);
      fragment.appendChild(child)
    }
    return fragment
  }
<!-- 定义Vue构造函数,传入对象包含数据和元素id,碎片化文档函数结束后,再将这个碎片化文档容器加入到文档中-->
  function Vue(options){
    this.data=options.data;
    let id=options.el;
    let dom=nodeToFragment(document.getElementById(id),this);
    document.getElementById(id).appendChild(dom);
  }
  let vm=new Vue({
    el:'app',
    data:{
      text:'hello3333333'
    }
  })

</script>

documentFragment 碎片化文档

  • 可以把它当作是一个dom节点容器
  • 每当一个dom节点插入到文档中就会引发一次浏览器的回流,十分消耗资源
  • 使用碎片化文档,就是把所有节点先放入到一个容器中,最后再把容器插入到文档中,浏览器只回流了一次
  • 使用appendChild方法将原dom树的节点添加到documentFragment中,会删除原来的节点

vue实现数据双向绑定,第二步:view层影响modle层

  • 通过defineProperty把vm的data设置为访问器属性
  • 通过事件监听,获取最新的值,给data赋值,触发set函数
  • 自己手动实现代码
<body>
   <div id="app">
      <input type="text" v-model="text">
      {{text}}
   </div>
</body>
<script>
  function compile(node,vm){
    let reg=/\{\{(.*)\}\}/;
    if(node.nodeType===1){
      let attr=node.attributes;
      for(let i=0;i<attr.length;i++){
        if(attr[i].nodeName=='v-model'){
          let name=attr[i].nodeValue;
		  <!-- 给input节点绑定监听事件,改变访问器属性的数据,触发set函数-->
          node.addEventListener('input',function(e){
            vm[name]=e.target.value
          })
          node.value=vm[name];
          node.removeAttribute('v-model')
        }
      }
    }
    if(node.nodeType===3){
      if(reg.test(node.nodeValue)){
        let name=RegExp.$1;
        name=name.trim();
        node.nodeValue=vm[name]
      }
    }
  }

  function nodeToFragment(node,vm){
    let fragment=document.createDocumentFragment();
    let child;
    while(child=node.firstChild){
      compile(child,vm);
      fragment.appendChild(child)
    }
    return fragment
  }

 <!-- 给vm对象,增加访问器属性-->
  function defineReactive(obj,key,val){
    Object.defineProperty(obj,key,{
      get:function(){
        return val
      },
      set:function(newValue){
        if(newValue===val){
          return
        }
        val=newValue
        console.log('newvalue: '+val)
      }
    })
  }
  <!-- 将vm的data对应的属性,增加到vm作为访问器属性-->
  function observe(obj,vm){
    for(let key of Object.keys(obj)){
      defineReactive(vm,key,obj[key])
    }
  }

  function Vue(options){
    this.data=options.data;
    let data=this.data;
    observe(data,this)
    let id=options.el;
    let dom=nodeToFragment(document.getElementById(id),this);
    document.getElementById(id).appendChild(dom);
  }
  let vm=new Vue({
    el:'app',
    data:{
      text:'22'
    }
  })

</script>


vue实现数据双向绑定,第三步:modle层影响view层

  • 发布订阅者模式
 <!-- 三个订阅者-->
  let sub1={update:function(){console.log(1)}}
  let sub2={update:function(){console.log(2)}}
  let sub3={update:function(){console.log(3)}}

<!-- 调度中心有几个订阅者-->
  function Dep(){
    this.subs=[sub1,sub2,sub3]
  }
<!-- 调度中心构造函数的原型对象增加一个notify方法,遍历调度中心订阅者去执行update方法-->
  Dep.prototype.notify=function(){
    this.subs.forEach(function(sub){
      sub.update();
    })
  }
<!-- 构造一个调度中心实例对象-->
  let dep=new Dep()

<!-- 定义一个发布者-->
  let pub={
    publish:function(){
      dep.notify();
    }
  }
 <!-- 发布者发布消息-->
  pub.publish()

  • set函数的第二个作用就是发布消息
  • 每当new一个vue,主要做两件事情,监听数据和编译html
  • 监听数据的过程,会给data的每一个属性生成一个dep(调度中心)
  • 在编译html,会给数据绑定的节点生成一个订阅者watcher,watcher会把自己添加到相应属性的dep容器中
<body>
   <div id="app">
      <input type="text" v-model="text">
      {{text}}
   </div>
</body>
<script>
<!-- 调度中心构造函数-->
  function Dep(){
    this.subs=[]
  }
<!-- 定义调度中心构造函数的原型对象,添加订阅者,订阅者更新数据-->
  Dep.prototype={
    addSub(sub){
      this.subs.push(sub)
    },
    notify(){
      this.subs.forEach(function(sub){
        sub.update();
      })
    }
  }
<!-- 定义订阅者构造函数,Dep.target是个全局变量,先赋值为订阅者实例,订阅者更新数据后,Dep.target再改为null-->
  function Watcher(vm,node,name){
    Dep.target=this;
    this.vm=vm;
    this.node=node;
    this.name=name;
    this.update();
    Dep.target=null;
  }
<!-- 定义订阅者构造函数的原型对象,更新界面的节点,获取vm访问器属性的数据-->
  Watcher.prototype={
    update(){
      this.get();
      this.node.nodeValue=this.value;
    },
    get(){
      this.value=this.vm[this.name]
    }
  }




  function compile(node,vm){
    let reg=/\{\{(.*)\}\}/;
    if(node.nodeType===1){
      let attr=node.attributes;
      for(let i=0;i<attr.length;i++){
        if(attr[i].nodeName=='v-model'){
          let name=attr[i].nodeValue;
          node.addEventListener('input',function(e){
            vm[name]=e.target.value
          })
          node.value=vm[name];
          node.removeAttribute('v-model')
        }
      }
    }
    if(node.nodeType===3){
      if(reg.test(node.nodeValue)){
        let name=RegExp.$1;
        name=name.trim();
		<!-- 不是直接修改界面的节点,而是新建订阅者实例-->
        // node.nodeValue=vm[name]
        new Watcher(vm,node,name)
      }
    }
  }

  function nodeToFragment(node,vm){
    let fragment=document.createDocumentFragment();
    let child;
    while(child=node.firstChild){
      compile(child,vm);
      fragment.appendChild(child)
    }
    return fragment
  }

  function defineReactive(obj,key,val){
    // 每一个访问器属性,都定义一个调度中心
    let dep=new Dep();
    Object.defineProperty(obj,key,{
      get:function(){
	   // 如果Dep.target是个watcher实例,就要增加一个订阅者
        if(Dep.target){
          dep.addSub(Dep.target)
        }
        return val
      },
      set:function(newValue){
        if(newValue===val){
          return
        }
        val=newValue
        console.log('newvalue: '+val)
		// 修改数据值,调度者中心就要通知订阅者更新
        dep.notify();
      }
    })
  }

  function observe(obj,vm){
    for(let key of Object.keys(obj)){
      defineReactive(vm,key,obj[key])
    }
  }


  function Vue(options){
    this.data=options.data;
    let data=this.data;
    observe(data,this)
    let id=options.el;
    let dom=nodeToFragment(document.getElementById(id),this);
    document.getElementById(id).appendChild(dom);
  }
  let vm=new Vue({
    el:'app',
    data:{
      text:'22'
    }
  })


</script>

总结

  • 给data中数据的属性,通过defineProperty方法定义为访问器属性,这样就拥有了set函数和get函数和调度中心
  • 通过createDocumentFragment方法定义一个碎片化文档,可以把它理解为一个容器,然后将dom节点放进这个容器中
  • 在放每个节点进入容器中的过程,可以将vue模版代码编译为正常的html代码
  • 编译过程,需要将访问器属性的数据值替换vue的插值语法和v-model语法的值。在替换的过程,就会调用get函数,给调度中心添加订阅者。
  • 替换v-model的值时,需要绑定监听事件,一旦input值改变,就会修改访问器属性的数据值,就会修改dom节点值。
  • 访问器属性的数据值改变,就会触发set函数,调度中心就会发出通知订阅者更新,其实就是改变节点值
posted on 2022-04-09 09:50  社会优先于个人  阅读(508)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3