JS自定义双向数据绑定的小插件

293 阅读4分钟

前言

我在项目中有使用过Vue2Vue3框架,但还没好好的看过Vue源码。 最近打算研究下源码的,在深究之前,我想根据自己的理解和思路写写核心的功能。

先试试双向数据绑定的,这一篇文章是记录开发JS双向数据绑定小插件的笔记。

思路

参考Vue的MVVM的设计模式 和 (Vue2)双向数据绑定的原理。

MVVM

image.png MVVM(Model-View-ViewModel)设计模式,简单理解是 视图 和 数据模型 是比较独立的,他们通过 视图模型 层通信。

双向数据绑定 指的是 数据更新会渲染到视图,视图上有更新,数据也会同步更新。

ViewModel的两个核心任务:DOM ListenersData Bindings

发布者-订阅者模式(观察者模式)

它定义对象间的一种一对多的依赖关系,多个观察者对象都依赖于一个目标对象,当目标对象的状态发生变化时,所有依赖于这个对象的观察者对象都会收到通知。

在双向数据绑定中,可以把绑定的页面元素看作订阅数据的观察者,当数据变化时,通知所有订阅者进行处理。

DOM Listeners

监听视图的变化,即给数据绑定的页面元素,通过addEventListener添加响应事件回调,更新绑定的数据。

Data Bindings 数据绑定

基于ES5Object.definedProperty()方法对数据进行劫持,自定义setter方法,通知订阅者数据有更新。

开发笔记

目标

形式参考Vue,给页面添加自定义属性,通过插件可实现双向数据绑定。如输入框修改内容,实际数据也进行修改了;数据修改了,视图也同步更新。

双向数据绑定实现流程

  1. 记录所有的“订阅者们”(双向/单向绑定的页面元素),给页面元素添加唯一标识的属性k-id
  2. 针对“订阅者们”,定义数据变化后的视图更新方法updateVM(),找到数据的订阅者(数据绑定的页面元素)进行更新;
  3. 对数据进行劫持,自定义setter方法,即数据更新时,调用updateVM()通知订阅者们更新;
  4. 对数据进行初始化赋值,从而执行上述setter方法,渲染到视图层;
  5. 针对双向数据绑定的页面元素,添加DOM数据更新相关事件的监听,事件回调中找到对应的绑定数据进行setter更新
  6. 最后,针对自定义的事件方法,处理上下文this为该实例对象,使其能获取到实际数据。

最终效果

图像.gif

代码

demo.html

   <div class="main">
        <h4>自定义双向数据绑定插件KVM</h4>
        <h5>双向绑定&lt;input&gt; k-model-value="name",默认onchange时更新数据</h5>
        <label>用户名:</label><input k-model-value="name" />
        <h5>定义&lt;input&gt; keyup事件修改数据</h5>
        <label>用户名:</label><input k-value="name" onkeyup="kvm.methods.onkeyup(this)" />
        <br />
        <br />

        <!-- onchange -->
        <h5>单向绑定&lt;input&gt; k-value="name"</h5>
        <div><label>用户名:</label><input k-value="name"></span></div>
        <h5>单向绑定&lt;span&gt; k-text="name"</h5>
        <div><label>用户名:</label><span k-text="name"></span></div>
        <br />
        <button onclick="kvm.methods.onsubmit()">提交</button>
    </div>

demo.js

     const kvm = new KVM({
            data: {
                name: '小可'
            },
            methods: {
                onsubmit: function () {
                    alert('正在提交的数据-用户名:' + this.data.name);
                },
                onkeyup: function (ele) {
                    this.data.name = ele[0].value;
                }
            }
        });

demo.css

body {
    padding: 50px;
    background-color: #f5f5f5;
}

h5 {
    color: #3f3f3f;
}

label {
    font-size: 12px;
}

.main {
    width: 50%;
    max-width: 400px;
    background: #fff;
    margin: auto;
    padding: 20px 50px;
}

button {
    outline: none;
    cursor: pointer;
    width: 100px;
    height: 30px;
    float: right;
}

.main::after {
    content: "";
    display: block;
    clear: both;
}

KVM.js

const KVM = function (config) {

    //1、根据k-model-value、k-value、k-text找到订阅者们,添加唯一标志k-id
    const kvmNodes = new Array(); //订阅者们的集合
    const kModelValueEles = document.querySelectorAll('[k-model-value]');
    const kValueEles = document.querySelectorAll('[k-value]');
    const kTextEles = document.querySelectorAll('[k-text]');
    const currentTimeStamp = Date.parse(new Date());
    let kidx = 0;
    kModelValueEles.forEach((item) => {
        const kid = currentTimeStamp + '_' + (kidx++);
        item.setAttribute('k-id', kid);
        kvmNodes.push({
            type: 'k-model-value',
            isValue: true,
            isModel: true,
            isText: false,
            name: item.getAttribute('k-model-value'),
            id: kid
        })
    })
    kValueEles.forEach((item) => {
        const kid = currentTimeStamp + '_' + (kidx++);
        item.setAttribute('k-id', kid);
        kvmNodes.push({
            type: 'k-value',
            isValue: true,
            isModel: false,
            isText: false,
            name: item.getAttribute('k-value'),
            id: kid
        })
    })
    kTextEles.forEach((item) => {
        const kid = currentTimeStamp + '_' + (kidx++);
        item.setAttribute('k-id', kid);
        kvmNodes.push({
            type: 'k-text',
            isValue: false,
            isModel: false,
            isText: true,
            name: item.getAttribute('k-text'),
            id: kid
        })
    })

    //2、基于订阅发布机制,定义数据变化后的视图的更新方法,即处理数据setter后视图的渲染
    const updateVM = function (modelKey, modelVal) { //modelKey 绑定的变量名  modelVal 值
        kvmNodes.forEach((item) => {
            if (modelKey == item.name) {
                if (item.type == 'k-model-value' || item.type == 'k-value') {
                    document.querySelectorAll('[k-id="' + item.id + '"]')[0].value = modelVal;
                } else if (item.type == 'k-text') {
                    document.querySelectorAll('[k-id="' + item.id + '"]')[0].innerText =
                        modelVal;
                }
            }
        })
    }

    //3、对数据进行的数据劫持Object.definedProperty(观察数据的变化)
    const gvmValue = config.data; //原数据
    const kvmModel = {}; //关联gvmValue进行数据劫持

    for (const key in gvmValue) {
        if (Object.hasOwnProperty.call(gvmValue, key)) {
            const element = config.data[key];
            Object.defineProperty(kvmModel, key, {
                get: function () {
                    return gvmValue[key]
                },
                set: function (val) {
                    gvmValue[key] = val;
                    updateVM(key, val)
                }
            })
            kvmModel[key] = element; //4、数据初始化渲染
        }
    }

    //5、监听DOM事件,绑定事件进行预处理,找到对应的绑定数据进行set
    kModelValueEles.forEach((item) => {
        if (item.tagName == 'INPUT') {
            function bindChangeEvent(ele, kid) {
                ele.addEventListener('change', function (e) {
                    const node = kvmNodes.find((item) => item.id == kid);
                    kvmModel[node.name] = e.target.value;
                });
            }
            const kid = item.getAttribute('k-id');
            bindChangeEvent(item, kid)
        }
    })

    //6、处理自定义事件的上下文this为该实例对象
    this.methods = {};
    const _this = this;

    for (const key in config.methods) {
        if (Object.hasOwnProperty.call(config.methods, key)) {
            const element = config.methods[key];
            this.methods[key] = function () {
                return element.call(_this, arguments);
            }
        }
    }

    this.data = kvmModel;
}

本文是参考相关原理,基于自己的思路简单实现的双向数据绑定的基础功能,只作练手的,是个人的开发笔记的。

这是我在掘金的第一篇文章,希望从此在这记录更多的实践,可以分享好玩的东西,也吸收更多的养分的,我相信会有更多的交流和碰撞的,加油!~