精华内容
下载资源
问答
  • 主要介绍了浅析vue中的MVVM实现原理,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • vue中MVVM实现原理

    2021-09-15 11:13:18
    vue中MVVM实现原理 - 知乎

    往页面中添加数据,从传统的dom操作,改变成数据层操作。不需要关注dom层操作,专注于操作数据,数据是什么,页面就显示什么。

    最常用的是用vue-cli脚手架的方式来创建一个项目。它是以一种单文件组件的方式,即为.vue文件,它里面包含了模板、业务逻辑和样式。


     

    1. 响应式数据绑定,当数据(model层)发生改变,它会自动更新视图(view),内部实现原理是利用Es5中的Object.definedProperty中的setter/getter代理数据,监控对数据的操作

    2. 视图组件,UI界面对应的每个功能模块,可视为组件,划分组件是为了更好管理,维护,实现代码的复用,减少代码之间的依赖,也就是逼格高一词,高内聚,低耦合

    3. 虚拟DOM:运行js的速度是很快的,大量的操作DOM就会很慢,时常在更新数据后会重新渲染页面,这样造成在没有改变数据的地方也重新渲染了DOM节点,这样就造成了很大程度上的资源浪费,用内存中生成与真实DOM与之对应的数据结构,这个在内存中生成的结构称为虚拟DOM,当model中数据发生变化时,Vue会将模板编译成虚拟 DOM 渲染函数,并结合响应系统,在应用状态改变时,vuejs能够智能地计算出重新渲染组件,并以最小代价并应用到DOM操作上

    4. MVVM模式:其中M:model数据模型,V:view视图模板,而VM(观察者,vue帮我们实现了的):view model负责业务处理,对数据进行加工,处理,之后交给视图,它是通过模板中指令与数据进行关联控制的,使用mvvm模式,编码的重点是在于view层和model层,以前是面对DOM开发(MVP),现在更多的是面向数据编程

    展开全文
  • Vue的MVVM实现原理

    2020-11-11 19:54:33
    MVVM原理实现mvvm的双向绑定,就必须要实现以下几点: 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者 实现一个指令解析器Compile,对每个元素节点的指令...

    Vue源码解析
    文章链接

    vue 实现数据绑定

    数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

    MVVM原理

    要实现mvvm的双向绑定,就必须要实现以下几点:

    1. 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者

    2. 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数

    3. 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

    4. mvvm入口函数,整合以上三者

    优化编译使用文档碎片

    每次找到一个数据替换,都要重新渲染一遍,可能会造成页面的回流和重绘,那么我们最好的办法就是把以上的元素放在内存中,在内存中操作完成之后,再替换掉.

    阐述MVVM原理

    数据劫持+发布者、订阅者模式

    MVVM 作为数据的入口

    Observer 监听model数据变化

    Compile 解析编译模板指令

    Watcher 通信桥梁 达到双向绑定: 数据变化——>视图更新 视图交互——>数据变更

    Compile

    <!DOCTYPE html>
    <html lang="zh">
    <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></title>
    </head>
    <script src="vue.js" type="text/javascript" charset="utf-8"></script>
    <body>
        <div id="app">
    		<!-- text -->
            <h2>{{obj.name}}--{{obj.age}}</h2>
            <h2>{{obj.age}}</h2>
            <h3 v-text='obj.name'></h3>
            <h4 v-text='msg'></h4>
    		
    		<!-- html -->
            <div v-html='htmlStr'></div>
            <div v-html='obj.fav'></div>
    		
    		<!-- model -->
    		<h3>{{msg}}</h3>
            <input type="text" v-model='msg'>
    		
    		<!-- 属性 -->
            <img v-bind:src="imgSrc" v-bind:alt="altTitle" width="100px">
    		
    		<!-- event -->
            <button v-on:click='handlerClick'>按钮1</button>
            <button v-on:click='handlerClick2'>按钮2</button>
            <button @click='handlerClick2'>按钮3</button>
    		
        </div>
        <script>
            let vm = new Myvue({
                el: '#app',
                data: {
                    obj: {
                        name: 'zy',
                        age: 20,
                        fav:'<h4>前端Vue</h4>'
                    },
    				
                    msg: '这里是msg消息',
    				
                    htmlStr:"<h3>hello MVVM</h3>",
    				
                    imgSrc:'https://tse3-mm.cn.bing.net/th/id/OIP.PHiS7phW6XrKB3G7m4Q5LQHaNK?pid=Api&rs=1',
                    altTitle:'眼睛',
                    isActive:'true'
    
                },
                methods: {
                    handlerClick() {
    					this.obj.name = 'test';
                        // console.log(this);
                    },
                    handlerClick2(){
                        console.log(this);
                    }
                }
            })
        </script>
    </body>
    
    </html>
    
    class Myvue {
    	// 传入一个大对象
    	constructor(arg) {
    		// 后面数据处理要用到
    		this.$arg = arg;
    		this.$el = arg.el;
    		this.$data = arg.data;
    		if (this.$el) {
    			// console.log(this.$el,this)
    			new Observer(this.$data);
    
    			
    			new Compile(this.$el, this) ;
    			
    			this.proxyData(this.$data) ;
    			
    		}
    	}
    	
    	proxyData(data){
    		// console.log(this);
    		for(const key in data){
    			// console.log(key)
    			Object.defineProperty(this,key,{
    				// 调用this.  返回data[key]  ($data[key])
    				get(){
    					return data[key] ;
    					console.log(data[key]);
    				},
    				set(newVal){
    					data[key] = newVal;
    					console.log(data[key]);
    				}
    				
    			})
    		}
    	}
    	
    	
    }
    
    
    class Compile {
    	constructor(el, vm) {
    		this.el = this.isElementNode(el) ? el : document.querySelector(el);
    		this.vm = vm;
    		const fragment = this.node2Fragment(this.el);
    
    		this.compile(fragment);
    
    		this.el.appendChild(fragment)
    
    	}
    	compile(fragment) {
    		// 获得子节点
    		const childNodes = fragment.childNodes;
    		[...childNodes].forEach((child) => {
    			// 是元素节点
    			if (this.isElementNode(child)) {
    				this.compileElement(child);
    			} else { // 文本节点
    				this.compileText(child);
    			}
    			if (child.childNodes && child.childNodes.length) {
    				this.compile(child);
    			}
    
    		})
    	}
    
    	compileText(node) {
    		const content = node.textContent;
    		if (/\{\{(.+?)\}\}/.test(content)) {
    			compileUtil['text'](node, content, this.vm)
    		}
    	};
    
    	compileElement(node) {
    		// console.log(node)   //<h2>{{obj.name}}--{{obj.age}}</h2>
    		// 获取改节点的所有属性
    		const attributes = node.attributes;
    		// 属性遍历
    		[...attributes].forEach((attr) => {
    			// console.log(attr) ;   // v-text='obj.name'   v-bind:src="imgSrc"
    			const {
    				name,
    				value
    			} = attr;
    			// console.log(name) ;
    			if (this.isDirective(name)) {
    				// console.log(name)
    				const [, directive] = name.split("-") // text   bind:src
    
    				const [dirName, eventName] = directive.split(":"); //src
    
    				// console.log(dirName)
    
    				compileUtil[dirName] && compileUtil[dirName](node, value, this.vm, eventName)
    
    				node.removeAttribute('v-' + directive)
    
    			} else if (this.isEventName(name)) {
    				let [, eventName] = name.split("@");
    				compileUtil['on'](node, value, this.vm, eventName)
    			}
    
    		})
    
    	}
    
    	isDirective(attrName) {
    		return attrName.startsWith("v-")
    	}
    
    	isEventName(attrName) {
    		return attrName.startsWith("@")
    	}
    
    
    	isElementNode(node) {
    		return node.nodeType === 1;
    	}
    	node2Fragment(el) {
    		// 创建一个新的空白的文档片段
    		const fragment = document.createDocumentFragment();
    		let firstChild;
    		while (firstChild = el.firstChild) {
    			fragment.appendChild(firstChild);
    		}
    		return fragment;
    	}
    }
    
    
    
    
    const compileUtil = {
    
    	// 获取值的方法  obj.name   
    	getVal(expr, vm) {
    		// console.log(expr.split("."))
    		return expr.split(".").reduce((data, currentVal) => {
    			// console.log(data)
    			return data[currentVal] //obj[name]
    		}, vm.$data)
    	},
    
    
    	//设置值
    	setVal(vm, expr, val) {
    		return expr.split('.').reduce((data, currentVal, index, arr) => {
    			return data[currentVal] = val
    		}, vm.$data)
    	},
    
    
    	//获取新值 对{{a}}--{{b}} 这种格式进行处理
    	getContentVal(expr, vm) {
    		return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
    			return this.getVal(args[1], vm);
    		})
    	},
    
    
    
    	text(node, content, vm) { // content  属性的值  person.name
    		let val;
    		// console.log(content)
    		if (content.indexOf('{{') !== -1) {
    			val = content.replace(/\{\{(.+?)\}\}/g, (...args) => {
    				
    				new Watcher(vm,args[1],()=>{
    					this.updater.textUpdater(node,this.getContentVal(content,vm))
    				}) 
    				
    				return this.getVal(args[1], vm);
    			})
    		} else { //也可能是v-text='obj.name' v-text='msg'
    			val = this.getVal(content, vm);
    		}
    
    		this.updater.textUpdater(node, val);
    	},
    	html(node, content, vm) { // content    v-html='obj.fav'  属性值
    		let val = this.getVal(content, vm);
    		
    		// console.log(content)
    		// 对数据进行监听
    		new Watcher(vm, content, (newVal) => {
    			// console.log(this.updater)
    			this.updater.htmlUpdater(node, newVal);
    		});
    
    
    		this.updater.htmlUpdater(node, val);
    	},
    	model(node, content, vm) {
    		let val = this.getVal(content, vm);
    
           // 数据==>视图
    		new Watcher(vm,content,(newVal)=>{
    			this.updater.modelUpdater(node,newVal)
    		})
    		
    		// 视图==>数据
    		node.addEventListener('input',(e)=>{
    			this.setVal(vm,content,e.target.value)
    		})
    		
    		this.updater.modelUpdater(node, val);
    	},
    
    	bind(node, content, vm, attrName) {
    		// console.log(content)
    		let val = this.getVal(content, vm);
    		this.updater.attrUpdater(node, attrName, val);
    	},
    
    	updater: {
    		attrUpdater(node, attrName, attrVal) {
    			node.setAttribute(attrName, attrVal);
    		},
    		modelUpdater(node, value) {
    			node.value = value;
    		},
    		textUpdater(node, value) {
    			node.textContent = value;
    		},
    		htmlUpdater(node, value) {
    			node.innerHTML = value;
    		}
    	},
    
    	on(node, value, vm, eventName) { //     value:handlerClick2   eventName:@click
    		// console.log(vm.$arg.methods)
    		// console.log(vm.$arg.methods[value])
    		let fn = vm.$arg.methods && vm.$arg.methods[value];
    		// console.log(fn)
    		node.addEventListener(eventName, fn.bind(vm), false);
    	}
    
    
    }
    
    
    /
    
    class Observer {
    	constructor(arg) {
    		this.observe(arg)
    	}
    
    
    	observe(data) {
    		if (!data || typeof data !== 'object') {
    			return
    		}
    		Object.keys(data).forEach((key) => {
    			this.defineReactive(data, key, data[key])
    		})
    	}
    
    
    	defineReactive(data, key, val) {
    		// this.$data.obj.name='ztt'
    		this.observe(val);
    		const dep = new Dep();
    		Object.defineProperty(data, key, {
    			enumerable: true,
    			configurable: false,
    			get: function() {
    				// 如果有值 将观察者添加进来
    				// 注意一个Dep是全局的  一个dep是局部的 找了好久才找到这个错误。。
    				Dep.target && dep.addSub(Dep.target)
    				return val;
    			},
    			set: function(newVal) {
    				// console.log("hahasda");
    				if (newVal !== val) {
    					val = newVal;
    				}
    				// 改值 通知dep
    				dep.notify();
    			}
    		});
    
    
    	};
    
    }
    
    // 通知 添加 Wathcer
    class Dep {
    	constructor() {
    		this.subs = []
    	}
    	addSub(watcher) {
    		this.subs.push(watcher)
    	}
    	notify() {
    		// console.log(this.subs)
    		this.subs.forEach(w => w.update())
    	}
    }
    
    class Watcher {
    	constructor(vm, expr, cb) {
    		// console.log(expr)
    		this.vm = vm;
    		this.expr = expr;
    		// 回调函数 数据的更新
    		this.cb = cb;
    		this.oldVal = this.getOldVal();
    	}
    	getOldVal() {
    		// 获得老值时将观察者挂载到Dep上
    		Dep.target = this;
    		// console.log(this.expr)
    		let oldVal = compileUtil.getVal(this.expr, this.vm);
    		// console.log(oldVal)
    		// 用完销毁
    		Dep.target = null;
    		return oldVal;
    	}
    	update() {
    		let newVal = compileUtil.getVal(this.expr, this.vm);
    		// console.log(newVal)
    		if (newVal !== this.oldVal) {
    			// 一通知  将新的值回调回去
    			this.cb(newVal);
    		}
    	}
    
    }
    
    
    展开全文
  • vue的MVVM实现原理

    2018-03-24 13:58:09
    MVVMMVVM框架主要包含3个部分:model、view和 viewmodel。   Model:指的是数据部分,对应到前端就是javascript对象 View:指的是视图部分,对应前端就是dom Viewmodel:就是连接视图与数据的中间件 即后台...

    MVVM:

    MVVM框架主要包含3个部分:model、view和 viewmodel。

     

    Model:指的是数据部分,对应到前端就是javascript对象

    View:指的是视图部分,对应前端就是dom

    Viewmodel:就是连接视图与数据的中间件

    即后台数据通过Viewmodel中间件控制视图的更新变化,视图的变化通过Viewmodel中间件更新后台的数据,从而来分离视图view和模型model;

    优点:1)可复用性高,可将通用的视图View模块放在一个ViewModel里面,让很多View可以复用

              2)独立开发:开发人员可以专注业务逻辑和书记员的开发,设计人员可以专注于页面的设计

             3)低耦合:视图可以独立于Model变化和修改,一个ViewModel可以绑定在不同的View上,当View改变时Model不一 定改变,反之一样

    View→Model :可以用事件监听方法来获取视图中的数据,传给model中的data,例v-model等方法

    Model →View:数据更新视图的关键在于如何准确获得后面变化的数据,此时就可用到Vue.set()或vm.$set实例(全局vue.set()别名,主要用于当对象中某个属性值动态时生成处理)方法来设置变化的数据,来更新页面数据

    Vue.set(target,key,value)

    target:object/array;    注意: object不能是vue实例,也不能是vue实例的根数据对象 

    key:string/number

    value:any

     

     

     

     

     

     

     

     

    展开全文
  • MVVM实现原理(数据变更的实现)

    千次阅读 2017-09-15 18:02:10
    手动触发绑定 手动触发指令绑定是比较直接的实现方式,主要思路是通过在数据对象上定义get()方法和set()方法。ES6 proxy实现,数据劫持,脏检查机制

    手动触发绑定

    手动触发指令绑定是比较直接的实现方式,主要思路是通过在数据对象上定义get()方法和set()方法,调用时手动触发get()或set()函数来获取、修改数据,改变数据后会主动触发get()和set()函数中View层的重新渲染功能。根据View来驱动ViewModel变化的场景主要应用于<input>、<select>、<textarea> 等元素,当用户输入内容变化时,通过监听DOM的change,select、keyup等事件来触发操作改变ViewModel的数据。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
    <input q-value="value" type="text" id="input">
    <span q-text="value" id="el"></span>
    <script>
    let elems=[document.getElementById('input'),document.getElementById('el')];
    let data={value:'hello'};
    let directive={
        text:function (text) {
            this.innerHTML=text;
        },
        value:function (value) {
            this.setAttribute('value',value);
        }
    }
    function scan() {
        for(let elem of elems){
            elem.directive=[];
            for(let attr of elem.attributes){
                if(attr.nodeName.indexOf('q-')>=0){
                    directive[attr.nodeName.slice(2)].call(elem,data[attr.nodeValue]);
                    elem.directive.push(attr.nodeName.slice(2));
                }
            }
        }
    }
    function ViewModelSet(key,value) {
        data[key]=value;
        scan();
    }
    scan();
    setTimeout(function () {
        ViewModelSet('value','helloouvenzhang');
    },1000);
    if(document.addEventListener){
        elems[0].addEventListener('keyup',function (e) {
            ViewModelSet('value',e.target.value);
        },false);
    }
    </script>
    </body>
    </html>

    前端数据对象挟持

    其基本思路是使用Object.defineProperty和Object.defineProperties对ViewModel数据对象进行属性get()和set()的监听,当有数据读取和赋值操作时则扫描元素节点,运行对应节点的Directive指令。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
    <input q-value="value" type="text" id="input">
    <div q-text="value" id="el"></div>
    <script>
    let elems=[document.getElementById('el'),document.getElementById('input')];
    let data={value:'hello'};
    let directive={
        text:function (text) {
            this.innerHTML=text;
        },
        value:function (value) {
            this.setAttribute('value',value);
        }
    };
    let bValue;
    scan();
    defineGetAndSet(data,'value');
    if(document.addEventListener){
        elems[1].addEventListener('keyup',function (e) {
            data.value=e.target.value;
        },false);
    } else {
        elems[1].attachEvent('onkeyup',function (e) {
            data.value=e.srcElement.value;
        },false);
    }
    setTimeout(function () {
        data.value='helloouvenzhang';
    },2000);
    function scan() {
        for(let elem of elems){
            elem.directive=[];
            for(let attr of elem.attributes){
                if(attr.nodeName.indexOf('q-')>=0){
                    directive[attr.nodeName.slice(2)].call(elem,data[attr.nodeValue]);
                    elem.directive.push(attr.nodeName.slice(2));
                }
            }
        }
    }
    function defineGetAndSet(obj,propName) {
        Object.defineProperty(obj,propName,{
            get:function () {
                return bValue;
            },
            set:function (newValue) {
                bValue=newValue;
                scan();
            },
            enumerable:true,
            configurable:true
        })
    }
    </script>
    </body>
    </html>

    ES6 Proxy

    Proxy特性可以用于在已有的对象基础上重新定义一个对象,并重新定义对象原型上的方法,包括get()和set()方法。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
    <input q-value="value" type="text" id="input">
    <span q-text="value" id="el"></span>
    <script>
        let elems=[document.getElementById('el'),document.getElementById('input')];
        let directive={
            text:function (text) {
                this.innerHTML=text;
            },
            value:function (value) {
                this.setAttribute('value',value);
            }
        };
        let data = new Proxy({},{
            get:function (target,key,receiver) {
                return target.value;
            },
            set:function (target,key,value,receiver) {
                target.value=value;
                scan();
                return target.value;
            }
        });
        data['value']='hello';
        scan();
        if(document.addEventListener){
            elems[1].addEventListener('keyup',function (e) {
                data.value=e.target.value;
            },false);
        } else {
            elems[1].attachEvent('onkeyup',function (e) {
                data.value=e.srcElement.value;
            },false);
        }
        setTimeout(function () {
            data.value='hello ouvenzhang';
        },1000);
        function scan() {
            for(let elem of elems){
                elem.direction=[];
                for(let attr of elem.attributes){
                    if(attr.nodeName.indexOf('q-')>=0){
                        directive[attr.nodeName.slice(2)].call(elem,data[attr.nodeValue]);
                        elem.directive.push(attr.nodeName.slice(2));
                    }
                }
            }
        }
    
    </script>
    </body>
    </html>

    vue.js实现原理

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Two-way data-binding</title>
    </head>
    <body>
    
    <div id="app">
        <input type="text" v-model="text">
        {{ text }}
    </div>
    
    <script>
        function observe (obj, vm) {
            Object.keys(obj).forEach(function (key) {
                defineReactive(vm, key, obj[key]);
            });
        }
        function defineReactive (obj, key, val) {
            var dep = new Dep();
            Object.defineProperty(obj, key, {
                get: function () {
                    if (Dep.target) dep.addSub(Dep.target);
                    return val
                },
                set: function (newVal) {
                    if (newVal === val) return
                    val = newVal;
                    dep.notify();
                }
            });
        }
        function nodeToFragment (node, vm) {
            var flag = document.createDocumentFragment();
            var child;
            while (child = node.firstChild) {
                compile(child, vm);
                flag.appendChild(child);
            }
            return flag;
        }
        function compile (node, vm) {
            var reg = /\{\{(.*)\}\}/;
            // 节点类型为元素
            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; // 获取v-model绑定的属性名
                        node.addEventListener('input', function (e) {
                            // 给相应的data属性赋值,进而触发该属性的set方法
                            vm[name] = e.target.value;
                        });
                        node.value = vm[name]; // 将data的值赋给该node
                        node.removeAttribute('v-model');
                    }
                }
                new Watcher(vm, node, name, 'input');
            }
            // 节点类型为text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    new Watcher(vm, node, name, 'text');
                }
            }
        }
    
        function Watcher (vm, node, name, nodeType) {
    //        this为watcher函数
            Dep.target = this;
    //        console.log(this);
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.nodeType = nodeType;
            this.update();
            Dep.target = null;
        }
        Watcher.prototype = {
            update: function () {
                this.get();
                if (this.nodeType == 'text') {
                    this.node.nodeValue = this.value;
                }
                if (this.nodeType == 'input') {
                    this.node.value = this.value;
                }
            },
            // 获取daa中的属性值
            get: function () {
                this.value = this.vm[this.name]; // 触发相应属性的get
            }
        }
        function Dep () {
            this.subs = []
        }
        Dep.prototype = {
            addSub: function(sub) {
                this.subs.push(sub);
            },
            notify: function() {
                this.subs.forEach(function(sub) {
                    sub.update();
                });
            }
        };
        function Vue (options) {
            this.data = options.data;
            var data = this.data;
            observe(data, this);
            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id), this);
            // 编译完成后,将dom返回到app中
            document.getElementById(id).appendChild(dom);
        }
        var vm = new Vue({
            el: 'app',
            data: {
                text: 'hello world'
            }
        });
    </script>
    
    </body>
    </html>
    展开全文
  • 什么是MVVM MVVM是Model-View-ViewModel的简写。即模型-视图-视图模型。 模型指的是后端传递的数据。 视图指的是所看到的页面。 视图模型是mvvm模式的核心,它是连接view和model的桥梁。它能够将数据转化为视图,也...
  • 1. MVVM angular - 脏值检测 vue - 数据劫持+发布订阅模式(不兼容低版本:因为其依赖于Object.defineProperty) 2. Object.defineProperty() 1.1 概念 Object.defineProperty() 方法会直接在一个对象上定义一个新...
  • 现成MVVM 菜单教程 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Vue 测试实例 - 菜鸟教程(runoob.com)</title> <script src=...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 20,328
精华内容 8,131
关键字:

mvvm实现原理