目录
3,new vue / vue.extend / vue.component
1,指令
1,单次绑定v-once,仅渲染一次,随后的重新渲染会被跳过
<span v-once>{{sex}}</span>
2,v-html和v-text相当于element.innerHTML 和innerText
<div v-html="gethtml"></div>
data(){
return{
gethtml: '<span>来吧</span>'
}
}
3,v-bind的两种写法
<div v-bind:class="container"></div>
//或者
<div :class="container"></div>
4,v-show和v-if
区别:
1,v-show切换display属性,v-if控制节点的存在与否
2,v-show每次都会被编译,v-if只有自身状态改变才会编译卸载
3,假设控制DOM的变量初始化不存在,建议使用v-if
4,假设频繁切换DOM显隐,使用v-show更好
5,v-else
前面必须有v-if或者v-else-if
<div v-if="show">看得见我</div>
<div v-else >看不见我</div>
6,v-else-if
和v-else用法一样,前面也要有v-if或者v-else-if
7,v-model
只能用在input,textarea,select中,用于双向绑定。
v-model就是dom可以改变data,data也可以改变dom。只是把我们自定义组件的事件触发封装了起来。
<input v-model = "name" />
//相当于自定义组件的,事件触发
<input :value ="name" @change = "changeName" />
methods:{
changeName(val){
this.name = val
}
}
8,v-slot
插槽作用是为了在父组件使用子组件的时候,能向子组件的DOM结构中插入元素
1,匿名插槽
2,具名插槽
3,作用域插槽
//匿名插槽
//父组件
<div>
<child>
<p> 一些内容</p>
</child>
</div>
//子组件
<div>
<span>我是子组件</span>
<slot></slot>
</div>
//最终形态
<div>
<div>
<span>我是子组件</span>
<p> 一些内容</p>
</div>
</div>
//具名插槽
//父组件
<div>
<child>
<p slot="amy"> 一些内容</p>
</child>
</div>
//子组件
<div>
<slot name="amy"></slot>
<span>我是子组件</span>
<slot name="bob"></slot>
</div>
//最终形态
<div>
<div>
<p> 一些内容</p>
<span>我是子组件</span>
</div>
</div>
//作用域插槽
//子组件的插槽能向父组件传递值
//父组件
<div>
<child>
<span v-for="item in names">{{item}}</span>
</child>
</div>
//子组件
<div>
<h>我是儿子</h>
<slot :data="names"></slot>
</div>
2,组件传值
父子组件通信
父组件向子组件传值:props
子组件向父组件传值:子组件函数内this.$emit("childEvent", "该参数作为eventclik函数参数")
父组件DOM内:<div @childEvent="eventclick" ></div>
还可以this.$parent访问父组件里data、method或者生命周期方法。
还可以this.$children[0]访问子组件内容
还可以this.$ref.名称访问子组件内容
多层级嵌套组件(爷孙)通信
主动传值的组件:
provide(){
return {
msg:"hello"
}
}
被动接收的组件:
inject(){
msg:{
from: 'A',
default:"world"
}
}
兄弟组件(没关系的组件)通信vuex或者bus
//组件A
import bus from '@util/bus'
mounted(){
bus.$emit('test','hello world')
}
//组件B
import bus from '@util/bus'
mounted(){
bus.$on('test',this.test)
}
methods:{
test(e){}
}
//bus.js
import Vue from 'vue';
const bus = new Vue();
export default bus;
使用js实现event bus:
class EventBus{
constructor(){
this.callback=[];
}
on(type,fn){
this.callback[type] = this.callback[type]||[];
this.callback[type].push(fn)
}
emit(type,args){
if( this.callback[type]){
this.callback[type].map(item=>{
item(args)
})
}
}
}
3,new vue / vue.extend / vue.component
new vue表示创建一个实例对象,并将实例和容器(el)绑定
vue.extend 表示创建一个实例组件,并将实例和(template)绑定
vue.component表示创建或获取一个组件,并注册为全局组件
new Vue中的data参数是对象,vue.extend、vue.component中的data参数是函数(因为实例组件需要共享,所以使用函数,大家有自己的作用域,互不影响data中变量值;而实例对象一般一个应用中只有一个)
最核心的原因我认为是data在prototype上有定义,在构造器内也有定义,所以如果prototype上不是函数而是对象,则大家指向同一个地址。
var Component = function() {
this.data = this.data()
}
Component.prototype.data = function(){
return {
message: '10'
}
}
new vue和Vue.extend的参数列表:
vue.component的参数列表:
Vue.component('my-component', Vue.extend({ }))
第二个参数可以是一个对象(自动转为extend),可以没有。
4,vue.use()
通常情况下,我们写一个组件流程是这样的:
方式一:父组件直接引用子组件
组件
<template>
<script>
export default{
name:'child',
data(){}
}
</script>
父组件
<script>
import child from "./child.vue";
const MB= Vue.extend({
name: "MB",
components: {
child
}
})
</script>
方式二:父组件通过中间js转换引入子组件(此时子组件是全局组件了)
子组件不变
中间js文件
Vue.component("Child-app",()=>import("./Child.vue")
父组件
<template>
<div>
<Child />
</div>
<template>
<script>
import './inside.js'
const MB=Vue.extend({
name:"MB"
})
</script>
无论父组件是直接将子组件挂在components属性下,还是将子组件挂在Vue.component下,子组件可以是Vue.extend({ }),也可以直接是个对象{ },假如父组件既不挂在components下,也不挂在Vue.components下,我要像使用elementUI那样直接在main.js中首次挂载,注册为全局使用?
Vue.use()函数就是用来加载这样的npm包或者插件的:
Vue.use(plugin)
plugin = {
install:function(Vue) {
Vue.component('SideBar',SideBar);
}
}
5,computed和watch
computed计算的变量,不在data中;
一旦computed计算的变量的函数,中的引用data的值改变了,就出发computed计算
watch监控的变量必在data中或者computed中;
watch监控的变量要么是单一变量,要么是对象,如果监听对象中的属性有两种方式:
方式一,只监听对象某一个属性变化(开销小):
data(){
return {
name:'bob',
person:{
age:11
}
}
},
watch:{
name(newval,oldval){},
person(new,old){},
"person.age"(new,old){},
}
方式二,监听对象中任意一个属性变化(开销大):
watch:{
person: {
handler: function() { },
deep: true
}
}
方式三:巧用computed
computed:{
getage(){
return this.person.age
},
watch:{
getage(){ }
}
6.1 vue触发响应的条件
针对直接赋值的操作都会响应(无论是number还是对象,数组等)
如果是数组,调用数组的方法类似push,pop等,
如果是数组,修改数组某个元素(对象)的属性值(arr[0].name = "jeans" )
如果是对象,修改对象的属性会响应
不会引起响应的操作:
如果是数组,修改数组某个元素值不会响应(arr[0] = 100)
如果是对象,向对象中添加或者删除属性不会响应(obj[name] = "bob",或delete obj[name])
强行让不会引起响应的操作响应起来:
修改数组某个元素值:Vue.set(arr, 0, 10086)
给对象添加属性:Vue.set(obj, "name", "bob")
给对象删除属性:Vue.delete(obj, "name")
6.2,vue的响应式原理 (vue原理合集)
在做题的时候遇到这
//store中
const store = new Vuex.store({
state:{
attr1:{ name:"bob"},
attr2:[1,2,3]
}
})
//在A.vue中
<div>{{ store }}</div>
data:{
manager:store.state
}
//在A.js中
import store from store
//无论是修改manager还是直接修改manager引用的store.state,情况都是一样的
this.manager.attr1.name="hello" //和
store.state.attr1.name="hello" //效果一样都会响应
this.manager.attr2[0] = 55
store.state.attr2[0] = 55 //效果一样都没响应
我把vue响应式分为三种情况
一个是data中的属性改变,引发UI层面数据响应;
一个是data中的属性值改变,引发watch层面响应;
一个就是上面这个问题,直接改变data赋予的值,data的响应问题。
他们都可以用vue的数据响应式原理来解释。其中以上的问题就是第三种,而判断是否触发data的响应,主要靠判断data的赋值改变时,是否触发了set,get函数。
以下主要是data值为对象时候的解释,如果data为数据,则监听的是是否使用了函数类似push,pop,slice等
vue在初始化会给data绑定一个defineProperty
function observer( obj,key,value){
Object.defineProperty(obj,key ,{
get:function(){
console.log("observer get",key)
return value
},
set:function(newVal){
console.log('observer set',key)
if(value!==newVal){
value = newVal
}
}
})
}
const data={
name:"bob",
age:11
}
//遍历observer函数,为data对象的每个属性赋值get,set函数
for(let key in data){
observer(data,key,data[key])
}
data.name = "amy" //observer set name ;amy 此时触发observer函数中name属性的set函数
console.log(data.name) //observer get name;bob 此时触发name属性的get函数
然而,此时的data里面两个属性name和age都是简单的number,string类型,如果data里面的属性赋值为引用类型,比如对象和数组,使用上面的observer函数会有一些弊端:
const data = {
person:{
child: 0,
adult: 1
}
}
for(let key in data){
observer(data,key,data[key])
}
data.person.child = 100; //observer get person 此时触发的是child默认的set函数
console.log(data.person.child); //observer get person ; 100 ;此时触发的是child默认的get函数
因为只遍历data的属性,而person是一个对象时,不对person的属性进行遍历,所以child和adult都没有set和get函数,此时我们需要做的就是:
增加一个递归,循环遍历对象所有属性,给所有属性添加到set,get函数
function observer( obj,key,value){
ccc(value);
Object.defineProperty(obj,key ,{
get:function(){
console.log("observer get",key)
return value
},
set:function(newVal){
console.log('observer set',key)
if(value!==newVal){
value = newVal
}
}
})
}
function ccc(obj){
if(!obj || typeof obj !== 'object'){ return }
for(let key in obj){
observer(obj,key,obj[key])
}
}
这个时候再次调用对象就执行的是数据劫持后的get,set函数了
const person = {
adult:{
age:18
}
}
ccc(person)
person.adult.age ;//observer get adult
//observer get age 18
person.adult.age = 16; //observer get adult
//observer set name
但是我们没有考虑一个问题,假如给一个属性赋值为对象,那这个赋值后的对象又不存在get,set函数了
person.adult.age = { firstage: 11}
同理,我们可以在set函数中添加递归
以上针对的是对象的数据双向绑定,接下来是针对数组的数据双向绑定
const currentArray = Array.prototype;
//创建新对象,将新对象的原型指向currentArray,再扩展新对象的方法,不会影响原型
const newArray = Object.create(currentArray)
['push','pop','slice','shift','unshift'].forEach(everyFun=>{
newArray[everyFun] = function(){
updateUI();
currentArray[everyFun].call(this,...argument)
}
})
const data = {
nameList: ["bob","amy"]
}
function observer(obj,key,value){
if(Array.isArray(value)){
value.__proto__ = newArray
}
}
for(let key in data){
observer(data,key,data[key])
}
以上代码就是data双向数据绑定原理(针对对象),因为data引发UI改变只是简单的赋值调用问题,不需要讨论,接下来需要讨论data和watch之间的响应原理。
watch的原理其实是订阅者和发布者模式。在初始化的时候,立刻就判断有哪些属性是被订阅了,被订阅后,一旦有属性发生改变,就会触发对应的动作。在vue中,一个属性只允许被订阅一次,但其实可以订阅很多次,产生很多个订阅者,一旦这个属性改变会通知所有的订阅者(但是个人感觉很没有必要啊,既然大家都监听这个属性的变化,那把所有操作放在一个函数(订阅者)里就可以了)
一个属性可以产生很多订阅者,为了管理这些订阅者,需要有个中转站(订阅器),类似 一个数组,把所有的watcher订阅者都push到数组里,一旦属性变化,遍历数组中的订阅者进行函数操作
//订阅器
function Dep(){
this.subs = []
}
Dep.prototype = {
addSubs:function(watcher){ //这个属性用来向某个属性的订阅器中添加订阅者
this.subs.push(watcher)
},
notify:function(){ //这个方法用来唤醒所有的订阅者
this.subs.map(sub=>{ })
}
}
如果以上不理解,可以看看源码片段,下面有个判断是否为数组那句话充分说明一个属性可以有多个订阅者
function initWatch(vm,watch){
for(const key in watch){
const handler = watch[key]
if(Array.isArray(handler)){ //为什么会是数组呢?因为一个属性可以有很多个订阅者啊
for(let i = 0;i<handler.length;i++){
crateWatcher(vm,key,handler[i])
}
}else{
createWatcher(vm,key,handler)
}
}
}
我们来给一个属性添加多个订阅者:
watch:{
bookNums: function(){},
}
订阅器构造好了,应该放到defineProperty中,这样在data初始化的时候就同时把watch的属性也初始化好了。 (每个属性都有一个自己的订阅器)
function observer(obj,key ,value){
ccc(value);
const dep = new Dep(); //每个属性都会执行一次该语句
Object.defineProperty(obj,key,{
get:function(){
if(Dep.target){ //判断是否需要添加watch
dep.addSubs()
}
return value;
},
set:function(newVal){
if(newVal !== value){
ccc(newVal)
value = newVal;
dep.notify(); //订阅器提醒订阅者,数据变更了
}
}
})
}
接下来就是定义订阅者了,订阅者的构造器需要做两个事,一个是把自己缓存到订阅器里,一个是监听到更新后的操作函数。
缓存到订阅器只需要每个watch第一次调用属性对应的get函数就可以了:
function Watcher(){
this.
7,vue的虚拟dom
虚拟dom存在的意义?
①在一个普通的html文件中,想要更改某个元素中的内容一般:element.innerHTML = "something"
②而vue中,首先在created之前将data,methods,computed等初始化,在beforeMount之前将template转化为render函数,在render函数中生成虚拟DOM(vdom)vdom结构(json),然后根据defineProperty检测data中属性的变化,一旦发生变化,则重新生成vdom,然后利用diff算法对比新vdom和旧vdom的差别,将有变化的内容生成一个patch补丁,最后将真实dom对应的地方使用补丁替换上去。
那么①和②相比,哪个速度更快呢?
①中,使用js去操作dom是一个跨线程的操作,非常消耗性能,假如数据改变比较多,需要多次操作dom
②中,因为js的计算在v8引擎中很快,所以操作dom的复杂操作转移一部分到js中,由js模拟dom结构,帮助dom提前计算出哪些部分改变了,然后一次性操作dom更新所有的数据,减少js跨线程多次操作dom的行为,提升性能
虚拟dom(vdom,vnode)是啥?
因为html和xml格式差不多,xml可以用json代替,所以dom结构可以用json代替,这个代替的片段叫vnode,就是虚拟dom:
<div id ="div1" class="container">
<p>vdom</p>
<img style = "width:100px" />
</div>
相当于:
{
tag:'div',
props: {
id: 'div1',
className: "container"
},
children: [
{
tag: 'p',
children: 'vodm'
},
{
tag: 'img',
props: {
styles: "width:100px"
}
}
]
}
一般使用snabbdom(虚拟dom库),核心就是
patch(container, vnode)将虚拟dom初始化到容器;
patch(vnode,newVnode)更新dom;以及
patch(newVnode, null),销毁dom。
snabbdom的核心是h函数,vnode,以及patch函数:
var h = require('snabbdom/h').default; //h函数产生vnode结构
var vnode = h('div',{on:{click:somefun}}, [ child..... ] );
patch(vnode,newVnode)
8,diff算法
diff算法概念
snabbdom的核心,对比新旧数据不同的部分。这部分的算法叫做diff算法。diff算法在react和vue组件中key值能够体现。diff算法不是vdom独有的,而是个广泛的概念,
linux diff,git diff都用到了diff算法去对比文件的差异
两个对象之间也可以用diff算法对比差异
以及两棵树之间做对比,比如vdom
复杂度分析
树的diff算法的时间复杂度是O(n^3),是不可用的,第一,遍历tree1;第二,遍历tree2;第三,排序;三者相乘 ,得出O(n^3)这个结果,后来通过以下三种方法将时间复杂度优化到O(n)
1. 只比较同一级的节点,不跨级比较
2. 一旦tag不相同,直接删除重建,不再深度比较
3. 一旦tag和key(v-for里面的key)都相同,认为两节点相同,不再深度比较(我猜测这里的不再深度比较指的是不再比较类似style,className等属性,而children还是要比较的)
疑问:比如更新了某个节点的innerHTML,根据 vnode对象,就是children变了,但是tag和key都没有变,这个时候两个节点并不相同啊,需要比较的啊
diff算法大概实现的思路
patch(oldVNode,newVNode)比较old和new的tag和key,相同则patchNode,不同则,删除重建
patchNode(oldVnode,newVnode)对比text节点,text不同则删除或者添加(默认text和children互斥) 对比children updateChildren()
updateChildren(oldChildren,newChildren)优化二个children数组循环比较,采用第一个和第一个比较,最后一个和最后一个比较,都不匹配,从 newChildren中获取一个child的key去oldChildren找,找到则对比selctor,不同则新建,同则patchNode,找不到则插入
diff算法重点函数一:patchVnode
patchVnode(oldnode:vnode, vnode: vNode,insertedVnodeQueue:VnodeQueue)
patchVnode先给新的vnode的容器绑定到旧的vnode的容器上
然后判断新vnode是否包含children
如果新的vnode不含children,旧的vnode包含children若不等,则移除旧的vnode的children并设置为新的text
如果新旧vnode都包含children, 则对比,若两者children不同,则执行updateChildren
如果新的vnode包含children,旧的没,将旧的text设为空,并执行addVnodes给旧的添加chidren
如果新的vnode不含children,旧的含,执行removeVnodes将旧的children移除
如果新的vnode不包含text,旧的vnode包含text的话就移除旧的text
diff算法重点函数二:updateChildren
updateChildren(elm, oldCh, ch, insertedVnodeQueue),
这个函数会定义startIndex和endIndex,新旧vnode的children从两头分别往中间排序进行对比。
oldVnode,newVnode
开始节点和开始节点对比
结束节点和结束节点对比
开始节点和结束节点对比
结束节点和开始节点对比
如果相同则执行patchVnode,并且oldStartVnode = oldCh[++oldStartIndex]新旧节点指针同时移动
如果以上四个情况都没有命中 ,则新节点的第一个节点和旧节点的每个节点对比key值看是否能命中
如果还是没命中说明这个节点是个新节点,直接createNewElem就可以了
如果命中了,再判断二者的tag(sel)是否相同,不相同说明也是个新节点
如果命中了,且二者的tag也相同,则执行patchNode
key不可以是随机数,最好也不是0,1,2,3这样的数字
9,vue设计思想
一个项目可以分为两个部分,数据和组件设计
数据结构设计格式:
1,用数据来描述页面上所有的内容
2,数据具有结构化(比如对象结构)
3,数据具有拓展性(能够添加新的属性,能够保证数据源改变,引用的数据相应改变)
组件设计格式:
1,根据功能划分组件层次
2,组件尽量原子化(一个组件只做一件事)
3,容器组件(专门负责数据存储,触发事件)和UI组件(只负责显示,不负责函数)
比如设计一个购物车,结构如下:
//index.vue
<div>
<ProductList />
<CarList />
</div>
<script>
data(){
return {
product:{
id:'jeans',
title:'蓝色牛仔裤',
sale: 100
},
carItems:{ ... }
}
}
methods:{
addProduct(){ },
deletePrd(){ }
}
<script>
//ProductList.vue
<ul>
<li v-for="item in product">{{item.title}}</li>
</ul>
<script>
this.$parent.addProduct(id)
</script>
如果不适用容器组件管理数据,而是使用vuex管理
10,组件插入到body中
子组件本来应该在父组件中,但是如果我想直接插入到body下,可以直接获取该子组件节点,使用:
document.body.appendChild(node)
在elementUI中有体现:el-dropdown插件自动加载放到body元素下,可以使用:append-to-body="false"
11.vuex
主要包括5个重要属性:state,modules,mutations,actions,getters。
state存储数据,getters类似computed计算属性,mutations改变state,actions使用同步commit或者异步dispatch调用mutations里的方法,modules就是多个store合并为一个。
12,生命周期
1,异步请求应该放在哪一个生命周期去执行?
假如这个异步请求得到的数据是想加载到data里,那么一般放在created里执行,因为这个时候data已经存在了;那么其实在beforeCreate里执行异步请求也可以,因为是异步请求,所以会先加载出来data,再得到异步结果,但是这个异步请求无法调用methods里的方法,代码只能全部写在beforeCreate里,一大坨。
2,什么时候会用到beforeDestory?
做一些资源释放的操作:比如清除定时器,清除事件监听(鼠标滚动点击),或者清除页面上已经增加的交互内容
3,介绍一下各个生命周期做了什么?
在beforeCreate之前:创建vue对象,new Vue()
在created之前:props,遍历data,methods,computed,watch(假如immdiate: true)
在beforeMount之前:把data和模板结合生成html
在mounted之前:把生成的html挂到对应的DOM容器里
在beforeUpdate之前:watch监听data变化(假如immdiate:false)
在updated之前:将更新的虚拟DOM重新渲染到页面
在activated之前:keep-alive专用,组件被激活时候调用
在deactivated之前:keep-alive专用,组件被移除时候调用
在beforeDestory之前:一般卸载一些鼠标事件,定时器,事件监听等
在destroyed之前:
4,父子组件生命周期执行顺序
父组件beforeCreate,created,beforeMount,子组件beforeCreate,created,beforeMount,mounted 然后父组件mounted。
13. vue3.0
1,响应式原理不再使用Object.defineProperty,而是采用new Proxy(),因为前者只能劫持对象,不能监听数组,且一次只能劫持对象的一个属性,如果属性是对象还需要深度遍历消耗性能。而proxy可以监听对象和数组,而且初始化的时候不会遍历所有属性所以性能也好
2,vue3.0采用和react相似的class编程
3,vue3.0的生命周期去掉vue2.0的beforeCreated和created,增加一个setUp()在这个函数里编辑生命周期函数,并且基于vue2.0的生命周期函数前面加一个on,将beforeDestory和destoryed改为beforeUnmount和unmounted。
4,vue3diff算法优化:vue2是通过标记静态节点优化,vue3通过标记和提升静态节点。
vue2.0因为会遍历所有节点变化,所以会不停调用patchnode函数,假如父子嵌套多就会慢,而react因为子组件变了就不再对比直接整个组件一块更新,所以在这块有区别。为了提高vue这块的性能,vue3.0在生成vnode对象中添加一个标记,标记这个节点内容是动态还是静态的,这样下次遍历的时候就可以直接跳过静态节点了
14,函数式组件、动态组件
函数组件
存在意义:函数组件不需要实例化,没有data,没有生命周期,渲染性能比普通组件好。只是单纯的展示从props接收的数据
必备属性:props,functional,render
注意点:因为没有实例化,所以没有this。在render的两个参数createElement,context
使用示例:
动态组件
<component v-for=" " />
15,路由vue-router
使用方式:
1,import Router from 'vue-router'
2,export default new Router({
mode: 'hash',
routes:[
{
path:'/first',
name:'first',
component: first
},{
path:'/first',
name:'first',
children: [
{path:'',component:''}
]
}]})
3,然后使用<router-view></router-view>默认将路由加载至此
4,最后使用<router-link to=""></router-link>
两种路由模式:hash和history
hash
比如网易云单页面内从发现音乐,跳转到朋友,就是hash的后缀名变了
我们可以通过代码设置hash值控制页面跳转:
location.hash = ""; //此时跳转到发现音乐
location.hash = "#/firend"; //此时跳转到朋友
history
这个history也是浏览器内置的对象,可以使用以下来操作路由:
history.back()
history.forward()
history.go("friend")
然后这个和vue扯上关系,就是这样的,因为vue是单页应用,大家入口文件都是index.html,项目打包后放在服务器中就一个index.html,没有其他的html文件,但是在页面跳转的时候你会发现比如跳转到friend.html,这个friend.html就是浏览器解析打包后的文件动态生成的页面,在服务器中实际是不存在这个文件的。
而history的底层实际上根据更新的url,向服务器发起http请求返回页面来实现页面跳转的,所以单页应用由于 大部分页面都是浏览器动态生成的,所以很多虚假的页面根本不存在于服务器,发起请求就会返回404。
而hash不同,hash根本不发起http请求,它只是一个页面内元素查找跳转。
二者区别
hash:hash路由虽然出现在url中,但是在请求的时候不会被包含在http请求中,对后端来说没有什么影响,所以从另一方面来说hash的改变不会重新加载页面
history:这种模式是利用H5中新增的pushState()和replaceState() 方法,这两个方法用于浏览器的历史记录栈,在当前已有的 back,forword,go 基础上,提供了修改历史记录的功能,当执行修改的时候,虽然修改了当前的url ,浏览器不会立即向后端发送请求
16,keep-alive
比如路由跳转的时候,两个页面来回切换,组件会不停的创建(created)和销毁(destroyed),同时页面的状态也不会保留,为了减少这种开销,使用keep-alive。
<keep-alive>
<Children />
</keep-alive>
试题
1,说说你理解的MVVM?
mvvm表示model,view,viewModel;model表示的是数据层,view表示的是视图层,本来数据和视图是不相关的,viewModel起到两者的联系作用,当view视图层由于用户操作改变内容时,viewModel控制model对应数据改变,model层的数据变化时,也会反应到视图层。