vue中的双向数据绑定详解
前言
什么是数据双向绑定?
vue 是一个 mvvm 框架,即数据双向绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化。这也算是 vue 的精髓之处了。值得注意的是,我们所说的数据双向绑定,一定是对于 UI 控件来说的,非 UI 控件不会涉及到数据双向绑定。 单向数据绑定是使用状态管理工具(如 redux)的前提。如果我们使用 vuex,那么数据流也是单项的,这时就会和双向数据绑定有冲突,我们可以这么解决。
为什么要实现数据的双向绑定?
在 vue 中,如果使用 vuex,实际上数据还是单向的,之所以说是数据双向绑定,这是用的 UI 控件来说,对于我们处理表单,vue 的双向数据绑定用起来就特别舒服了。
即两者并不互斥, 在全局性数据流使用单项,方便跟踪; 局部性数据流使用双向,简单易操作。
一、访问器属性
Object.defineProperty() 函数可以定义对象的属性相关描述符, 其中的 set 和 get 函数对于完成数据双向绑定起到了至关重要的作用,下面,我们看看这个函数的基本使用方式。
var obj = {
foo: 'foo'
}
Object.defineProperty(obj, </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">foo</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, {
</span><span style="color: rgba(0, 0, 255, 1)">get</span><span style="color: rgba(0, 0, 0, 1)">: function () {
console.log(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">将要读取obj.foo属性</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">);
},
</span><span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">: function (newVal) {
console.log(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">当前值为</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, newVal);
}
});
obj.foo; </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将要读取obj.foo属性</span>
obj.foo = <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">name</span><span style="color: rgba(128, 0, 0, 1)">'</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 当前值为 name</span></pre>
可以看到,get 即为我们访问属性时调用,set 为我们设置属性值时调用。
二、简单的数据双向绑定实现方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>forvue</title>
</head>
<body>
<input type="text" id="textInput">
输入:<span id="textSpan"></span>
<script>
var obj = {},
textInput = document.querySelector('#textInput'),
textSpan = document.querySelector('#textSpan');
Object.defineProperty(obj, </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">foo</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, {
</span><span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">: function (newValue) {
textInput.value </span>=<span style="color: rgba(0, 0, 0, 1)"> newValue;
textSpan.innerHTML </span>=<span style="color: rgba(0, 0, 0, 1)"> newValue;
}
});
textInput.addEventListener(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">keyup</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, function (e) {
obj.foo </span>=<span style="color: rgba(0, 0, 0, 1)"> e.target.value;
});
</script>
</body>
</html>
最终效果图:
可以看到,实现一个简单的数据双向绑定还是不难的: 使用 Object.defineProperty() 来定义属性的 set 函数,属性被赋值的时候,修改 Input 的 value 值以及 span 中的 innerHTML;然后监听 input 的 keyup 事件,修改对象的属性值,即可实现这样的一个简单的数据双向绑定。
三、 实现任务的思路
上面我们只是实现了一个最简单的数据双向绑定,而我们真正希望实现的时下面这种方式:
<div id="app">
<input type="text" v-model="text">
{{text}}
</div>
<script>
<span style="color: rgba(0, 0, 255, 1)">var</span> vm = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Vue({
el: </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">#app</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,
data: {
text: </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">hello world</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">
}
});
</span></script></pre>
即和 vue 一样的方式来实现数据的双向绑定。那么,我们可以把整个实现过程分为下面几步:
- 输入框以及文本节点与 data 中的数据绑定
- 输入框内容变化时,data 中的数据同步变化。即 view => model 的变化。
- data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化。
四、DocumentFragment
如果希望实现任务一,我们还需要使用到 DocumentFragment 文档片段,可以把它看做一个容器,如下所示:
<div id="app">
</div>
<script>
<span style="color: rgba(0, 0, 255, 1)">var</span> flag =<span style="color: rgba(0, 0, 0, 1)"> document.createDocumentFragment(),
span </span>= document.createElement(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">),
textNode </span>= document.createTextNode(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">hello world</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">);
span.appendChild(textNode);
flag.appendChild(span);
document.querySelector(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">#app</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">).appendChild(flag)
</span></script></pre>
这样,我们就可以得到下面的 DOM 树:
使用文档片段的好处在于:在文档片段上进行操作 DOM,而不会影响到真实的 DOM,操作完成之后,我们就可以添加到真实 DOM 上,这样的效率比直接在正式 DOM 上修改要高很多 。
vue 进行编译时,就是将挂载目标的所有子节点劫持到 DocumentFragment 中,经过一番处理之后,再将 DocumentFragment 整体返回插入挂载目标。
如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>forvue</title>
</head>
<body>
<div id="app">
<input type="text" id="a">
<span id="b"></span>
</div>
<script>
<span style="color: rgba(0, 0, 255, 1)">var</span> dom = nodeToFragment(document.getElementById(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">app</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">));
console.log(dom);
function nodeToFragment(node) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> flag =<span style="color: rgba(0, 0, 0, 1)"> document.createDocumentFragment();
</span><span style="color: rgba(0, 0, 255, 1)">var</span><span style="color: rgba(0, 0, 0, 1)"> child;
</span><span style="color: rgba(0, 0, 255, 1)">while</span> (child =<span style="color: rgba(0, 0, 0, 1)"> node.firstChild) {
flag.appendChild(child);
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> flag;
}
document.getElementById(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">app</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">).appendChild(dom);
</span></script>
</body>
</html>
即首先获取到 div,然后通过 documentFragment 劫持,接着再把这个文档片段添加到 div 上去。
五、初始化数据绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>forvue</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
{{text}}
</div>
<script><span style="color: rgba(0, 0, 0, 1)">
function compile(node, vm) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> reg = /\{\{(.*)\}\}/<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 节点类型为元素</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (node.nodeType === <span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> attr =<span style="color: rgba(0, 0, 0, 1)"> node.attributes;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 解析属性</span>
<span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">var</span> i = <span style="color: rgba(128, 0, 128, 1)">0</span>; i < attr.length; i++<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (attr[i].nodeName == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">v-model</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> name = attr[i].nodeValue; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取v-model绑定的属性名</span>
node.value = vm.data[name]; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将data的值赋值给该node</span>
node.removeAttribute(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">v-model</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">);
}
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 节点类型为text</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (node.nodeType === <span style="color: rgba(128, 0, 128, 1)">3</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (reg.test(node.nodeValue)) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> name = RegExp.$<span style="color: rgba(128, 0, 128, 1)">1</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取匹配到的字符串</span>
name =<span style="color: rgba(0, 0, 0, 1)"> name.trim();
node.nodeValue </span>= vm.data[name]; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将data的值赋值给该node</span>
}
}
}
function nodeToFragment(node, vm) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> flag =<span style="color: rgba(0, 0, 0, 1)"> document.createDocumentFragment();
</span><span style="color: rgba(0, 0, 255, 1)">var</span><span style="color: rgba(0, 0, 0, 1)"> child;
</span><span style="color: rgba(0, 0, 255, 1)">while</span> (child =<span style="color: rgba(0, 0, 0, 1)"> node.firstChild) {
compile(child, vm);
flag.appendChild(child); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将子节点劫持到文档片段中</span>
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> flag;
}
function Vue(options) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.data =<span style="color: rgba(0, 0, 0, 1)"> options.data;
</span><span style="color: rgba(0, 0, 255, 1)">var</span> id =<span style="color: rgba(0, 0, 0, 1)"> options.el;
</span><span style="color: rgba(0, 0, 255, 1)">var</span> dom = nodeToFragment(document.getElementById(id), <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 编译完成后,将dom返回到app中。</span>
document.getElementById(id).appendChild(dom);
}
</span><span style="color: rgba(0, 0, 255, 1)">var</span> vm = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Vue({
el: </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">app</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,
data: {
text: </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">hello world</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">
}
});
</span></script>
</body>
</html>
以上的代码实现而立任务一,我们可以看到,hello world 已经呈现在了输入框和文本节点中了。
六、响应式的数据绑定
我们再来看看任务二的实现思路: 当我们在输入框输入数据的时候,首先触发的时 input 事件(或者 keyup、change 事件),在相应的事件处理程序中,我们获取输入框的 value 并赋值给 vm 实例的 text 属性。 我们会利用 defineProperty 将 data 中的 text 设置为 vm 的访问器属性,因此给 vm.text 赋值,就会触发 set 方法。 在 set 方法中主要做两件事情,第一是更新属性的值,第二留在任务三种说。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>forvue</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
{{text}}
</div>
<script><span style="color: rgba(0, 0, 0, 1)">
function compile(node, vm) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> reg = /\{\{(.*)\}\}/<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 节点类型为元素</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (node.nodeType === <span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> attr =<span style="color: rgba(0, 0, 0, 1)"> node.attributes;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 解析属性</span>
<span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">var</span> i = <span style="color: rgba(128, 0, 128, 1)">0</span>; i < attr.length; i++<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (attr[i].nodeName == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">v-model</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> name = attr[i].nodeValue; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取v-model绑定的属性名</span>
node.addEventListener(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">input</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, function (e) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 给相应的data属性赋值,进而触发属性的set方法</span>
vm[name] =<span style="color: rgba(0, 0, 0, 1)"> e.target.value;
})
node.value </span>= vm[name]; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将data的值赋值给该node</span>
node.removeAttribute(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">v-model</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">);
}
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 节点类型为text</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (node.nodeType === <span style="color: rgba(128, 0, 128, 1)">3</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (reg.test(node.nodeValue)) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> name = RegExp.$<span style="color: rgba(128, 0, 128, 1)">1</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取匹配到的字符串</span>
name =<span style="color: rgba(0, 0, 0, 1)"> name.trim();
node.nodeValue </span>= vm[name]; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将data的值赋值给该node</span>
}
}
}
function nodeToFragment(node, vm) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> flag =<span style="color: rgba(0, 0, 0, 1)"> document.createDocumentFragment();
</span><span style="color: rgba(0, 0, 255, 1)">var</span><span style="color: rgba(0, 0, 0, 1)"> child;
</span><span style="color: rgba(0, 0, 255, 1)">while</span> (child =<span style="color: rgba(0, 0, 0, 1)"> node.firstChild) {
compile(child, vm);
flag.appendChild(child); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将子节点劫持到文档片段中</span>
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> flag;
}
function Vue(options) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.data =<span style="color: rgba(0, 0, 0, 1)"> options.data;
</span><span style="color: rgba(0, 0, 255, 1)">var</span> data = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.data;
observe(data, </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 0, 255, 1)">var</span> id =<span style="color: rgba(0, 0, 0, 1)"> options.el;
</span><span style="color: rgba(0, 0, 255, 1)">var</span> dom = nodeToFragment(document.getElementById(id), <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 编译完成后,将dom返回到app中。</span>
document.getElementById(id).appendChild(dom);
}
</span><span style="color: rgba(0, 0, 255, 1)">var</span> vm = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Vue({
el: </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">app</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,
data: {
text: </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">hello world</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">
}
});
function defineReactive(obj, key, val) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 响应式的数据绑定</span>
Object.defineProperty(obj, key, {
get: function () {
return val;
},
set: function (newVal) {
if (newVal === val) {
return;
} else {
val = newVal;
console.log(val); // 方便看效果
}
}
});
}
function observe (obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
});
}
</span></script>
</body>
</html>
以上,任务二也就完成了,text 属性值会和输入框的内容同步变化。
七、 订阅 / 发布模式(subscribe & publish)
text 属性变化了,set 方法触发了,但是文本节点的内容没有变化。 如何才能让同样绑定到 text 的文本节点也同步变化呢? 这里又有一个知识点: 订阅发布模式。
订阅发布模式又称为观察者模式,定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有的观察者对象。
发布者发出通知 =>主题对象收到通知并推送给订阅者 => 订阅者执行相应的操作。
// 一个发布者 publisher,功能就是负责发布消息 - publish
var pub = {publish: function () {dep.notify();
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 多个订阅者 subscribers, 在发布者发布消息之后执行函数</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> sub1 =<span style="color: rgba(0, 0, 0, 1)"> {
update: function () {
console.log(</span><span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">);
}
}
</span><span style="color: rgba(0, 0, 255, 1)">var</span> sub2 =<span style="color: rgba(0, 0, 0, 1)"> {
update: function () {
console.log(</span><span style="color: rgba(128, 0, 128, 1)">2</span><span style="color: rgba(0, 0, 0, 1)">);
}
}
</span><span style="color: rgba(0, 0, 255, 1)">var</span> sub3 =<span style="color: rgba(0, 0, 0, 1)"> {
update: function () {
console.log(</span><span style="color: rgba(128, 0, 128, 1)">3</span><span style="color: rgba(0, 0, 0, 1)">);
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 一个主题对象</span>
function Dep() {
this.subs = [sub1, sub2, sub3];
}
Dep.prototype.notify = function () {
this.subs.forEach(function (sub) {
sub.update();
});
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 发布者发布消息, 主题对象执行notify方法,进而触发订阅者执行Update方法</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> dep = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Dep();
pub.publish();</span></pre>
不难看出,这里的思路还是很简单的: 发布者负责发布消息、 订阅者负责接收接收消息,而最重要的是主题对象,他需要记录所有的订阅这特消息的人,然后负责吧发布的消息通知给哪些订阅了消息的人。
所以,当 set 方法触发后做的第二件事情就是作为发布者发出通知: “我是属性 text,我变了”。 文本节点作为订阅者,在接收到消息之后执行相应的更新动作。
八、 双向绑定的实现
回顾一下,每当 new 一个 Vue,主要做了两件事情:第一是监听数据:observe(data),第二是编译 HTML:nodeToFragment(id)
在监听数据的过程中,会为 data 中的每一个属性生成一个主题对象 dep。
在编译 HTML 的过程中,会为每一个与数据绑定相关的节点生成一个订阅者 watcher,watcher 会将自己添加到相应属性的 dep 中。
我们已经实现了: 修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的 set 方法。
接下来我们要实现的是: 发出通知 dep.notify() => 触发订阅者 update 方法 => 更新视图。
这里的关键逻辑是: 如何将 watcher 添加到关联属性的 dep 中。
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 节点类型为元素</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (node.nodeType === <span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> attr =<span style="color: rgba(0, 0, 0, 1)"> node.attributes;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 解析属性</span>
<span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">var</span> i = <span style="color: rgba(128, 0, 128, 1)">0</span>; i < attr.length; i++<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (attr[i].nodeName == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">v-model</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> name = attr[i].nodeValue; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取v-model绑定的属性名</span>
node.addEventListener(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">input</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, function (e) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 给相应的data属性赋值,进而触发属性的set方法</span>
vm[name] =<span style="color: rgba(0, 0, 0, 1)"> e.target.value;
})
node.value </span>= vm[name]; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将data的值赋值给该node</span>
node.removeAttribute(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">v-model</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">);
}
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 节点类型为text</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (node.nodeType === <span style="color: rgba(128, 0, 128, 1)">3</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (reg.test(node.nodeValue)) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> name = RegExp.$<span style="color: rgba(128, 0, 128, 1)">1</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取匹配到的字符串</span>
name =<span style="color: rgba(0, 0, 0, 1)"> name.trim();
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> node.nodeValue = vm[name]; </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将data的值赋值给该node</span>
<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Watcher(vm, node, name);
}
}
}</span></pre>
在编译 HTML 的过程中,为每个和 data 关联的节点生成一个 Watcher。那么 Watcher 函数中发生了什么呢?
function Watcher(vm, node, name) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.update();
Dep.target = null;
}
Watcher.prototype </span>=<span style="color: rgba(0, 0, 0, 1)"> {
update: function () {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.<span style="color: rgba(0, 0, 255, 1)">get</span><span style="color: rgba(0, 0, 0, 1)">();
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.node.nodeValue = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.value;
},
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取data中的属性值</span>
<span style="color: rgba(0, 0, 255, 1)">get</span><span style="color: rgba(0, 0, 0, 1)">: function () {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.value = <span style="color: rgba(0, 0, 255, 1)">this</span>.vm[<span style="color: rgba(0, 0, 255, 1)">this</span>.name]; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 触发相应属性的get</span>
}
}
首先,将自己赋值给了一个全局变量 Dep.target;
其次,执行了 update 方法,进而执行了 get 方法,get 方法读取了 vm 的访问器属性, 从而触发了访问器属性的 get 方法,get 方法将该 watcher 添加到对应访问器属性的 dep 中;
再次,获取顺序性的值, 然后更新视图。
最后将 Dep.target 设置为空。 因为他是全局变量,也是 watcher 和 dep 关联的唯一桥梁,任何时候,都必须保证 Dep.target 只有一个值。
最终如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>forvue</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text"> <br>
{{text}} <br>
{{text}}
</div>
<script><span style="color: rgba(0, 0, 0, 1)">
function observe(obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
});
}
function defineReactive(obj, key, val) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> dep = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Dep();
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 响应式的数据绑定</span>
Object.defineProperty(obj, key, {
get: function () {
// 添加订阅者 watcher 到主题对象 Dep
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: function (newVal) {
if (newVal === val) {
return;
} else {
val = newVal;
// 作为发布者发出通知
dep.notify()
}
}
});
}
function nodeToFragment(node, vm) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> flag =<span style="color: rgba(0, 0, 0, 1)"> document.createDocumentFragment();
</span><span style="color: rgba(0, 0, 255, 1)">var</span><span style="color: rgba(0, 0, 0, 1)"> child;
</span><span style="color: rgba(0, 0, 255, 1)">while</span> (child =<span style="color: rgba(0, 0, 0, 1)"> node.firstChild) {
compile(child, vm);
flag.appendChild(child); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将子节点劫持到文档片段中</span>
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> flag;
}
function compile(node, vm) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> reg = /\{\{(.*)\}\}/<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 节点类型为元素</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (node.nodeType === <span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> attr =<span style="color: rgba(0, 0, 0, 1)"> node.attributes;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 解析属性</span>
<span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">var</span> i = <span style="color: rgba(128, 0, 128, 1)">0</span>; i < attr.length; i++<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (attr[i].nodeName == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">v-model</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> name = attr[i].nodeValue; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取v-model绑定的属性名</span>
node.addEventListener(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">input</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, function (e) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 给相应的data属性赋值,进而触发属性的set方法</span>
vm[name] =<span style="color: rgba(0, 0, 0, 1)"> e.target.value;
})
node.value </span>= vm[name]; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将data的值赋值给该node</span>
node.removeAttribute(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">v-model</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">);
}
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 节点类型为text</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (node.nodeType === <span style="color: rgba(128, 0, 128, 1)">3</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (reg.test(node.nodeValue)) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> name = RegExp.$<span style="color: rgba(128, 0, 128, 1)">1</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取匹配到的字符串</span>
name =<span style="color: rgba(0, 0, 0, 1)"> name.trim();
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> node.nodeValue = vm[name]; </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将data的值赋值给该node</span>
<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Watcher(vm, node, name);
}
}
}
function Watcher(vm, node, name) {
Dep.target </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.name =<span style="color: rgba(0, 0, 0, 1)"> name;
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.node =<span style="color: rgba(0, 0, 0, 1)"> node;
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.vm =<span style="color: rgba(0, 0, 0, 1)"> vm;
</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.update();
Dep.target </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
}
Watcher.prototype </span>=<span style="color: rgba(0, 0, 0, 1)"> {
update: function () {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.<span style="color: rgba(0, 0, 255, 1)">get</span><span style="color: rgba(0, 0, 0, 1)">();
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.node.nodeValue = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.value;
},
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取data中的属性值</span>
<span style="color: rgba(0, 0, 255, 1)">get</span><span style="color: rgba(0, 0, 0, 1)">: function () {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.value = <span style="color: rgba(0, 0, 255, 1)">this</span>.vm[<span style="color: rgba(0, 0, 255, 1)">this</span>.name]; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 触发相应属性的get</span>
}
}
function Dep () {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.subs =<span style="color: rgba(0, 0, 0, 1)"> [];
}
Dep.prototype </span>=<span style="color: rgba(0, 0, 0, 1)"> {
addSub: function (sub) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.subs.push(sub);
},
notify: function () {
</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.subs.forEach(function (sub) {
sub.update();
});
}
}
function Vue(options) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.data =<span style="color: rgba(0, 0, 0, 1)"> options.data;
</span><span style="color: rgba(0, 0, 255, 1)">var</span> data = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.data;
observe(data, </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 0, 255, 1)">var</span> id =<span style="color: rgba(0, 0, 0, 1)"> options.el;
</span><span style="color: rgba(0, 0, 255, 1)">var</span> dom = nodeToFragment(document.getElementById(id), <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 编译完成后,将dom返回到app中。</span>
document.getElementById(id).appendChild(dom);
}
</span><span style="color: rgba(0, 0, 255, 1)">var</span> vm = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Vue({
el: </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">app</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,
data: {
text: </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">hello world</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">
}
});
</span></script>
</body>
</html>
参考文章: