原生js实现双向数据绑定-类似vue.js的数据绑定效果

原生js实现双向数据绑定-类似vue.js的数据绑定效果

JavaScript Vue.js相关

详细介绍

demo在线地址

https://wengzs.github.io/JS-MVVM-demo/test.html

原理

  • Vue.js实现数据双向绑定的方法是:getter/setter劫持 + 发布/订阅模式

源码大致步骤

  • DOM劫持

在new根组件的时候,会将DOM树劫持,通过document.createDocumentFragment()创建一个文档节点碎片,将劫持来的DOM节点都移到这个节点碎片里,并对每个节点进行匹配,规则如下:

如匹配到{{}}插值节点,会将这个节点通过watcher进行订阅;如匹配到带有v-model属性的节点,会直接将添加对input事件的监听,并在回调函数中触发vue.data的get方法,以获得vue.data中对应的值;

  • 重写setter/getter

重写vue.data的getter和setter,并在里面进行数据劫持,添加订阅和发布消息。

  • watcher / pool

定义一个watcher对象来进行数据的监听和交换;定义一个pool全局对象,来储存订阅和发布的消息。

个人精简

  • DOM劫持

匹配到{{}}的时候,会直接将插值内的value和这个插值节点作为一个键值对,直接储存到watcher原型属性中。

  • 重写getter/setter

在setter中直接调用watcher的dispatch方法,修改{{}}处的值

  • 定义一个watcher

定义watcher的两个原型方法:commit / dispatch和一个储存作用的空对象属性

一点思路

  • 数据从哪里开始变动? 1、data里的setter; 2、html的标签,如input的值。
    只要我们在这两个变动源添加相应的事件监听就可以了。

源码

<!DOCTYPE html>
<html>
<head>
  <title>test</title>
</head>
<body>

  <div id='app'>
    <input class='inp' v-model='number' placeholder="请输入number值...">
    <span>number值:</span>
    {{number}}
  </div>
  <style>
    #app {
      display: flex;
      align-items: center;
      justify-content: center;
      margin: 100px auto;
      height: 200px;
      width: 800px;
      border: 1px solid #A5A5A5;
      border-radius: 10px;
      font-size: 22px;
    }
    .inp {
      height: 22px;
      width: 200px;
      margin-right: 30px;
      font-size: 16px;
      border-radius: 5px;
      border: 1px solid #A5A5A5;
    }
  </style>

  <script>
    // 定义构造函数Vue()
    function Vue (options) {
      this.data = options.data;
      let data = this.data;
      // 给setter添加监听
      setterObserver(data, this);
      // 劫持DOM节点,并对<input> 和 {{ }} 进行处理
      let id = options.el;
      let dom = hijackDom(document.getElementById(id), data);
      // 归还处理后的DOM
      document.getElementById(id).appendChild(dom);
    }

    // 添加setter监听
    function setterObserver(data, vue) {
      if (!data || typeof data !== 'object') {
        return
      }
      Object.keys(data).forEach(function (key) {
        let value = data[key];
        setterObserver(value);
        Object.defineProperty(data, key, {
          get: function () {
            return value;
          },
          set: function (val) {
            let watcher = new Watcher();
            watcher.dispatch(key, val);
            value = val;
          }
        })
      })
    }

    // 劫持DOM节点
    function hijackDom (node, data) {
      let dom = document.createDocumentFragment();
      let child;
      while (child = node.firstChild) {
        compile(child, data);
        dom.appendChild(child);
      }
      return dom
    }

    // 处理 {{}} 和 input
    function compile (node, data) {
      if (node.nodeType === 1) {
        let attr = node.attributes;
        for (let i = 0; i < attr.length; i++) {
          if (attr[i].nodeName == 'v-model') {
            let key = attr[i].nodeValue;
            node.addEventListener('input', function (e) {
              data[key] = e.target.value;
            });
            node.removeAttribute('v-model');
          }
        }
      }
      if (node.nodeType === 3) {
        let reg = /\{\{(.*)\}\}/;
        if (reg.test(node.textContent)) {
          let name = RegExp.$1;
          name.replace(/(^\s*)|(\s*$)/g, '');
          node.textContent = data[name];
          let watcher = new Watcher();
          watcher.commit(name, node);
        }
      }
    }

    // watcher 用原型模式
    function Watcher () {}
    Watcher.prototype = {
      pool: {},
      commit: function (key, node) {
        if (!this.pool[key]) {
          this.pool[key] = node;
        }
      },
      dispatch: function (key, newValue) {
        if (this.pool[key]) {
          Watcher.prototype.pool[key].textContent = newValue;
        }
      }
    }

    let vm = new Vue({
      el: 'app',
      data: {
        number: '1'
      }
    })
  </script>
</body>
</html>
推荐源码