JavaScript实现简单的双向数据绑定(EmberAngularVue)

Posted 刻刻帝丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript实现简单的双向数据绑定(EmberAngularVue)相关的知识,希望对你有一定的参考价值。

tip:有问题或者需要大厂内推的+我脉脉哦:丛培森 ٩( ‘ω’ )و

【本文源址: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$odata-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()由于严重的性能问题已经被移除了

主页传送门

以上是关于JavaScript实现简单的双向数据绑定(EmberAngularVue)的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript实现简单的双向数据绑定(EmberAngularVue)

用jquery实现的简单数据双向绑定

vue双向绑定原理(简单实现原理附demo)

Vue2从入门到精通详解Vue数据双向绑定原理及手动实现双向绑定

JavaScript实现数据的双向绑定

js实现简单的双向绑定