Vue.js双向绑定原理

Vue.js最核心的功能有两个,一个是响应式的数据绑定系统,另一个是组件系统。本文仅仅探究双向绑定是怎样实现的。先讲涉及的知识点,再用简化的代码实现一个简单的hello world示例。

一、访问器属性

访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()方法单独定义。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <script>
      var obj = {};
      Object.defineProperty(obj, ‘hello‘, {
        get: function() {
          console.log(‘get方法被调用了‘);
        },
        set: function(val) {
          console.log(‘set方法被调用了,参数是‘ + val);
        }
      });
      obj.hello; //get方法被调用了
      obj.hello = ‘abc‘; //set方法被调用了,参数是abc
    </script>
  </body>
</html>

get和set方法内部的this都指向obj,这意味着get和set函数可以操作对象内部的值。另外,访问器属性的会“覆盖”同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略。

二、极简的双向绑定实现

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <input type="text" id="a" />
    <span id="b"></span>
    <script>
      var obj = {};
      Object.defineProperty(obj, ‘hello‘, {
        set: function(newval) {
          document.getElementById(‘a‘).value = newval;
          document.getElementById(‘b‘).innerHTML = newval
        }
      });
      document.addEventListener(‘keyup‘, function(e) {
        obj.hello = e.target.value;
      })
    </script>
  </body>
</html>

此例实现的效果是:随着文本框输入文字的变化,span中会同步显示相同的内容。在js或者在控制台上显式的修改obj.hello的值,视图会相应的更新。这样就实现了model=>view以及view=>model的双向绑定。

以上就是Vue实现双向绑定的基本原理。

三、分解任务

上述示例仅仅是为了说明原理,我们最终要实现的是:

<div id="app">
  <input type="text" v-model="text">
  {{ text }}
</div>

var vm = new Vue({
  el:‘#app‘,
  data:{
    text:‘hello world‘
  }
})

首先将该任务分成几个子任务:

1、输入框以及文本节点与data中的数据绑定;

2、输入框内容变化时,data中的数据同步变化,即view =>model的变化;

3、data中的数据变化时,文本节点的内容同步变化,即model =>view的变化;

要实现任务1,需要对DOM进行编译,这里有一个知识点:DocumentFragment。

四、DocumentFragment

DocumentFragment(文档片段)可以看做节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用DocumentFragment处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过append方法,DOM中的节点会被自动删除)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <div id="app">
      <input type="text" id="a" />
      <span id="b"></span>
    </div>
    <script>
      var dom = nodeToFragment(document.getElementById(‘app‘));
      console.log(dom);

      function nodeToFragment(node) {
        var flag = document.createDocumentFragment();
        var child;
        while(child == node.firstChild) {
          flag.appendChild(child); //劫持node的所有子节点
        }
        return flag;
      }

      document.getElementById(‘app‘).appendChild(dom); //返回到app中
    </script>
  </body>
</html>

五、数据初始化绑定

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

    <script>
      function compile(node, vm) {
        var reg = /\{\{(.*)\}\}/;
        // 节点类型为元素
        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; // 获取 v-model 绑定的属性名
              node.value = vm[name]; // 将 data 的值赋给该 node
              node.removeAttribute(‘v-model‘);
            }
          };
        }
        // 节点类型为 text
        if(node.nodeType === 3) {
          if(reg.test(node.nodeValue)) {
            var name = RegExp.$1; // 获取匹配到的字符串
            name = name.trim();
            node.nodeValue = vm.data[name]; //将data的值赋给该node
          }
        }
      }

      function nodeToFragment(node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        // 所有表达式必然会返回一个值,赋值表达式亦不例外
        // 理解了上面这一点,就能理解 while (child = node.firstChild) 这种用法
        // 其次,appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
        // 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
        while(child = node.firstChild) {
          compile(child, vm);
          flag.appendChild(child); // 将子节点劫持到文档片段中
        }
        return flag;
      }

      function Vue(options) {
        this.data = options.data;
        var id = options.el;
        var dom = nodeToFragment(document.getElementById(id), this);
        // 编译完成后,将 dom 返回到 app 中
        document.getElementById(id).appendChild(dom);
      }
      var vm = new Vue({
        el: ‘app‘,
        data: {
          text: ‘hello world‘
        }
      });
    </script>
  </body>
</html>

以上代码实现了任务一,我们可以看到,hello world已经呈现在输入框和文本节点中。

六、响应式的数据绑定

再来看任务2的是实现思路:当我们在输入框输入数据的时候,首先触发input事件或者keyup、change事件,在相应的事件处理程序中,我们获取输入框的value并赋值给vm实例的text属性。我们会利用defineProperty将data中的text设置为vm的访问器属性,因此给vm.text赋值就会触发set方法。在set方法中主要做两件事,第一是更新属性的值,第二留到任务3来说。

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

    <script>
      function observe(obj, vm) {
        Object.keys(obj).forEach(function(key) {
          defineReactive(vm, key, obj[key]);
        })
      }

      function defineReactive(obj, key, val) {
        Object.defineProperty(obj, key, {
          get: function() {
            return val
          },
          set: function(newVal) {
            if(newVal === val) return
            val = newVal;
            console.log(val); //方便看效果
          }
        });
      }

      function nodeToFragment(node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        // 所有表达式必然会返回一个值,赋值表达式亦不例外
        // 理解了上面这一点,就能理解 while (child = node.firstChild) 这种用法
        // 其次,appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
        // 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
        while(child = node.firstChild) {
          compile(child, vm);
          flag.appendChild(child); // 将子节点劫持到文档片段中
        }
        return flag;
      }

      function compile(node, vm) {
        var reg = /\{\{(.*)\}\}/;
        // 节点类型为元素
        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; // 获取 v-model 绑定的属性名
              node.addEventListener(‘input‘, function(e) {
                // 给相应的 data 属性赋值,进而触发该属性的 set 方法
                vm[name] = e.target.value;
              });
              node.value = vm[name]; // 将 data 的值赋给该 node
              node.removeAttribute(‘v-model‘);
            }
          };
        }
        // 节点类型为 text
        if(node.nodeType === 3) {
          if(reg.test(node.nodeValue)) {
            var name = RegExp.$1; // 获取匹配到的字符串
            name = name.trim();
            node.nodeValue = vm[name]; //将data的值赋给该node
          }
        }
      }

      function Vue(options) {
        this.data = options.data;
        var data = this.data;
        observe(data, this);
        var id = options.el;
        var dom = nodeToFragment(document.getElementById(id), this);
        // 编译完成后,将 dom 返回到 app 中
        document.getElementById(id).appendChild(dom);
      }
      var vm = new Vue({
        el: ‘app‘,
        data: {
          text: ‘hello world‘
        }
      });
    </script>
  </body>
</html>

任务2也就完成了,text属性值会与输入框的内容同步变化(打开浏览器后台进行查看)。

七、订阅/发布模式(subscribe&publish)

text属性变化了,set方法触发了,但是文本节点的内容没有变化。如何让同样绑定到text的文本节点也同步变化呢?这里又有一个知识点:订阅发布模式。

订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。发布者发出通知 =>主题对象收到通知并推送给订阅者 =>订阅者执行相应操作。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Two-way-data-binding</title>
  </head>
  <body>
    <script>
      //一个发布者publisher
      var pub = {
        publish: function() {
          dep.notify();
        }
      }

      //三个订阅者subscribers
      var sub1 = {
        update: function() {
          console.log(1)
        }
      };
      var sub2 = {
        update: function() {
          console.log(2)
        }
      };
      var sub3 = {
        update: function() {
          console.log(3)
        }
      };

      //一个主题对象
      function Dep() {
        this.subs = [sub1, sub2, sub3];
      }
      Dep.prototype.notify = function() {
        this.subs.forEach(function(sub) {
          sub.update();
        })
      }

      //发布者发布消息,主题对象执行notify方法,进而触发订阅者执行update方法
      var dep = new Dep();
      pub.publish(); //1,2,3
    </script>
  </body>
</html>

之前提到的,当set方法触发后做的第二件事就是作为发布者发出通知:“我是属性text,我变了”。文本节点则是作为订阅者,在收到消息后执行相应的更新操作。

八、双向绑定的实现

回顾一下,每当 new 一个 Vue,主要做了两件事:第一个是监听数据:observe(data),第二个是编译 HTML:nodeToFragement(id)。

在监听数据的过程中,会为data中的每一个属性生成一个主题对象dep。

在编译HTML的过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中。

我们已经实现:修改输入框内容 =>在事件回调函数中修改属性值 =>触发属性的set方法。接下来我们要实现的是:发出通知dep.notify() =>触发订阅者的update方法 =>更新视图。

这里的关键逻辑时:如何将watcher添加到关联属性的dep中。

 function compile(node, vm) {
   var reg = /\{\{(.*)\}\}/;
   // 节点类型为元素
   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; // 获取 v-model 绑定的属性名
         node.addEventListener(‘input‘, function(e) {
           // 给相应的 data 属性赋值,进而触发该属性的 set 方法
           vm[name] = e.target.value;
         });
         node.value = vm[name]; // 将 data 的值赋给该 node
         node.removeAttribute(‘v-model‘);
       }
     };
     new Watcher(vm, node, name, ‘input‘);
   }
   // 节点类型为 text
   if(node.nodeType === 3) {
     if(reg.test(node.nodeValue)) {
       var name = RegExp.$1; // 获取匹配到的字符串
       name = name.trim();
       new Watcher(vm, node, name, ‘text‘);
     }
   }
 }

在编译HTML过程中,为每个与data关联的节点生成一个watcher,watcher函数中发生了什么呢?

function Watcher(vm, node, name, nodeType) {
  Dep.target = this;
  this.name = name;
  this.node = node;
  this.vm = vm;
  this.update();
  Dep.target = null;
}
Watcher.prototype = {
  update: function() {
    this.get();
    this.node.nodeValue = this.value;
  },
  // 获取 data 中的属性值
  get: function() {
    this.value = this.vm[this.name]; // 触发相应属性的 get
  }
}

首先,将自己赋给了一个全局变量Dep.target;

其次,执行了update方法,进而执行了get方法,get的方法读取了vm的访问器属性,从而触发了访问器属性的get方法,get方法中将该watcher添加到了对应访问器属性的dep中;

接着,获取属性的值,然后更新视图。

最后,将Dep.target设为空,因为它是全局变量,也是watcher与dep关联的唯一桥梁,任何时刻都必须保证Dep.target只有一个值。

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;
      // 作为发布者发出通知
      dep.notify();
    }
  });
}

function Dep() {
  this.subs = []
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },
  notify: function() {
    this.subs.forEach(function(sub) {
      sub.update();
    });
  }
}

至此,hello world双向绑定就基本实现了。文本内容会随输入框内容同步变化,在控制器中修改vm.text的值,会同步反映到文本内容中。以下是完整代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Two-way-data-binding</title>
  </head>
  <body>

    <div id="app">
      <input type="text" v-model="text"> {{ text }}
    </div>

    <script>
      function observe(obj, vm) {
        Object.keys(obj).forEach(function(key) {
          defineReactive(vm, key, obj[key]);
        })
      }

      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;
            // 作为发布者发出通知
            dep.notify();
          }
        });
      }

      function nodeToFragment(node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        // 所有表达式必然会返回一个值,赋值表达式亦不例外
        // 理解了上面这一点,就能理解 while (child = node.firstChild) 这种用法
        // 其次,appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
        // 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
        while(child = node.firstChild) {
          compile(child, vm);
          flag.appendChild(child); // 将子节点劫持到文档片段中
        }
        return flag
      }

      function compile(node, vm) {
        var reg = /\{\{(.*)\}\}/;
        // 节点类型为元素
        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; // 获取 v-model 绑定的属性名
              node.addEventListener(‘input‘, function(e) {
                // 给相应的 data 属性赋值,进而触发该属性的 set 方法
                vm[name] = e.target.value;
              });
              node.value = vm[name]; // 将 data 的值赋给该 node
              node.removeAttribute(‘v-model‘);
            }
          };
          new Watcher(vm, node, name, ‘input‘);
        }
        // 节点类型为 text
        if(node.nodeType === 3) {
          if(reg.test(node.nodeValue)) {
            var name = RegExp.$1; // 获取匹配到的字符串
            name = name.trim();
            new Watcher(vm, node, name, ‘text‘);
          }
        }
      }

      function Watcher(vm, node, name, nodeType) {
        Dep.target = this;
        this.name = name;
        this.node = node;
        this.vm = vm;
        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;
          }
        },
        // 获取 data 中的属性值
        get: function() {
          this.value = this.vm[this.name]; // 触发相应属性的 get
        }
      }

      function Dep() {
        this.subs = []
      }
      Dep.prototype = {
        addSub: function(sub) {
          this.subs.push(sub);
        },
        notify: function() {
          this.subs.forEach(function(sub) {
            sub.update();
          });
        }
      }

      function Vue(options) {
        this.data = options.data;
        var data = this.data;
        observe(data, this);
        var id = options.el;
        var dom = nodeToFragment(document.getElementById(id), this);
        // 编译完成后,将 dom 返回到 app 中
        document.getElementById(id).appendChild(dom);
      }
      var vm = new Vue({
        el: ‘app‘,
        data: {
          text: ‘hello world‘
        }
      })
    </script>
  </body>
</html>

参考文章1:https://github.com/DDFE/DDFE-blog/issues/7

参考文章2:https://segmentfault.com/a/1190000006599500

原文地址

原文地址:https://www.cnblogs.com/fengxiongZz/p/8120337.html

时间: 2024-10-10 20:50:19

Vue.js双向绑定原理的相关文章

vue的双向绑定原理及实现

前言 使用vue也好有一段时间了,虽然对其双向绑定原理也有了解个大概,但也没好好探究下其原理实现,所以这次特意花了几晚时间查阅资料和阅读相关源码,自己也实现一个简单版vue的双向绑定版本,先上个成果图来吸引各位: 代码:                                                                    效果图:   是不是看起来跟vue的使用方式差不多?接下来就来从原理到实现,从简到难一步一步来实现这个SelfVue.由于本文只是为了学习和分享

vue的双向绑定原理浅析与简单实现

很久之前看过vue的一些原理,对其中的双向绑定原理也有一定程度上的了解,只是最近才在项目上使用vue,这才决定好好了解下vue的实现原理,因此这里对vue的双向绑定原理进行浅析,并做一个简单的实现. vue双向绑定的原理浅析 vue数据的双向绑定是通过数据劫持,并结合发布-订阅模式的方式来实现的. 我们先通过一个最简单的vue例子来查看vue初始化数据上的对象到底是什么东西. var vm = new Vue({ data: { // 双向绑定的数据对象 obj: { a: 1 } }, cre

Vue.js双向绑定的实现原理

Vue.js最核心的功能有两个,一是响应式的数据绑定系统,二是组件系统.本文仅探究几乎所有Vue的开篇介绍都会提到的hello world双向绑定是怎样实现的.先讲涉及的知识点,再参考源码,用尽可能少的代码实现那个hello world开篇示例. 参考文章:https://segmentfault.com/a/1190000006599500 一.访问器属性 访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()方法单独定义. var obj = {

Vue数据双向绑定原理及简单实现

嘿,Goodgirl and GoodBoy,点进来了就看完点个赞再go. Vue这个框架就不简单介绍了,它最大的特性就是数据的双向绑定以及虚拟dom.核心就是用数据来驱动视图层的改变.先看一段代码. 一.示例 var vm = new Vue({ data: { obj: { a: 1 } }, created: function () { console.log(this.obj); } }); 二.实现原理 vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的. 1)数据劫

Vue的双向绑定原理

Vue的构造函数分析 vm就是MVVM中的View Model var vm = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }) /* 相关于Vue的构造函数 */ function Vue(options) { // 将选项对象保存到vm this.$options = options; // 将data对象保存到vm和data变量中 var data = this._data = this.$options.data; //将

深入vue源码,了解vue的双向数据绑定原理

大家都知道vue是一种MVVM开发模式,数据驱动视图的前端框架,并且内部已经实现了双向数据绑定,那么双向数据绑定是怎么实现的呢? 先手动撸一个最最最简单的双向数据绑定 1 <div> 2 <input type="text" name="" id="text"> 3 <span id="show"></span> 4 </div> 5 6 <script>

[Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅

有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了.本文我们一起通过学习双向绑定原理来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vue模板编译原理(一)-Template生成AST 一起来学Vue模板编译原理(二)-AST生成Render字符串 一起来学Vue虚拟DOM解析-Virtual Dom实现和Dom-diff算法 这些文章统一放在我的git仓库:https://github.com/yzsun

vue双向绑定原理

vue双向绑定原理的核心 它的实现的核心是通过Object.defineProperty(),对data的每个属性进行了get.set的拦截. 其实只要Object.defineProperty()已经可以实现双向绑定,只是这样做效率非常低. 观察者模式 它在双向绑定当中是什么角色呢? 它其实是让双向绑定更有效率 为什么? 观察者模式,它是一对多的一种模式,在vue里面,"一"是改了某一data数据,"多"是页面上凡是用了这个数据的地方,都更新.这就是页面上的很多&

事件双向绑定原理

事件 案例: vue的事件绑定原理:改变图片的背景颜色问题来实现这个框架的使用方法, new Vue({ el:"", data:{}, methord:{}, computed:{}   计算属性的使用:用于大量基于数据模型的计算,但并不是每一次渲染都发生更改,计算属性就会将数据直接返回,这样提高计算效率. mounthd:{} }) 我们在事件绑定的时候传入参数:比如点击事件,@click=“funname($event,index)” <!DOCTYPE html>