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 相关文章推荐
Using the TextRange Object
Oct 14 Javascript
理解Javascript_05_原型继承原理
Oct 13 Javascript
$.ajax返回的JSON无法执行success的解决方法
Sep 09 Javascript
jQuery UI Autocomplete 体验分享
Feb 14 Javascript
javascript版2048小游戏
Mar 18 Javascript
通过正则表达式获取url中参数的简单实现
Jun 07 Javascript
jQuery Ajax传值到Servlet出现乱码问题的解决方法
Oct 09 Javascript
Angular 4.x中表单Reactive Forms详解
Apr 25 Javascript
原生JavaScript实现滑动拖动验证的示例代码
Dec 06 Javascript
javascript中的相等操作符(==与===区别)
Dec 21 Javascript
如何基于JavaScript判断图片是否加载完成
Dec 28 Javascript
基于Electron实现桌面应用开发代码实例
Jul 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
解析PHP自带的进位制之间的转换函数
2013/06/08 PHP
php获取textarea的值并处理回车换行的方法
2014/10/20 PHP
PHP浮点数精度问题汇总
2015/05/13 PHP
非常重要的php正则表达式详解
2016/01/04 PHP
jQuery动画效果-slideUp slideDown上下滑动示例代码
2013/08/28 Javascript
js全屏显示显示代码的三种方法
2013/11/11 Javascript
JavaScript控制网页平滑滚动到指定元素位置的方法
2015/04/17 Javascript
Jsonp 关键字详解及json和jsonp的区别,ajax和jsonp的区别
2015/12/30 Javascript
详解JavaScript UTC时间转换方法
2016/01/07 Javascript
javascript实现瀑布流加载图片原理
2016/02/02 Javascript
nodeJS删除文件方法示例
2016/12/25 NodeJs
使用MUI框架模拟手机端的下拉刷新和上拉加载功能
2017/09/04 Javascript
vue-cli 3 全局过滤器的实例代码详解
2019/06/03 Javascript
element 中 el-menu 组件的无限极循环思路代码详解
2020/04/26 Javascript
利用H5api实现时钟的绘制(javascript)
2020/09/13 Javascript
jQuery实现二级导航菜单的示例
2020/09/30 jQuery
python实现的文件夹清理程序分享
2014/11/22 Python
Python中使用ElementTree解析XML示例
2015/06/02 Python
Python的迭代器和生成器
2015/07/29 Python
使用apidoc管理RESTful风格Flask项目接口文档方法
2018/02/07 Python
Selenium chrome配置代理Python版的方法
2018/11/29 Python
Python:Numpy 求平均向量的实例
2019/06/29 Python
Python 从subprocess运行的子进程中实时获取输出的例子
2019/08/14 Python
wxPython实现列表增删改查功能
2019/11/19 Python
python 实现在无序数组中找到中位数方法
2020/03/03 Python
美国独家设计师眼镜在线光学商店:Glasses Gallery
2017/12/28 全球购物
Magee 1866官网:Donegal粗花呢外套和大衣专家
2019/11/01 全球购物
学习十八大精神心得体会
2013/12/31 职场文书
公司市场专员岗位职责
2014/06/29 职场文书
2014年个人业务工作总结
2014/11/17 职场文书
综合实践活动报告
2015/02/05 职场文书
综合素质自我评价评语
2015/03/06 职场文书
读《人生的智慧》有感:闲暇是人生的精华
2019/12/25 职场文书
Python opencv缺陷检测的实现及问题解决
2021/04/24 Python
Python中可变和不可变对象的深入讲解
2021/08/02 Python
js面向对象编程OOP及函数式编程FP区别
2022/07/07 Javascript