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>  
&lt;script&gt; <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>&lt;/script&gt;</pre>

  即和 vue 一样的方式来实现数据的双向绑定。那么,我们可以把整个实现过程分为下面几步: 

  • 输入框以及文本节点与 data 中的数据绑定
  • 输入框内容变化时,data 中的数据同步变化。即 view => model 的变化
  • data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化

  

 

四、DocumentFragment

  如果希望实现任务一,我们还需要使用到 DocumentFragment 文档片段,可以把它看做一个容器,如下所示:

<div id="app">
&lt;/div&gt; &lt;script&gt; <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>&lt;/script&gt;</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>
&lt;script&gt; <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>&lt;/script&gt;

</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>
&lt;script&gt;<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 &lt; 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>&lt;/script&gt;

</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>
&lt;script&gt;<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 &lt; 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>&lt;/script&gt;

</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 &lt; 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>
&lt;script&gt;<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 &lt; 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>&lt;/script&gt;

</body>
</html>

 

 

 

 

 

 

 

 

 

  

 

 

 

 

 

 

 

 

 

 

 

 

参考文章: