精华内容
下载资源
问答
  • 在当今前端越来越普遍的使用,今天来剖析一下Vue的深入响应式原理。 tips:转自我的博客唐益达博客,此为原创。转载请注明出处,原文链接 一、Vue对比其他框架原理 Vue相对于React,Angular更加综合一点。...
        
    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();
        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'
      }
    })

    四、回顾

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

    clipboard.png

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

    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

    展开全文
  • vue原理之手写vue-router

    2021-01-14 14:42:45
    自己手动创作vue-router插件时,我先做了一些准备工作,便于深入的理解。 window.location对象详解 { ancestorOrigins: DOMStringList,//决定是否当前origin页面是否可以与其关联的父origin页面直接通过js来...

    在自己手动创作vue-router插件时,我先做了一些准备工作,便于深入的理解。

    window.location对象详解

    {
        ancestorOrigins: DOMStringList,//决定是否当前origin页面是否可以与其关联的父origin页面直接通过js来通讯(calls)
        assign:f,//加载新的文档 window.location.assign(“https://www.baidu.com”)
        hash:"#/test01",//从井号 (#) 开始的 URL(锚)
        host:"www.test.com:8080",//主机=域名+端口号
        hostname:"www.test.com",//域名
        href:"http://www.test.com:8080/test/#/test01",//完整的 URL
        origin:"http://www.test.com:8080",//源(标识最初的请求是从哪里发的)
        pathname:"/test",//路径部分
        port:"8080",//端口
        protocol:"http:",//协议
        reload:f,//重新加载当前文档 window.location.reload()
        replace:f,//用一个新文档取代当前文window.location.replace("https://www.baidu.com")
        search:"?id=123&username=xxx",//从问号 (?) 开始的 URL(查询部分)
        toString:f//把url转换成字符串
    }

    虚拟dom浅析

    虚拟dom简单来说就是一个普通的JavaScript对象,包含tag,props,children三个属性。

    <div id="app">
      <p className="text">lxc</p>
    </div>

    上边的HTML代码转为虚拟DOM如下:  

    {
        tag:"div",
        props:{
            id:"app"
        },
        children:[
            {
                tag:"p",
                props:{
                    className:"text"
                },
                children:[
                    "lxc"
                ]
            }
        ]
    }

    在vue脚手架中,我们经常会看到这样一段代码:

      const app = new Vue({
        ··· ···
        render: h => h(App)
      })

    这个render方法也可以写成这样:

      const app = new Vue({
        ··· ···
        render:function(createElement){
            return createElment(App)
        }
      })

    h函数就是vue中的createElement方法,这个函数作用就是创建虚拟dom,追踪dom变化的

    function h(tag,props,...children){//h函数,返回一个虚拟dom对象
        return {
            tag,
            props:props || {},
            children:children.flat()//扁平化数组,降至一维数组
        }
    }

    所谓的虚拟dom,因为dom对象是属性结构,所以使用JavaScript对象就可以简单表示。而原生dom有许多属性、事件,即使创建一个空div也要付出昂贵的代价。而虚拟dom提升性能的点在于DOM发生变化的时候,通过diff算法对比,计算出需要更改的DOM,只对变化的DOM进行操作,而不是更新整个视图。

    手写vue-router插件

    实现install方法即可

    1、首先创建一个名为kvue-router.js的文件并在main.js中引入

    2、构建大致轮廓

    // VueRouter类: new VueRouter({routes: [...]})
    class VueRouter {
        constructor(options){
    
        }
    }
    
    / 实现静态install方法即可
    // 参数1:_Vue构造函数
    // 参数2: options
    VueRouter.install = function (_Vue, _options) {
        
    }

    3、正式编写

    // 引用传入Vue构造函数
    let Vue
    
    // VueRouter类: new VueRouter({routes: [...]})
    class VueRouter {
      constructor(options) {
        // 保存选项备用
        this.$options = options
    
        // 处理routes
        this.routeMap = {}
        this.$options.routes.forEach(route => {
          this.routeMap[route.path] = route
        })
    
        // 创建current保存当前url
        // 为了让使用current的组件重新渲染
        // 他应该是响应式的
        Vue.util.defineReactive(this, 'current', '/')
    
        // 监听hashchange时间
        window.addEventListener('hashchange', this.onHashChange.bind(this))
      }
    
      onHashChange() {
        // 修改当前url, hash的格式#/xxx
        this.current = window.location.hash.slice(1)
        console.log(this.current);
    
      }
    }
    
    // 实现install方法
    // 实现静态install方法即可
    // 参数1:Vue构造函数,Vue.use(VueRouter)
    VueRouter.install = function (_Vue) {
      Vue = _Vue
    
      // 1.挂载VueRouter实例
      // 为了能够拿到Vue根实例中的router实例
      // 可以利用全局混入
      Vue.mixin({
        beforeCreate() {
          // 上下文已经是组件实例了
          if (this.$options.router) {
            Vue.prototype.$router = this.$options.router
          }
        }
      })
    
    
      // 2.注册两个组件router-view,router-link
      Vue.component('router-view', {
        render(h) {
          // console.log('router-view render', this.$router.current);
          const {routeMap, current} = this.$router
          const component = routeMap[current] ? routeMap[current].component : null
          return h(component)
        }
      })
    
      // <router-link to="/">xxx</router-link>
      Vue.component('router-link', {
        props: {
          to: {
            type: String,
            default: ''
          },
        },
        render(h) {
          // 参数1tag类型
          // 参数2传入各种属性和事件
          return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
          // 也可以使用jsx
          // return <a href={'#' + this.to}>{this.$slots.default}</a>
        }
      })
    }
    
    export default VueRouter

    学过来,倒回去,发现js基础真的很重要,自己落下的坑,之后总是要填的,走的越远越发现甚是如此!

    展开全文
  • 一、Vue对比其他框架原理 Vue相对于React,Angular更加综合一点。AngularJS则使用了“脏值检测”。 React则采用避免直接操作DOM的虚拟dom树。而Vue则采用的是Object.defineProperty特性(这在ES5中是无法slim的,...

    一、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();
        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'
      }
    })

    四、回顾

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

    clipboard.png

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

    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

    转载于:https://www.cnblogs.com/chris-oil/p/11268659.html

    展开全文
  • 前置知识 前端路由的两种模式 Hash模式 ...新建一个没有vue-router的vue项目 在项目中新建router相关文件 新建一个route文件夹,文件夹下新建一个index.js,用于存放路由规则 import Vue from '

    前置知识

    前端路由的两种模式

    1. Hash模式
      URL中#后面的内容作为路径地址
      监听HashChange事件
      根据当前路由地址找到对应的组件重新渲染
    2. History模式
      通过history.pushState()方法改变地址栏
      监听popstate 事件
      根据当前路由地址找到对应组件重新渲染

    手写路由的准备工作

    新建一个没有vue-router的vue项目

    在项目中新建router相关文件

    1. 新建一个route文件夹,文件夹下新建一个index.js,用于存放路由规则
    import Vue from 'vue'
    import VueRouter from '../vuerouter'
    //从我们自己的router中导入VueRouter
    import Index from '../components/Index.vue'
    Vue.use(VueRouter)
    
    const routes = [
        {
            path:'/',
            name:'Index',
            component : Index
        },
        {
            path:'/about',
            name:'about',
            component: ()=> import('@/views/about.vue')
        },
        {
            path:'/home',
            name:'home',
            component: ()=> import('@/views/home.vue')
        },
    ]
    const router = new VueRouter({
        mode:'history',
        base: process.env.BASE_URL,
        routes
    })
    
    export default router
    
    1. 新建一个vuerouter文件夹,文件夹下新建一个index.js 用于存放手写的router代码
      在这里插入图片描述

    2. 别忘了在main.js中引用我们写到的router

    import Vue from 'vue'
    import App from './App.vue'
    import router from './route'
    
    
    Vue.config.productionTip = false
    
    new Vue({
      router,
      render: h => h(App),
    }).$mount('#app')
    

    开始手写Vue-Router

    首先要明确手写Vue-router要做什么,下图是Vue-router的一个类图,详细的记录了VueRouter需要什么属性与方法
    在这里插入图片描述

    options:记录构造函数中传入的对象(这个对象里面有路由规则)
    routeMap:是一个对象,用来记录路由地址和组件的对应关系,将来会把路由规则解析到routeMap里
    data:是一个对象,里面有一个属性current,用来记录当前路由地址,这里使用data 是为了实现一个响应式的对象。

    install方法实现

    通过类图中的_可以发现install是一个静态的方法

    let _Vue = null
    
    export default class VueRouter {
    
        static install (Vue) {
            //判断当前插件是否被安装
            if(VueRouter.install.installed){
                return;
            }
            VueRouter.install.installed = true
    
            // 把vue构造函数记录到全局变量
            _Vue = Vue
            // 把创建vue实例的时候传入的router对象注入到Vue实例上
            //混入 mixed
            _Vue.mixin({
                beforeCreate() {
                if(this.$options.router)
                    _Vue.prototype.$router = this.$options.router
                },
            })
        }
    
    }
    

    构造函数的实现

        constructor(options){
            this.options = options
    
            this.routerMap = {} // 用于存储解析的router对象
            this.data = _Vue.observable({
                current:'/'
            })
            // this.init()
        }
    

    这样就实现了类图中的相关属性

    createRouterMap方法实现

    这个方法的作用是把构造函数中传入的路由规则转变为键值对的形式,存储到上文提到的routeMap中

    //遍历所有的路由规则,解析路由规则变为键值对的形式,存储到routermap里
        createRouterMap(){
            this.options.routes.forEach(route =>{
                this.routerMap[route.path] = route.component
            })
        }
    

    initComponents方法实现

        initComponents (Vue) {
            Vue.component('router-link',{
                props:{
                    to:String
                    //接收外部传入的to
                },
                // template:'<a :href="to"><slot></slot></a>'
                render (h) {
                    return h('a',{
                        attrs:{
                            href:this.to
                        },
                        on:{
                            click:this.clickHandler
                        }
                    },[this.$slots.default])
                },
                methods: {
                    clickHandler(e){
                        history.pushState({},'',this.to)
                        this.$router.data.current = this.to
                        e.preventDefault();
                    }
                },
            })
            const  _this = this
            Vue.component('router-view', {
                render(h) {
                    const component = _this.routerMap[_this.data.current]
                    return h(component)
                },
            })
        }
    

    initEvent 方法实现

    initEvent(){
            window.addEventListener('popstate',()=>{
                this.data.current = window.location.pathname
            })
        }
    

    这个方法是为了在浏览器中后退和前进的时候来加载并且渲染组件

    完整手写路由代码

    let _Vue = null
    
    export default class VueRouter {
    
        static install (Vue) {
            //判断当前插件是否被安装
            if(VueRouter.install.installed){
                return;
            }
            VueRouter.install.installed = true
    
            // 把vue构造函数记录到全局变量
            _Vue = Vue
            // 把创建vue实例的时候传入的router对象注入到Vue实例上
            //混入 mixed
            _Vue.mixin({
                beforeCreate() {
                if(this.$options.router)
                    _Vue.prototype.$router = this.$options.router
                    // this.$options.router.init()
                },
            })
        }
    
        constructor(options){
            this.options = options
    
            this.routerMap = {} // 用于存储解析的router对象
            this.data = _Vue.observable({
                current:'/'
            })
             this.init()
        }
        init(){
            this.createRouterMap()
            this.initComponents(_Vue)
            this.initEvent()
        }
        //遍历所有的路由规则,解析路由规则变为键值对的形式,存储到routermap里
        createRouterMap(){
            this.options.routes.forEach(route =>{
                this.routerMap[route.path] = route.component
            })
        }
        initComponents (Vue) {
            Vue.component('router-link',{
                props:{
                    to:String
                },
                // template:'<a :href="to"><slot></slot></a>'
                render (h) {
                    return h('a',{
                        attrs:{
                            href:this.to
                        },
                        on:{
                            click:this.clickHandler
                        }
                    },[this.$slots.default])
                },
                methods: {
                    clickHandler(e){
                        history.pushState({},'',this.to)
                        this.$router.data.current = this.to
                        e.preventDefault();
                    }
                },
            })
            const  _this = this
            Vue.component('router-view', {
                render(h) {
                    const component = _this.routerMap[_this.data.current]
                    return h(component)
                },
            })
        }
        initEvent(){
            window.addEventListener('popstate',()=>{
                this.data.current = window.location.pathname
            })
        }
    }
    

    参考资料:
    拉勾教育

    展开全文
  • 首先index.html rem js动态利用屏幕宽度修改html的font-size 2.xxx.vue 推荐 新闻 娱乐 体育 图片 财经 2017网易时尚跨界胜典 天津市人民政府副秘书长,办公厅党组...
  • 文章目录1.简述功能2.创建Vue3.Observer 实现响应式数据 1.简述功能 我们实现的一个带有响应式功能的...前三个是Vue的属性,最后一个是Vue的方法,用于将data设置为setter/getter,并纳入Vue实例中。 项目初始结构: 2
  • Vue 1.0 源码原理 直接上代码 ~~本人自己练习 大神勿喷 ~~ // xxVue.js // 属性响应 function define (obj, key, val) { observable(val) const dep = new Dep() Object.defineProperty(obj, key, { get()...
  • 我们学习知识并不只是为了应付面试那种程度,对于面试应该作为我们的最低要求。这里就体现了数据安全思想,前端程序员或许考虑的没有...由于工作的需要,我们也要努力提升自己的代码质量!让自己的职业生涯更加辉煌!

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 402
精华内容 160
关键字:

vue原理自己写

vue 订阅