参考:百度安全验证
本文转载自【微信公众号:手机电脑双黑客,ID:heikestudio】,经微信公众号授权转载,如需转载与原文作者联系
不多说了,我们直接来看看Vue的双向绑定机制从最简单到最终实现版本。
一、访问器属性
大家可能都知道,
关于Vue的双向绑定,
核心是Object.defineProperty()方法,那接下来我们就简单介绍一下!
语法:
Object.defineProperty(obj,prop,descriptor)
其中:
obj
要在其上定义属性的对象。
prop
要定义或修改的属性的名称。
descriptor
将被定义或修改的属性描述符。
其实,简单点来说,就是通过此方法来定义一个值。
调用,使用到了get方法,
赋值,使用到了set方法。
我们来看个例子:
当我们调用时候,就会自动打印出两行文字。注意:get 和 set 方法内部的 this 都指向 obj,这意味着 get 和 set 函数可以操作对象内部的值。另外,访问器属性的会”覆盖”同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略。
二、实现双向绑定
既然我们已经知道了,每当有改变的时候都会调用到set方法,我们可以根据此来实现一个双向绑定!
实现效果如下:
此例实现的效果是:随文本框输入文字的变化,span 中会同步显示相同的文字内容;在js或控制台显式的修改 obj.hello 的值,视图会相应更新。这样就实现了 model => view 以及 view => model 的双向绑定。
通过添加事件监听keyup来触发调用set方法,而set在修改了访问器属性的同时,也修改了dom样式,改变了span标签内的文本节点。
三、实现Vue双向绑定
3.1 实现效果
我们真正要实现的双向绑定是这样的:
3.2 任务拆分
为了实现效果我们需要拆分一下任务:
① 将vm实例中的data中的内容绑定到输入框以及文本节点当中
② 当输入框改变时,vm实例中的data的内容也跟着改变,实现 【view => modle】
③当data中的内容发生变化的时候,输入框的内容以及文本节点的内容也发生变化,实现 【modle=> view】
3.2.1 实现任务—内容绑定原理
先来了解一下DocumentFragment
说到内容绑定,我们不得不来介绍DocuemntFragment(碎片化文档)这个概念,简单的来讲,你可以把它认为是一个dom节点的容器,当你创造了10个节点,当每个节点都插入到文档当中都会引发一次浏览器的回流,也就是说浏览器要回流10次,十分消耗资源。
而使用碎片化文档,也就是说我把10个节点都先放入到一个容器当中,最后我再把容器直接插入到文档就可以了!浏览器只回流了1次。
注意:还有一个很重要的特性是,如果使用appendChid方法将原dom树中的节点添加到DocumentFragment中时,会删除原来的节点。
举个例子,我使用:
console.log(document.getElementById('app'));
可以看到,我的app中有两个子节点,一个元素节点,一个文本节点 ;但是,当我通过DocumentFragment 劫持数据一下后 :
注意:我的碎片化文档是将子节点都劫持了过来,而我的id为app的div内已经没有内容了。
同时要主要我while的判断条件。判断是否有子节点,因为我每次appendChild都把node中的第一个子节点劫持走了,node中就会少一个,直到没有的时候,child也就变成了undefined,也就终止了循环。
3.2.2 如何来实现内容绑定
我们要考虑两个问题,一个是如何绑定要input上,另一个是如何绑定要文本节点中。
这样思路就来了,我们已经获取到了div的所以子节点了,就在DocumentFragment里面,然后对每一个节点进行处理,看是不是有跟vm实例中有关联的内容,如果有,修改这个节点的内容。然后重新添加入DocumentFragment中。
首先,我们写一个处理每一个节点的函数,如果有input绑定v-model属性或者有{{ xxx }}的文本节点出现,就进行内容替换,替换为vm实例中的data中的内容 :
然后,在向碎片化文档中添加节点时,每个节点都处理一下。
创建Vue的实例化函数
效果图如下:
我们成功将内容都绑定到了输入框与文本节点上!
3.2.3 实现任务—【view => model】
对于此任务,我们从输入框考虑,输入框的问题,输入框如何改变data。我们通过事件监听器keyup,input等,来获取到最新的value,然后通过Object.defineProperty将获取的最新的value,赋值给实例vm的text,我们把vm实例中的data下的text通过Object.defineProperty设置为访问器属性,这样给vm.text赋值,就触发了set。set函数的作用一个是更新data中的text,另一个等到任务三再说。
首先实现一个响应式监听属性的函数。一旦有赋新值就发生变化 :
然后,实现一个观察者,对于一个实例 每一个属性值都进行观察。
改写编译函数,注意由于改成了访问器属性,访问的方法也产生变化,同时添加了事件监听器,把实例的text值随时更新 。
实例函数中,观察data中的所有属性值,注意增添了observe 。
最终,效果如下:
效果实现了,任务二也完成了,view => model 通过修改输入框 vm实例中的属性也跟着变化了!
3.2.4 实现任务—【model => view】
通过修改vm实例的属性,该改变输入框的内容与文本节点的内容。
这里涉及到一个问题 需要我们注意,当我们修改输入框,改变了vm实例的属性,这是1对1的。
但是,我们可能在页面中多处用到 data中的属性,这是1对多的。也就是说,改变1个model的值可以改变多个view中的值。
这就需要我们引入一个新的知识点:
订阅/发布者模式
订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作
举个例子:
之前提到的set函数的第二个作用 就是来提醒订阅者 进行noticy操作,告诉他们:“我的text变了!” 文本节点变成了订阅者,接到消息后,立马进行update操作
回顾一下,每当 new 一个 Vue,主要做了两件事:
第一个是监听数据:observe(data),
第二个是编译 HTML:nodeToFragement(id)。
在监听数据的过程中,我们会为 data 中的每一个属性生成一个主题对象 dep。
在编译 HTML 的过程中,会为每个与数据绑定相关的节点生成一个订阅者 watcher,watcher 会将自己添加到相应属性的 dep 容器中。
我们已经实现:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的 set 方法。
接下来我们要实现的是:发出通知 dep.notify() => 触发订阅者的 update 方法 => 更新视图。
这里的关键逻辑是:如何将 watcher 添加到关联属性的 dep 中。
注意:我把直接赋值的操作改为了添加一个 Watcher 订阅者 :
那么,Watcher又该做些什么呢?
首先,将自己赋给了一个全局变量 Dep.target;
其次,执行了 update 方法,进而执行了 get 方法,get 的方法读取了 vm 的访问器属性,从而触发了访问器属性的 get 方法,get 方法中将该 watcher 添加到了对应访问器属性的 dep 中;
再次,获取属性的值,然后更新视图。
最后,将 Dep.target 设为空。因为它是全局变量,也是 watcher 与 dep 关联的唯一桥梁,任何时刻都必须保证 Dep.target 只有一个值。
到此,就实现了一个双向绑定的机制,如果你能自己手敲一遍,肯定会有很大的帮助。