谈谈框架中的双向绑定

从事前端有一段时间了,在使用框架的api文档中,经常看见在vue或者angular的介绍说自己的特色是双向数据绑定,在看react的介绍中,说自己的优势和特色是单向数据绑定。 但是对于各个框架中的单向和双向绑定却是似懂非懂的,每次跟朋友聊起都解释得模棱两可,于是整理一篇各个框架双向绑定的原理以及内部实现的核心代码块,对于双向绑定简单的说就是界面的操作能实事反应到数据,数据的更改也能在界面呈现。
这里写图片描述

前端模式史

简单扯一扯哈,为了能少走几步路,人们发明了汽车、飞机;为了能少手算,人们发明了计算机;为了能少出去买菜购物,人们发明了京东上门等等。人的懒惰使科技的进步,同样的,为了让我们程序猿少打代码,少加班,能有更多时间去撩妹,大程序猿们也发明了各式框架,虽然并没有解决加班的问题,但是却解决了很多代码冗余,逻辑混乱的问题。ok,闲扯结束,来瞧瞧我们的大程序猿是怎么一步一步发展到MVVM(双向绑定Model-View-ViewModel)的:

1、1993年,互联网崛起,各大商业网站暴涨,html完全是静态文件,由服务器的url返回html文件的内容,完全是请求刷新页面,稍大型网站需要准备几千个html文件。网速慢的话,如果一不小心页面点一下页面,发个form表单,可以去喝个茶再回来,说不定页面已经加载了一大半,调皮的不要不要的。

2、当时微软、SUN和开源社区开发这几位大哥就很不爽了,他们觉得很多html就一个或者几个字段长得不一样,完全可以合并成一个html,于是就出现了新的创建动态HTML的方式:ASP、JSP和PHP,用特殊的<%=var%>标记出来了,瞬间感觉自己萌萌哒了。

3、1995年,终于,我们的javascript修道成仙,横空出世,在浏览器中有了一展身手的机会,直接用JavaScript操作DOM节点,使用浏览器提供的原生API,前端人员开始有了一席之地,瞬间上了一个档次。

4、由于原生API不好用,还要考虑浏览器兼容性,前端人员欲仙欲死的, 2006年,jQuery独特而又优雅的代码风格改变JavaScript程序员的设计思路和编写程序的方式,于是jQuery成了主流框架,程序猿们都美滋滋的

5、为了能更好的跟服务器端配合,MVC模式(模型(model)-视图(view)-控制器(controller))出现了,JavaScript可以在前端修改服务器渲染后的数据,酱紫不同的开发人员可同时开发视图、控制器逻辑和业务逻辑,于是web全栈程序猿出现了,很多小伙伴们就开始暗暗下决心要成为全栈工程师,然后都呵呵了。

6、微软这位大哥就是不一样,偷师了mvc模式后,创新出mvvm的新模式,把Model用纯JavaScript对象表示,View负责显示,ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model,实现了model和view的最大分离。双向绑定的浪潮打破了前端以往的编写风格,简直狂拽酷炫吊炸天。

注:这是笔者自我的见解,本篇文章主要是针对双向绑定进行细化讨论,如果对前半部分react的单数据传输、asp、php等感兴趣,不妨在讨论区讨讨论,怼一怼,毕竟js才是世界上最好的语言,php救不了国民,但是javascript可以

###实现页面数据绑定
即view到model和model到view的绑定过程,中间的数据层等会儿再聊哈,简单的说,就是页面中的ng-、{{}}、v-这些怎么绑定的,当数据发生响应时,又是怎么渲染到页面的?
咱们分步描述吧:

new Vue({
  el: '#app',
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})	

来瞧瞧这块,el绑定的其实是一个vue实例中的dom模板,而当vue创建实例时,会开始进行Compile,即对id为app下的所有html进行解析,正则匹配,来看看源码,他们是怎么匹配的:

function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/;
        var text = node.textContent;
 
        if (self.isTextNode(node) && reg.test(text)) {  // 判断是否是符合这种形式{{}}的指令
            self.compileText(node, reg.exec(text)[1]);
        }
 
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  // 继续递归遍历子节点
        }
    });
}

可以看出,代码通过递归的方式对el下的dom进行递归遍历,每次去匹配{{}},如果能匹配上,则渲染对应初始化的数据,当然也会创建一个watcher订阅者,数据是怎么绑定的,我们看下一个目录,咋们先瞧瞧html怎么编译的,当能匹配上时,则会渲染到模板上,实现模板功能。

我们有的时候看到框架解释的时候说,双向绑定,当改变一个数据的时候,会先渲染到一个虚拟的dom,然后再渲染到html上,这是为什么呢?啥意思?ok,我们接着来讲讲,什么是虚拟的dom哈,其实就是DocumentFragment,我们先来看看mdn对这个属性是怎么解释的:

DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当做一个轻量版的 Document 使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为DocumentFragment不是真实DOM树的一部分,它的变化不会引起DOM树的重新渲染的操作(reflow) ,且不会导致性能等问题。

根据上面官方的说法就是,DocumentFragment是没有父级元素,即不会挂在html任何一个dom上,当使用js在DocumentFragment中进行append、insert时,并不会触发浏览器重绘和重排,那么这样就可以做一件事了,就是说每次更新很多dom,需要重绘或者重排的时候,我们可以先在DocumentFragment把所有的dom排好后,再插入html中,这样速度就更快了,不需要浏览器不断的重绘,顺便贴一下vue实现的源码哈:

function nodeToFragment (el) {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while (child) {
        // 将Dom元素移入fragment中
        fragment.appendChild(child);
        child = el.firstChild
    }
    return fragment;
}

这一段代码,首先是使用createDocumentFragment();创建了一个虚拟的dom,然后将编译好的dom片段,插入到虚拟dom中,最后再append到el的node节点中,这就是页面编译和页面绑定过程。

###实现数据流绑定的方法
在笔者的薄识中,实现数据绑定的方法分为以下几种:
1、发布-订阅者模式
2、脏值检查
3、数据劫持
来,没有对比,就没有伤害,来瞧瞧咱们平时手动获取数据去渲染数据:

var $input = $('#new_input'),//输入框
   $save = $('#btn_save'),//保存按钮
   $name_text = $('#name_text');//数据名称
   $input.val(name);
   $name_text.text(name);

当我们需要有其他响应事件时,我们又需要重复去拿dom,去重新手动赋值到dom中,蠢的一匹,举个vue的实例吧,直接初始化绑定即可,并不需要手动去渲染刷新,当需要发生其他响应事件时并不需要再去手动写多余的代码去渲染

var vm = new Vue({
    el:'app',
    data:{
        data: {}
    }
})

ok,come on,来瞧瞧大神们是怎么实现这些数据流响应的:
####发布-订阅者模式
使用javascript实现pub/sub模式还是相对比较简单的,定义一个订阅者,当发布消息时,将所有的订阅者循环发布一下就可以,而下面有一个triger的方法,即发布信息:

var shoeObj = {}; // 定义发布者
shoeObj.list = []; // 缓存列表 存放订阅者回调函数
// 增加订阅者
shoeObj.listen = function (fn) {
    shoeObj.list.push(fn);  // 订阅消息添加到缓存列表
}
// 发布消息
shoeObj.trigger = function () {
    for (var i = 0, fn; fn = this.list[i++];) {
        fn.apply(this, arguments);
    }
}
// 小红订阅如下消息
shoeObj.listen(function (color, size) {
    console.log("颜色是:" + color);
    console.log("尺码是:" + size);
});
shoeObj.trigger("红色", 40);
shoeObj.trigger("黑色", 42);

输出的结果是
这里写图片描述
####脏值检测
以angualar框架的机制分析:记录所有变量的当前值,当发生某些操作之后,通过 a p p l y apply或者 digest进入脏检查环节,页面上的指令有compile和link阶段,compile的时候搜索匹配,然后执行指令定义时写的compile函数,link阶段将那些变量插入watch队列。触发脏检查时全部遍历一次watch队列,实现视图的更新。
其实在笔者看来,这只是另一种形式的发布/订阅者模式,ok,先来瞧瞧angular是怎么实现的,源码比较多,我们来瞧瞧简化版的,再来聊聊他们的实现思想哈:
首先,先构造一个scope对象,内部包含了监听器

function Scope() {
    this.$$watchersCount = 0;
    this.$$watchList = [];
}

KaTeX parse error: Can't use function '$' in math mode at position 25: …存储watcher对象,而下面$̲watch 是订阅者部分,主要…watchList 里面添加 watcher 对象,注意 $$watchersCount 记录了当前作用域和其子作用域的 watchList 的总数,这个是类似于订阅-发布者模式中的订阅者:

Scope.prototype.$watch = function (name, getNewValue, listener) {
    var watch = {
        name: name,
        getNewValue: getNewValue,
        listener: listener
    };
    this.$$watchList.push(watch);
}

等会儿哈,我们再继续看会儿发布者$digest这个角色长什么样哈:

Scope.prototype.$$digestOnce = function () {
    var dirty;
    var list = this.$$watchList;
    for (var i = 0, l = list.length; i < l; i++) {
        var watch = list[i];
        var newValue = watch.getNewValue(this.name);
        var oldValue = watch.last;
        if (newValue !== oldValue) {
            watch.listener(newValue, oldValue);
            // 因为listener操作,已经检查过的数据可能变脏
            dirty = true;
        }
        watch.last = newValue;
    }
    return dirty;
};
Scope.prototype.$digest = function () {
    var dirty = true;
    var checkTimes = 0;
    while (checkTimes<10 && dirty) {
        checkTimes++
        dirty = this.$$digestOnce();
    }
};

感兴趣的程序猿还是要瞧瞧源码长什么样才行,看看他们的逻辑,ok,上面一点简化版的代码看的是不是感觉蛮简单的,我们来个简单的整体流程,看看这个监听器是怎么执行的哈:

var scope = new Scope();
scope.first = 1;
scope.second = 10;
scope.$watch('first', function () {
    return scope[''+this.name]
}, function (newValue, oldValue) {
    scope.second++;
    console.log('first:newValue:' + newValue + '~~~~' + 'oldValue:' + oldValue);
})
scope.$watch('second', function () {
    return scope[''+this.name]
}, function (newValue, oldValue) {
    scope.first++;
    console.log('second:newValue:' + newValue + '~~~~' + 'oldValue:' + oldValue);
})
scope.$digest();

看看结果:
这里写图片描述
那我们进行一点点的分析下,先看看下面的逻辑结构图哈,跟着逻辑结构图走走:
这里写图片描述
scope会先生成一个watch对象,对象有四个属性:last旧数据,getNewValue:返回新数据,listener:更新数据,name:对应的key记录,当scope的first值变化或者是赋值初始化时,会执行$digest进行循环脏值检测,如上图.

####数据劫持
通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
ok,咱先来了解下Object.defineProperty()这个方法哈:

var data = {name: 'kindeng'};
Object.defineProperty(data, 'name2', {
	enumerable: false,//当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
    configurable: false,//当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
    get: function() {
		console.log('获取当前值'+this.name);
		return this.name
    },
    set: function(newVal) {
        console.log('监听到值变化了,值为'+newVal);
		data.name = newVal
    }
});
console.log(data)
data.name2='cjfpersonal'
console.log(data)

思考下,输出的是啥?
答案是:
这里写图片描述
mdn里面对这两个方法的解释是:

	get
	一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
	默认为 undefined。
	set
	一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。
	默认为 undefined。

简单的说就是:get能获取当前对象的this属性,而set是当对象属性值发生改变时,会先做一层拦截,再刷新数据,如图:
这里写图片描述

理解了这个属性,我们就来个简单的实例,来实践到页面中的双向绑定:

<div id="app">
    <input type="text" id="input">
    <span id="inputShow"></span>
</div>
<script type="text/javascript">
    var obj = {};
    Object.defineProperty(obj, "bindtext", {
        //修改obj对象bindtext属性的set方法
        set: function (newVal) {
            document.getElementById('input').value = newVal;
            document.getElementById('inputShow').innerHTML = newVal;
        }
    });
    document.addEventListener('keyup', function (e) {
        //监听键盘操作,修改
        obj.bindtext = e.target.value;
    });
</script>

来,我们来看看效果:
这里写图片描述
输入时,已经将内容绑定,简直爽的一匹,这是最简单的数据绑定实例,如果还想深入了解内部整一套比较复杂的双向绑定,可以去了解下vue的源码,结合了发布-订阅者模式,毕竟每一个复杂的绑定都是由简单基础的思想经过不断的踩坑,最后才输出一版比较成熟的框架

猜你喜欢

转载自blog.csdn.net/qq_26648427/article/details/81050433
今日推荐