前言
我在项目中有使用过Vue2、Vue3框架,但还没好好的看过Vue源码。
最近打算研究下源码的,在深究之前,我想根据自己的理解和思路写写核心的功能。
先试试双向数据绑定的,这一篇文章是记录开发JS双向数据绑定小插件的笔记。
思路
参考Vue的
MVVM的设计模式 和 (Vue2)双向数据绑定的原理。
MVVM
MVVM(Model-View-ViewModel)设计模式,简单理解是 视图 和 数据模型 是比较独立的,他们通过 视图模型 层通信。
双向数据绑定 指的是 数据更新会渲染到视图,视图上有更新,数据也会同步更新。
ViewModel的两个核心任务:DOM Listeners、Data Bindings。
发布者-订阅者模式(观察者模式)
它定义对象间的一种一对多的依赖关系,多个观察者对象都依赖于一个目标对象,当目标对象的状态发生变化时,所有依赖于这个对象的观察者对象都会收到通知。
在双向数据绑定中,可以把绑定的页面元素看作订阅数据的观察者,当数据变化时,通知所有订阅者进行处理。
DOM Listeners
监听视图的变化,即给数据绑定的页面元素,通过addEventListener添加响应事件回调,更新绑定的数据。
Data Bindings 数据绑定
基于ES5的Object.definedProperty()方法对数据进行劫持,自定义setter方法,通知订阅者数据有更新。
开发笔记
目标
形式参考Vue,给页面添加自定义属性,通过插件可实现双向数据绑定。如输入框修改内容,实际数据也进行修改了;数据修改了,视图也同步更新。
双向数据绑定实现流程
- 记录所有的“订阅者们”(双向/单向绑定的页面元素),给页面元素添加唯一标识的属性
k-id; - 针对“订阅者们”,定义数据变化后的视图更新方法
updateVM(),找到数据的订阅者(数据绑定的页面元素)进行更新; - 对数据进行劫持,自定义
setter方法,即数据更新时,调用updateVM()通知订阅者们更新; - 对数据进行初始化赋值,从而执行上述
setter方法,渲染到视图层; - 针对双向数据绑定的页面元素,添加DOM数据更新相关事件的监听,事件回调中找到对应的绑定数据进行
setter更新 - 最后,针对自定义的事件方法,处理上下文
this为该实例对象,使其能获取到实际数据。
最终效果
代码
demo.html
<div class="main">
<h4>自定义双向数据绑定插件KVM</h4>
<h5>双向绑定<input> k-model-value="name",默认onchange时更新数据</h5>
<label>用户名:</label><input k-model-value="name" />
<h5>定义<input> keyup事件修改数据</h5>
<label>用户名:</label><input k-value="name" onkeyup="kvm.methods.onkeyup(this)" />
<br />
<br />
<!-- onchange -->
<h5>单向绑定<input> k-value="name"</h5>
<div><label>用户名:</label><input k-value="name"></span></div>
<h5>单向绑定<span> 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;
}
本文是参考相关原理,基于自己的思路简单实现的双向数据绑定的基础功能,只作练手的,是个人的开发笔记的。
这是我在掘金的第一篇文章,希望从此在这记录更多的实践,可以分享好玩的东西,也吸收更多的养分的,我相信会有更多的交流和碰撞的,加油!~