基于Proxy实现Vue的双向数据绑定

基于Proxy实现Vue的双向数据绑定

在得知Vue3.0中将使用Proxy取代现有的Object.defineProperty之后,我便去学习了Proxy和Reflect这两个ES6的新特性,

也顺带了解了使用Proxy取代Object.defineProperty的原因,最后自己动手实现了基于Proxy实现Vue的双向数据绑定。

此文用作总结。

vue2.x在实现观察者机制的时候,使用的是对象的Object.defineProperty属性,它可以对一个对象的属性进行监控,

对属性的默认方法进行改写,然后达到监控属性变化的效果。但是Object.defineProperty有两个缺点:

  • Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。

    为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组。

  • Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。

    Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,

    显然如果能劫持一个完整的对象是才是更好的选择。

和Object.defineProperty不同的是,

Proxy可以劫持整个对象,并返回一个新的对象。

Proxy有13种劫持操作。(get,set,has,deleteProperty,ownKeys,getOwnPropertyDescriptor等等)


那么什么是Proxy呢?

proxy用于修改某些操作的默认行为,也可以理解为在目标对象前面设一层拦截,外部的所有访问都必须通过这层拦截

proxy会返回一个对象A,这个对象就是用户与源对象B的一个中介,当用户对A进行操作时,都会实现A里面的逻辑(大部分都是对一些操作的改写),然后间接地对源对象进行改写。

例如:

    let objA = {a: 1};

    let objB = new Proxy(objA,{
        set: function(target,key,value){
            target[key] = value *2;
        }
    })
    console.log(objB.a); // 1
    objB.a = 4;
    console.log(objB.a); //8
    console.log(objA.a); //8

那么什么是Reflect呢?

Reflect是ES6为操作对象而提供的新API,而这个API设计的目的只要有:

  • 将Object对象的一些属于语言内部的方法放到Reflect对象上,从Reflect上能拿到语言内部的方法
  • 修改某些object方法返回的结果
  • 让Object的操作都变成函数行为
  • Reflect对象的方法与Proxy对象的方法一一对应,只要proxy对象上有的方法reflect也能找到

简而言之,Reflect就是一种操作对象的新方法,他可以让让Object的操作都变成函数行为,也可以改写方法返回的结果。


正题:基于Proxy实现Vue的双向数据绑定

首先,建立基本的dom元素,并实现垂直水平居中样式:

<style>
#app{
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-50%);
}


</style>
<body>
    <div id="app">
        <input type="text" id="input" /><button type="button" name="button" id="btn">添加</button>
        <div>您输入的是:<span id="text"></span></div>
        <ul id="list"></ul>
    </div>
</body>

然后,让输入框和文本做到数据绑定。

    var obj = {};
    var input = document.getElementById('input');
    var text = document.getElementById('text')

    let newObj = new Proxy(obj,{
        get: function(target,key,receiver) {
            console.log('I GOT IT');
            return Reflect.get(target,key,receiver);
        },
        set: function(target,    key, value, receiver){
            if (key === "text") {
                  input.value = value;
                  text.innerHTML = value;
              }
              return Reflect.set(target, key, value, receiver);
        }
    })

    input.addEventListener("keyup", function(e) {
      newObj.text = e.target.value;
      });

实现列表渲染的逻辑:

    const Render = {
      // 初始化
      init: function(arr) {
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < arr.length; i++) {
          const li = document.createElement("li");
          li.textContent = arr[i];
          fragment.appendChild(li);
        }
        list.appendChild(fragment);
      },
      addList: function(val) {
        const li = document.createElement("li");
        li.textContent = val;
        list.appendChild(li);
      }
  };

最后监听list数组的变化,当用户按下添加键时,触发Render的addList方法:

const arr = [];
  // 监听数组
  const newArr = new Proxy(arr, {
      get: function(target, key, receiver) {
        return Reflect.get(target, key, receiver);
      },
      set: function(target, key, value, receiver) {
        console.log(target, key, value, receiver);
        if (key !== "length") {
          Render.addList(value);
        }
        return Reflect.set(target, key, value, receiver);
      }
    });

    // 初始化
  window.onload = function() {
    Render.init(arr);
  };

  btn.addEventListener("click", function() {
    newArr.push(newObj.text);
  });