精华内容
下载资源
问答
  • vue mvvm原理

    2020-07-20 11:49:27
    vue mvvm原理: m相当于数据层,vm视图层,v是之间的调用者,主要实现流程如下: 通过js原生的方法Object.defineProperty()来劫持各个属性的setter,getter,在属性对应数据改变时,发布消息给订阅者,然后触发相应...

    vue mvvm原理:

    m相当于数据层,vm视图层,v是之间的调用者,主要实现流程如下:
    通过js原生的方法Object.defineProperty()来劫持各个属性的setter,getter,在属性对应数据改变时,发布消息给订阅者,然后触发相应的监听回调,达到视图更新的目的。
    具体流程内容:
    1:observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter getter。
    2:实现数据劫持后,接下来的任务怎么通知订阅者了,我们需要在监听数据时实现一个消息订阅器,具体的方法是:定义一个数组,用来存放订阅者,数据变动通知(notify)订阅者,再调用订阅者的update方法。与Compile建立连接
    3.Compile 解析指令

    展开全文
  • vue mvvm 原理

    2021-02-19 17:34:33
    2: MVVM: 原理配合数据劫持: objectDefiniproperty: 通过set 方法和get 方法劫持。 当我们数据发生变化的时候。 通知我们的视图进行更新。 实现了那些东西: objectserver() watcher() 方法: React: 框架实现...

    1: M ; Modle: 模型,  view: 视图。  VM:  代表的是,相当于MVC 控制器。 数据的变化影响了视图。 视图的变化影响数据。 

    2: MVVM:  原理配合数据劫持: objectDefiniproperty:   通过set 方法和get 方法劫持。  当我们数据发生变化的时候。 通知我们的视图进行更新。

         实现了那些东西:  objectserver()    watcher() 方法:

    React: 框架实现了典型数据单向流, vue 框架实现了数据双向流。  双向数据绑定: 数据 => 视图。   视图=> 数据  视图就是在vue 中表单控件。   监听改变事件。 on-change: 事件。

      实现数据就绑定框架:  发布者- 订阅者模式  (backbone.js)

      脏值检查: (angular.js)

      vue 框架则采取的是数据劫持 加上backbone.js 发布者订阅者模式:   通过原生js  Objectdefineproperty: 进行数据劫持。 来劫持各个属性getter 和setteer 方法:

      new vue 实例的时候: 就会有提个observer 方法:  监听劫持所有的属性:  就会有一个setter 方法,  getter 方法: setter 设置值,  getter  获取值。

      当外界改变值的时候,  就相当于angular 里边脏值检查了, 在vue 中发布者订阅者模式。

      watcher: 观察者也叫做订阅者。  把所有订阅者放到订阅器中。  订阅器,二个作用: 

      第一:通过我们当前变化属性的观察者,  来更新视图

      第二:  有多少属性就会创建多少个观察者。

    展开全文
  • ./MVVM.js"></script> <script> let vm = new Vue({ el:'#app', data () { return { school:{ name:'gmh', age:21, value:[1,2,3], }, message:'<h1>...
  • vuemvvm原理解析

    2019-10-12 09:01:42
    title: vuemvvm原理解析 mvvm 原理解析 文章目录mvvm 原理解析mvvm 面试论述mvvm的编译过程以及使用分解vue实例实现Complie编译模板vue类-入口文件编译模板节点转文档碎片编译模板数据劫持数据劫持 observer类...

    title: vue之mvvm原理解析

    mvvm 原理解析

    mvvm 面试论述


    MVVM分为Model、View、ViewModel三者

    • Model:代表数据模型,数据和业务逻辑都在Model层中定义;
    • View:代表UI视图,负责数据的展示;
    • ViewModel:负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

    这种模式实现了ModelView的数据自动同步,也就是双向绑定,mvvm双向绑定,采用的是数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的 setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

    大致的过程:

    1. 实现一个指令解析器Compile,对每个元素节点的之类进行解析,根据指令模板替换数据,以及搬到相应的更新函数

    2. 实现一个数据监控器Observer,将所有数据设置成响应式,并进行监听,如有变动可以拿到最新值并通知订阅者

    3. 实现一个订阅者Watcher,作为连接Observer(数据劫持)Compile(模板)的桥梁,在对应模板数据更新处,添加监听数据的订阅者,并将其添加到订阅者容器Dep中,当属性变动时,通过Dep发布通知,执行指令绑定的相应回调函数,从而更新视图

    4. mvvm的入口函数,主要是整合调控以上的,模板编译(compile)数据劫持(Observe)订阅者(Watcher)

    mvvm的编译过程以及使用


    • 编译的流程图

    编译流程图

    • 整体分析

    整体分析

    过程分析
    new MVVM()后的编译主要分为两个部分

    1. 一部分是模板的编译 Compile

      • 编译元素和文本,将插值表达式进行替换
      • 编译模板指令的标签,例如:v-model
    2. 一部分是数据劫持 Observer

      • 将所有的数据响应式处理
      • 给模板的每个编译处设置一个观察者,并将观察者存放在Dep中
      • Watcher如果数据发生改变,在ObjectdefinePropertyset函数中调用Watcher的update方法
      • Dep发布订阅,将所有需要通知变化的data添加到一个数组中

    具体步骤

    1、需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter,这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化

    2、 compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

    3、 Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:

    • 在自身实例化时往属性订阅器(dep)里面添加自己
    • 自身必须有一个 update() 方法
    • 待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。

    4、MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过Observer来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

    分解vue实例


    vue的使用,

    let vm = new Vue({
    		el:"#app",
    		data:{
    			school:{
    				name:"beida",
    				age:100
    			}
    		},
    	})
    

    首先使用new创建一个vue实例,传递一个对象参数,包含eldata

    实现Complie编译模板


    index.html页面的使用

    enter description here

    vue类-入口文件


    enter description here

    在入口之处,先处理了模板的编译(Compile),数据劫持(Observe)在后期进行使用。

    编译模板


    编译模板的主要的入口,分为,将节点转成文档碎片,替换模板中的常量数据

    enter description here

    节点转文档碎片

    enter description here

    将节点转换成文档碎片,然后返回,

    编译模板

    //编译模板
    class Compiler{
    
    
        //判断一个节点是否是元素节点
        isElementNode(node){
            return node.nodeType ```= 1;
        }
    
        //判断一个属性是否是一个指令
        isDirective(attrName){
            return attrName.startsWith("v-");
        }
    
        //编译模板
        compile(node){
            //node.childNodes 包含了元素节点与文本节点
            //得到的是一个伪数组
            let childNodes = node.childNodes;
            [...childNodes].forEach(child=>{
                if(this.isElementNode(child)){  //元素节点
                    //编译元素节点
                    this.compileElementNode(child)
                    //递归编译所有的节点
                    this.compile(child)
    
                }else{  //文本节点
                    //编译文本节点
                    this.compileText(child)
                }
            })
        }
    
        //编译元素节点
        compileElementNode(node){
            //获取元素的属性节点(伪数组)
            let attributes = node.attributes;
            [...attributes].forEach(attr=>{
                let {name,value: expr} = attr;
    
                //判断是否是一个指令
                if(this.isDirective(name)){
                    let [,directive] = name.split("-")
                    
                    //根据指令,调用对呀的指令方法
                    CompilerUtil[directive](node,expr,this.vm);
                }
            })
            
        }
    
        //编译文本节点
        compileText(node){
            //得到所有的文本节点
            let content = node.textContent;
            //使用正则得到所有文本里面的内容
            let reg = /\{\{(.+?)\}\}/;
            if(reg.test(content)){
    
                //{{}} 是v-text的语法糖,所有调用text指令
                CompilerUtil['text'](node,content,this.vm)
            }
            
        }
    
    
        //将节点转成文档碎片
        node2fragme(node){
            //创建键一个文档碎片
            let fragment = document.createDocumentFragment();
            let firstChild ;
            while(firstChild = node.firstChild){
                fragment.appendChild(firstChild)
            }
            return fragment;
        }
    
        
    }
    
    //编译指令处理对象--处理不同的指令
    CompilerUtil = {
    
        //获取到data中对应的数据 
        getVal(vm,expr){
            return  expr.split(".").reduce((data,current)=>{
                  return data[current]
              },vm.$data)
        },
    
        //设置$data中的数据
        setVal(vm,expr,value){
            expr.split(".").reduce((data,current,index,arr)=>{
                if(index ```arr.length -1){
                    return data[current] = value;
                }
                return data[current]
            },vm.$data)
        },
    
        //处理 v-model 指令的数据
        model(node,expr,vm){
            
            
            //更新模板中在data中对应的数据
            let fn = this.updater['modelUpdater']
    
            //当input 框的数据相互绑定
            node.addEventListener("input",(e)=>{
                let value = e.target.value;
                //当输入框数据改变时,同步更改$data中的数据
                this.setVal(vm,expr,value);
            })
    
            let value = this.getVal(vm,expr)
            //替换模板中的数据
            fn(node,value)
        },
    
        // 处理v-text指令的数据
        text(node,expr,vm){
    
            let fn = this.updater['textUpdater'];
            //获取到要替换的内容
            let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
                return this.getVal(vm,args[1])
            })
            fn(node,content)
        },
    
        //更新模板中的数据
        updater:{
            //将v-model绑定的数据进行替换
            modelUpdater(node,value){
                node.value = value;
            },
    
            //将v-text绑定的数据进行替换
            textUpdater(node,content){
                node.textContent = content;
            }
        }
    }
    
    

    编译模板,主要是对模板中的一些数据常量进行替换,对于一些指令进行相关的处理,特别是指令v-model的数据的绑定。

    数据劫持


    数据劫持,实在模板进行编译之前进行,将data中的所有的数据都变成响应式数据,

    Observer的调用 vue类
    enter description here

    数据劫持 observer类

    //数据劫持,将数据变成响应式数据
    class Observer{
        constructor(data){
            //将数据变成响应式数据
            this.observer(data)
        }
    
        //将数据变成响应式数据
        observer(data){
            //判断数据是否是一个对象
            if(data && typeof data ```'object'){
                for(let key in data){
                    //设置响应式
                    this.defindReactive(data,key,data[key])
                }
            }
        }
    
        //设置响应式
        defindReactive(obj,key,value){
            //如果数据是一个对象,继续递归设置
            this.observer(value);
            let dep = new Dep();    //不用的watcher存放到不同的dep中
            Object.defineProperty(obj,key,{
    
                //当获取数据时会调用get
                get(){
                    return value;
                },
    
                //当设置数据时会调用set
                set: (newValue)=>{
                    if(newValue != value){
                        //将新数据设置成响应式
                        this.observer(newValue);
                        value = newValue;
                    }
                }
            })
            
        }
    }
    

    订阅者的Watcher的实现


    订阅者watcher

    //观察者
    class Watcher{
        constructor(vm,expr,cb){
            this.vm = vm;
            this.expr = expr;
            this.cb = cb;   //状态改变后要进行的操作
            //获取老数据--保存一个老状态
            this.oldValue = this.get();
        }
    
        //获取状态的方法
        get(){
            Dep.target = this;
            //当获取旧的值得时候便已经触发响应式数据
            let value = CompilerUtil.getVal(this.vm,this.expr)
            Dep.target = null;
            return value;
        }
    
        //当状态发生改变的时候,观察者更新当前的状态
        update(){
            let newVal = CompilerUtil.getVal(this.vm,this.expr);
            if(this.oldValue !```newVal){
                this.cb(newVal)
            }
        }
    }
    

    存放订阅者Dep

    //存储观察者的类
    class Dep {
        constructor(){
            this.subs = []; //存放所有的watcher
        }
        //添加watcher 订阅
        addSub(watcher){
            this.subs.push(watcher)
        }
    
        //通知发布
        notify(){
            this.subs.forEach(watcher=>watcher.update())
        }
    }
    

    订阅者,连接编译模板与数据劫持

    编译模板处

    //编译指令处理对象--处理不同的指令
    CompilerUtil = {
    
         // 此处省略若该代码 ......
    
        //处理 v-model 指令的数据
        model(node,expr,vm){
            
            //更新模板中在data中对应的数据
            let fn = this.updater['modelUpdater']
    
            
            //给输入框添加一个观察者,如果数据改变,通知data数据改变
            new Watcher(vm,expr,(newValue) =>{
                fn(node,newValue)
            })
    
            // 此处省略若该代码 ......
        },
    
        // 处理v-text指令的数据
        text(node,expr,vm){
    
            let fn = this.updater['textUpdater'];
            let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
    
                //添加一个订阅者
                new Watcher(vm,args[1],()=>{
                    fn(node,this.getVal(vm,args[1]))
                })
                return this.getVal(vm,args[1])
            })
            fn(node,content)
        },
    
         // 此处省略若该代码 ......
    }
    

    数据劫持处

    //数据劫持,将数据变成响应式数据
    class Observer{
    
        // 此处省略若该代码 ...
    
        //设置响应式
        defindReactive(obj,key,value){
            // 此处省略若该代码 ...
    
            Object.defineProperty(obj,key,{
    
                //当获取数据时会调用get
                get(){
                    Dep.target && dep.subs.push(Dep.target)
                    return value;
                },
    
                //当设置数据时会调用set
                set: (newValue)=>{
                    if(newValue != value){
                        //将新数据设置成响应式
                        this.observer(newValue);
                        value = newValue;
                        //当数据发生改变时,通知观察者
                        dep.notify();
                    }
                }
            }) 
        }
    }
    

    总述:订阅者是,编译模板与数据劫持之间的桥梁,模板编译之处添加订阅者,并将订阅者存储在Dep中,在数据劫持处添加发布者,当数据发生改变的时候,通知订阅者。

    参考文档

    展开全文
  • 手写实现vuemvvm原理

    2019-08-14 07:11:23
    手写实现vuemvvm原理
  • Vue MVVM框架实现原理

    2020-02-23 13:20:37
    MVVM的框架原理 数据劫持 发布订阅模式 实现原理的过程 遍历data选项中的属性,添加数据的观测,执行observe的方法,使用Object.defineProperty方法转换为getter和setter方法,实现数据的劫持,并且添加一个...

    MVVM的框架原理

    1. 数据劫持

    2. 发布订阅模式

    实现原理的过程

    1. 遍历data选项中的属性,添加数据的观测,执行observe的方法,使用Object.defineProperty方法转换为get和set方法,实现数据的劫持,并且添加一个compiler方法,对每个元素节点进行判断,如果是文本节点,根据指令模板去替换数据
    2. 当数据发生变化时,observe中的set方法被触发,会立即调用Dep.notify()方法,开始遍历所有的订阅者,调用执行者的Update方法,订阅者收到通知之后对视图进行更新
    3. v-model实现数据的双向绑定
      在complier方法中对每个元素节点类型进行判断时,如果是标签节点,判断是否存在v-model指令,存在即创建一个订阅者,并且设置该标签节点的初始值,添加input事件监听,在输入框输入的值改变时,执行set,get方法,对视图进行更新

    实现原理的代码

    index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        {{ message }}
        <div>{{message}}</div>
        <p>{{count}}</p>
        <input type="text" v-model="message"/>
        </div>
      </div>
      	<!-- Vue CDN -->
    	<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    	<!-- MvvM js -->
    	<script src="./mvvm.js"></script>
    	<script>
    	    var app = new MVVM({
    	      el: '#app',
    	      data: {
    	        message: 'hello world',
    	        count: 1
    	      }
    	    });
    	 </script>
     </body>
    

    mvvm.js

    
    // 发布者  publish
    class Dep{
      constructor(){
        this.subs = [];
      }
      // 订阅方法
      addSub(watcher){
        this.subs.push(watcher);
      }
      // 遍历所有的订阅者,发布更新方法 
      notify(newVal){
        this.subs.forEach(sub=>{
          sub.update(newVal);
        })
      }
    }
    
    // 订阅者   subscribe 更新dom方法
    class Watcher{
      constructor(cb){
        this.cb = cb;
      }
      update(newVal){
        console.log('更新了....');
        this.cb(newVal);
      }
    }
    
    // 在数据劫持的时候,一个key劫持执行前,选new一个Dep
    // new Dep()
    // 第一次渲染前,先new一个watcher
    // new Watcher()
    // 第一次渲染的时候,添加订阅者
    // addSub()
    // 更新数据,执行发布
    // notify()
    
    let watch = null;
    
    class MVVM{
      constructor(options){
        // 配置实例上的基础属性
        this.$options = options;
        this.$data = this._data = options.data;
        // 添加数据观测
        this.observer(this.$data);
        // 编译模版
        this.compiler(options.el);
      }
      // 数据观测
      observer(data){
        // 获得data的所有key和value
        Object.entries(data).forEach(([key, value])=>{
          // 创建发布者
          console.log('发布者:', key);
          const dep = new Dep();
          // 对key进行数据观测 数据观测
          Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            // 设置数据
            set(newVal){
              // console.log('set run......');
              if(newVal !== value){
                // 设置新数据
                value = newVal;
                // 重新渲染dom
                console.log('执行发布.....');
                dep.notify(newVal);
              }
            },
            // 读取数据
            get(){
              // console.log('get run......');
              console.log('执行订阅.....');
              if(watcher){
                dep.addSub(watcher);
                watcher = null;
              }
              return value;
            }
          });
        })
      }
    
      // 编译模版
      compiler(el){
        // 获得实例作用的dom
        const element = document.querySelector(el);
        // 遍历实例作用的dom
        this.compilerNode(element);
      }
    
      compilerNode(element){
        // 获得需要编译的dom的每一个子节点
        const childNodes = element.childNodes;
        // 转为可遍历的对象,进行遍历
        Array.from(childNodes).forEach(node=>{
          const {nodeType, textContent} = node;
          //判断是文本节点
          if(nodeType === 3){
            // 判断是否有插值表达式在文本节点中
            let reg = /\{\{\s*(\S*)\s*\}\}/;
            //有
            if(reg.test(textContent)){
              //数据变,dom需要更新
              // 创建订阅者
              console.log('订阅者:', RegExp.$1);
              watcher = new Watcher((newVal)=>{
                //更新数据的渲染
                node.textContent = newVal;
              });
              // 第一次渲染dom
              node.textContent = this.$data[RegExp.$1];
            }
          }
          
          // 判断是标签
          else if(nodeType === 1){
            // 拿到标签的所有属性
            let attrs = Array.from(node.attributes);
            // 遍历每一属性
            attrs.forEach(attr=>{
              //判断属性是否是指令
              if(attr.name.startsWith('v-')){
                //是指令,取指令名字
                let dirName = attr.name.substr(2);
                if(dirName === 'model'){  //v-model="message"
                  let key = attr.value;
                  // 创建订阅者
                  watcher = new Watcher((newVal)=>{
                    node.value = newVal;
                  });
                  // 设置初始值
                  node.value = this.$data[key];
                  // 添加输入事件监听
                  node.addEventListener('input', (ev)=>{
                    this.$data[key] = ev.target.value;
                  });
                }
                else if(dirName === 'bind'){
    
                }
              }
            })
          }
    
          // 有子节点,需要编译子节点
          if(node.childNodes.length > 0){
            // 遍历子节点
            this.compilerNode(node);
          }
        })
      }
    }
    	
    
    展开全文
  • VUE MVVM实现

    2020-03-31 21:32:34
    VUE MVVM实现 详细代码请参考: https://github.com/osorso/VUE_MVVM 理解MVVM mvvm - Model View ViewModel 数据 视图 视图模型 其中Model —> data, View —> template, ViewModel —> new Vue({…}) view...
  • vueMVVM 原理

    2021-05-10 09:02:09
    文章目录 一.... vue 的特点是不能增加不存在的属性,因为不存在的属性,就没有 get 和 set 深度响应,因为每赋予一个新对象,就会对这个新对象增加数据劫持。 把上一步get 到的属性,编译就行了。
  • MVVM原理 es5 Object.defineProperty(obj,‘属性’,{ value:1, set:function(){}, get:function(){} }) 注意:Object.defineProperty不兼容IE8及以下浏览器 所有vue不兼容IE8及以下浏览器 当我们创建一个实例(new ...
  • Vue MVVM 模式 解析

    2020-04-23 19:37:08
    Vue MVVM模式 解析前言...Vue成为如今最火的框架,其MVVM模式也是让大家十分喜爱,本文仅解析其原理,并不是Vue的基础使用教学。 本文源码下载:https://github.com/li-car-fei/Vue-MVVM-Model 结构 先给出代码的...
  • 主要介绍了Vue中的MVVM原理和实现方法,文中讲解非常细致,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
  • Vue MVVM模型

    2021-03-21 12:23:20
      Vue.js 是一个提供了 MVVM 风格的双向数据绑定的 Javascript 库,专注于View 层。它的核心是 MVVM 中的 VM,也就是 ViewModel。 ViewModel负责连接 View 和 Model,保证视图和数据的一致性,这种轻量级的
  • VueMVVM原理

    千次阅读 2017-07-04 15:55:44
  • vue MVVM

    2019-10-10 23:25:53
    2.观察者-订阅者(数据劫持):vueObserver 数据监听器,把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用Object.defineProperty()方法把这些属性全部转成setter、...
  • 相信只要你去面试vue,都会被问到vue的双向数据绑定,你要是就说个mvvm就是视图模型模型视图,只要数据改变视图也会同时更新!那你离被pass就不远了!那么读完本文后,相信你就可以说出很多东西了! vuemvvm由两...
  • vueMVVM原理及其实现

    2020-05-23 18:51:05
    要实现一个mvvm的库,我们首先要理解清楚其实现的整体思路。先看看下图的流程: 1.实现compile,进行模板的编译,包括编译元素(指令)、编译文本等,达到初始化视图的目的,并且还需要绑定好更新函数; 2.实现...
  • VueMVVM原理

    2019-03-11 17:16:25
    我对MVVM的理解 Module View ViewModule Module是业务逻辑层和数据层 View是页面视图层 ViewModule用来监听Module层数据发生改变后控制View层更新,处理Module和View的交互 Module和View没有直接联系,是通过...
  • 接下来,我将从零实现一套完整的基于VueMVVM,提供给来年“金三银四”跳槽高峰期的小伙伴们阅读也详细梳理一下自己对MVVM的理解。 MVVM是什么 在了解MVVM之前,我们来对MVC说明一下。MVC架构起初以及现在一直存在...
  • vue相信大家都很熟悉,如何使用,本人就不在这里演示了,相信这个大家都比我熟,今天我们来说说vuemvvm原理,比较代码简单实现。下面简单了解一下 Vue.js 关于双向绑定的一些实现细节: Vue.js 是采用 Object....
  • VueMVVM原理及其实现

    2020-01-03 15:47:48
    什么是mvvm MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。 MVVM分为三个部分:分别是M(Model,模型层 ),V(View,视图层),VM(ViewModel,V与M连接的桥梁,也可以看作为控制器) 1、 M:模型...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,360
精华内容 4,144
关键字:

vue的mvvm原理

vue 订阅