文章目录
- 写在前面
- 计算属性优势在哪?
- watch深度监听
- 内连样式写法
- 条件渲染需要注意的template
- 列表渲染和条件渲染的爱恨纠葛
- v-for 怎么获取Symbol的属性值
- 为什么v-for一定要有key
- 数组触发视图更新
- 事件处理
- v-model
- vue组件中data为什么一定要是一个函数
- prop对象和数组的默认值问题
- $parent不太建议频繁使用
- 具名插槽需要注意的三个点
- 跨级数据传递
- setup
- watchEffect
- computed
- 钩子函数
- vue3-props
- provide and inject
- setup语法糖
- router-view
- 动态路由获取页面参数
- 特殊页面和参数配置
- 嵌套路由注意事项
- route 和 router
- 编程式跳转
- 但页面展示不同区域的组件内容
- 不同的路由模式、不同的记录模式
- 路由守卫、导航守卫
- 路由懒加载
- 全局状态管理 - provide和inject的方式
- 代理解决get请求的跨域问题
- vuex的基本使用记录
- 写在后面
写在前面
这篇文章通过标题你们也可以大概猜出来了,是的,这篇文章就是闲聊一下在使用vue的时候,一些我们不太注意的但是其实很有必要的一些点,总结一下,不管是给你们面试还是写项目都是很有帮助的,另外一个原因是最近一只写的都是关于原生js的一些内容,而没有关于框架的东西,vue的很久没有写了 ,废话不多说,开始闲聊,因为是闲聊,只是说一些我们平常不怎么注意但是其实对代码编程有好处的一些东西,不一定是不懂的或者是很难的知识点,只是希望引起注意!另外这篇文章的一个目的是将新版的vue和旧版本的vue进行一个比较,将一些差异化很大的地方进行一个总结说明,将最基本的用法展示出来,这样对比着学习效率相对会比较高一些,所以下面有一些是关于注意项,还有一些是关于新版本的改动的内容!
计算属性优势在哪?
- 缓存
可以毫不保留的说,计算属性缓存做的很好,这也是为什么很多稍微一些数据操作的时候,如果不是频繁的需要改变数据,一般情况下是使用computed属性的,这里一个需要注意的点就是它是一个计算属性,不是计算方法,所以我们使用的时候是可以直接按照属性的使用方法进行使用的,也就是说可以当做一个变量进行直接获取,不仅写法方便,得益于他本身的缓存(计算属性基于响应依赖关系缓存【官方爸爸的原话】),会让你的代码变得异常的流畅和优雅! 说白了就是性能提升会比较的明显!
<script>
export default {
data() {
return {
msg: "helloCSDN",
};
},
computed: {
reverseMsg() {
console.log("computed"); //执行了一次
return this.msg.split("").reverse().join("");
},
},
watch: {
msg: {
handler(n, o) {
if (o) {
this.msg = o.split("").reverse().join("");
} else {
this.msg = n.split("").reverse().join("");
}
console.log("watch"); //执行两次
},
immediate: true,
},
},
methods: {
reverseMsgFunc() {
console.log("methods"); //执行三次
return this.msg.split("").reverse().join("");
},
},
};
</script>
<template>
<div>
<!-- 实现一些基础的表达式,过于复杂的不太容易维护,不是不可以写,不建议写 -->
<p>{{ msg.split("").reverse().join("") }}</p>
<p>{{ msg.split("").reverse().join("") }}</p>
<p>{{ msg.split("").reverse().join("") }}</p>
<!-- 基于vue设计的计算属性中的缓存机制,只要原始值不发生改变,计算属性的内容是不会进行重新执行的 ,即使你进行改变了,后面也只是改变一次之后后两次直接获取缓存-->
<p>{{ reverseMsg }}</p>
<p>{{ reverseMsg }}</p>
<p>{{ reverseMsg }}</p>
<!-- 因为是方法,只要调用,就一定会执行,所以不涉及到缓存,但是不排除使用js的技术手段进行缓存的处理,进而实现计算属性一样的效果,但是代码成本就很高了 -->
<p>{{ reverseMsgFunc() }}</p>
<p>{{ reverseMsgFunc() }}</p>
<p>{{ reverseMsgFunc() }}</p>
<!-- 使用watch 会被执行两次,因为它是根据数据变化进行监听的,所以这种一般不会这么写,显的就比较的sb-->
<p>{{ msg }}</p>
<p>{{ msg }}</p>
<p>{{ msg }}</p>
</div>
</template>
<style scoped>
</style>
-
计算属性完整写法总结
承接上文代码,上面的关于计算属性的写法只是一个简写形式,也是我们见过的最多的一种写法形式,为什么直接写属性就会执行该方法呢?因为计算属性默认执行的就是get方法,所以我们不需要进行()主动调用 ,而set是当我们进行改变计算属性本身的值的时候,才会被触发!
reverseMsg: {
get() {
console.log("computed"); //执行了一次
return this.msg.split("").reverse().join("");
},
//一般情况下我们是不需要进行写这个的,我们默认计算属性是一个只读的属性,但是非要有这种需求的时候,也不想通过方法进行实现的时候,这种也是可以进行更改值的
set(nv) {
console.log(nv); //你好,csdn
//当然也可以进行重新赋值
this.msg = nv;
},
},
<template> <button @click="reverseMsg = '你好,csdn'">改变计算属性的值</button> </template>
计算属性功能本身是比较强大的,但是我们一般这种改变更倾向于使用方法或者是一些需要监听的数据,计算属性只是用做一个数据的简单处理或者一些单一的复杂逻辑处理
- 和watch进行区别使用
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过
watch
选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的,这是官网的一句总结,我个人觉得比较好的应用场景是当我们需要进行执行异步操作的时候,一般使用watch进行是比较合适的,也就是说computed无法实现的场景的时候,如果使用计算属性可以实现的话,我还是建议使用计算属性进行处理,毕竟他的依赖缓存机制还是比较提升性能的!毕竟两个都是有依赖值的,所以需要进行区别什么情况进行使用不同的技术点总结一句话:当一个数据影响多个数据的时候,我们可以使用watch,关于深度监听,上面的例子中已经写过了
- 动态操作样式
当我们的样式需要动态进行变化,而且同时样式比较多的时候,我们可以直接在内连上写很长一段,vue也是支持的,只是后期维护起来会比较难受,这个时候我们可以在data中进行声明一个对象进行存储,或者最好是使用计算属性进行保存样式的变化
data() {
return {
isShow: true,
isError: false,
};
},
<p :class="{ active: isShow }">我是活跃的页面</p>
<p :class="{ active: !isShow }">我是停止加载的页面</p>
<style scoped>
.active {
font-size: 50px;
color: blue;
}
.error {
color: red;
}
</style>
- 使用computed进行实现
computed:{
styleObj() {
return {
active: this.isShow,
error: !this.isShow,
};
},
}
<p :class="styleObj">我是停止加载的页面</p>
watch深度监听
上面的代码里面写了关于watch如何实现监听的立即执行的代码操作,其实就是另一个比较麻烦的写法,但是一般情况下我们监听写的只是一个监听基础变量的能力,而没有监听复杂变量的能力,这里写一下关于watch进行实现深度监听对象的代码实现
data() {
return {
user: {
name: "jim",
age: 18,
sex: "男",
},
};
},
watch: {
user: {
handler(n) {
console.log(n);
},
deep: true,
},
},
这样可以实现一个基础的深度监听,但是vue的做法是当我们使用deep的时候,他会将每一个对象中的属性都进行添加上监听,那么我们大多数的时候需要关注对象中的某一个属性的变化,而不需要所有的属性变化的监听,这个时候我们需要改变写法,
代码如下:
"user.name": {
handler(n) {
console.log(n); //你好,mary
},
deep: true,
},
那么这个时候我们再进行改变除name之外的属性的时候,就不会处罚监听了,注意,这里使用对象单个属性监听的时候,打印出来的是name也就是被监听的那个值,而不是整个对象。
"user['name']": { handler(n) { console.log(n); //你好,mary }, deep: true, },
这种写法不生效
内连样式写法
当然这块很多人都会写,不过这里需要注意一个点就是关于我们书写内连样式的时候,如果是短线链接的样式,我们需要进行转为驼峰命名法进行书写,不然需要进行双引号的一个转换
font-size :'40px'
在使用内连样式进行动态变化的时候,我们需要进行
fontSize : '40px'
条件渲染需要注意的template
这里你们一定认为我会写关于v-if和v-show的区别对不对,其实不是,都知道v-if和v-show的区别,前者是避免元素渲染,后者是元素渲染之后隐藏,前者是开销比较大的,不建议频繁显示和隐藏的操作,后者是相对只是css样式级别的隐藏,所以性能开销会小很多,频繁的显示与隐藏还是可以使用的,但是如果这个元素本身就是一个不需要渲染的,只是渲染一次就不使用了,那么这个时候就建议使用v-if,虽然说不说,但是还是说了一下,我要说的是关于template上面不可以使用v-show的情况
<template v-if="true"> <h1 v-show="true">H1</h1> </template>
这个时候不要在template上面进行使用v-show,第一会报错,第二也不会出现效果,因为templatge是一个不可渲染的元素,所以不建议使用v-show这种css级别的样式操作
列表渲染和条件渲染的爱恨纠葛
因为版本更新之后会发现,v-if的优先级是高于v-for的,那么就意味着如果我们这样写的话,就会报错
arr: [ { id: 0, name: "jim", }, { id: 1, name: "kim", }, { id: 2, name: "tom", }, ],
<span v-for="it in arr" :key="it.id" v-if="it.id !== 2">{{ it.name }}</span>
报错:Uncaught TypeError: Cannot read properties of undefined (reading ‘id’)
建议:You should not mix ‘v-for’ with ‘v-if’
很明显我们使用ID的,但是因为v-if的优先级比较高,所以这个时候,v-for还没有进行渲染结束,导致了v-if获取不到当前属性的id值,所以这也就不奇怪了,如果非要一起使用的话,可以分开写,比如下面的:
<template v-for="it in arr" :key="it.id"> <span v-if="it.id !== 0">{{it.name}}</span> </template>
这个优先级是一个比较小的问题,但是会导致你写代码的时候出现莫名其妙的报错,而且这种报错会让你摸不着头脑,会让你卡住很久代码写不下去,所以需要重视起来
v-for 怎么获取Symbol的属性值
我们正常写对象属性的时候,symbol属性修饰的数据是获取不到,这个时候我们需要进行一个改变写法
user: { name: "jim", age: 18, sex: "男", [Symbol("secret")]: "456", },
<p v-for="(i, k) in user" :key="i">{{ i }} -- {{ k }}</p> <!-- 这样很明显是获取不到symbol的值的 -->
- 通过使用computed的进行获取到所有的key,然后进行user的遍历
computed:{ getOwnKeys() { return Reflect.ownKeys(this.user); }, } // 关于Reflect的用法和替代方案,我在之前的Proxy一篇文章中已经写过,这里就不做赘述了
<p v-for="(i, k) in getOwnKeys" :key="k">{{ user[i] }} - {{ k }} - {{i}}</p> <!-- 因为这里是数组,所以这里的k一定是数组的下标,而不是对应的key 这里需要注意 -->
为什么v-for一定要有key
我一直觉得这个问题很好,但是一只不知道怎么可以比较简单的不说那么高大上的讲明白他的好处,我说他可以提升效率吧,我也不太好演示,就直接说一下大概的意思,然后给你们写一个比较直观的demo看一下效果吧,因为v-for涉及到vue的diff算法,也就是说当我们改变数组本身的时候,他是按照一个位置一个位置进行替换查找的,也就是说当一个位置被替换的时候,那么这个时候他后面的数据就会自然的向后移动,直到没有位置,这样就会导致一个问题,当我们的数组本身发生变化的时候,之前被选中的元素就会因为数组的变化,值和位置错开,比如:
personList: ["jim", "tom", "kim"],
<ul> <li v-for="i in personList"><input type="checkbox" />{{ i }}</li> </ul> <button @click="addPerson">添加一个人</button>
当我们不添加key的时候,我们选中一个tom
当我们点击添加一个人的时候
原来tom的位置就会被jim给替换掉,这是我们不愿意看到的情况,当我们加上key的时候,你选中的就是你选中的,☑️tom不会被因为位置变化而变化
addPerson() { this.personList.unshift("mary"); },
- 修复之后的写法,你可以简单的理解为添加key值是为vue更加方便跟踪我们的节点
<li v-for="i in personList" :key="i"><input type="checkbox" />{{ i }}</li>
数组触发视图更新
这里其实要说一个点就是,vue3之前的版本,我们直接更改数组的时候,视图是做不到直接更新的, 但是最新的版本是通过peoxy进行完全的一个重写,所以最新的版本里面是可以直接进行操作数据,进而实现试图的更新的
arr : [1,23,43,5]
changeArr(){ this.arr[5] = 7 } //这个时候数据就会直接导致视图的更新
当然也可以使用vue重写的一些数组本身的方法
push() pop() shift() unshift() splice() sort() reverse()
上述的方法都是可以直接导致视图的更新
splice() 还是值得一说的,不然总觉得少说了,上面的除了splice()之外的都是比较简单的用法,直接使用就可以了,如果不知道效果的话,就直接自己试试吧,关于splice下面写一个例子
arrNum: [1, 2, 4, 5, 6, 7], this.arrNum.splice(2, 0, 333); //从第二个位置开始插入 插入的内容为333,中间的0不可以少 //1,2,333,4,5,6,7 this.arrNum.splice(3) //从第三个位置开始删除,后面全部删除 //1,2,4 this.arrNum.splice(2, 4, 0, 4); //从第二个位置开始,替换4个 替换为0,4 // 1,2,0,4
事件处理
这个都知道是写一个函数进行处理一些逻辑,当然还是有一些需要注意的点,比如一个按钮同时监听多个事件的时候,写的时候需要添加小括号,比如我们常规的是这样的
<button @click="optionArrNum">测试</button>
直接写方法是可以的,不需要进行添加小括号,当然这个时候我们的函数自带的点击事件也是可以被打印出来的
optionArrNum(e){console.log(e)} //PointEvent
但是当我们需要添加多个点击事件的时候,就必须这样写了
<button @click="clickT($event), clickO($event)">自定义事件</button>
如果你有自带的参数,也可以直接传递,但是多个的时候不可以不写小括号,否则自定义事件是触发不了的,会直接undiefined
v-model
这里我当然不是要讲他是一个语法糖,这是大家都知道的事情,我这里要说的是一个他不使用语法糖的时候原来应该是怎么样的
<input v-model="bindVal" /> <span>{{ bindVal }}</span>
这里我们都知道,当我们输入值的时候,下面的对应的双向绑定的值也是会发生变化的,这是v-model的最基本的用法,当时当我们只是希望他被绑定,但是改变的时候不需要响应别的地方,我们可以这样写
<input :value="bindVal" />
这个时候你再改变bin呆Val的时候,span里面是不会发生改变的
当我们需要进行函数监听改变的时候,我们可以将完整的写法实现
<input :value="bindVal" @input="changeVal" />
changeVal(e) { this.bindVal = e.target.value; },
以上两行代码也是v-model的基本原理实现,也就是他的语法糖
vue组件中data为什么一定要是一个函数
一如既往的解释就没意义,直接看效果,首先data是一个函数的话,可以提供一个函数最基本的特性就是函数作用域,这样函数返回的一个对象都是一个全新的,这是重点,重点就是他的返回对象是一个全新的,这样可以避免同一个组件被多次使用的时候,一个值改变引起别的值变化,下面我们演示一下:
<template> <div> {{ msg }} <button @click="changeMsg">改变组件的值</button> </div> </template> <script> export default { name: "Content", data() { return { msg: "我是一个组件", }; }, methods: { changeMsg() { this.msg = "hello-csdn"; }, }, }; </script>
<script> import Content from "./components/Content.vue"; export default { components: { Content, }, }; </script> <template> <div> <Content></Content> <Content></Content> <Content></Content> </div> </template>
效果就是当我们改变其中一个的msg的时候,另外的组件msg不会发生变化,因为组件的值是不关联的
如果我们没有将对象独立出来,比如下面:
<template> <div> {{ msg }} <button @click="changeMsg">改变组件的值</button> </div> </template> <script> const obj = { msg: "我是一个组件", }; export default { name: "Content", data() { return obj; }, methods: { changeMsg() { this.msg = "hello-csdn"; }, }, }; </script>
这个时候你就会发现,当我们点击改变msg的值的按钮的时候,另外两个组件的值也发生了变化,这个时候数据其实就是被污染了,这就是为什么一定要data是一个函数,说白了就是可以比较有效的进行数据的隔离
prop对象和数组的默认值问题
这里说一下关于我们父组件给子组件传递数据的时候,当我们需要给数组或者对象的时候,我们的默认值需要一个使用一个函数返回的格式进行
//子组件 props: { message: { type: Array, default() { return []; }, }, },
//父组件 data() { message: [1,2,3]; }, <Content :message="message"></Content>
对象也是如此
$parent不太建议频繁使用
一般我们在进行父子组件之间值传递的时候,子组件想要给父组件传递值,是需要使用emit自定义事件的,那么我们获取父组件的数据也比较简单,只需要props进行获取就可以了,但是我们其实还有一种获取父组件的数据的办法,就是$parent,这里不建议频繁的使用这个方法,因为当我们一个组件被多个父组件引用的时候,我们想要精准的获取某一个父组件的数据就会显的比较麻烦也不太精准
mounted() { console.log(this.$parent.msg); //当我们获取msg的时候其实只有父组件一里面是存在的,二里面是不存在的,但是也会去拿,不过会获取到underfined },
- 父组件一
import Hello from "./Hello.vue"; components: { Hello, }, data() { return { msg: "我是一个组件", }; },
- 父组件二
import Hello from "./components/Hello.vue"; components: { Hello, }, data() { return { message: [1, 2, 3], }; },
但是**$root还是可以使用的,因为只有一个根组件,所以不存在复用耦合的问题**
具名插槽需要注意的三个点
- v-slot只能使用在template标签上
我们在想给一个组件动态的传递一些不同的元素的时候,一般可以使用的比较好的方式是插槽,那么我们在使用具名插槽的时候,有一个点需要注意,也是在vue最新版本里面才有的要求,就是我们定义好之后,v-slot只能使用在template标签上
<Content :message="message"> <template v-slot:header> header </template> <template v-slot:body> body </template> </Content>
<template> <div> <slot name="header"></slot> <slot name="body"></slot> </div> </template>
- 具名插槽当有默认值的时候,不提供v-slot也会被渲染
<Content :message="message"> <template v-slot:header> {{ name }} </template> <template v-slot:body> body </template> </Content> <Content></Content>
<slot name="header"><h2>我是一个默认值的H2</h2></slot>
这里会发现,我们第二个引用的Content并没有使用template的v-slot进行具名插槽的引用,但是页面上其实也是出现了我是一个默认值H2的渲染,这就是他的一个需要注意的点,当我们提供了备用值的时候,具名插槽不管你是不是使用了v-slot,只要不传递值,都会被渲染出来
- 作用域的问题
当我们使用插槽的时候,想要使用vue的模板语法获取data里的数据的时候,我们只能获取到当前父组件中的data数据,因为他自己本身的数据是获取不到的,代码:
<template v-slot:header> {{name}} </template>
那么这个name一定是引用组件的这个组件本身具有的才可以,这个就是插槽作用域的问题,需要格外注意
总结一句话就是:父模板的所有内容都是父级作用域下面进行编译的,子组件的所有内容都是子级作用域下面进行编译的
- 作用域插槽
说明白了上一个问题,就要说一下为什么会有作用域插槽了,因为不同的组件中的作用域是不一样的,也就是说我们要在父组件中使用子组件中的数据,我们需要给父传递一个数据过去,在使用插槽的时候
<slot :list="list" :secondData="secondData"></slot>
list: [1, 2, 3, 4, 5], secondData: "helloCSDN",
<Hello> <template v-slot:default="slotProps">{{ slotProps.list }} </template> </Hello>
{ "list": [ 1, 2, 3, 4, 5 ], "secondData": "helloCSDN" } //打印的结果值
跨级数据传递
前面说过,当我们需要进行父子传递数据的时候,有很多种办法,其中最基础的props和emit到后面的parent和children以及root的使用,这里不是介绍provide和inject的用法,是要说一下当我们需要传递动态数据的时候,需要注意的点是什么,我们都知道provide和inject是可以直接跨级也就是无视中间多少级的引用都是可以直接获取传递数据的,这里我们需要注意点是:当我们传递动态数据的时候需要使用函数进行返回,否则是直接报错的
provide: { message: "this is message by provide", }, //根组件 App.vue
inject: ["message"], //子组件 Hello.vues
这里的message是一个常量,不是一个变量,所以这里需要进行传递变量,如果我们直接这样写的话:
provide: { message: this.message, }, //会直接报错
正确的写法是:
provide(){ return{ message : this.message } },
这样就可以直接将父组件中data里面的数据作为参数进行传递
- 另外一个需要注意的点就是provide的值不是一个响应式的,也就是说父组件的改变,并不会引起子组件的变化,只是我们可以通过这种方式进行父组件的获取而已,想要进行改为响应式的这么做呢?我们可以直接将数据改为复杂数据类型,也就是对象的形式,比如:
provide(){ return{ obj : this.obj } }, data() { return { obj:{ message : 'this is jim' } }; },
<button @click="obj.message = 'helloCCCC'">改变obj</button>
inject: ["obj"],
这个时候就会因为父组件的变化而发生变化,当然如果你不想这么麻烦的话,可以换一种比较简单的写法,也就是可以直接使用简单数据类型实现一个响应式的功能
provide() { return { message: () => this.message, }; }, //父组件
{{ message() }} // 这里就需要当做一个函数进行执行才可以,否则是不可以的 inject: ["message"], //子组件
所以两种写法都是可以的,看自己的喜好,个人建议还是使用中间的那种写法, 因为最后这种写法虽然是可以直接实现这样的蕾丝功能,但是你获取数据的时候其实是执行函数进行获取的,所以我们一般情况下函数的执行的根据你的页面渲染的次数进行的,也就是说我们每一次进来的时候其实都会执行,我们在前面写关于计算属性的时候也写过这方便的content,第一个原因是我们一般不希望在我们模板语法里面写比较复杂的逻辑,第二个原因是执行次数过多是影响性能的,到那时如果你非要使用最后一种方法进行实现的话,也是可以优化的,使用计算属性进行获取,利用它本身的缓存机制
computed: { getInjectVal() { return this.message(); }, },
{{ getInject }}
这样就可以利用计算属性的缓存机制进行性能的优化
setup
组合式API是vue最新版本的一个比较重要的更新,这里几个需要注意的点,因为我个人一直习惯使用的是2.0的版本,所以这里我没有很好的项目例子展示给你们,所以就简单的写一下关于setup比较容易忽略的几个知识点
- this为什么不可以被使用
回答这个问题其实是和setup的调用时间有关系,setup是在组件还没有进行初始化的时候就开始被调用了,我们都知道组件中的this是指向的组件本身实例,所以这个时候this其实是没有被创建出来的, 说白一点就是data或者一些别的属性都还没有进行开始加载,所以这个时候我们的this是没办法指向他们的,
- 为什么一定要return出去
vue上一个版本中,如果你想要使用一个data中的变量,只需要在data中进行一个声明就可以了,但是这里还要进行return出去,是不是麻烦 了, 其实我们在上一个版本的时候也是有return出去的,只是我们的data执行也是第一个阶段,我们比较的无感罢了
setup数据默认是不具响应式的
ref的value
这里需要说明一个点就是模板会自动解析value值,因为ref定义的一个变量时响应式的,因为ref本身是一个函数,他返回的是一个对象,那么我们操作值的时候其实是操作的变量本身的value值,但是模板语法进行获取的时候其实是会自动解析value的值的,代码:
setup() { let count = ref(0); function changeCount() { count.value++; //这里需要注意是value } return { count, changeCount, }; } <button @click="changeCount">改变count</button> <span>{{ count }}</span> //这里则不是
- ref定义基本数据类型的响应式,reactive进行定义引用数据类型的响应式
let objV = reactive({ name: "lisi", }); function changeObj() { objV.name = "wangwu"; } <button @click="changeObj">改变Obj</button> <span>{{ objV.name }}</span>
当然不是教给你们怎么使用了,我想说的是,当我们内嵌一些数据变量的时候,想直接获取到数据的值,我们是可以进行解构导出的,但是解构导出的数据要明白,不具有响应式
let objV = reactive({ name: "lisi", }); return { ...objV, }; <span>{{ name }}</span>
这个时候会发现,我们的name不具有响应式了,所以使用的时候,根据自己的实际需求进行吧
- toRefs
当然如果你使用解构的时候,还想要响应式,可以使用toRefs进行包裹,toRefs是一个函数,参数是一个对象
- 方式一:
let { name } = toRefs(objV); return {name}
- 方式二:
return {...toRefs(objV),}
watchEffect
这里就说一下需要注意的点,因为是新版本的东西,所以还是值得提一下的
setup(){ let objV = reactive({ name: "lisi", }); let objN = reactive({ name: "zhaosi", }); function changeObj() { objV.name = "wangwu"; objN.name = "wanger"; } watchEffect(() => { console.log(objV.name); console.log(objN.name); }); }
<button @click="changeObj">改变Obj</button> <span>{{ objV.name }}</span> <span>{{ name }}</span>
- watchEffect 可以监听的是对象 也就是具备了vue2.0里面对象监听的深度监听和立即执行的属性
- 因为他是自动收集依赖,所以这里是不需要进行参数说明的,也就是说,直接回调函数里面的响应式变量就是他会自动收集监听的变量
- 他会自动执行一次,里面有多好响应式的变量,他都会将依赖收集进行监听
watch
顺便提一下watch,因为和之前的版本差异化比较大, 所以这里说一下,watch新版的是两个参数,第一个是需要监听的变量,第二个是回调函数,回调函数的两个参数就是他的新旧值
let msgc = ref("this is msgc"); watch(msgc, (n, o) => { console.log(n); console.log(o); });
这里不要忘记注册watch from vue
computed
计算属性用法和vue之前的版本也有很大的不同,所以这里也记录一下,computed我个人是不太习惯的,因为我还没有进行源码的查看,所以这里对计算属性的设计让我不太适应
setup(){ let msg = ref("hello csdn"); const cover = computed(() => { return msg.value.split(""); }); console.log(cover); //ComputedRefImpl 一个对象 里面的value属性才是我们需要的 return { msg, cover }; }
<span>{{ cover }}</span>
这里比较不适应的是计算属性返回的是一个对象,我们还需要进行进一步value的获取才可以
钩子函数
在vue3中的setup函数里面,钩子函数是可以多次执行的,这里就不演示了,自己多写几遍就可以了
新版本的钩子函数中,有一个回调函数作为参数,当执行的时候,回调函数会自动执行
vue3-props
props这里设计的一个很奇怪的点,我们获取父组件的数据的时候,通过props进行接收,但是因为setup函数没有this,所以我们获取起来就比较困难了,所以这里的setup的形参之一的props就派上用场了,
<template> <div> {{ message }} </div> </template> <script> export default { name: "Content", props: { message: { type: String, default() { return ""; }, }, }, setup(props) { console.log(props); //{message: 'hello csdn'} }, }; </script>
因为setup的是在beforeCreated之前执行的,这个时候我们的数据代理都还没有开始进行,所以this是一定不能用的,但是这里我们使用形式参数props获取到数据之后,我们上面的props的写法还要和之前的一样,这块是需要注意的,所以单纯的看,其实这里是增加了代码量的
- 解构会丢失响应式,但是可以使用toRefs
setup(props) { console.log(props); //{message: 'hello csdn'} //如果使用解构操作,那么会丢失数据本身的响应式,这个时候和之前的操作一样,可以通过torefs进行数据的响应式处理,但是toRefs返回的是一个对象,所以处理之后要进行value的获取 let { message } = toRefs(props); console.log(message.value) },
#### vue3-context
Context是setup的另一个形式参数,这里的context具备的四个参数是attr,expose,emit,slot,看名字就知道什么意思了,这里就不多介绍了,说一个emit和expose,很好玩的一个点是,因为vue是向下兼容的,所以我们在写vue3的时候,其实vue2.0的写法也是同样适用的,但是当我们写了其中一个的2.0的写法的时候,3.0的就会失效,比如
- 父组件
setup() { let msg = ref("hello csdn"); function sendData(val) { console.log(val.value); //这里是失效的,但是如果没有methods的话,这里是生效的 } return { msg, sendData }; }, methods: { sendData(v) { console.log(v.value); //此时这里是生效的 }, },
<Content class="sty" id="id" :message="msg" @sendData="sendData"></Content>
- 子组件
setup(props, context) { let msg = ref("hello csdn"); function sendData() { context.emit("sendData", msg); } return { msg, sendData, }; },
<button @click="sendData">发送数据</button>
expose
关于这个可能会有一点不太好理解,其实也不算难理解,就是不太容易接受他的用法,比如我现在setup最后return的不是一个对象,而是一个渲染函数,这个时候我还希望我可以使用组件中的属性,怎么办呢?这个时候就可以使用expose进行属性的暴露
- 父组件
export default{ methods: { sendData(v) { console.log(v.value); }, }, mounted() { console.log(this.$refs.content); this.$refs.content.sendData(); }, }
<Content class="sty" id="id" ref="content" @sendData="sendData"></Content>
- 子组件
setup(props, context) { let msg = ref("hello csdn"); function sendData() { context.emit("sendData", msg); } context.expose({ sendData, }); return () => h("div", msg.value); },
这里需要注意的是,方法不需要点击就可以执行了,你只需要执行你需要执行的函数就可以
provide and inject
这里要说的一个点是和之前使用的时候略有不同,因为前面说明了ref返回的是一个对象,所以我们在setup中使用的时候需要进行变量.value进行值的操作,但是使用provide的时候我们不可以使用value,这样会丢失响应式
- 父组件
setup() { let name = ref("this is provide"); provide("name", name); function changeName() { name.value = "wanger"; } return { changeName }; },
<button @click="changeName">改变name</button>
- 子组件
let name = inject("name");
注意这里的 provide(“name”, name); 不可以进行 provide(“name”, name.value);
setup语法糖
所谓的setup语法糖其实就是之前setup函数的简写,只是我们需要注意几个点:
- 顶层的绑定会暴露给模板
也就是说我们不需要再进行return出去了,这也是前面写代码的时候一直很纠结的一件事,感觉写了还没有之前的好写,这样的话就不用了
- import组件的时候也不需要使用关键字进行引入了
直接inport之后就可以直接使用组件就可以了
router-view
这里就说一下它的存在的意义,其实router-view我们可以简单的将他理解为一个router-link的占位符即可,这样会比较好理解
<router-link to="/">home</router-link> <router-link to="/about">about</router-link> <router-link to="/use/123">use</router-link> <router-view></router-view>
这里需要注意的是不要忘记添加/
/* * @use: * @description: * @SpecialInstructions: 无 * @Author: clearlove * @Date: 2022-08-02 16:28:12 * @LastEditTime: 2022-08-02 16:51:21 * @FilePath: /vue3STUBYLOCAL/Users/leimingwei/Desktop/LeiMingWei/vue3/vue3byvite/src/router/index.js */ import { createRouter, createWebHashHistory } from 'vue-router'; import About from '../views/about.vue'; import Home from '../views/home.vue'; import User from '../views/user.vue'; const routes = [ { path: '', component: Home }, { path: '/about', component: About }, { path: '/use/:id', component: User }, ] const router = createRouter({ history: createWebHashHistory(), routes }) export default router
/* * @use: * @description: * @SpecialInstructions: 无 * @Author: clearlove * @Date: 2022-07-27 11:42:23 * @LastEditTime: 2022-08-02 16:32:45 * @FilePath: /vue3STUBYLOCAL/Users/leimingwei/Desktop/LeiMingWei/vue3/vue3byvite/src/main.js */ import { createApp } from 'vue' import './style.css' import App from './App.vue' import router from './router' const app = createApp(App) // 这里的顺序不可以错,先挂载路由,再进行app的挂载 app.use(router) app.mount('#app')
动态路由获取页面参数
动态路由很常见了,这里因为有setup的出现,所以需要特殊说明一下,我们知道,获取动态路由页面参数的办法是路由添加:id
然后我们通过this.$route.params可以获取到,但是setup没有this,所以这里说明一下
{ path: '/use/:id', component: User },
<router-link to="/use/123">use</router-link>
<template> <div>use {{ params }}</div> </template> <script> export default { name: "use", data() { return { params: "", }; }, mounted() { this.params = this.$route.params; console.log(this.$route.params); }, methods: {}, }; </script>
vue2中是这样获取页面参数的
但是vue3中是这样的
<script setup> import { useRoute } from "vue-router"; console.log(useRoute()); let params = useRoute().params.id; </script>
- 使用props进行传递 [vue2中]
export default { props: ["id"], name: "use", data() {}, mounted() { console.log(this.id); }, };
{ name: 'use', path: '/use/:id', component: User, props: true, // 意味着我们可以直接通过props的形式进行数据的获取 },
- 使用props进行传递 [vue3中]
<script setup> const props = defineProps({ id : String //路由传递参数一般都默认是String类型 }) console.log(props.id) //即为参数 </script>
当时命名式多页组件在一个页面渲染的时候,我们需要给每一个都设置一个props
{ path: '/shop/:id', components: { default: Main, top: Top, footer: Footer }, props: { default: true, top: false, footer: false } },
特殊页面和参数配置
这里就说一下页面找不到的时候路由应该如何配置
- 找不到页面
{ path : '/:path(.*)', component : NotPage } //这里的位置最好是要放到配置映射表的最后,这样是一个一个找,找不到就默认到了notpage页面
- 对动态路由参数进行格式限制
{ path: '/news/:id(\\d+)', component: News } // 比如路由参数只能是数字 ()里面添加正则表达式即可
- 动态参数有多个参数的时候的情况
{ path: '/news/:id+', component: News }
这样就可以直接传递多个参数进行
- 参数可有可无的情况
{ path: '/news/:id?', component: News } // 虽然是可有可无的,但是不可以重复叠加
- 重定向和重命名 [重定向]
{ path: '/', redirect: '/home' //当访问/的时候,我们需要重定向到home组件 这个前提是home组件必须是存在的 }, { path: '/home', component: Home }, // 反过来也是一样的 { path: '/home', redirect: '/' //当访问/的时候,我们需要重定向到home组件 这个前提是home组件必须是存在的 }, { path: '/', component: Home }, // 换一种写法 { path: '/', redirect: { name: 'home' } }, { path: '/home', name: 'home', component: Home }, //换一种写法 函数的写法 { path: '/', redirect: (to) => { console.log(to) //{fullPath: '/', path: '/', query: {…}, hash: '', name: undefined, …} return { name: 'home' } } }, { path: '/home', name: 'home', component: Home }, // 以上都是可以使用重定向进行操作的
- 重定向和重命名[重命名]
{ path: '/', redirect: { name: 'home' } }, { path: '/home', name: 'home', //alias:'/main', //这个就相当于一个小名,也是可以进行访问的 不要忘记/ alias:['/main','other'],//可以是多个 component: Home },
嵌套路由注意事项
{ path: '/parent', component: Parent, children: [ { path: '', component: Two }, { path: 'one', component: One }, { path: 'two', component: Two } ] },
子页面中的path是不可以写/的,因为浏览器会默认添加一个/,所以添加以后可能会出现找不到页面的情况
route 和 router
一句话:route 是当前活跃的路由对象,router 是当前所有的路由页面 前者是当前的,后者是全局的
第二句话:router-link 的to跳转就相当于使用router.push只是 前者名字是声明式跳转后者叫做编程式跳转
编程式跳转
上面也提到了编程式跳转,其实就是我们说的js跳转页面
- 基础写法
this.$router.push("/about");
- 对象写法
this.$router.push({ path: "/about", });
- 携带参数
this.$router.push({ path: "/use/123", });
- 通过name进行跳转和携带参数
this.$router.push({ name: "use", params: { id: 123 }, }); // 不过这种路由配置的时候需要添加name才可以 如下: { name : 'use', path: '/use/:id', component: User },
- 通过name 使用声明式跳转
<router-link :to="{ name: 'use', params: { id: 789 } }">声明式</router-link>
- 问号传递参数
this.$router.push({ path: "/use", query: { name: "lisi", }, }); // http://127.0.0.1:3030/#/use?name=lisi // 获取页面参数的时候。直接可以使用this.$route.query即可
- 不添加历史记录到router中 方法一
this.$router.push({ path: "/use", replace: true, });
- 不添加历史记录到router中 方法二
this.$router.replace({ path: "/use", });
第二种方式也是可以和push一样添加参数的
- 后退操作
this.$router.go(-1) //后退一步 可以是-2 -3 ... 也可以是1 表示前进一步 this.$router.back() //后退一步 相当于go(-1) 但是back不可以传递参数 this.$router.forword() //前进一步, 相当于go(1)
但页面展示不同区域的组件内容
上面的都是一个页面展示一个默认的组件,但是很多时候项目是需要展示很多组件的,这个时候我们需要的是将多个组件按照我们自己的要求进行一个排放,所以这里说明一下
- 路由文件
import Top from '../views/top.vue'; import Main from '../views/main.vue'; import Footer from '../views/foooter.vue'; { path: '/shop', //这个是我们需要进入的时候的名字,只是一个名字,没有这个组件 components: { default: Main, top : Top, footer : Footer } }, // components 这里使用的是components 而不是component,单个组件的时候使用的是component
- router-view
<router-view name="top"></router-view> <router-view></router-view> <router-view name="footer"></router-view>
这里不写就是默认的default
不同的路由模式、不同的记录模式
- Hash
createWebHashHistory, 也是默认的一种模式,这种模式当后面的改变,不会造成服务器接收请求,因为他不会发请求,除非我们在钩子函数里自己配置,另一个缺点就是有一个#号,不太好看
- history
createWebHistory,其实就是H5模式,他是一种相对比较正常的模式,但是这里依靠后台服务器的配置,如果配置不好的话,会导致直接访问或者刷新的时候404,也就是找不到页面,当然这种模式看起来URL会比较正常一点。官方给的一个简单的处理办法就是当我们发现没有资源的时候,应该重定向到默认页面进行处理,这样会比较好看一些
路由守卫、导航守卫
- 每路守卫:或者叫做单独路由守卫,当我们访问某一个路由的时候会进行一个拦截,别的是不进行拦截的
{ path: '/about', component: About, // 每路守卫 / 单独路由守卫 beforeEnter: (to, from, next) => { console.log(to) console.log(from) next() } },
- 全局守卫:不管哪一个路由都会进行拦截
//全局守卫 router.beforeEach((to, from, next) => { console.log(to) //从哪儿来 console.log(from) //到哪儿去 next() //是否继续通行 })
全局守卫的优先级高于单独路由守卫
- 组件路由守卫
顾名思义就是在组件内可以使用的路由卫士,组件内部使用
beforeRouteEnter(to, from, next) { // ... }, beforeRouteLeave(to, from, next) { // ... }, beforeRouteUpdate(to, from, next) {},
这里需要注意的一个点是:beforeRouteEnter 当他执行的时候,我们data中的数据是获取不到的,也就是说我们直接this.变量是不行的,会直接报错,我们可以通过下面的方式进行获取
beforeRouteEnter(to, from, next) { next((vm)=>{ vm.id // id即是data中定义的变量 这样就可以进行获取 }) },
路由懒加载
这个就比较简单了,直接改为什么时候需要什么时候进行加载就可以了,之前的方式叫做静态导入,项目启动的时候,会直接将所有的路由都进行一个加载,所以性能相对来说没有懒加载的效果好,代码如下:
- 方式一:
{ path: '/about', component: () => import('../views/about.vue'), },
方式二:
const Parent = () => import('../views/parent.vue'); { path: '/parent', component:Parent },
全局状态管理 - provide和inject的方式
比较常见的全局状态管理的方式是vuex,这里先不说,我们说另一种方式,使用provide和inject的方式进行实现一样的效果
- Store/index.js
/* * @use: 用于管理项目中需要全局管理的变量 * @description: 全局状态管理 * @SpecialInstructions: 无 * @Author: clearlove * @Date: 2022-08-03 17:43:34 * @LastEditTime: 2022-08-03 17:54:22 * @FilePath: /vue3STUBYLOCAL/Users/leimingwei/Desktop/LeiMingWei/vue3/vue3byvite/src/store/index.js */ import { reactive } from 'vue' const store = { //存放当前的全局变量 state: reactive({ token: new Date() }), //用于更新当前的状态 updateState() { this.state.token = new Date() } } export default store
- App.vue中提供出去
import Hello from "./components/Hello.vue"; import store from "./store"; export default { provide: { store, }, components: { Hello, }, }
- Hello组件中注入使用
<template> <div> {{ store.state.token }} <button @click="changeStore">改变数据</button> </div> </template> <script> export default { inject: ["store"], name: "Hello", methods: { changeStore() { this.store.updateState(); }, }, }; </script>
Vuex 一般用于相对比较大的一些项目中,小的项目我们可以直接使用这种方式进行即可,关于vuex的教程,大家可以翻看我之前写的,这里就不做文章了。
代理解决get请求的跨域问题
我们都知道,因为浏览器同源策略的原因,不同的域名或者协议等会出现跨域的问题,这个时候如果后端进行配置了允许不同源进行请求的话,是可以的,但是一般为了安全考虑都会按照浏览器的策略进行,但是我们调试项目的时候一般也不想麻烦后端,项目上线之后就不存在这个问题了, 因为一定是同一个服务器下面的,如果不是就放到同一个去,废话不多说了,我就是想说我们前端也可以通过一些方式进行解决这个问题
- 使用vite中的配置进行 vite.config.js
export default defineConfig({ plugins: [vue()], server: { port: 3030, open: true | 'google', proxy: { //path 随便写,这里写什么,请求的时候前面就写什么就好了 '/path': { target: 'https://i.maoyan.com',// 这就是原本应该的地址 changeOrigin: true, // 是否开启代码 允许跨域 rewrite: path => path.replace(/^\/path/, '') //将真正请求的时候的添加的path使用正则替换为空,因为本来就不存在path 是代理的时候加上的,相当于重写路径 } } }, preview: { port: 9000, strictPort: true,//不可以被替换别的端口 } })
axios.get('/path/api......')
- 使用vue-cli 创建的项目进行解决跨域问题 vue.config.js
module.expores = defineConfig({ // 这里的名字不同 devServer: { proxy: { //path 随便写,这里写什么,请求的时候前面就写什么就好了 '/path': { target: 'https://i.maoyan.com', changeOrigin: true, // 这里路径替换的写法不同 pathRewrite:{ '^/path' : '' } } } } })
vuex的基本使用记录
前面说不准备进行记录vuex的基本使用了,但是我看了一下文档因为和之前的用法是有一些区别的,这里还是简单的记录一下
- store.js --> 基本变量存储
/* * @use: vuex的基本使用 * @description: vuex 进行全局状态管理 * @SpecialInstructions: 无 * @Author: clearlove * @Date: 2022-08-03 23:10:44 * @LastEditTime: 2022-08-03 23:11:45 * @FilePath: /vue3STUBYLOCAL/Users/leimingwei/Desktop/LeiMingWei/vue3/vue3byvite/src/vuexStore/index.js */ import { createStore } from "vuex"; const store = createStore({ state() { return { count: 0 } } }) export default store
- main.js
import store from './vuexStore' const app = createApp(App) app.use(store) //使用vuex进行全局状态的管理 因为这里挂载了store 那么全局就会有一个 $store app.mount('#app')
- 组件使用
<h4>{{ $store.state.count }}</h4>
- 改变store中的数据 --> 方式一 (不建议)
不建议这样做的原因是这样做vuex就不能跟踪到实例中数据的变化了,官方的建议也是通过一个函数进行改变数据 也是唯一的途径 这样可以直接进行跟踪数据的变化,否则是无法进行数据追踪的,这里是需要注意的一个点
<button @click="$store.state.count++">改变store</button>
- 改变store中的数据–> 方式二 (推荐)
import { createStore } from "vuex"; const store = createStore({ state() { return { count: 0 } }, mutations: { changeState(state, val) { state.count += val } } }) export default store
下面的代码不再贴全部的
<button @click="changeMutations">通过mutations进行改变数据</button>
methods:{ changeMutations(){ this.$store.commit('changeState',1) } }
mutaion需要注意的是,他是一个同步函数,后面会配合action的异步函数进行使用
- 当然也可以通过对象的方式进行传递参数
changeMutations() { // this.$store.commit("changeState", 1); this.$store.commit({ type: "changeState", val: 1, }); },
获取的时候就要稍微改变一下
changeState(state, val) { state.count += val.val }
- getters
Getters 可以理解为计算属性,它具有依赖缓存的功能,是一个对象,里面返回一个一个的函数
getters: { optionMsg(state) { return state.msg + 'nb' } },
使用方法:
<h4>{{ $store.getters.optionMsg }}</h4>
/** * * @param {*} state 当前state中的属性 * @param {*} getterVal 当前getter中的属性 */ otherGetters(state, getterVal) { console.log(state) console.log(getterVal.optionMsg) }
- action
action是用来处理异步函数的,action提交的其实是mutation 而不是直接变更状态,action中是可以处理任意异步操作的
// 异步方法 actions: { /** * * @param { } context 就是当前store的实例对象 包含了当前的state和getters和mutation的属性和方法 */ getDatas(context) { console.log(context) //{getters: {…}, state: Proxy, rootGetters: {…}, dispatch: ƒ, commit: ƒ, …} //.... 可以实现很多异步操作和请求 比如一些fetch请求 axios请求等等 } }
- 调用mutation中的方法
actions: { // payload 当前需要传递的值 optionsMutations(context,payload) { context.commit({ type: 'changeState', val: 3 }) } }
我们不建议直接改变state中的值,这里需要通过mutation中的方法进行改变
- 调用action中的函数
this.$store.dispatch('functionName','params') changeAction() { this.$store.dispatch("optionsMutations"); },
- 模块化 操作
模块化的概念已经很多了,目的都是一个,都是为了代码的可维护性增强,这里不多做解释
const mudulesOne = { state() { return { name: 'jim' } } } const store = createStore({ state() { return { count: 0, msg: 'hellocsdn' } }, modules: { one: mudulesOne }, )}
- 取值
<!-- 这里如果是state属性里面的,就需要进行模块名字加上变量名字 如果是getters里面的,则直接可以进行获取,不需要添加模块名字,因为它会自动添加到模块里面 --> <h3>{{ $store.state.one.name }}</h3>
- 最好是分离出去
/* * @use: 分离出去的模块 * @description: * @SpecialInstructions: 无 * @Author: clearlove * @Date: 2022-08-04 14:43:24 * @LastEditTime: 2022-08-04 14:43:25 * @FilePath: /vue3STUBYLOCAL/Users/leimingwei/Desktop/LeiMingWei/vue3/vue3byvite/src/vuexStore/user/index.js */ const mudulesOne = { state() { return { name: 'jim' } } } export default mudulesOne
- 使用
const store = createStore({ modules: { one: user }, })
- 辅助函数
顾名思义,辅助函数就是不用也可以,但是用了可以提升一些效率的写法,这里官方给到的也比较全面,我这里简单的写一个demo 他需要借助vuex本身的mapstate进行处理
import { mapState } from "vuex"; //test 即为vuex store里面定义的state的变量属性 computed: mapState({ //test: (state) => state.test, //写法一 test: "test", //写法二 }),
computed: mapState(["test"]), //写法三
- 全部用法
import { mapState, mapMutations, mapActions, mapGetters } from "vuex"; computed: { ...mapGetters(["getterFunctionName"]), //解构getters ...mapState({ test: (state) => state.test, //解构state }), }, methods: { ...mapMutations(["changeState"]), //解构mutation ...mapActions(["actionFunctionName"]), //解构actions //使用 就不用commit或者dispacth 直接使用函数名字就可以了 changeMutations() { this.changeState({ val: 1 }); }, }
- 命名空间
上面说了,当我们使用模块化的时候,state是需要进行加上模块名字进行使用的,但是getter和mutation和action都是不需要的,他们都是默认混入进去到一起的,所以当我们比较多的操作的时候,就会比较难以分辨他们,这个时候就需要命名空间进行区分
- 在之前的user.js 里面添加一个getNum
const mudulesOne = { namespaced: true, //默认就是false state() { return { name: 'jim', num: 31415926 } }, getters: { getNum(state) { return state.num } } } export default mudulesOne
- 命名空间为false 的时候使用
<h4>{{ $store.getters.getNum }}</h4>
- 命名空间为true的时候使用
<h4>{{ $store.getters["one/getNum"] }}</h4>
注意:使用命名空间之后 我们调用方法什么写法也要加上那个前面的模块名字
...mapMutations('one',["changeState"]), //解构mutation 使用命名空间且使用辅助解构函数
changeMutations(){ this.$store.commit('one/changeState',1) } // 使用命名空间但是不使用辅助解构函数
注意:当前组件如果也想要使用计算属性的时候,会发现就不行了,这个时候我们需要使用扩展运算符进行兼容
computed: { // ...mapState([ // "test", //写法二 // ]) ...mapState({ test: (state) => state.test, //写法一 }), //other 需要的计算属性方法 },
- 使用
mounted() { console.log(this.test); //this is test },
<h4>{{ test }}</h4>
注意事项
getters 有三个参数(state, getters,rootstate) 分别是当前模块的state数据,当前实例的getters对象属性,根结点的state属性
写在后面
看了一下篇幅有点长了,这里就不写了,后面的还有一些自定义指令和混入等知识点,后面有时间再更新吧!感谢大家的阅读!