实现双向绑定的两种方式

1,252 阅读5分钟

我的理解,所谓的双向绑定,其实就是将Model和View绑定在一起,任何一方改变的同时,改变另外一方。 在流行框架中,react是单向绑定(只支持Model改变=>View改变),要实现双向绑定得加value和onChange事件从而实现(View改变=>调起事件=>改变Model)。 而vue是双向绑定的,因为它事先已经帮我们绑定好了事件。

什么是Model

我理解为Model就是一个JS对象,用来存储页面中的数据。

什么是View

我理解是页面中所显示的DOM对象的集合。

怎么实现双向绑定呢?

  1. Object.defineProperty()

点击查看实现效果

Model => View 实现的原理:

当Model改变时,得到事件响应(数据劫持),获取到Dom节点,我们就可以通过Dom.value来改变View。而Object.defineProperty主要帮我们来获得这个过程的事件响应,或者常说的数据劫持,可以劫持到改变后的新值。

View => Model 实现原理:

当View改变时,调起onKeyup之类的事件,然后改变响应的Model,这个其实是很简单的。

  1. Proxy() -- vue3中启用了该方式

点击查看实现效果

实现原理与上面基本相似,但是为什么vue3中会使用它呢?这个后面会解释。

Object.defineProperty()

这个建议去看一下红宝书的介绍可以帮助快速理解。或者点击MND

主要使用到了它的访问器属性:get和set

  1. get 当获取对象属性值时触发。这个起到的作用不大。
  2. set 当改变对象属性值时触发。比如Model对象的某个属性值发生了变化,就会调起set方法,我们可以在set方法中改变对应View中对应的某个Dom节点的值。

我们开始实现吧!!!

Object.defineProperty()版本

首先我们准备一下界面,比较简单,左侧是input框,右边是model转成的字符串。我们会使用到console来改变model的属性值,来测试是否会改变DOM

在这里插入图片描述
源码在这 demo在这

界面代码(index.html)如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>bidreactional binding</title>
</head>
<style>
    div {
        width: 40%;
        float: left;
        border: 1px dashed;
        padding: 20px;
        height: 100vh;
    }
</style>
<body>
    <div>
        <p>View:</p>
        <input id="view" />
    </div>
    <div>
        <p>Model:</p>
        <span id="model"></span>
    </div>
    <script src="./index.js"></script>
</body>
</html>

接下来是实现绑定逻辑,我们都写在了index.js中,都有对应的注释。

// 获取DOM节点
var view = document.getElementById('view');
var model = document.getElementById('model');
// 设置model对象
var data = {};
// 设置get函数的中转站,封装后可以去掉
let temp = 0;
//在data对象中定义number属性,并给他赋值两个访问器属性,来代理或者说劫持number的值的获取与设置
Object.defineProperty(data, "number", {
	//可枚举,这个主要是用来将Model显示在前端的,可以省去
    enumerable:true,
    // 获取值时的处理方法 就相当于代理执行获取值的操作,返回什么都又他决定,这里不能return data.number会造成无限循环的
    get: function () {
        return temp;
    },
    // data的number值发生变化时调用
    set: function (value) {
    	// 改变View节点的值
        view.value = value;
        // 将值存在temp中,在get时要用到
        temp = value;
        // 这个主要是用来将Model显示在前端的,可以省去
        model.innerHTML = `"data":${JSON.stringify(data)}`;
    },
})
// 绑定事件,当view改变时将改变的值赋值给data对象中的number属性
view.addEventListener("keyup", function (event) {
    data.number = event.target.value;
})

以上就可以实现一个基于单个输入框的双向绑定了。这里放代码链接和demo链接!假设我现在有三个输入框怎么办呢?我要整个流程再来三次吗?其实不用的,我们可以对流程封装一下。

源码在这 demo在这

index.html

.....
<div>
    <p>View:</p>
    <p>username: <input id="username" /></p>
    <p>password: <input id="password" /></p>
    <p>sex: <input id="sex" /></p>
</div>
 ......  

index.js

// model 是用来显示model字符串的 可省去
var model = document.getElementById('model');
// model对象
var data = {};
// 所有input的id
const keys = ["username", "password", "sex"];
// 给每个id都实现上双向绑定
keys.forEach(item => {
	// 封装后绑定View和Model的方法
    bindViewAndModel(item, "", document.getElementById(item))
})

function bindViewAndModel(key, val, dom) {
	// Model => View
    Object.defineProperty(data, key, {
    	// 这里只是为了前端展示model 可以省去
        enumerable: true,
        get: function () {
            return val; // 	去掉了temp
        },
        set: function (newValue) {
            val = newValue;
            dom.value = newValue;
            // 这里只是为了前端展示model 可以省去
            model.innerHTML = JSON.stringify(data);
        },
    })
	// View => Model
    dom.addEventListener("keyup", function (event) {
        data[key] = event.target.value;
    })
}

到这里我们就能实现多个input框的双向绑定了,可以在这里测试一下。 其实这里还会设计不同的表单元素的变化,这里就不再深入了。 接下来探究一下proxy。为什么会用proxy替换掉Object.defineProperty()呢?因为defineProperty无法监听数组变化(这里我还在试验中),也只能劫持对象的属性。而proxy而劫持整个对象。

Proxy版本双向绑定

源码在这 demo在这

index.html 同上

index.js

// 这里只是为了前端展示model 可以省去
var model = document.getElementById('model');
// 所有dom的id
const domKeys =["username","password","sex"];
// 枚举信息 根据 {domkey:dom}
const domEnum = {};
// model
var data = {};
// proxy 代理整个data 
const proxy = new Proxy(data, {
	// taget 即为代理的对象 prop为属性值
    get: function (target, prop) {
        return target[prop];
    },
    // value为新值
    set: function (target, prop, value) {
        target[prop] = value;        
        domEnum[prop+'Dom'].value = target[prop];
        // 这里只是为了前端展示model 可以省去
        model.innerHTML = JSON.stringify(data);
    }
})
// 加上key事件
domKeys.forEach(item=>{
    const dom = document.getElementById(item);
    domEnum[item+'Dom'] = dom;
    dom.addEventListener("keyup", function (event) {
        proxy[item] = event.target.value;
    })
})

总结一下

本文主要帮助理解了双向绑定的原理,以及实现双向绑定的两种方法。如有不对之处还望帮忙指出~~