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 相关文章推荐
ExtJS的FieldSet的column列布局
Nov 20 Javascript
JavaScript 页面编码与浏览器类型判断代码
Jun 03 Javascript
关于图片的预加载过程中隐藏未知的
Dec 19 Javascript
js获取dom的高度和宽度(可见区域及部分等等)
Jun 13 Javascript
jquery ajax对特殊字符进行转义防止js注入使用示例
Nov 21 Javascript
ExtJS4利根据登录后不同的角色分配不同的树形菜单
May 02 Javascript
javascript为下拉列表动态添加数据项
May 23 Javascript
Javascript实现鼠标右键特色菜单
Aug 04 Javascript
基于jQuery实现点击弹出层实例代码
Jan 01 Javascript
jQuery基于$.ajax设置移动端click超时处理方法
May 14 Javascript
JS Math对象与Math方法实例小结
Jul 05 Javascript
微信小程序页面滚动到指定位置代码实例
Sep 07 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
上海地方志办公室-上海电子仪表工业志
2021/03/04 无线电
php中将时间差转换为字符串提示的实现代码
2011/08/08 PHP
PHP-CGI进程CPU 100% 与 file_get_contents 函数的关系分析
2011/08/15 PHP
thinkphp3.0 模板中函数的使用
2012/11/13 PHP
php实现水仙花数的4个示例分享
2014/04/08 PHP
getimagesize获取图片尺寸实例
2014/11/15 PHP
在Nginx上部署ThinkPHP项目教程
2015/02/02 PHP
跨浏览器PHP下载文件名中的中文乱码问题解决方法
2015/03/05 PHP
非集成环境的php运行环境(Apache配置、Mysql)搭建安装图文教程
2016/04/12 PHP
总结对比php中的多种序列化
2016/08/28 PHP
javascript常用方法、属性集合及NodeList 和 HTMLCollection 的浏览器差异
2010/12/25 Javascript
jQuery EasyUI API 中文文档 - Draggable 可拖拽
2011/09/29 Javascript
js中 关于undefined和null的区别介绍
2013/04/16 Javascript
js中return false(阻止)的用法
2013/08/14 Javascript
21个值得收藏的Javascript技巧
2014/02/04 Javascript
jquery 无限级下拉菜单的简单实现代码
2014/02/21 Javascript
JQuery导航菜单选择特效
2016/04/11 Javascript
jQuery Ajax页面局部加载方法汇总
2016/06/02 Javascript
jquery基本选择器匹配多个元素的实现方法
2016/09/05 Javascript
javascript九宫格图片随机打乱位置的实现方法
2017/03/15 Javascript
详解Vue-cli代理解决跨域问题
2017/09/27 Javascript
JavaScript实现单图片上传并预览功能
2019/09/30 Javascript
JavaScript代码实现简单计算器
2020/12/27 Javascript
javascript实现随机抽奖功能
2020/12/30 Javascript
Django中使用locals()函数的技巧
2015/07/16 Python
Windows上使用virtualenv搭建Python+Flask开发环境
2016/06/07 Python
python 使用多线程创建一个Buffer缓存器的实现思路
2020/07/02 Python
个人自我鉴定
2013/11/07 职场文书
应届生法律顾问求职信
2013/11/19 职场文书
办公室前台的岗位职责
2013/12/20 职场文书
面试后感谢信
2014/02/01 职场文书
函授本科自我鉴定
2014/02/04 职场文书
军训自我鉴定范文
2014/02/13 职场文书
文明寝室申报材料
2014/05/12 职场文书
学生检讨书怎么写
2015/05/07 职场文书
2015领导干部廉洁自律工作总结
2015/07/23 职场文书