Vue原理解析——自己写个Vue

2018-09-18 admin

Vue由于其高效的性能和灵活入门简单、轻量的特点下变得火热。在当今前端越来越普遍的使用,今天来剖析一下Vue的深入响应式原理。


tips:转自我的博客唐益达博客,此为原创。转载请注明出处,原文链接


一、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方法被调用,修改了新值

输出结果如下:

clipboard.png

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

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

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初始化已经完成。

clipboard.png

在线演示: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

实现效果:

clipboard.png

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();//这里的结果是跟上面一样的

实现效果:

clipboard.png

到这,我们已经实现了:

  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();
    this.node.nodeValue = this.value;//这里去改变视图的值
}
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节点new Watcher的方法来触发以上的内容:

//如果节点类型为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.4 最终效果

在线演示: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();
    this.node.nodeValue = this.value;
  },
  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。

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

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

参考链接:

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

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

原文链接:https://segmentfault.com/a/1190000016434836

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-40548.html

文章标题:Vue原理解析——自己写个Vue

相关文章
JavaScript短路原理精简代码
js中||和&amp;&amp;的特性帮我们精简了代码的同时,也带来了代码可读性的降低,虽然高效,但请灵活使用。 在js逻辑运算中,0、&quot;&quot;、null、false、undefined、NaN都会判为false,其他都为t...
2015-11-12
7个让JavaScript变得更好的注意事项
随着浏览器性能提高,伴随着新的HTML5的编程接口的稳步采用,网页上的JavaScript的音量在逐渐增加。然而,一个写得不好的程序编码却拥有着打破整个网站,让用户为之沮丧和驱赶潜在客户的潜力。 开发人员必须使用所有供他们任意使用的工具和技...
2015-11-12
React Native 用JavaScript编写原生ios应用
ReactNative 可以基于目前大热的开源JavaScript库React.js来开发iOS和Android原生App。而且React Native已经用于生产环境——Facebook Groups iOS 应用就是基于它开发的。 Re...
2015-11-12
如何编写干净高效的CSS代码
其实CSS的学习并不困难,但在一些较为大型的项目中就显得杂乱无章,变得很难管理,尤其是不同的人编写CSS的风格总会略有不同,从团队合作的层面上来说,就更加难以沟通,所以,我们为此总结了一些如何实现高效整洁的CSS代码原则: 使用Reset但...
2015-11-12
vue.js实现请求数据的方法示例
vue2.0示例代码如下: var vm = new Vue({ el:&quot;#list&quot;, data:{ gridData: &quot;&quot;, }, ...
2017-03-20
HTML5会是下一个风口吗?
2014年10月底, W3C(万维网联盟)正式宣布HTML5正式定稿,科技圈就像发现了可以打破谷歌、苹果所统领的原生APP世界的方法,发表了很多宣讲HTML5将真正开始颠覆原生(Native)App的文章,也开始着力发展HTML5,开始抢占...
2015-11-12
最细致的vue.js基础语法 值得收藏!
介绍 前段时间接触到一个库叫做Vue.js, 个人感觉很棒,所以整理了一篇博文做个介绍。 Vue读音/vju:/,和view类似。是一个数据驱动的web界面库。Vue.js只聚焦于视图层,可以很容易的和其他库整合。代码压缩后只有24kb。 ...
2017-03-21
vuejs通过filterBy、orderBy实现搜索筛选、降序排序数据
直接贴代码了: 先上输入前的样子: &lt;style&gt; #example{margin:100px auto;width:600px;} .show{margin:10px;} #searchText{display: block...
2017-03-17
搜狐发力html5让用户更爽,自媒体人更嗨
曾经有资深互联网分析师说过”2015年,新闻综合类 APP 如果还没有大量的个性化阅读功能,将被淘汰出局。”枣哥非常同意此观点,移动互联网时代已经席卷全球,移动互联网代表的就是个性张扬的时代,在互联网圈说道个性张扬首先想到的是搜狐老板张朝阳...
2015-11-12
javascript教程:关于if简写语句优化的方法
UglifyJS是一个对javascript进行压缩和美化的工具,在它的文档说明中,我看到了几种关于if语句优化的方法。尽管我还没使用它去做一些尝试性的测试,但从这里可以看到它的确对js作了美化的工作。也许有人认为if语句就那么简单,能优化...
2015-11-12
回到顶部