zhengwei1949 / myblog

个人博客
10 stars 6 forks source link

双向数据绑定的理解 #83

Open zhengwei1949 opened 7 years ago

zhengwei1949 commented 7 years ago

理解双向数据绑定

首先我们要理解数据绑定。我们看到的网站页面中,是由数据和ui两部分组合而成。将ui转换成浏览器能理解的语言,便是html和css主要做的工作。而将数据显示在页面上,并且有一定的交互效果(比如点击等用户操作及对应的页面反应)则是js主要完成的工作。 在以前的开发模式中,这一步一般通过jq操作DOM结构:

<div class="wrapper">
    <div></div>
    <p>
        <span>这里有内容</span>
        <span></span><!--我们想把内容写在这里面-->
    </p>
</div>
<script>
    var str = "Hello World";//这个数据是这里是写死的,现实案例肯定是通过ajax来自数据库的
    var oDemo = document.querySelector('.wrapper span:nth-child(2)');
    oDemo.innerHTML = str;
</script>

从而进行更新页面。但这样带来的问题是大量的dom操作,如果我们的页面结构发生了变化,我们的js业务代码被迫也要进行变更。

如果能在开始的时候,便已经确定好从后端获取的数据到页面上需要进行的操作,当数据发生改变,页面的相关内容也自动发生变化,这样便能极大地方便前端工程师的开发。在新的框架中(angualr,react,vue等),通过对数据的监视,发现变化便根据已经写好的规则进行修改页面,便实现了数据绑定。可以看出,数据绑定是M(model,数据)通过VM(model-view,数据与页面之间的变换规则)向V(view)的一个修改。

各框架当中针对双向数据绑定的语法

如果我们用angular可以写成:

<div class="wrapper">
    <div>
        <input type="text" ng-model="abc">
    </div>
    <p>
        <span>这里有内容</span>
        <span>{{abc}}</span><!--我们想把内容写在这里面-->
    </p>
</div>
<script>
    var myApp = angular.module('myApp',[]);
    myApp.controller('myController',['$scope',function($scope){
        $scope.abc = 'Hello World';
    }])
</script>

如果我们用vue,我们可以写成:

{{abc}}

这里有内容

如果我们用react,我们可以写成:

```jsx
//这里我采用的是非受控表单控件实现的
import React from 'react'
class Hello extends React.Component{
    constructor(){
        super(props);
        this.state = {
            abc:'Hello World'
        }
    }
    handleChange = ()=>{
        this.setState({
            abc:this.refs.txt.value
        })
    }
    render(){
        return (
            <div class="wrapper">
                <div>{{abc}}</div>
                <input type="text" onInput={this.handleChange} ref="txt">
                <p>
                    <span>这里有内容</span>
                    <span>{abc}</span>{<!--我们想把内容写在这里面-->}
                </p>
            </div>
        )
    }
}

在用户操作页面(比如在Input中输入值)的时候,数据能及时发生变化,并且根据数据的变化,页面的另一处也做出对应的修改。有一个常见的例子就是淘宝中的购物车,在商品数量发生变化的时候,商品价格也能及时变化。

自己实现双向数据绑定

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <input type="text" id="txt" ng-model="abc">
    <div ng-bind="abc"></div>
    <input type="text"  ng-model="abc">
    <script>
    var eleList = document.querySelectorAll('[ng-bind="abc"]');
    var inputEleList = document.querySelectorAll('[ng-model="abc"]');    
    var obj = {
        val:100,
        set:function(val){
          this.val = val;
          apply();
        }
    }

    function apply(){
        for(var i=0;i<eleList.length;i++){
        eleList[i].innerHTML = obj.val;
        }

        for(var i=0;i<inputEleList.length;i++){
            inputEleList[i].value = obj.val;
            inputEleList[i].oninput = function(){
                obj.set(this.value);
            }
        }
    }
    render()
    </script>
</body>
</html>

实现原理:

  1. 创建一个render函数,把变量中的值渲染到页面当中
  2. 通过给input绑定oninput事件(这块大家可以试一下onchange事件就知道,onchange事件有一个缺点,必须要input输入框失去焦点才能触发),在oninput的回调函数当中,我们修改变量的值的同时,顺手重新执行一次render函数

其实这里的关键点就是对input的oninput的回调函数里面多做了一些事情。

vue的双向数据绑定的原理

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <input type="text" v-model="b">
    <input type="text" v-model="b">
    <input type="text" v-model="b">
    <input type="text" v-model="b">
    <p v-text="b"></p>
    <p v-text="b"></p>
    <script>
      var bValue = 'Hello World';    
      var obj = {};
      Object.defineProperty(obj,'b',{
          get:function(){//getter属性
            return bValue;
          },
          set:function(newVal){//setter属性
            bValue = newVal;
            render();
          }
      })
      var oPList = document.querySelectorAll('[v-text="b"]');
      var inputList = document.querySelectorAll('[v-model="b"]');
      function render(){      
        for(var i=0;i<oPList.length;i++){
            oPList[i].innerHTML = obj.b;
        }
        for(var i=0;i<inputList.length;i++){
          inputList[i].value = obj.b;
        }
      }
      render();
      for(var i=0;i<inputList.length;i++){
          inputList[i].oninput = function(){
              console.log(1111)
              obj.b = this.value;
          }
      }
    </script>
</body>
</html>

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。

Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。

上面是我们自己写的demo,只是简单的理解一下vue实现的原理,但是真实的vue代码比这要更加的复杂。因为如果你做任何的改变都立马进行DOM的更新,性能会变得很差。

vue的解决方案是:只要观察到数据的变化,vue会开启一个队列,并缓冲在同一个事件循环中发生的所有的数据改变。如果同一个watcher被多次触发,只会一次推入到队列中。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要。然后,在下一个的事件循环tick中,vue刷新队列并执行实际(已去重)的工作。Vue在内部尝试对异步队列使用原生的Promise.then和MutationObserver,如果执行环境不支持,会采用setTimeout(fn,0)代替。

如果你确实需要数据立马更新到视图当中,你可以使用Vue.nextTick(callback).(换句话说,如果你改变了数据模型,但是视图并没有更新,你可以考虑是不是需要使用Vue.nextTick)

angular双向数据绑定原理

只要我们给页面中的标签加上了ng-bind,ng-model等指令,或者在控制器当中使用了如上的angular内置的api的话,angular会为这些地方绑定了一个watcher,也就是记录一下这块的值(oldValue),然后当我们尝试去改变比如改变绑定了ng-model的地方的值(newValue)的时候,angular就会通过$apply重新计算一遍我们$scope上面所有的函数返回值、表达式的值,如果发现newValue的值和oldValue的值发生了变化,则会去更新视图。

会导致页面view和model刷新的还有:ajax相关的$http等angular的内置的api。

当然,如果我们使用自己的方法,不能触发这个过程,但我们也是可以通过主动调用$scope.$appl强制进行刷新。

zhengwei1949 commented 6 years ago

https://www.imooc.com/video/16704 参考这个讲解Object.defineProperty