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 相关文章推荐
一个无限级XML绑定跨框架菜单(For IE)
Jan 27 Javascript
javascript 三种编解码方式
Feb 01 Javascript
两种简单实现菜单高亮显示的JS类代码
Jun 27 Javascript
在多个页面使用同一个HTML片段的代码
Mar 04 Javascript
js实现一个省市区三级联动选择框代码分享
Mar 06 Javascript
jQuery点击自身以外地方关闭弹出层的简单实例
Dec 24 Javascript
ajax提交表单实现网页无刷新注册示例
May 08 Javascript
纯javascript移动优先的幻灯片效果
Nov 02 Javascript
详解js界面跳转与值传递
Nov 22 Javascript
react router 4.0以上的路由应用详解
Sep 21 Javascript
在vue中封装的弹窗组件使用队列模式实现方法
Jul 23 Javascript
使用PDF.js渲染canvas实现预览pdf的效果示例
Apr 17 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获取URL中带#号等特殊符号参数的解决方法
2014/09/02 PHP
Linux下安装oracle客户端并配置php5.3
2014/10/12 PHP
PHP将二维数组某一个字段相同的数组合并起来的方法
2016/02/26 PHP
thinkPHP通用控制器实现方法示例
2017/11/23 PHP
PHP实现基本留言板功能原理与步骤详解
2020/03/26 PHP
网页设计常用的一些技巧
2006/12/22 Javascript
JS模拟面向对象全解(一、类型及传递)
2011/07/13 Javascript
jQuery 选择器项目实例分析及实现代码
2012/12/28 Javascript
如何将一个String和多个String值进行比较思路分析
2013/04/22 Javascript
jQuery实现指定内容滚动同时左侧或其它地方不滚动的方法
2015/08/08 Javascript
jquery带有索引按钮且自动轮播切换特效代码分享
2015/09/15 Javascript
js简单设置与使用cookie的方法
2016/01/22 Javascript
EasyUI的DataGrid每行数据添加操作按钮的实现代码
2017/08/22 Javascript
ES6之模版字符串的具体使用
2018/05/17 Javascript
微信小程序学习笔记之目录结构、基本配置图文详解
2019/03/28 Javascript
vue中使用微信公众号js-sdk踩坑记录
2019/03/29 Javascript
js遍历详解(forEach, map, for, for...in, for...of)
2019/08/28 Javascript
Vue.js中provide/inject实现响应式数据更新的方法示例
2019/10/16 Javascript
Antd的Table组件嵌套Table以及选择框联动操作
2020/10/24 Javascript
Python中使用dom模块生成XML文件示例
2015/04/05 Python
Python OpenCV之图片缩放的实现(cv2.resize)
2019/06/28 Python
Python re 模块findall() 函数返回值展现方式解析
2019/08/09 Python
Python 实现将某一列设置为str类型
2020/07/14 Python
HTML5 LocalStorage 本地存储详细概括(多图)
2017/08/18 HTML / CSS
Russell Stover巧克力官方网站:美国领先的精美巧克力制造商
2016/11/27 全球购物
全球最大最受欢迎的旅游社区:Tripadvisor
2017/11/03 全球购物
俄罗斯的精英皮具:Wittchen
2018/01/29 全球购物
纽约市的奢华内衣目的地:Anya Lust
2019/08/02 全球购物
Silk Therapeutics官网:清洁、抗衰老护肤品
2020/08/12 全球购物
输入N,打印N*N矩阵
2012/02/20 面试题
北大自主招生自荐信
2013/10/19 职场文书
大学校园活动策划书
2014/02/04 职场文书
鉴定评语大全
2014/05/05 职场文书
继续教育个人总结
2015/03/03 职场文书
python 如何将两个实数矩阵合并为一个复数矩阵
2021/05/19 Python
基于Redission的分布式锁实战
2022/08/14 Redis