vue双向绑定的简单实现


Posted in Javascript onDecember 22, 2016

研究了一下vue双向绑定的原理,所以简单记录一下,以下例子只是简单实现,还请大家不要吐槽~

之前也了解过vue是通过数据劫持+订阅发布模式来实现MVVM的双向绑定的,但一直没仔细研究,这次深入学习了一下,借此机会分享给大家。

首先先将流程图给大家看一下

vue双向绑定的简单实现

参考文章:Vue.js双向绑定的实现原理

我虽然参考的是这篇文章,下面的代码也是在阅读几遍后仿造的,自己只是简单添加了个递归实现所有dom子节点的双向绑定,以及添加了一些理解,但真正让我了然于心,让我可以独立写出2遍完整逻辑的其实是这张图,所以个人认为这张流程图才是最重要的,而我参考的这篇文章的作者也是参考这幅图的原作者的。

原文章:剖析Vue原理&实现双向绑定MVVM

站在阅读和理解MVVM的完整逻辑的话,推荐大家看第一篇,但是第二篇原文章的图文更能说明一些问题

如果大家看了我的解释也能够完全理解的话,那就更好啦啦啦啦啦~哈哈

好,下面我会从2个角度开始讲解,先上单向绑定,再由单向绑定过渡到双向绑定;

首先,先为大家解释一下单向绑定model => view层的逻辑
1、劫持dom结构;
2、创建文档碎片,利用文档碎片重构dom结构;
3、在重构的过程中解析dom结构实现MVVM构造函数实例化后的数据初始化视图数据;
4、利用判断dom一级子元素是否依然有子元素从而进行所有子元素的单向绑定;
5、将文档碎片添加至根节点中.

这就是我总结的关于单向绑定的逻辑了,下面利用代码跟大家解释

//dom结构
<div id="app">
 <input type="text" v-model="msg">
 <p>{{msg}}</p>
 <ul>
 <li>1</li>
 <li>{{msg}}</li>
 <li>{{test}}</li>
 </ul>
</div>

//one-way-binding.js
 //判断每个dom节点是否拥有子节点,若有则返回该节点
 function isChild(node){
 //这里使用childNodes可以读取text文本节点,所以不用children
 if(node.childNodes.length ===0){
 return false;
 }
 else{
 return node;
 }
 }

 //利用文档碎片劫持dom结构及数据,进而进行dom的重构
 function nodeToFragment(node,vm){
 var frag = document.createDocumentFragment();
 var child;
 while(child = node.firstChild){
 //一级dom节点数据绑定
 compile(child,vm);
 //判断每个一级dom节点是否有二级节点,若有则递归处理文档碎片
 if(isChild(child)){
 //递归实现二级dom节点的重构
 nodeToFragment(isChild(child),vm);
 }
 frag.appendChild(child);
 }
 //将文档碎片添加至对应node中,最后为id为app的元素下
 node.appendChild(frag);
 }

 //初始化绑定数据
 function compile(node,vm){
 //node节点为元素节点时
 if(node.nodeType === 1){
 var attr = node.attributes;
 //遍历当前节点的所有属性
 for(var i=0;i<attr.length;i++){
 if(attr[i].nodeName === 'v-model'){
 //属性名
 var name = attr[i].nodeValue;
 //将data下对应属性名的值赋值给当前节点值
 //这里因为node是input标签所以值为node.value
 node.value = vm.data[name];
 //最后标签中的v-model属性也可以功成身退了,删除它
 node.removeAttribute(attr[i].nodeName);
 }
 }
 }

 //node节点为text文本节点#text时
 if(node.nodeType === 3){
 var reg = /\{\{(.*)\}\}/;
 if(reg.test(node.nodeValue.trim())){
 //将正则匹配到的{{}}中的字符串赋值给name
 var name = RegExp.$1;
 //利用name对应赋值相应的节点值
 node.nodeValue = vm.data[name];
 }
 }
 }

 //MVVM构造函数,这里我就写成Vue了
 function Vue(options){
 this.id = options.el;
 this.data = options.data;
 //将根节点与实例化后的对象作为参数传入
 nodeToFragment(document.getElementById(this.id),this);
 }
 //实例化
 var vm = new Vue({
 el:'app',
 data:{
 msg:'hello,two-ways-binding',
 test:'test key'
 }
 })

上述就是简单的单向绑定了,整个逻辑实际上非常简单,我再来跟大家说明一下

1、为了令model层的数据可以绑定到view层的dom上,所以我们想了一个办法来替换dom中的一些元素值,而明显一个个替换时不可取的,因为大量的dom操作会降低程序的运行效率,你想想,每次dom操作可都是一次对dom整体的遍历过程~,所以我们觉得采用文档碎片的形式,将dom一次全部劫持,在内存中执行全部数据绑定操作,最后只进行一次dom操作,即添加子节点来解决这个频繁操作dom的问题,你也可以理解为中间的一层存在于内存中的虚拟dom;

2、那么既然如此,我们就要首先劫持所有dom节点,这里我们利用nodeToFragment函数来劫持;

3、在每次劫持对应dom节点的过程中,我们也会相对应的实现对该dom元素的数据绑定,以求在最后直接添加到为根节点的子元素即可,这个过程我们就在nodeToFragment函数中插入了compile函数来初始化绑定,并且添加递归函数实现所有子元素的初始绑定;

4、在compile函数中我们添加的数据又从何而来呢?对,正是因为这点,所以我们建立MVVM的构造函数Vue来实现数据支持,并实现在实例化时就执行nodeToFragment同时重构dom和实现初始化绑定compile;

5、好了,单向绑定就是这么简单,4个函数即可Vue => nodeToFragment => compile => isChild。

完成图如下

vue双向绑定的简单实现

好了,再回过来看看整体的流程图,我们已经实现了这一块了

vue双向绑定的简单实现

接下来,休息下,大家准备开始流程图后面的双向绑定,ok,还是按照单向绑定的顺序,先跟大家讲明实现逻辑;

1、创建数据监听者observer去监听view层数据的变化;(利用Object.defineProperty劫持所有要用到的数据)

2、当view层数据变化后,通过通知者Dep通知订阅者去实现数据的更新;(通知后,遍历所有用到数据的订阅者更新数据)

3、订阅者watcher接收到view层数据变更后,重新对变化的数据进行赋值,改变model层,从而改变所有view层用到过该数据的地方。(更新数据,并改变view层所有用到该数据的节点值)

上面是实现逻辑,下面将通过具体代码告诉大家每一步的做法,由于双向绑定中订阅者会涉及初始化绑定的过程,所以代码量较多,我会在大更改处用——为大家框出来

//判断每个dom节点是否拥有子节点,若有则返回该节点
 function isChild(node){
 if(node.childNodes.length ===0){
 return false;
 }
 else{
 return node;
 }
 }

 //利用文档碎片劫持dom结构及数据,进而进行dom的重构
 function nodeToFragment(node,vm){
 var frag = document.createDocumentFragment();
 var child;
 while(child = node.firstChild){
 //一级dom节点数据绑定
 compile(child,vm);
 //判断每个一级dom节点是否有二级节点,若有则递归处理文档碎片
 if(isChild(child)){
 nodeToFragment(isChild(child),vm);
 }
 frag.appendChild(child);
 }
 node.appendChild(frag);
 }

 //初始化绑定数据
 function compile(node,vm){
 //node节点为元素节点时
 if(node.nodeType === 1){
 var attr = node.attributes;
 for(var i=0;i<attr.length;i++){
 if(attr[i].nodeName === 'v-model'){
 var name = attr[i].nodeValue;
 //特殊处理input标签
 //------------------------
 if(node.nodeName === 'INPUT'){
 node.addEventListener('keyup',function(e){
 vm[name] = e.target.value;
 })
 }
 //由于数据已经由data劫持至vm下,所以直接赋值vm[name]即可触发getter访问器
 node.value = vm[name];
 //-------------------------
 node.removeAttribute(attr[i].nodeName);
 }
 }
 }

 //node节点为text文本节点时
 if(node.nodeType === 3){
 var reg = /\{\{(.*)\}\}/;
 if(reg.test(node.nodeValue.trim())){
 var name = RegExp.$1;
 //node.nodeValue = vm[name];
 //----------------------
 //为每个节点建立订阅者,通过订阅者watcher初始化及更新视图数据
 new watcher(vm,node,name);
 //-----------------------
 }
 }
 }
 //----------------------------------------------------------------
 //订阅者(为每个节点的数据建立watcher队列,每次接受更改数据需求后,利用劫持数据执行对应节点的数据更新)
 function watcher(vm,node,name){
 //将每个挂载了数据的dom节点添加到通知者列表,要保证每次创建watcher时只有一个添加目标,否则后续会因为watcher是全局而被覆盖,所以每次要清空目标

 Dep.target = this;
 this.vm = vm;
 this.node = node;
 this.name = name;
 //执行update的时候会调用监听者劫持的getter事件,从而添加到watcher队列,因为update中有访问this.vm[this.name]
 this.update();
 //为保证只有一个全局watcher,添加到队列后,清空全局watcher
 Dep.target = null;
 }

 watcher.prototype = {
 update(){
 this.get();
 //input标签特殊处理化
 if(this.node.nodeName === 'INPUT'){
 this.node.value = this.value;
 }
 else{
 this.node.nodeValue = this.value;
 }
 },
 get(){
 //这里调用了数据劫持的getter
 this.value = this.vm[this.name];
 }
 };

 //通知者(将监听者的更改信息需求发送给订阅者,告诉订阅者哪些数据需要更改)
 function Dep(){
 this.subs = [];
 }

 Dep.prototype = {
 addSub(watcher){
 //添加用到数据的节点进入watcher队列
 this.subs.push(watcher);
 },
 notify(){
 //遍历watcher队列,令相应数据节点重新更新view层数据,model => view
 this.subs.forEach(function(watcher){
 watcher.update();
 })
 }
 };

 //监听者(利用setter监听view => model的数据变化,发出通知更改model数据后再从model => view更新视图所有用到该数据的地方)
 function observer(data,vm){
 //遍历劫持data下所有属性
 Object.keys(data).forEach(function(key){
 defineReactive(vm,key,data[key]);
 })
 }

 function defineReactive(vm,key,val){
 //新建通知者
 var dep = new Dep();
 //灵活利用setter与getter访问器
 Object.defineProperty(vm,key,{
 get(){
 //初始化数据更新时将每个数据的watcher添加至队列栈中
 if(Dep.target) dep.addSub(Dep.target);
 return val;
 },
 set(newVal){
 if(val === newVal) return ;
 //初始化后,文档碎片中的虚拟dom已与model层数据绑定起来了
 val = newVal;
 //同步更新model中data属性下的数据
 vm.data[key] = val;
 //数据有改动时向通知者发送通知
 dep.notify();
 }
 })
 }
 //---------------------------------------------------------------
 function Vue(options){
 this.id = options.el;
 this.data = options.data;
 observer(this.data,this);
 nodeToFragment(document.getElementById(this.id),this);
 }

 var vm = new Vue({
 el:'app',
 data:{
 msg:'hello,two-ways-binding',
 test:'test key'
 }
 })

好的,到这里双向绑定的讲解也就结束了,代码量确实有点多,但是我们看到其实逻辑在你熟悉后并不复杂,特别是参照了上文的流程图后,其实就是:

1、通过observer劫持所有model层数据到vue下,并在劫持时灵活运用getter与setter访问器属性来在虚拟dom初始化数据绑定时,利用此时的get方法绑定初始化数据进入通知者队列,后续初始化完成后,在view层数据发生变化时,利用set方法及时利用通知者发出通知;

2、在dep通知者接收到有一处dom节点数据更改的通知时,遍历watcher队列及告诉watcher订阅者,view层数据有所变动model层已经相应改变,你要重新执行update将model层的数据更新到view层所有用到该数据的地方(比如我们利用input实现的双向绑定就不止一个dom节点内使用了,而是多个,所以必须整体遍历修改)。

3、这是一个model => view => model =>view的过程,真正的逻辑顺序为model => view,view => model,model => view,反复的过程~

贴上结果图

初始未改动view层数据图

vue双向绑定的简单实现

修改view层数据后图

vue双向绑定的简单实现

最后大家再看一次流程图,看看整个逻辑是不是跟流程图一模一样,通过流程图就可以回忆起这个逻辑过程,写多2遍就可以记住!

vue双向绑定的简单实现

以上只是通过简单实现来告诉大家vue的数据劫持+订阅发布模式这个双向绑定的原理,其中有很多细节上的不足可能未作处理,还请见谅~

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS Timing
Apr 21 Javascript
Display SQL Server Version Information
Jun 21 Javascript
浅谈JavaScript 执行环境、作用域及垃圾回收
May 31 Javascript
javascript中数组和字符串的方法对比
Jul 20 Javascript
javascript特效实现——当前时间和倒计时效果的简单实例
Jul 20 Javascript
Javascript的this用法
Jan 16 Javascript
JS简单判断函数是否存在的方法
Feb 13 Javascript
对angularJs中controller控制器scope父子集作用域的实例讲解
Oct 08 Javascript
通过JS深度判断两个对象字段相同
Jun 14 Javascript
五分钟搞懂Vuex实用知识(小结)
Aug 12 Javascript
JavaScript判断数组类型的方法
Oct 23 Javascript
简介JavaScript错误处理机制
Aug 04 Javascript
jQuery使用Layer弹出层插件闪退问题
Dec 22 #Javascript
深入理解jquery中extend的实现
Dec 22 #Javascript
jQuery插件DataTable使用方法详解(.Net平台)
Dec 22 #Javascript
JS实现间歇滚动的运动效果实例
Dec 22 #Javascript
javascript-解决mongoose数据查询的异步操作
Dec 22 #Javascript
Bootstrap popover用法详解
Dec 22 #Javascript
深入学习jQuery中的data()
Dec 22 #Javascript
You might like
php 异常处理实现代码
2009/03/10 PHP
CodeIgniter辅助之第三方类库third_party用法分析
2016/01/20 PHP
PHP实现从PostgreSQL数据库检索数据分页显示及根据条件查找数据示例
2018/06/09 PHP
PHP+mysql防止SQL注入的方法小结
2019/04/27 PHP
对laravel的csrf 防御机制详解,及form中csrf_token()的存在介绍
2019/10/24 PHP
改进:论坛UBB代码自动插入方式
2006/12/22 Javascript
jquery $.ajax()取xml数据的小问题解决方法
2010/11/20 Javascript
JavaScript 的继承
2011/10/01 Javascript
Javascript跨域请求的4种解决方式
2013/03/17 Javascript
javascript为按钮注册回车事件(设置默认按钮)的方法
2015/05/09 Javascript
JQuery勾选指定name的复选框集合并显示的方法
2015/05/18 Javascript
js同源策略详解
2015/05/21 Javascript
Node.js事件驱动
2015/06/18 Javascript
基于JavaScript实现根据手机定位获取当前具体位置(X省X市X县X街道X号)
2015/12/29 Javascript
基于zepto.js实现登录界面
2017/10/09 Javascript
基于jQuery中ajax的相关方法汇总(必看篇)
2017/11/08 jQuery
ionic2中使用自动生成器的方法
2018/03/04 Javascript
Node.js一行代码实现静态文件服务器的方法步骤
2019/05/07 Javascript
解决layer.prompt无效的问题
2019/09/24 Javascript
[05:04]DOTA2上海特级锦标赛主赛事第二日TOP10
2016/03/04 DOTA
[01:16:28]DOTA2-DPC中国联赛 正赛 iG vs Magma BO3 第二场 2月23日
2021/03/11 DOTA
Python代理抓取并验证使用多线程实现
2013/05/03 Python
Python3实战之爬虫抓取网易云音乐的热门评论
2017/10/09 Python
Python模拟简单电梯调度算法示例
2018/08/20 Python
python英语单词测试小程序代码实例
2019/09/09 Python
django框架使用views.py的函数对表进行增删改查内容操作详解【models.py中表的创建、views.py中函数的使用,基于对象的跨表查询】
2019/12/12 Python
CSS3实现跳动的动画效果
2016/09/12 HTML / CSS
CSS3盒子模型详解
2013/04/24 HTML / CSS
周年庆典邀请函范文
2014/01/23 职场文书
大专毕业自我鉴定
2014/02/04 职场文书
升职自荐信怎么写
2015/03/05 职场文书
2015年防汛工作总结
2015/05/15 职场文书
暑期家教宣传单
2015/07/14 职场文书
2016年校园社会综合治理宣传月活动总结
2016/03/16 职场文书
Jedis操作Redis实现模拟验证码发送功能
2021/09/25 Redis
无线电通信名词解释
2022/02/18 无线电