一、Vue对比其他框架原理

Vue相对于React,Angular更加综合一点。AngularJS则使用了“脏值检测”。

React则采用避免直接操作DOM的虚拟dom树。而Vue则采用的是 Object.defineProperty特性(这在ES5中是无法slim的,这就是为什么vue2.0不支持ie8以下的浏览器)

Vue可以说是尤雨溪从Angular中提炼出来的,又参照了React的性能思路,而集大成的一种轻量、高效,灵活的框架。

二、Vue的原理

Vue的原理可以简单地从下列图示所得出

  1. 通过建立虚拟dom树document.createDocumentFragment(),方法创建虚拟dom树。
  2. 一旦被监测的数据改变,会通过Object.defineProperty定义的数据拦截,截取到数据的变化。
  3. 截取到的数据变化,从而通过订阅——发布者模式,触发Watcher(观察者),从而改变虚拟dom的中的具体数据。
  4. 最后,通过更新虚拟dom的元素值,从而改变最后渲染dom树的值,完成双向绑定

Vue的模式是m-v-vm模式,即(model-view-modelView),通过modelView作为中间层(即vm的实例),进行双向数据的绑定与变化。

而实现这种双向绑定的关键就在于:

Object.defineProperty订阅——发布者模式浙两点。

下面我们通过实例来实现Vue的基本双向绑定。

三、Vue双向绑定的实现

3.1 简易双绑

首先,我们把注意力集中在这个属性上:Object.defineProperty。

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

语法:Object.defineProperty(obj, prop, descriptor)

什么叫做,定义或修改一个对象的新属性,并返回这个对象呢?

var obj = {};
Object.defineProperty(obj,'hello',{
get:function(){
//我们在这里拦截到了数据
console.log("get方法被调用");
},
set:function(newValue){
//改变数据的值,拦截下来额
console.log("set方法被调用");
}
});
obj.hello//输出为“get方法被调用”,输出了值。
obj.hello = 'new Hello';//输出为set方法被调用,修改了新值

输出结果如下:

可以从这里看到,这是在对更底层的对象属性进行编程。简单地说,也就是我们对其更底层对象属性的修改或获取的阶段进行了拦截(对象属性更改的钩子函数)。

在这数据拦截的基础上,我们可以做到数据的双向绑定:

var obj = {};
Object.defineProperty(obj,'hello',{
get:function(){
//我们在这里拦截到了数据
console.log("get方法被调用");
},
set:function(newValue){
//改变数据的值,拦截下来额
console.log("set方法被调用");
document.getElementById('test').value = newValue;
document.getElementById('test1').innerHTML = newValue;
}
});
//obj.hello;
//obj.hello = '123';
document.getElementById('test').addEventListener('input',function(e){
obj.hello = e.target.value;//触发它的set方法
})

html:

<div id="mvvm">
<input v-model="text" id="test"></input>
<div id="test1"></div>
</div>

在线演示:demo演示

在这我们可以简单的实现了一个双向绑定。但是到这还不够,我们的目的是实现一个Vue。

3.2 Vue初始化(虚拟节点的产生与编译)

3.2.1 Vue的虚拟节点容器
function nodeContainer(node, vm, flag){
var flag = flag || document.createDocumentFragment(); var child;
while(child = node.firstChild){
compile(child, vm);
flag.appendChild(child);
if(child.firstChild){
// flag.appendChild(nodeContainer(child,vm));
nodeContainer(child, vm, flag);
}
}
return flag;
}

这里几个注意的点:

  1. while(child = node.firstChild)把node的firstChild赋值成while的条件,可以看做是遍历所有的dom节点。一旦遍历到底了,node的firstChild就会未定义成undefined就跳出while。
  2. document.createDocumentFragment();是一个虚拟节点的容器树,可以存放我们的虚拟节点。
  3. 上面的函数是个迭代,一直循环到节点的终点为止。
3.2.2 Vue的节点初始化编译

先声明一个Vue对象

function Vue(options){
this.data = options.data; var id = options.el;
var dom = nodeContainer(document.getElementById(id),this);
document.getElementById(id).appendChild(dom);
} //随后使用他
var Demo = new Vue({
el:'mvvm',
data:{
text:'HelloWorld',
d:'123'
}
})

接下去的具体得初始化内容

//编译
function compile(node, vm){
var reg = /\{\{(.*)\}\}/g;//匹配双绑的双大括号
if(node.nodeType === 1){
var attr = node.attributes;
//解析节点的属性
for(var i = 0;i < attr.length; i++){
if(attr[i].nodeName == 'v-model'){
var name = attr[i].nodeValue;
node.value = vm.data[name];//讲实例中的data数据赋值给节点
//node.removeAttribute('v-model');
}
}
}
//如果节点类型为text
if(node.nodeType === 3){ if(reg.test(node.nodeValue)){
// console.dir(node);
var name = RegExp.$1;//获取匹配到的字符串
name = name.trim();
node.nodeValue = vm.data[name];
}
}
}

代码解释:

  1. 当nodeType为1的时候,表示是个元素。同时我们进行判断,如果节点中的指令含有v-model这个指令,那么我们就初始化,进行对节点的值的赋值。
  2. 如果nodeType为3的时候,也就是text节点属性。表示你的节点到了终点,一般都是节点的前后末端。我们常常在这里定义我们的双绑值。此时一旦匹配到了双绑(双大括号),即进行值的初始化。

至此,我们的Vue初始化已经完成。

在线演示:demo1

3.3 Vue的声明响应式

3.3.1 定义Vue的data的属性响应式
function defineReactive (obj, key, value){
Object.defineProperty(obj,key,{
get:function(){
console.log("get了值"+value);
return value;//获取到了值
},
set:function(newValue){
if(newValue === value){
return;//如果值没变化,不用触发新值改变
}
value = newValue;//改变了值
console.log("set了最新值"+value);
}
})
}

这里的obj我们这定义为vm实例或者vm实例里面的data属性。

PS:这里强调一下,defineProperty这个方法,不仅可以定义obj的直接属性,比如obj.hello这个属性。也可以间接定义属性比如:obj.middle.hello。这里导致的效果就是两者的hello属性都被定义成响应式了。

用下列的observe方法循环调用响应式方法。

function observe (obj,vm){
Object.keys(obj).forEach(function(key){
defineReactive(vm,key,obj[key]);
})
}

然后再Vue方法中初始化:

function Vue(options){
this.data = options.data;
var data = this.data;
-------------------------
observe(data,this);//这里调用定义响应式方法
-------------------------
var id = options.el;
var dom = nodeContainer(document.getElementById(id),this);
document.getElementById(id).appendChild(dom); //把虚拟dom渲染上去
}

在编译方法中v-model属性找到的时候去监听:

function compile(node, vm){
var reg = /\{\{(.*)\}\}/g;
if(node.nodeType === 1){
var attr = node.attributes;
//解析节点的属性
for(var i = 0;i < attr.length; i++){
if(attr[i].nodeName == 'v-model'){ var name = attr[i].nodeValue;
-------------------------//这里新添加的监听
node.addEventListener('input',function(e){
console.log(vm[name]);
vm[name] = e.target.value;//改变实例里面的值
});
-------------------------
node.value = vm[name];//讲实例中的data数据赋值给节点
//node.removeAttribute('v-model');
}
}
}
}

以上我们实现了,你再输入框里面输入,同时触发getter&setter,去改变vm实例中data的值。也就是说MVVM的图例中经过getter&setter已经成功了。接下去就是订阅——发布者模式。

在线演示:demo2

实现效果:

3.4 订阅——发布者模式

什么是订阅——发布者?简单点说:你微信里面经常会订阅一些公众号,一旦这些公众号发布新消息了。那么他就会通知你,告诉你:我发布了新东西,快来看。

这种情景下,你就是订阅者,公众号就是发布者

所以我们要模拟这种情景,我们先声明3个订阅者:

var sub1 = {
update:function(){
console.log(1);
}
}
var sub2 = {
update:function(){
console.log(2);
}
}
var sub3 = {
update:function(){
console.log(3);
}
}

每个订阅者对象内部声明一个update方法来触发订阅属性。

再声明一个发布者,去触发发布消息,通知的方法::

function Dep(){
this.subs = [sub1,sub2,sub3];//把三个订阅者加进去
}
Dep.prototype.notify = function(){//在原型上声明“发布消息”方法
this.subs.forEach(function(sub){
sub.update();
})
}
var dep = new Dep();
//pub.publish();
dep.notify();

我们也可以声明另外一个中间对象

var dep = new Dep();
var pub = {
publish:function(){
dep.notify();
}
}
pub.publish();//这里的结果是跟上面一样的

实现效果:

到这,我们已经实现了:

  1. 修改输入框内容 => 触发修改vm实例里的属性值 => 触发set&get方法
  2. 订阅成功 => 发布者发出通知notify() => 触发订阅者的update()方法

接下来重点要实现的是:如何去更新视图,同时把订阅——发布者模式进去watcher观察者模式?

3.5 观察者模式

先定义发布者:

function Dep(){
this.subs = [];
}
Dep.prototype ={
add:function(sub){//这里定义增加订阅者的方法
this.subs.push(sub);
},
notify:function(){//这里定义触发订阅者update()的通知方法
this.subs.forEach(function(sub){
console.log(sub);
sub.update();//下列发布者的更新方法
})
}
}

再定义观察者(订阅者):

function Watcher(vm,node,name){
Dep.global = this;//这里很重要!把自己赋值给Dep函数对象的全局变量
this.name = name;
this.node = node;
this.vm = vm;
this.update();
Dep.global = null;//这里update()完记得清空Dep函数对象的全局变量
}
Watcher.prototype.update = function(){
this.get();
switch (this.node.nodeType) { //这里去通过判断节点的类型改变视图的值
case 1:
this.node.value = this.value;
break;
case 3:
this.node.nodeValue = this.value;
break;
default: break;
};
}
Watcher.prototype.get = function(){
this.value = this.vm[this.name];//这里把this的value值赋值,触发data的defineProperty方法中的get方法!
}

以上需要注意的点:

  1. 在Watcher函数对象的原型方法update里面更新视图的值(实现watcher到视图层的改变)。
  2. Watcher函数对象的原型方法get,是为了触发defineProperty方法中的get方法!
  3. 在new一个Watcher的对象的时候,记得把Dep函数对象赋值一个全局变量,而且及时清空。至于为什么这么做,我们接下来看。
function defineReactive (obj, key, value){
var dep = new Dep();//这里每一个vm的data属性值声明一个新的订阅者
Object.defineProperty(obj,key,{
get:function(){
console.log(Dep.global);
-----------------------
if(Dep.global){//这里是第一次new对象Watcher的时候,初始化数据的时候,往订阅者对象里面添加对象。第二次后,就不需要再添加了
dep.add(Dep.global);
}
-----------------------
return value;
},
set:function(newValue){
if(newValue === value){
return;
}
value = newValue;
dep.notify();//触发了update()方法
}
})
}

这里有一点需要注意:

在上述圈起来的地方:if(Dep.global)是在第一次new Watcher()的时候,进入update()方法,触发这里的get方法。这里非常的重要的一点!在此时new Watcher()只走到了this.update();方法,此刻没有触发Dep.global = null函数,所以值并没有清空,所以可以进到dep.add(Dep.global);方法里面去。

而第二次后,由于清空了Dep的全局变量,所以不会触发add()方法。

PS:这个思路容易被忽略,由于是参考之前一个博主的代码影响,我自己想了很多方法改变,但是在这种情景下难以实现别的更好的交互方式。

所以我暂时现在只能使用Dep的全局变量的方式,来实现Dep函数与Watcher函数的交互。(如果是ES6的模块化方法会不一样)

而后我会尽量找寻其他更好的方法来实现Dep函数与Watcher函数的交互。

紧接着在text节点和绑定了的input节点(别忘记了这个节点)new Watcher的方法来触发以上的内容:

// 如果节点为input
if(node.nodeType === 1){
...........
----------
new Watcher(vm,node,name) // 别忘记给input添加观察者模式
---------- }
//如果节点类型为text
if(node.nodeType === 3){ if(reg.test(node.nodeValue)){
// console.dir(node);
var name = RegExp.$1;//获取匹配到的字符串
name = name.trim();
// node.nodeValue = vm[name];
-------------------------
new Watcher(vm,node,name);//这里到了一个新的节点,new一个新的观察者
-------------------------
}
}

至此,vue双向绑定已经简单的实现。

3.6 最终效果

在线演示:Codepen实现Vue的demo(有时候要翻墙)点击预览

在线源码参考:demo4

下列是全部的源码,仅供参考。

HTML:

<div id="mvvm">
<input v-model="d" id="test">{{text}}
<div>{{d}}</div>
</div>

JS:

var obj = {};

function nodeContainer(node, vm, flag){
var flag = flag || document.createDocumentFragment(); var child;
while(child = node.firstChild){
compile(child, vm);
flag.appendChild(child);
if(child.firstChild){
nodeContainer(child, vm, flag);
}
}
return flag;
} //编译
function compile(node, vm){
var reg = /\{\{(.*)\}\}/g;
if(node.nodeType === 1){
var attr = node.attributes;
//解析节点的属性
for(var i = 0;i < attr.length; i++){
if(attr[i].nodeName == 'v-model'){ var name = attr[i].nodeValue;
node.addEventListener('input',function(e){
vm[name] = e.target.value;
}); node.value = vm[name];//讲实例中的data数据赋值给节点
node.removeAttribute('v-model');
}
}
}
//如果节点类型为text
if(node.nodeType === 3){ if(reg.test(node.nodeValue)){
// console.dir(node);
var name = RegExp.$1;//获取匹配到的字符串
name = name.trim();
// node.nodeValue = vm[name];
new Watcher(vm,node,name);
}
}
} function defineReactive (obj, key, value){
var dep = new Dep();
Object.defineProperty(obj,key,{
get:function(){
console.log(Dep.global);
if(Dep.global){
dep.add(Dep.global);
}
console.log("get了值"+value);
return value;
},
set:function(newValue){
if(newValue === value){
return;
}
value = newValue;
console.log("set了最新值"+value);
dep.notify();
}
})
} function observe (obj,vm){
Object.keys(obj).forEach(function(key){
defineReactive(vm,key,obj[key]);
})
} function Vue(options){
this.data = options.data;
var data = this.data;
observe(data,this);
var id = options.el;
var dom = nodeContainer(document.getElementById(id),this);
document.getElementById(id).appendChild(dom);
} function Dep(){
this.subs = [];
}
Dep.prototype ={
add:function(sub){
this.subs.push(sub);
},
notify:function(){
this.subs.forEach(function(sub){
console.log(sub);
sub.update();
})
}
} function Watcher(vm,node,name){
Dep.global = this;
this.name = name;
this.node = node;
this.vm = vm;
this.update();
Dep.global = null;
} Watcher.prototype = {
update:function(){
this.get();
switch (this.node.nodeType) {
case 1:
this.node.value = this.value;
break;
case 3:
this.node.nodeValue = this.value;
break;
default: break;
}
},
get:function(){
this.value = this.vm[this.name];
}
} var Demo = new Vue({
el:'mvvm',
data:{
text:'HelloWorld',
d:'123'
}
})

四、回顾

我们再来通过一张图回顾一下整个过程:

从上可以看出,大概的过程是这样的:

  1. 定义Vue对象,声明vue的data里面的属性值,准备初始化触发observe方法。
  2. 在Observe定义过响应式方法Object.defineProperty()的属性,在初始化的时候,通过Watcher对象进行addDep的操作。即每定义一个vue的data的属性值,就添加到一个Watcher对象到订阅者里面去。
  3. 每当形成一个Watcher对象的时候,去定义它的响应式。即Object.defineProperty()定义。这就导致了一个Observe里面的getter&setter方法与订阅者形成一种依赖关系。
  4. 由于依赖关系的存在,每当数据的变化后,会导致setter方法,从而触发notify通知方法,通知订阅者我的数据改变了,你需要更新。
  5. 订阅者会触发内部的update方法,从而改变vm实例的值,以及每个Watcher里面对应node的nodeValue,即视图上面显示的值。
  6. Watcher里面接收到了消息后,会触发改变对应对象里面的node的视图的value值,而改变视图上面的值。
  7. 至此,视图的值改变了。形成了双向绑定MVVM的效果。

五、后记

至此,我们通过解析vue的绑定原理,实现了一个非常简单的Vue。

我们可以再借鉴此思路的情况下,进行我们需要的定制框架的二次开发。如果开发人数尚可的话,可以实现类似微信小程序自己有的一套框架。

我非常重视技术的原理,只有真正掌握技术的原理,才能在原有的技术上更好地去提高和开发。

ps:此文是较早之前写的,不够规范,后面会修改一个ES6的版本。下方是参考链接,灵感来源于其他博主,我进行了修正优化和代码解释。

参考链接:

  1. Vue.js双向绑定的实现原理
  2. Vue 源码解析:深入响应式原理
  3. 深入响应式原理

原文地址(原创博客):http://www.tangyida.top/detail/150

最新文章

  1. ASP.NET MVC Model元数据(一)
  2. 通过html和css做出下拉导航栏的效果
  3. C/C++ makefile自动生成工具(comake2,autotools,linux),希望能为开源做点微薄的贡献!
  4. gene_abundance_estimation
  5. Linux版MonoDevelop无法连接调试器的解决方案(Could not connet to the debugger)
  6. 有关CLR的初学小整理2(可能理解不深刻,望大牛指出)
  7. UVa 437 The Tower of Babylon
  8. C# DateTime 日期加1天 减一天 加一月 减一月 等方法(转)
  9. DP Leetcode - Maximum Product Subarray
  10. maven的介绍
  11. electron+antd详细教程
  12. Jmeter之数据库性能测试
  13. 【SqlServer系列】数据库三大范式
  14. day 49-css补充(终结)[浮动和定位]
  15. IntelJ idea下lombok 不生效的问题(@Builder等注解不生效的问题)解决,lombok Plugin插件安装
  16. Type in Chakra
  17. 从实例角度分析java的public、protected、private和default访问权限
  18. js中准确判断数据类型的方法
  19. CentOS中安装JDK与Intellij idea
  20. Java CodeFormatter

热门文章

  1. 十九、Python之socket编程
  2. django 导出xls文件
  3. django基本过程
  4. CanvasRenderingContext2D.drawImage()无效,not working
  5. 【JZOJ100209】【20190705】狂妄之人
  6. JavaScript插件开发
  7. Spring注解和标签的比较说明
  8. Apache的安装部署 2(加密认证 ,网页重写 ,搭建论坛)
  9. python 之类的继承
  10. kafka(四) 网络通讯