前端mvvm框架中双向绑定的原理和实现

2019-1-24 Jon js库

1、通过Object.defineProperty(obj, prop, descriptor)劫持对象的属性读写,其中obj是要在上面定义属性的对象,prop是要定义或修改的属性名称,descriptor是属性的描述符。描述符中可选get和set键值。get是属性的getter方法,返回属性值;set为setter方法,接受唯一参数,并将该参数的值赋值给属性,get和set的默认值均为undefined。

2、双向绑定的简单实现。

    <input type="input" id="input">
    <span id="show"></span>
    <script>
        var obj = {};
        Object.defineProperty(obj, 'txt', {
            get: function () {
                return obj;
            },
            set: function (newValue) {
                document.getElementById('input').value = newValue;
                document.getElementById('show').innerHTML = newValue;
            }
        });
        document.getElementById('input').addEventListener('keyup', function (e) {
            obj.txt = e.target.value;
        });
    </script>

当通过input进行输入时,obj.txt的值会相应更新;当通过控制台改变obj.txt的值时,setter会改变view,从而实现了view=>model,model=>view的双向绑定。不过这种简单绑定不会真正执行obj.txt = e.target.value,obj永远为{},如果在set方法中赋值obj.txt = e.target.value,则会造成无限循环。为解决这个问题,将Object.defineProperty()封装为一个函数,即可在其中保存状态obj.txt,修改如下:

<input type="text" id="input">
<div id="show"></div>
<script>
    function defineProperty(obj, attr){
        var val;
        Object.defineProperty(obj, attr, {
            get: function () {
                return val;
            },
            set: function (newValue) {
                if (newValue === val){
                    return;
                }
                val = newValue;
                document.getElementById("input").value = newValue;
                document.getElementById("show").innerHTML = newValue;
            }
        });
    }
 
    var obj = {};
    defineProperty(obj, "txt");
    document.getElementById("input").addEventListener("keyup", function(e){
        obj.txt = e.target.value;
    })
</script>
上面就是一个最简单的双向绑定,但要实现类似于Vue的功能还需要继续改进。 
3、model=>view绑定
<div id='app'>
        <input type="text" v-model="input">
        {{text}}
    </div>
    <script>
      function compile(node, vm){
          if(node.nodeType === 1){
              var 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){
              let reg = /\{\{(.*)\}\}/;
              if(reg.test(node.nodeValue)){
                  let name = RegExp.$1;
                  name = name.trim();
                  node.nodeValue = vm.data[name];
              }
          }
      }
      
      function nodeToFragment(node, vm){
          var flag = document.createDocumentFragment();
          var child;
          while(child = node.firstChild){
              compile(child, vm);
              flag.appendChild(child);
          }
          return flag;
      }
      
      function Vue(options){
          var id = options.el;
          this.data = options.data;
          var dom = nodeToFragment(document.getElementById(id), this);
          document.getElementById(id).appendChild(dom);
      }
      
      var vm = new Vue({
          el: 'app',
          data: {
              input: 'hello',
              text: 'world'
          }
      });
    </script>
实现功能:通过Vue实例vm的el属性查找作用范围,范围内input节点中有v-model属性时,将input的值改为vm下data对应键的值,当检测到双花括号{{}}时,将该位置替换为vm下data对应属性的值。
vm通过new关键字声明,构造函数Vue中将el确定的节点及vm自身传入nodeToFragment函数,之后再将当前子节点替换为nodeToFragment函数返回的DocumentFragment对象。nodeToFragment函数将当前节点的子节点逐个传入编译函数compile,并将编译好的节点添加到DocumentFragment对象返回。编译函数compile()判断当前节点类型,节点为元素时,判断是否有v-model属性,并替换input.value,当节点为文本时,利用正则表达式判断是否含有花括号,并查找花括号内对应的值替换该节点。
存在的问题:
当{{}}不是app的子节点时,无法编译。解决方法是在compile()函数中,当节点为元素节点时,判断其是否有子节点,有则再次调用compile函数。 
function compile(node, vm){
    if(node.nodeType === 1){
        var 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[name];
                node.removeAttribute('v-model');
            }
        }
        if (child = node.firstChild) {
            compile(child, vm);
        }
    }
    if(node.nodeType === 3){
        let reg = /\{\{(.*)\}\}/;
        if(reg.test(node.nodeValue)){
            let name = RegExp.$1;
            name = name.trim();
            node.nodeValue = vm.data[name];
        }
    }
}
4、view=>model绑定
依照2中方法,先将vm下data中所有值通过Object.defineProperty()赋值,之后在编译函数compile()中为所有v-model添加事件监听,改动部分如下。
function observe(data, vm){
    Object.keys(data).forEach(function(key){
        Object.defineProperty(vm, key, {
            get: function (){
                return vm[key];
            },
            set: function (newValue){
                document.getElementById("show").innerHTML = newValue;
                document.getElementById("input").value = newValue;
            }
        });
    });
}
 
function compile(node, vm){
    if(node.nodeType === 1){
        var attr = node.attributes;
        for(let i = 0; i<attr.length; i++){
            if(attr[i].nodeName === 'v-model'){
                let name = attr[i].nodeValue;
                node.addEventListener('keyup', function(e){
                    vm[name] = e.target.value;
                });
                node.value = vm.data[name];
                node.removeAttribute('v-model');
            }
        }
        if (child = node.firstChild) {
            compile(child, vm);
        }
    }
    ...
}
 
function Vue(options){
    var id = options.el;
    this.data = options.data;
    observe(this.data, this);//添加view=>model的绑定
    ...
}

<div id='app'>
    <input type="text" v-model="input" id="input">
    <span id="show"></span>
</div>
此时改变input的值时,span也会改变;通过console改变vm.input时,span也会改变。但当在console中输入vm.input时会出现无限循环的情况:
这是由于在getter中会反复调用自身,下面对observe进行改进,将Object.defineProperty从函数中单独取出,构成一个闭包。
function defineProperty(vm, key, val){
    Object.defineProperty(vm, key, {
        get: function (){
            return val;
        },
        set: function (newValue){
            document.getElementById("show").innerHTML = newValue;
            document.getElementById("input").value = newValue;
            if(newValue === val){
                return;
            }
            val = newValue;
        }
    });
}
 
function observe(data, vm){
    Object.keys(data).forEach(function(key){
        defineProperty(vm, key, data[key]);
    });
}

修改后vm.input的值就能正确改变和返回了。但此时view的变化是通过setter手动添加的,而且只能是元素形式的节点,如果节点是{{text}}模板字符串则无法动态改变。解决办法是采用订阅/发布模式进行修改,对每个节点绑定一个观察者Watcher,对每个数据绑定一个分发者Dep,数据改变时,调用分发者的通知方法,通知每个观察者进行相应的改变。
5、订阅/发布模式(subscribe&publish)
采用订阅/发布模式对代码进行修改。首先定义观察者Watcher,并在编译函数compile()中对每个节点添加观察着Watcher,当接收到分发者指令时,调用update方法更新视图。接下来定义消息分发者Dep,Dep维护观察者数组,当值发生变化时,通知各观察者调用update方法。完整代码如下:

//第三部分
function Watcher(vm, node, name, nodeType){
    Dep.target = this;
    this.vm = vm;
    this.node = node;
    this.name = name;
    this.nodeType = nodeType;
    this.update();
    Dep.target = null;
}
Watcher.prototype = {
    update: function(){
        this.get();
        if (this.nodeType === 'text') {
            this.node.nodeValue = this.value;
        }
        if (this.nodeType === 'input') {
            this.node.value = this.value;
        }
    },
    get: function(){
        this.value = this.vm[this.name];
    }
}
 
function Dep(){
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub){
        this.subs.push(sub);
    },
    notify: function(){
        this.subs.forEach(function(sub){
            sub.update();
        });
    }
}
 
//第二部分
function defineProperty(vm, key, val){
    var dep = new Dep();
    Object.defineProperty(vm, key, {
        get: function (){
            if(Dep.target){
                dep.addSub(Dep.target);
            }
            return val;
        },
        set: function (newValue){
            if(newValue === val){
                return;
            }
            val = newValue;
            dep.notify();
        }
    });
}
 
function observe(data, vm){
    //Object.keys(data)返回data的key数组
    Object.keys(data).forEach(function(key){
        defineProperty(vm, key, data[key]);
    });
}
 
//第一部分
function compile(node, vm){
    if(node.nodeType === 1){
        var attr = node.attributes;
        for(let i = 0; i<attr.length; i++){
            if(attr[i].nodeName === 'v-model'){
                let name = attr[i].nodeValue;
                node.addEventListener('keyup', function(e){
                    vm[name] = e.target.value;
                });
                node.value = vm[name];
                node.removeAttribute('v-model');
                new Watcher(vm, node, name, "input");
            }
        }
        if (child = node.firstChild) {
            compile(child, vm);
        }
    }
    if(node.nodeType === 3){
        let reg = /\{\{(.*)\}\}/;
        if(reg.test(node.nodeValue)){
            let name = RegExp.$1;
            name = name.trim();
            // node.nodeValue = vm.data[name];
            new Watcher(vm, node, name, "text");
        }
    }
}
 
function nodeToFragment(node, vm){
    var flag = document.createDocumentFragment();
    var child;
    while(child = node.firstChild){
        compile(child, vm);
        flag.appendChild(child);
    }
    return flag;
}
 
function Vue(options){
    var id = options.el;
    var data = options.data;
    observe(data, this);
    var dom = nodeToFragment(document.getElementById(id), this);
    document.getElementById(id).appendChild(dom);
}
 
var vm = new Vue({
    el: 'app',
    data: {
        input: 'hello'
    }
});

原文连接:https://blog.csdn.net/orangecsy/article/details/79759663

以上适用于类vue的mvvm框架,angular2使用的是自己实现的双向绑定

	

标签: 底层 框架

分享这篇文章
赞助鼓励:如果觉得内容对您有所帮助,您可以支付宝(左)或微信(右):

声明:如无特殊注明,所有博客文章版权皆属于作者,转载使用时请注明出处。谢谢!

发表评论:

皖ICP备15010162号-1 ©2015-2022 知向前端
qq:1614245331 邮箱:13515678147@163.com Powered by emlog sitemap