原生js实现双向数据绑定-类似vue.js的数据绑定效果
原生js实现双向数据绑定-类似vue.js的数据绑定效果
JavaScript Vue.js相关
共1Star
详细介绍
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>