简陋的双向绑定

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <input type="text" v-model="text">
    {{text}}
  </div>
</body>
<script type="text/javascript">

//将结点劫持
  function node2Fragment(node,vm){
    var flag=document.createDocumentFragment();
    var child;
    while(child=node.firstChild){
      //编译每个节点
      compile(child,vm)
      /*appendChild 会把节点从原来结构转移到目标父节点,所以相当于原来的节点被删除*/
      flag.appendChild(child)
    }
    return flag
  }

  /*
  DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。
  因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(reflow)(对元素位置和几何上的计算)。因此,使用文档片段document fragments 通常会起到优化性能的作用
   */
  
  //编译虚拟dom的辅助函数
  function compile(node,vm){
    /*匹配{{里的内容}}*/
    var reg=/{{(.*)}}/;

    /*nodeType -1元素节点 3-文本节点*/
    if(node.nodeType===1){
      var attr=node.attributes;
      for(var i=0;i<attr.length;i++){
        if(attr[i].nodeName=='v-model'){
          var name=attr[i].nodeValue;
          node.addEventListener('input',function(e){
            vm[name]=e.target.value;
          })
          node.value=vm[name];
        }
      }
    };
    if(node.nodeType===3){
      if(reg.test(node.nodeValue)){
        var name=RegExp.$1;
        name=name.trim();
        //初始化数据并且触发get函数
        new Watcher(vm,node,name);
      }
    }
  }
  //定义订阅者类
  function Watcher(vm,node,name){
    //Dep.target存储了当前的观察者,使get函数能够存储观察者
    Dep.target=this;
    this.name=name;
    this.node=node;
    this.vm=vm;
    //订阅者执行一次更新视图并把订阅者添加进去
    this.update();
    Dep.target=null;
  }
  Watcher.prototype={
    update:function(){
      //触发set函数
      this.get();
      //更新视图
      this.node.nodeValue=this.value;
    },
    get:function(){
      this.value=this.vm[this.name]//触发对应数据的get方法
    }
  }
  //响应式的数据绑定函数
  function defineReactive(obj,key,val){
    //定义一个主题实例
    var dep=new Dep()
    Object.defineProperty(obj,key,{
      get:function(){
        //添加订阅者watcher到主题对象Dep
        if(Dep.target)dep.addSub(Dep.target)
        return val
      },
      set:function(newVal){
        if(newVal===val)return ;
        val=newVal;
        //实例发出通知(更新所有订阅了这个属性消息的view)
        dep.notify();
      }
    })
  }
  function Dep(){
    //主题的订阅者们
    this.subs=[];
  }
  Dep.prototype={
    //添加订阅者的方法
    addSub:function(sub){
      this.subs.push(sub);
    },
    //发布信息的方法(让订阅者watcher们全部更新view)
    notify:function(){
      this.subs.forEach(function(sub){
        sub.update();
      })
    }
  }
  //监听vm这个对象的obj有的属性
  function observe(obj,vm){
    Object.keys(obj).forEach(function(key){
      console.log(vm)
      defineReactive(vm,key,obj[key])
    })
  }
  //构建Vue对象
  function Vue(options){
    this.data=options.data;
    var id=options.el;
    var data=this.data
    //监听this(即vm)这个对象的data属性
    observe(data,this)
    var dom=node2Fragment(document.getElementById(id),this);
    document.getElementById(id).appendChild(dom);
  }
  var vm=new Vue({
    el:'app',
    data:{
      text:'hello world',
      fuck:'fuckingboy'
    }
  })
  console.log(vm);

/*
  使用发布-订阅模式与闭包,很巧妙的实现了双向绑定,
  首先observe调用,遍历数据对象,并给每个属性利用闭包创建私有空间,
  每个私有空间都new出一个Dep对象,试每个空间都有自己的订阅发布对象,
  并在get访问器里添加订阅者,那么每个Watcher对象里设置当前订阅对象利用
  触发get访问器方法可以向当前属性的私有空间添加订阅者,然后只要每次
  设置这个属性的值时就会触发设置器就会直接触发向所有订阅了这个属性订阅者
  接收到发布的消息,那么就可以更新对应视图内容。
 */

</script>
</html>

扒一张图,代表一下。

一直听说vue的双向绑定很巧妙,很小巧,所以就大概学习一下。

一个双向绑定系统是要很严谨的代码实现的,所以代码量一定不少,

要考虑的因素很多,如data深递归,html各种节点的深递归,还有各种

数据对应视图的那些节点位置对应,还有正则的使用,这些种种考虑

就不是一般几天可以搞定的,所以学习一下就可以,没有必要造轮子,

以上的例子无法符合数据的多层嵌套和就实现了一个v-model,其他都没有,

只能加深双向绑定原理,好以后对这方面的使用更顺手。

原文地址:https://www.cnblogs.com/zhangzhicheng/p/8900722.html