用户一束光 發表於 2019-7-30 10:54:00

[转] Vue原理解析——自己写个Vue

<h3 id="articleHeader0">一、Vue对比其他框架原理</h3>
<p>Vue相对于React,Angular更加综合一点。AngularJS则使用了“脏值检测”。</p>
<p>React则采用避免直接操作DOM的虚拟dom树。而Vue则采用的是&nbsp;Object.defineProperty特性(这在ES5中是无法slim的,这就是为什么vue2.0不支持ie8以下的浏览器)</p>
<p>Vue可以说是尤雨溪从Angular中提炼出来的,又参照了React的性能思路,而集大成的一种轻量、高效,灵活的框架。</p>
<h3 id="articleHeader1">二、Vue的原理</h3>
<p>Vue的原理可以简单地从下列图示所得出</p>
<p><span class="img-wrap"><img title="" src="https://segmentfault.com/img/remote/1460000010352759" alt="" data-src="/img/remote/1460000010352759"></span></p>
<ol>
<li>通过建立虚拟dom树<code>document.createDocumentFragment()</code>,方法创建虚拟dom树。</li>
<li>一旦被监测的数据改变,会通过Object.defineProperty定义的数据拦截,截取到数据的变化。</li>
<li>截取到的数据变化,从而通过订阅——发布者模式,触发Watcher(观察者),从而改变虚拟dom的中的具体数据。</li>
<li>最后,通过更新虚拟dom的元素值,从而改变最后渲染dom树的值,完成双向绑定</li>
</ol>
<p>Vue的模式是m-v-vm模式,即(model-view-modelView),通过modelView作为中间层(即vm的实例),进行双向数据的绑定与变化。</p>
<p>而实现这种双向绑定的关键就在于:</p>
<p><strong>Object.defineProperty</strong>和<strong>订阅——发布者模式</strong>浙两点。</p>
<p>下面我们通过实例来实现Vue的基本双向绑定。</p>
<h3 id="articleHeader2">三、Vue双向绑定的实现</h3>
<h4>3.1 简易双绑</h4>
<p>首先,我们把注意力集中在这个属性上:Object.defineProperty。</p>
<blockquote><code>Object.defineProperty()</code>&nbsp;方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
<p>语法:Object.defineProperty(obj, prop, descriptor)</p>
</blockquote>
<p>什么叫做,定义或修改一个对象的新属性,并返回这个对象呢?</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">var obj = {};
<span class="hljs-built_in">Object.defineProperty(obj,<span class="hljs-string">'hello',{
<span class="hljs-attr">get:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
    <span class="hljs-comment">//我们在这里拦截到了数据
    <span class="hljs-built_in">console.log(<span class="hljs-string">"get方法被调用");
},
<span class="hljs-attr">set:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">newValue){
    <span class="hljs-comment">//改变数据的值,拦截下来额
    <span class="hljs-built_in">console.log(<span class="hljs-string">"set方法被调用");
}
});
obj.hello<span class="hljs-comment">//输出为“get方法被调用”,输出了值。
obj.hello = <span class="hljs-string">'new Hello';<span class="hljs-comment">//输出为set方法被调用,修改了新值</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>输出结果如下:</p>
<p><span class="img-wrap"><img title="clipboard.png" src="https://segmentfault.com/img/bVbg7xi?w=1164&amp;h=202" alt="clipboard.png" data-src="/img/bVbg7xi?w=1164&amp;h=202"></span></p>
<p>可以从这里看到,这是在对更底层的对象属性进行编程。简单地说,也就是我们对其<strong>更底层对象属性的修改或获取的阶段进行了拦截</strong>(对象属性更改的钩子函数)。</p>
<p>在这数据拦截的基础上,我们可以做到数据的双向绑定:</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">var obj = {};
<span class="hljs-built_in">Object.defineProperty(obj,<span class="hljs-string">'hello',{
<span class="hljs-attr">get:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
    <span class="hljs-comment">//我们在这里拦截到了数据
    <span class="hljs-built_in">console.log(<span class="hljs-string">"get方法被调用");
},
<span class="hljs-attr">set:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">newValue){
    <span class="hljs-comment">//改变数据的值,拦截下来额
    <span class="hljs-built_in">console.log(<span class="hljs-string">"set方法被调用");
    <span class="hljs-built_in">document.getElementById(<span class="hljs-string">'test').value = newValue;
    <span class="hljs-built_in">document.getElementById(<span class="hljs-string">'test1').innerHTML = newValue;
}
});
<span class="hljs-comment">//obj.hello;
<span class="hljs-comment">//obj.hello = '123';
<span class="hljs-built_in">document.getElementById(<span class="hljs-string">'test').addEventListener(<span class="hljs-string">'input',<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">e){
obj.hello = e.target.value;<span class="hljs-comment">//触发它的set方法
})</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>html:</p>
<pre class="xml hljs"><code class="html"><span class="hljs-tag">&lt;<span class="hljs-name">div <span class="hljs-attr">id=<span class="hljs-string">"mvvm"&gt;
   <span class="hljs-tag">&lt;<span class="hljs-name">input <span class="hljs-attr">v-model=<span class="hljs-string">"text" <span class="hljs-attr">id=<span class="hljs-string">"test"&gt;<span class="hljs-tag">&lt;/<span class="hljs-name">input&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">div <span class="hljs-attr">id=<span class="hljs-string">"test1"&gt;<span class="hljs-tag">&lt;/<span class="hljs-name">div&gt;
<span class="hljs-tag">&lt;/<span class="hljs-name">div&gt;</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>在线演示:demo演示</p>
<p>在这我们可以简单的实现了一个双向绑定。但是到这还不够,我们的目的是实现一个Vue。</p>
<h4>3.2 Vue初始化(虚拟节点的产生与编译)</h4>
<h5>3.2.1 Vue的虚拟节点容器</h5>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">nodeContainer(<span class="hljs-params">node, vm, flag){
<span class="hljs-keyword">var flag = flag || <span class="hljs-built_in">document.createDocumentFragment();

<span class="hljs-keyword">var child;
<span class="hljs-keyword">while(child = node.firstChild){
    compile(child, vm);
    flag.appendChild(child);
    <span class="hljs-keyword">if(child.firstChild){
      <span class="hljs-comment">// flag.appendChild(nodeContainer(child,vm));
      nodeContainer(child, vm, flag);
    }
}
<span class="hljs-keyword">return flag;
}</span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>这里几个注意的点:</p>
<ol>
<li><code>while(child = node.firstChild)</code>把node的firstChild赋值成while的条件,可以看做是遍历所有的dom节点。一旦遍历到底了,node的firstChild就会未定义成undefined就跳出while。</li>
<li><code>document.createDocumentFragment();</code>是一个虚拟节点的容器树,可以存放我们的虚拟节点。</li>
<li>上面的函数是个迭代,一直循环到节点的终点为止。</li>
</ol>
<h5>3.2.2 Vue的节点初始化编译</h5>
<p>先声明一个Vue对象</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">Vue(<span class="hljs-params">options){
<span class="hljs-keyword">this.data = options.data;

<span class="hljs-keyword">var id = options.el;
<span class="hljs-keyword">var dom = nodeContainer(<span class="hljs-built_in">document.getElementById(id),<span class="hljs-keyword">this);
<span class="hljs-built_in">document.getElementById(id).appendChild(dom);
}

<span class="hljs-comment">//随后使用他
<span class="hljs-keyword">var Demo = <span class="hljs-keyword">new Vue({
<span class="hljs-attr">el:<span class="hljs-string">'mvvm',
<span class="hljs-attr">data:{
    <span class="hljs-attr">text:<span class="hljs-string">'HelloWorld',
    <span class="hljs-attr">d:<span class="hljs-string">'123'
}
})</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>接下去的具体得初始化内容</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-comment">//编译
<span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">compile(<span class="hljs-params">node, vm){
<span class="hljs-keyword">var reg = <span class="hljs-regexp">/\{\{(.*)\}\}/g;<span class="hljs-comment">//匹配双绑的双大括号
<span class="hljs-keyword">if(node.nodeType === <span class="hljs-number">1){
    <span class="hljs-keyword">var attr = node.attributes;
    <span class="hljs-comment">//解析节点的属性
    <span class="hljs-keyword">for(<span class="hljs-keyword">var i = <span class="hljs-number">0;i &lt; attr.length; i++){
      <span class="hljs-keyword">if(attr.nodeName == <span class="hljs-string">'v-model'){
      <span class="hljs-keyword">var name = attr.nodeValue;
      node.value = vm.data;<span class="hljs-comment">//讲实例中的data数据赋值给节点
      <span class="hljs-comment">//node.removeAttribute('v-model');
      }
    }
}
<span class="hljs-comment">//如果节点类型为text
<span class="hljs-keyword">if(node.nodeType === <span class="hljs-number">3){
   
    <span class="hljs-keyword">if(reg.test(node.nodeValue)){
      <span class="hljs-comment">// console.dir(node);
      <span class="hljs-keyword">var name = <span class="hljs-built_in">RegExp.$<span class="hljs-number">1;<span class="hljs-comment">//获取匹配到的字符串
      name = name.trim();
      node.nodeValue = vm.data;
    }
}
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>代码解释:</p>
<ol>
<li>当nodeType为1的时候,表示是个元素。同时我们进行判断,如果节点中的指令含有<code>v-model</code>这个指令,那么我们就初始化,进行对节点的值的赋值。</li>
<li>如果nodeType为3的时候,也就是text节点属性。表示你的节点到了终点,一般都是节点的前后末端。我们常常在这里定义我们的双绑值。此时一旦匹配到了双绑(双大括号),即进行值的初始化。</li>
</ol>
<p>至此,我们的Vue初始化已经完成。</p>
<p><span class="img-wrap"><img title="clipboard.png" src="https://segmentfault.com/img/bVbhgZ5?w=1166&amp;h=616" alt="clipboard.png" data-src="/img/bVbhgZ5?w=1166&amp;h=616"></span></p>
<p>在线演示:demo1</p>
<h4>3.3 Vue的声明响应式</h4>
<h5>3.3.1 定义Vue的data的属性响应式</h5>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">defineReactive (<span class="hljs-params">obj, key, value){
<span class="hljs-built_in">Object.defineProperty(obj,key,{
    <span class="hljs-attr">get:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
      <span class="hljs-built_in">console.log(<span class="hljs-string">"get了值"+value);
      <span class="hljs-keyword">return value;<span class="hljs-comment">//获取到了值
    },
    <span class="hljs-attr">set:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">newValue){
      <span class="hljs-keyword">if(newValue === value){
      <span class="hljs-keyword">return;<span class="hljs-comment">//如果值没变化,不用触发新值改变
      }
      value = newValue;<span class="hljs-comment">//改变了值
      <span class="hljs-built_in">console.log(<span class="hljs-string">"set了最新值"+value);
    }
})
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>这里的obj我们这定义为vm实例或者vm实例里面的data属性。</p>
<p>PS:这里强调一下,defineProperty这个方法,不仅可以定义obj的直接属性,比如obj.hello这个属性。也可以间接定义属性比如:obj.middle.hello。这里导致的效果就是两者的hello属性都被定义成响应式了。</p>
<p>用下列的observe方法循环调用响应式方法。</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">observe (<span class="hljs-params">obj,vm){
<span class="hljs-built_in">Object.keys(obj).forEach(<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">key){
    defineReactive(vm,key,obj);
})
}</span></span></span></span></span></span></span></span></code></pre>
<p>然后再Vue方法中初始化:</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">Vue(<span class="hljs-params">options){
<span class="hljs-keyword">this.data = options.data;
<span class="hljs-keyword">var data = <span class="hljs-keyword">this.data;
-------------------------
observe(data,<span class="hljs-keyword">this);<span class="hljs-comment">//这里调用定义响应式方法
-------------------------
<span class="hljs-keyword">var id = options.el;
<span class="hljs-keyword">var dom = nodeContainer(<span class="hljs-built_in">document.getElementById(id),<span class="hljs-keyword">this);
<span class="hljs-built_in">document.getElementById(id).appendChild(dom); <span class="hljs-comment">//把虚拟dom渲染上去
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>在编译方法中v-model属性找到的时候去监听:</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">compile(<span class="hljs-params">node, vm){
<span class="hljs-keyword">var reg = <span class="hljs-regexp">/\{\{(.*)\}\}/g;
<span class="hljs-keyword">if(node.nodeType === <span class="hljs-number">1){
    <span class="hljs-keyword">var attr = node.attributes;
    <span class="hljs-comment">//解析节点的属性
    <span class="hljs-keyword">for(<span class="hljs-keyword">var i = <span class="hljs-number">0;i &lt; attr.length; i++){
      <span class="hljs-keyword">if(attr.nodeName == <span class="hljs-string">'v-model'){
      
      <span class="hljs-keyword">var name = attr.nodeValue;
      -------------------------<span class="hljs-comment">//这里新添加的监听
      node.addEventListener(<span class="hljs-string">'input',<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">e){
          <span class="hljs-built_in">console.log(vm);
          vm = e.target.value;<span class="hljs-comment">//改变实例里面的值
      });
      -------------------------
      node.value = vm;<span class="hljs-comment">//讲实例中的data数据赋值给节点
      <span class="hljs-comment">//node.removeAttribute('v-model');
      }
    }
}
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>以上我们实现了,你再输入框里面输入,同时触发getter&amp;setter,去改变vm实例中data的值。也就是说MVVM的图例中经过getter&amp;setter已经成功了。接下去就是订阅——发布者模式。</p>
<p>在线演示:demo2</p>
<p>实现效果:</p>
<p><span class="img-wrap"><img title="clipboard.png" src="https://segmentfault.com/img/bVbg7zx?w=1504&amp;h=402" alt="clipboard.png" data-src="/img/bVbg7zx?w=1504&amp;h=402"></span></p>
<h4>3.4 订阅——发布者模式</h4>
<p>什么是订阅——发布者?简单点说:你微信里面经常会订阅一些公众号,一旦这些公众号发布新消息了。那么他就会通知你,告诉你:我发布了新东西,快来看。</p>
<p>这种情景下,你就是<strong>订阅者</strong>,公众号就是<strong>发布者</strong>。</p>
<p>所以我们要模拟这种情景,我们先声明3个订阅者:</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">var sub1 = {
<span class="hljs-attr">update:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
    <span class="hljs-built_in">console.log(<span class="hljs-number">1);
}
}
<span class="hljs-keyword">var sub2 = {
<span class="hljs-attr">update:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
    <span class="hljs-built_in">console.log(<span class="hljs-number">2);
}
}
<span class="hljs-keyword">var sub3 = {
<span class="hljs-attr">update:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
    <span class="hljs-built_in">console.log(<span class="hljs-number">3);
}
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>每个订阅者对象内部声明一个update方法来触发订阅属性。</p>
<p>再声明一个发布者,去触发发布消息,通知的方法::</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">Dep(<span class="hljs-params">){
<span class="hljs-keyword">this.subs = ;<span class="hljs-comment">//把三个订阅者加进去
}
Dep.prototype.notify = <span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){<span class="hljs-comment">//在原型上声明“发布消息”方法
<span class="hljs-keyword">this.subs.forEach(<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">sub){
    sub.update();
})
}
<span class="hljs-keyword">var dep = <span class="hljs-keyword">new Dep();
<span class="hljs-comment">//pub.publish();
dep.notify();</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>我们也可以声明另外一个中间对象</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">var dep = <span class="hljs-keyword">new Dep();
<span class="hljs-keyword">var pub = {
<span class="hljs-attr">publish:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
    dep.notify();
}
}
pub.publish();<span class="hljs-comment">//这里的结果是跟上面一样的</span></span></span></span></span></span></span></span></code></pre>
<p>实现效果:</p>
<p><span class="img-wrap"><img title="clipboard.png" src="https://segmentfault.com/img/bVbg7xL?w=1492&amp;h=230" alt="clipboard.png" data-src="/img/bVbg7xL?w=1492&amp;h=230"></span></p>
<p>到这,我们已经实现了:</p>
<ol>
<li>修改输入框内容 =&gt; 触发修改vm实例里的属性值 =&gt; 触发set&amp;get方法</li>
<li>订阅成功 =&gt; 发布者发出通知notify() =&gt; 触发订阅者的update()方法</li>
</ol>
<p>接下来重点要实现的是:如何去更新视图,同时把订阅——发布者模式进去watcher观察者模式?</p>
<h4>3.5 观察者模式</h4>
<p>先定义发布者:</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">Dep(<span class="hljs-params">){
<span class="hljs-keyword">this.subs = [];
}
Dep.prototype ={
<span class="hljs-attr">add:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">sub){<span class="hljs-comment">//这里定义增加订阅者的方法
    <span class="hljs-keyword">this.subs.push(sub);
},
<span class="hljs-attr">notify:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){<span class="hljs-comment">//这里定义触发订阅者update()的通知方法
    <span class="hljs-keyword">this.subs.forEach(<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">sub){
      <span class="hljs-built_in">console.log(sub);
      sub.update();<span class="hljs-comment">//下列发布者的更新方法
    })
}
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>再定义观察者(订阅者):</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">Watcher(<span class="hljs-params">vm,node,name){
Dep.global = <span class="hljs-keyword">this;<span class="hljs-comment">//这里很重要!把自己赋值给Dep函数对象的全局变量
<span class="hljs-keyword">this.name = name;
<span class="hljs-keyword">this.node = node;
<span class="hljs-keyword">this.vm = vm;
<span class="hljs-keyword">this.update();
Dep.global = <span class="hljs-literal">null;<span class="hljs-comment">//这里update()完记得清空Dep函数对象的全局变量
}
Watcher.prototype.update = <span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
    <span class="hljs-keyword">this.get();
    <span class="hljs-keyword">switch (<span class="hljs-keyword">this.node.nodeType) { <span class="hljs-comment">//这里去通过判断节点的类型改变视图的值
      <span class="hljs-keyword">case <span class="hljs-number">1:
      <span class="hljs-keyword">this.node.value = <span class="hljs-keyword">this.value;
      <span class="hljs-keyword">break;
      <span class="hljs-keyword">case <span class="hljs-number">3:
      <span class="hljs-keyword">this.node.nodeValue = <span class="hljs-keyword">this.value;
      <span class="hljs-keyword">break;
      <span class="hljs-keyword">default: <span class="hljs-keyword">break;
    };
}
Watcher.prototype.get = <span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
    <span class="hljs-keyword">this.value = <span class="hljs-keyword">this.vm[<span class="hljs-keyword">this.name];<span class="hljs-comment">//这里把this的value值赋值,触发data的defineProperty方法中的get方法!
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>以上需要注意的点:</p>
<ol>
<li>在Watcher函数对象的原型方法update里面更新视图的值(实现watcher到视图层的改变)。</li>
<li>Watcher函数对象的原型方法get,是为了触发defineProperty方法中的get方法!</li>
<li>在new一个Watcher的对象的时候,记得把Dep函数对象赋值一个全局变量,而且及时清空。至于为什么这么做,我们接下来看。</li>
</ol>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">defineReactive (<span class="hljs-params">obj, key, value){
<span class="hljs-keyword">var dep = <span class="hljs-keyword">new Dep();<span class="hljs-comment">//这里每一个vm的data属性值声明一个新的订阅者
<span class="hljs-built_in">Object.defineProperty(obj,key,{
    <span class="hljs-attr">get:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
      <span class="hljs-built_in">console.log(Dep.global);
      -----------------------
      <span class="hljs-keyword">if(Dep.global){<span class="hljs-comment">//这里是第一次new对象Watcher的时候,初始化数据的时候,往订阅者对象里面添加对象。第二次后,就不需要再添加了
      dep.add(Dep.global);
      }
      -----------------------
      <span class="hljs-keyword">return value;
    },
    <span class="hljs-attr">set:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">newValue){
      <span class="hljs-keyword">if(newValue === value){
      <span class="hljs-keyword">return;
      }
      value = newValue;
      dep.notify();<span class="hljs-comment">//触发了update()方法
    }
})
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>这里有一点需要注意:</p>
<p>在上述圈起来的地方:<code>if(Dep.global)</code>是在第一次<code>new Watcher()</code>的时候,进入<code>update()</code>方法,触发这里的<code>get</code>方法。这里非常的重要的一点!在此时<code>new Watcher()</code>只走到了<code>this.update();</code>方法,此刻没有触发<code>Dep.global = null</code>函数,所以值并没有清空,所以可以进到<code>dep.add(Dep.global);</code>方法里面去。</p>
<p>而第二次后,由于清空了Dep的全局变量,所以不会触发add()方法。</p>
<blockquote>PS:这个思路容易被忽略,由于是参考之前一个博主的代码影响,我自己想了很多方法改变,但是在这种情景下难以实现别的更好的交互方式。
<p>所以我暂时现在只能使用Dep的全局变量的方式,来实现Dep函数与Watcher函数的交互。(如果是ES6的模块化方法会不一样)</p>
<p>而后我会尽量找寻其他更好的方法来实现Dep函数与Watcher函数的交互。</p>
</blockquote>
<p>紧接着在<code>text</code>节点和<strong>绑定了的<code>input</code>节点</strong>(别忘记了这个节点)<code>new Watcher</code>的方法来触发以上的内容:</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-comment">// 如果节点为input
    <span class="hljs-keyword">if(node.nodeType === <span class="hljs-number">1){
      ...........
      ----------
      <span class="hljs-keyword">new Watcher(vm,node,name) <span class="hljs-comment">// 别忘记给input添加观察者模式
      ----------

    }
<span class="hljs-comment">//如果节点类型为text
<span class="hljs-keyword">if(node.nodeType === <span class="hljs-number">3){
   
    <span class="hljs-keyword">if(reg.test(node.nodeValue)){
      <span class="hljs-comment">// console.dir(node);
      <span class="hljs-keyword">var name = <span class="hljs-built_in">RegExp.$<span class="hljs-number">1;<span class="hljs-comment">//获取匹配到的字符串
      name = name.trim();
      <span class="hljs-comment">// node.nodeValue = vm;
      -------------------------
      <span class="hljs-keyword">new Watcher(vm,node,name);<span class="hljs-comment">//这里到了一个新的节点,new一个新的观察者
      -------------------------
    }
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>至此,vue双向绑定已经简单的实现。</p>
<h4>3.6 最终效果</h4>
<p>在线演示:Codepen实现Vue的demo(有时候要FQ)<button class="btn btn-xs btn-default ml10 preview" data-url="xdnloveme/pen/YJYKLj" data-typeid="3">点击预览</button></p>
<p>在线源码参考:demo4</p>
<p>下列是全部的源码,仅供参考。</p>
<p>HTML:</p>
<pre class="xml hljs"><code class="html"><span class="hljs-tag">&lt;<span class="hljs-name">div <span class="hljs-attr">id=<span class="hljs-string">"mvvm"&gt;
   <span class="hljs-tag">&lt;<span class="hljs-name">input <span class="hljs-attr">v-model=<span class="hljs-string">"d" <span class="hljs-attr">id=<span class="hljs-string">"test"&gt;{{text}}
    <span class="hljs-tag">&lt;<span class="hljs-name">div&gt;{{d}}<span class="hljs-tag">&lt;/<span class="hljs-name">div&gt;
<span class="hljs-tag">&lt;/<span class="hljs-name">div&gt;</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>JS:</p>
<pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">var obj = {};

<span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">nodeContainer(<span class="hljs-params">node, vm, flag){
<span class="hljs-keyword">var flag = flag || <span class="hljs-built_in">document.createDocumentFragment();

<span class="hljs-keyword">var child;
<span class="hljs-keyword">while(child = node.firstChild){
    compile(child, vm);
    flag.appendChild(child);
    <span class="hljs-keyword">if(child.firstChild){
      nodeContainer(child, vm, flag);
    }
}
<span class="hljs-keyword">return flag;
}

<span class="hljs-comment">//编译
<span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">compile(<span class="hljs-params">node, vm){
<span class="hljs-keyword">var reg = <span class="hljs-regexp">/\{\{(.*)\}\}/g;
<span class="hljs-keyword">if(node.nodeType === <span class="hljs-number">1){
    <span class="hljs-keyword">var attr = node.attributes;
    <span class="hljs-comment">//解析节点的属性
    <span class="hljs-keyword">for(<span class="hljs-keyword">var i = <span class="hljs-number">0;i &lt; attr.length; i++){
      <span class="hljs-keyword">if(attr.nodeName == <span class="hljs-string">'v-model'){
      
      <span class="hljs-keyword">var name = attr.nodeValue;
      node.addEventListener(<span class="hljs-string">'input',<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">e){
          vm = e.target.value;
      });

      node.value = vm;<span class="hljs-comment">//讲实例中的data数据赋值给节点
      node.removeAttribute(<span class="hljs-string">'v-model');
      }
    }
}
<span class="hljs-comment">//如果节点类型为text
<span class="hljs-keyword">if(node.nodeType === <span class="hljs-number">3){
   
    <span class="hljs-keyword">if(reg.test(node.nodeValue)){
      <span class="hljs-comment">// console.dir(node);
      <span class="hljs-keyword">var name = <span class="hljs-built_in">RegExp.$<span class="hljs-number">1;<span class="hljs-comment">//获取匹配到的字符串
      name = name.trim();
      <span class="hljs-comment">// node.nodeValue = vm;
      <span class="hljs-keyword">new Watcher(vm,node,name);
    }
}
}

<span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">defineReactive (<span class="hljs-params">obj, key, value){
<span class="hljs-keyword">var dep = <span class="hljs-keyword">new Dep();
<span class="hljs-built_in">Object.defineProperty(obj,key,{
    <span class="hljs-attr">get:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
      <span class="hljs-built_in">console.log(Dep.global);
      <span class="hljs-keyword">if(Dep.global){
      dep.add(Dep.global);
      }
      <span class="hljs-built_in">console.log(<span class="hljs-string">"get了值"+value);
      <span class="hljs-keyword">return value;
    },
    <span class="hljs-attr">set:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">newValue){
      <span class="hljs-keyword">if(newValue === value){
      <span class="hljs-keyword">return;
      }
      value = newValue;
      <span class="hljs-built_in">console.log(<span class="hljs-string">"set了最新值"+value);
      dep.notify();
    }
})
}

<span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">observe (<span class="hljs-params">obj,vm){
<span class="hljs-built_in">Object.keys(obj).forEach(<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">key){
    defineReactive(vm,key,obj);
})
}

<span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">Vue(<span class="hljs-params">options){
<span class="hljs-keyword">this.data = options.data;
<span class="hljs-keyword">var data = <span class="hljs-keyword">this.data;
observe(data,<span class="hljs-keyword">this);
<span class="hljs-keyword">var id = options.el;
<span class="hljs-keyword">var dom = nodeContainer(<span class="hljs-built_in">document.getElementById(id),<span class="hljs-keyword">this);
<span class="hljs-built_in">document.getElementById(id).appendChild(dom);
}

<span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">Dep(<span class="hljs-params">){
<span class="hljs-keyword">this.subs = [];
}
Dep.prototype ={
<span class="hljs-attr">add:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">sub){
    <span class="hljs-keyword">this.subs.push(sub);
},
<span class="hljs-attr">notify:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
    <span class="hljs-keyword">this.subs.forEach(<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">sub){
      <span class="hljs-built_in">console.log(sub);
      sub.update();
    })
}
}


<span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">Watcher(<span class="hljs-params">vm,node,name){
Dep.global = <span class="hljs-keyword">this;
<span class="hljs-keyword">this.name = name;
<span class="hljs-keyword">this.node = node;
<span class="hljs-keyword">this.vm = vm;
<span class="hljs-keyword">this.update();
Dep.global = <span class="hljs-literal">null;
}

Watcher.prototype = {
<span class="hljs-attr">update:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
    <span class="hljs-keyword">this.get();
    <span class="hljs-keyword">switch (<span class="hljs-keyword">this.node.nodeType) {
      <span class="hljs-keyword">case <span class="hljs-number">1:
      <span class="hljs-keyword">this.node.value = <span class="hljs-keyword">this.value;
      <span class="hljs-keyword">break;
      <span class="hljs-keyword">case <span class="hljs-number">3:
      <span class="hljs-keyword">this.node.nodeValue = <span class="hljs-keyword">this.value;
      <span class="hljs-keyword">break;
      <span class="hljs-keyword">default: <span class="hljs-keyword">break;
    }
},
<span class="hljs-attr">get:<span class="hljs-function"><span class="hljs-keyword">function(<span class="hljs-params">){
    <span class="hljs-keyword">this.value = <span class="hljs-keyword">this.vm[<span class="hljs-keyword">this.name];
}
}


<span class="hljs-keyword">var Demo = <span class="hljs-keyword">new Vue({
<span class="hljs-attr">el:<span class="hljs-string">'mvvm',
<span class="hljs-attr">data:{
    <span class="hljs-attr">text:<span class="hljs-string">'HelloWorld',
    <span class="hljs-attr">d:<span class="hljs-string">'123'
}
})</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<h3 id="articleHeader3">四、回顾</h3>
<p>我们再来通过一张图回顾一下整个过程:</p>
<p><span class="img-wrap"><img title="clipboard.png" src="https://segmentfault.com/img/bVbhbjy?w=856&amp;h=449" alt="clipboard.png" data-src="/img/bVbhbjy?w=856&amp;h=449"></span></p>
<p>从上可以看出,大概的过程是这样的:</p>
<ol>
<li>定义Vue对象,声明vue的data里面的属性值,准备初始化触发observe方法。</li>
<li>在Observe定义过响应式方法Object.defineProperty()的属性,在初始化的时候,通过Watcher对象进行addDep的操作。即每定义一个vue的data的属性值,就添加到一个Watcher对象到订阅者里面去。</li>
<li>每当形成一个Watcher对象的时候,去定义它的响应式。即<code>Object.defineProperty()</code>定义。这就导致了一个Observe里面的getter&amp;setter方法与订阅者形成一种依赖关系。</li>
<li>由于依赖关系的存在,每当数据的变化后,会导致setter方法,从而触发notify通知方法,通知订阅者我的数据改变了,你需要更新。</li>
<li>订阅者会触发内部的update方法,从而改变vm实例的值,以及每个Watcher里面对应node的nodeValue,即视图上面显示的值。</li>
<li>Watcher里面接收到了消息后,会触发改变对应对象里面的node的视图的value值,而改变视图上面的值。</li>
<li>至此,视图的值改变了。形成了双向绑定MVVM的效果。</li>
</ol>
<h3 id="articleHeader4">五、后记</h3>
<p>至此,我们通过解析vue的绑定原理,实现了一个非常简单的Vue。</p>
<p>我们可以再借鉴此思路的情况下,进行我们需要的定制框架的二次开发。如果开发人数尚可的话,可以实现类似微信小程序自己有的一套框架。</p>
<p>我非常重视技术的原理,只有真正掌握技术的原理,才能在原有的技术上更好地去提高和开发。</p>
<p>ps:此文是较早之前写的,不够规范,后面会修改一个ES6的版本。下方是参考链接,灵感来源于其他博主,我进行了修正优化和代码解释。</p>
<p>参考链接:</p>
<ol>
<li>Vue.js双向绑定的实现原理</li>
<li>Vue 源码解析:深入响应式原理</li>
<li>深入响应式原理</li>
</ol>
<p>原文地址(原创博客):http://www.tangyida.top/detail/150</p>
<p></audio></p><br><br>
来源:https://www.cnblogs.com/chris-oil/p/11268659.html
頁: [1]
查看完整版本: [转] Vue原理解析——自己写个Vue