您的位置:首页 > Web前端 > JavaScript

JavaScript实现简单的双向数据绑定(Ember、Angular、Vue)

2017-06-13 16:14 1926 查看
【本文源址:http://blog.csdn.net/q1056843325/article/details/72999948 转载请添加该地址】

什么是双向数据绑定呢?

简单的说

就是UI视图与数据绑定在了一块

也就是数据和视图是同步改变的

双向数据绑定最常见的应用场景就是表单

(应用场景还是很有限的)



现在我们要实现这样一个简单的数据绑定

输入栏中输入字符

和它绑定的节点内容同步改变

此外还有一个按钮用于生成随机数改变input和div内的数据



首先我们先把需要把html的简单结构实现

<input id="input" data-bind="demo">
<div id="output" data-bind="demo"></div>
<button id="random">随机数</button>


还需要在js中获取这些DOM节点

let $ = document.querySelector.bind(document);
let $i = $('#input');
let $o = $('#output');
let $random = $('#random');


简易实现

如果仅仅是为了实现这样的效果实际上非常简单

我们很容易就可以想到input事件,然后动态改变

那么我们首先就来简单的实现一下

let def = 'default';
$i.value = def;
$o.textContent = def;

$i.oninput = function(){
$o.textContent = $i.value;
}

$random.onclick = function(){
let rand = Math.floor(Math.random()*10e5)
$i.value = rand;
$o.textContent = rand;
}


虽然实现了效果

但是实际上只有视图改变,影响数据改变的过程

而且也没有把节点联系在一起

数据模型(Ember.js原理)

Ember.js使用了这种数据模型的方法

虽然很麻烦,但是很容易让我们理解

实际上就是把数据还有要节点封装在了一起

这样后续的更新一定会经过这个模型

模型就了解了变化

从而做出处理

首先我们来实现一个数据模型的类

当我们需要绑定一组节点时

就可以实例化这个数据模型

(为了方便下面我都使用ES6语法)

class DataModel {
constructor(str = ''){
this.data = str;
this.nodes = [];
}
bindTo(node){
this.nodes.push(node);
this.update();
}
update(){
const INPUT_NODE = ['INPUT','TEXTAREA'];
let {nodes} = this;
for(let i = 0, node; node = nodes[i++];){
if(INPUT_NODE.includes(node.nodeName)){
if(node.value !== this.data){ //避免光标跳到尾部
node.value = this.data;
}
}else{
node.textContent = this.data;
}
}
}
set(str){
if(str !== this.value){
this.data = str;
this.update();
}
}
get(){
return this.data;
}
}


this.data
就是我们模型的数据

this.nodes
是我们绑定的节点列表

bindTo
方法接受我们的Dom节点并传入节点列表

既然有新节点进加入组织了(节点绑定),那么也肯定要让它接受新的数据(数据与UI改变)

update
方法用于更新视图,实际上是遍历所有绑定节点,判断类型然后做出改变

声明了数据模型类后我们就可以为input和div绑定到一个模型中了

let gModel = {
demo: new DataModel('default')
};
//数据->视图
gModel[$i.getAttribute('data-bind')].bindTo($i);
gModel[$o.getAttribute('data-bind')].bindTo($o);


gModel
是我们声明的一个全局数据模型对象

因为页面中不一定只有这一组数据绑定

$i
$o
data-bind
属性值就相当于它们的“组织名”

这里我就起名demo了

使用模型的API来绑定这两个节点

//视图->数据
$i.addEventListener('input', function(){
gModel[this.getAttribute('data-bind')].set(this.value);
});

$random.onclick = function(){
gModel.demo.set(Math.floor(Math.random()*10e5));
}


最后绑定input事件还有按钮的click事件

当输入值后,就改变这个模型的data

set方法改变data的同时还会触发所有与之绑定在一起的节点做出更新

脏检查(Angular.js原理)

Augular.js采用脏检查的方式来实现双向数据绑定

其原理是不会去监听数据的变化

而是我觉得你可能要发生数据变化的时候(用户交互,DOM操作等)

就去检查你的所有数据,看看到底有没有变化

不过这个数据检查是组件级别的

虽然如此,很多时候还是会产生很多没用的检查

我们需要模拟以下组件的核心函数

class Scope {
constructor(){
this.nodes = [];
this.watchers = [];
}
watch(watchExp, listener = function(){}){
this.watchers.push({
watchExp,
listener
});
}
digest(){
let dirty;
let {watchers} = this;
do {
dirty = false;
for(var i = 0, watcher; watcher = watchers[i++];){
let newValue = watcher.watchExp();
let oldValue = watcher.last;
if(newValue !== oldValue){
dirty = true;
watcher.listener(newValue, oldValue);
watcher.last = newValue;
}
}
}while(dirty);
}
update(newValue){
const INPUT_NODE = ['INPUT','TEXTAREA'];
let {nodes} = this;
for(let i = 0, node; node = nodes[i++];){
if(INPUT_NODE.includes(node.nodeName)){
if(node.value !== newValue){
node.value = newValue;
}
}else{
node.textContent = newValue;
}
}
}
bindTo(node){
let {nodes} = this;
let key = node.getAttribute('data-bind');
if(!key){
return;
}
nodes.push(node);
this.update(this[key]);
this.watch(() => {
return this[key];
}, (newValue, oldValue) => {
this.update(newValue);
});
}
}


区别于上一种方法

这里的
this.nodes
代表某组件的全部节点

this.watchers
数组存储着watcher

每一个watcher封装着用于脏检查的函数

而watch方法就负责向watchers中添加watcher

它接受两个参数,一个取值函数watchExp和一个回调函数listener

digest方法会遍历整个watcher

last存储着上一个值,再通过取值函数获取值

通过比较可以知道值有没有变脏

如果脏了,就触发回调函数(渲染数据)并且更新last值

还要重新检查一遍watchers确保last和数据一致

(这里没有处理互相绑定死循环的问题,可以设置检查上限)

声明完组件类,我们就可以实例化一个组件

绑定节点,监听事件,还要手动进行脏检查

let scope = new Scope();
scope.demo = 'default';

//数据->视图
scope.bindTo($i);
scope.bindTo($o);

//视图->数据
$i.addEventListener('input', function(){
scope[this.getAttribute('data-bind')] = this.value;
scope.digest();
});

$random.onclick = function(){
scope.demo = Math.floor(Math.random()*10e5);
scope.digest();
}


访问器监听(Vue.js原理)

vue.js实现数据变化影响视图变化的方式便是利用了ES5的setter

数据改变,触发setter渲染视图

视图影响数据没什么好说的,肯定需要监听input事件

这里我就写的简单点了

let data = {};
let def = 'default';
$i.value = def;
$o.textContent = def;

//数据->视图
Object.defineProperty(data, 'demo', {
set: function(newValue){
$i.value = newValue;
$o.textContent = newValue;
}
});

//视图->数据
$i.addEventListener('input', function() {
data[this.getAttribute('data-bind')] = this.value;
});

$random.onclick = function() {
data.demo = Math.floor(Math.random()*10e5);
};


实际上vue实现的要比这复杂多得多

因为setter在很多情况下并不是万金油

也就是说并不是对象属性的任何变动它都能够监听的到

比如说以下场景:

向对象添加新属性

删除现有属性

数组改变

关于这些问题这里就不讨论了

如果有时间的同学可以去研究以下源码

此外还要说明一下

原本ES7草案中的Object.observe()由于严重的性能问题已经被移除了

==主页传送门==
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息