1:vue特性
数据驱动视图:数据的变化会驱动视图自动更新
双向数据绑定
2:Vue的优点?Vue的缺点?
优点:渐进式,组件化,轻量级,虚拟dom,响应式,单页面路由,数据与视图分开
缺点:单页面不利于seo,不支持IE8以下,首屏加载时间长
为什么说Vue是一个渐进式框架?
渐进式:通俗点讲就是,你想用啥你就用啥,咱也不强求你。你想用component就用,不用也行,你想用vuex就用,不用也可以
3:常用vue指令
4:vue事件修饰符
5:为什么data是个函数并且返回一个对象呢?
data之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染。
6:v-for为啥要使用key,用index作为key可以吗?
什么是Vnode,他其实就是虚拟节点,本质上是一个javascrpt对象。很多个虚拟节点组成虚拟Dom(Vnode tree)。
diff算法:新旧vnods进行对比的过程
比如在b后面插入一个F,渲染到页面
<template>
<i v-for="item in arr">{{ item }}</i>
<button @click='charu'>插入F<button>
<template>
let arr = ['a','b','c''d]
let charu = ()=>{
arr.splice(2,0,'F')
}
有几种diff算法,一是销毁先前的Vnods,全部重新创建,二是把c变成f,d变成c,再加一个d。还有则是直接插入。
有无key决定了用那种,有key调用patchKeyedChildre,没有则是上面第二种。
没有key的做法:
遍历长度短的vnods,新旧vnode一一做比较,patch,相同,不改变,不同,改变,然后判断新旧vnode长度,新的比较长就会创建新节点。
有key的做法: 通过key可以精准的判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个patch过程更高效,减少DOM操作,提高性能
总结
用index做key也会影响性能。
7:template元素
template元素可以被当做不可见 ,当依赖的数据不发生变化,不会重新计算。变化,重新计算。
9:侦听器
watchEffect
为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 函数。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
停止侦听
当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听:
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
清除副作用
副作用函数执行时机
watch侦听器
他不会立即执行
侦听单个数据源:
侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref:
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听ref const count = ref(0) watch(count, (count, prevCount) => { /* ... */ }) 侦听器还可以使用数组同时侦听多个源: const firstName = ref('') const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => { console.log(newValues, prevValues) })
firstName.value = 'John' // logs: ["John", ""] ["", ""] lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""] 侦听响应式对象,立即执行 watch( () => state, (state, prevState) => { console.log('deep', state.attributes.name, prevState.attributes.name) }, { deep: true,immediately:true } )
10:vue生命周期
11:注册组件
通过components注册的数私有组件,可以通过vue.commponent('name',component)注册全局组件。
12:组件之间传值
-
父组件传值给子组件,子组件使用
props进行接收//子定义属性 //父组件 <test :name="name"></test> //子组件 props: { name: { type: String, default: 'zs', }, },传入一个对象的所有 property
如果想要将一个对象的所有 property 都作为 prop 传入,可以使用不带参数的
v-bind(用v-bind代替:prop-name)。例如,对于一个给定的对象post:post: { id: 1, title: 'My Journey with Vue' }下面的模板:
<blog-post v-bind="post"></blog-post>等价于:
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>Prop 验证
app.component('my-component', { props: { // 基础的类型检查 (`null` 和 `undefined` 值会通过任何类型验证) propA: Number, // 多个可能的类型 propB: [String, Number], // 必填的字符串 propC: { type: String, required: true }, // 带有默认值的数字 propD: { type: Number, default: 100 }, // 带有默认值的对象 propE: { type: Object, // 对象或数组的默认值必须从一个工厂函数返回 default() { return { message: 'hello' } } }, // 自定义验证函数 propF: { validator(value) { // 这个值必须与下列字符串中的其中一个相匹配 return ['success', 'warning', 'danger'].includes(value) } }, // 具有默认值的函数 propG: { type: Function, // 与对象或数组的默认值不同,这不是一个工厂函数——这是一个用作默认值的函数 default() { return 'Default function' } } } })我们可以为组件的 prop 指定验证要求,例如你知道的这些类型。如果有一个要求没有被满足,则 Vue 会在浏览器控制台中警告你。这在开发一个会被别人用到的组件时尤其有帮助。
为了定制 prop 的验证方式,你可以为
props中的值提供一个带有验证要求的对象,而不是一个字符串数组。例如: -
子组件传值给父组件,子组件使用
$emit+事件对父组件进行传值
- 使用组件的v-model进行传值 在子组件中
<template>
<div class="test">
<input type="text" v-model="values" />
</div>
</template>
<script>
import { ref, watch } from 'vue'
export default {
props: {
list: {
type: String,
required: true,
},
},
emits: ['update:list'],
setup(props, { emit }) {
let values = ref(props.list)
watch(values, (newVal) => {
emit('update:list', newVal)
})
return {
values,
}
},
}
在父组件中
<test v-model:list="list"></test>
setup() {
let list = ref('李四')
let ccc = () => {
console.log(list.value)
}
return {
list,
ccc,
}
},
- 组件中可以使用
$parent和$children获取到父组件实例和子组件实例,进而获取数据 - 使用
$attrs和$listeners,在对一些组件进行二次封装时可以方便传值,例如A->B->C - 使用
$refs获取组件实例,进而获取数据 - 使用
Vuex进行状态管理 - 使用
eventBus进行跨组件触发事件,进而传递数据 - 使用
provide和inject,官方建议我们不要用这个,我在看ElementUI源码时发现大量使用 - 使用浏览器本地缓存,例如
localStorage
13:组件样式冲突问题
默认情况下,写在.vue文件中的样式会全局生效,容易造成样式冲突问题。加Scoped解决。
产生冲突的原因:
1.单页面引用程序中,左右组组件的DOm结构,都是基于唯一的index.html页面呈现的
2.每个组件中的样式,都会影响index.html页面中的Dom元素。
当引用第三方库的时候,修改样式加/deep/
14:ref引用
可以使用ref获取一个vue组件的实例,可以用来传数据或者方法。
15:nextTick(cb)
理解nexttick,当数据更新时,dom渲染之后,会自动执行nextTick中的回调函数,vue中的渲染是异步的,通俗点说就是,同一事件循环内多次修改,会统一进行一次视图更新,这样才能节省性能嘛,比如当我们点击添加一个元素的子元素,然后获取他子元素的个数,会发现慢一拍,当我们点击,添加元素时,不会马上渲染dom,而是在下一个tick才会渲染。
nextTick使用场景 在修改数据,需要获取到最新的DOM的时候可以使用
16:keepAlive
keepAlive生命周期函数
动态组件
vue提供了用于动态组件的渲染
17:插槽
插槽是vue为组件的封装者提供的能力,允许开发者在封装组件时,把不确定的,希望由用户指定的部分定义为插槽。
插槽内容
将 <slot> 元素作为承载分发内容的出口。它允许你像这样合成组件:
<todo-button>
Add todo
</todo-button>
然后在 <todo-button> 的模板中,你可能有:
<!-- todo-button 组件模板 -->
<button class="btn-primary">
<slot></slot>
</button>
符串只是开始!插槽还可以包含任何模板代码,包括 HTML:或其他组件:
具名插槽
简单来说就是给插槽添加名字
slot> 元素有一个特殊的 attribute:name。通过它可以为不同的插槽分配独立的 ID,也就能够以此来决定内容应该渲染到什么地方:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
一个不带 name 的 <slot> 出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称,v-slot可以简写为 # :
base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
渲染作用域
请记住这条规则:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
作用域插槽
有时让插槽内容能够访问子组件中才有的数据是很有用的。当一个组件被用来渲染一个项目数组时,这是一个常见的情况,我们希望能够自定义每个项目的渲染方式。
要使 item 在父级提供的插槽内容上可用,我们可以添加一个 <slot> 元素并将其作为一个 attribute 绑定:
<ul>
<li v-for="( item, index ) in items">
<slot :item="item"></slot>
</li>
</ul>
绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在,在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
</todo-list>
动态插槽名
动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
18:自定义指令
它的作用价值在于当开发人员在某些场景下需要对普通 DOM 元素进行操作。
在Vue中,指令其实就是特殊的属性,Vue会根据指令,在背后做一些事,至于具体做什么事,Vue根据不同的指令会执行不同的操作。
Vue`指令有个明显的特点就是,都是以`v-`开头,例如:`v-text
分为全局自定义指令,局部自定义指令。
然而,在 Vue 3 中,我们为自定义指令创建了一个更具凝聚力的 API。正如你所看到的,它们与我们的组件生命周期方法有很大的不同,即使钩子的目标事件十分相似。我们现在把它们统一起来了:
- created - 新增!在元素的 attribute 或事件监听器被应用之前调用。
- bind → beforeMount
- inserted → mounted
- beforeUpdate:新增!在元素本身被更新之前调用,与组件的生命周期钩子十分相似。
- update → 移除!该钩子与
updated有太多相似之处,因此它是多余的。请改用updated。 - componentUpdated → updated
- beforeUnmount:新增!与组件的生命周期钩子类似,它将在元素被卸载之前调用。
- unbind -> unmounted
最终的 API 如下:
const MyDirective = {
created(el, binding, vnode, prevVnode) {}, // 新增 在元素的 attribute 或事件监听器被应用之前调用。
beforeMount() {},// 当指令第一次绑定到元素并且在挂载父组件之前调用
mounted() {},// 在绑定元素的父组件被挂载后调用
beforeUpdate() {}, // 新增 在更新包含组件的 VNode 之前调用
updated() {}, // 在包含组件的 VNode及其子组件的 VNode更新后调用
beforeUnmount() {}, // 新增 在卸载绑定元素的父组件之前调用
unmounted() {} // 当指令与元素解除绑定且父组件已卸载时,只调用一次
}
在Vue3中可以通过应用实例身上的directive()注册一个全局自定义指令。
<p v-highlight="'yellow'">以亮黄色高亮显示此文本</p>
const app = Vue.createApp({})
app.directive('highlight', {
beforeMount(el, binding, vnode) {
el.style.background = binding.value
}
})
19:路由
什么是前端路由:Hash地址与组件之间的对应关系。
路由有哪些模式呢?又有什么不同呢?
- hash模式:通过
#号后面的内容的更改,触发hashchange事件,实现路由切换 - history模式:通过
pushState和replaceState切换url,实现路由切换,需要后端配合
to#
-
详细内容:
表示目标路由的链接。当被点击后,内部会立刻把
to的值传到router.push(),所以这个值可以是一个string或者是描述目标位置的对象。
<!-- 字符串 -->
<router-link to="/home">Home</router-link>
<!-- 渲染结果 -->
<a href="/home">Home</a>
<!-- 使用 v-bind 的 JS 表达式 -->
<router-link :to="'/home'">Home</router-link>
<!-- 同上 -->
<router-link :to="{ path: '/home' }">Home</router-link>
<!-- 命名的路由 -->
<router-link :to="{ name: 'user', params: { userId: '123' }}">User</router-link>
<!-- 带查询参数,下面的结果为 `/register?plan=private` -->
<router-link :to="{ path: '/register', query: { plan: 'private' }}">
Register
</router-link>
replace#
-
类型:
boolean -
默认值:
false -
详细内容:
设置
replace属性的话,当点击时,会调用router.replace(),而不是router.push(),所以导航后不会留下历史记录。
<router-link to="/abc" replace></router-link>
占位符
路由链接
路由重定向
路由名称
嵌套路由
动态路由
获取动态路由的值
NotFound
动态添加路由
动态删除路由
路由导航守卫
router-link的v-slot
router-view的v-slot
20:vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
安装 Vuex 之后,让我们来创建一个 store。创建过程直截了当——仅需要提供一个初始 state 对象和一些 mutation:
import { createApp } from 'vue'
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
state () {
return {
count: 0
}
},
mutations: {
increment (state) {
state.count++
}
}
})
const app = createApp({ /* 根组件 */ })
// 将 store 实例作为插件安装
app.use(store)
现在,你可以通过 store.state 来获取状态对象,并通过 store.commit 方法触发状态变更:
store.commit('increment')
console.log(store.state.count) // -> 1
在 Vue 组件中, 可以通过 this.$store 访问store实例。现在我们可以从组件的方法提交一个变更:
methods: {
increment() {
this.$store.commit('increment')
console.log(this.$store.state.count)
}
}
getter
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。
他可以返回一个函数,传入参数
store.getters.totalPriceCounteGerater(2) // -> { id: 2, text: '...', done: false }
mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个 回调函数 (handler) 。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const store = createStore({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
你不能直接调用一个 mutation 处理函数。这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”要唤醒一个 mutation 处理函数,你需要以相应的 type 调用 store.commit 方法:
store.commit('increment')
你可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload) :
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
store.commit('increment', 10)
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
对象风格的提交方式
提交 mutation 的另一种方式是直接使用包含 type 属性的对象:
store.commit({
type: 'increment',
amount: 10
})
使用常量替代 Mutation 事件类型
使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = createStore({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// 修改 state
}
}
})
用不用常量取决于你——在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。
一条重要的原则就是要记住 mutation 必须是同步函数。
Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。
分发Action
Action 通过 store.dispatch 方法触发:
store.dispatch('increment')
Actions 支持同样的载荷方式和对象方式进行分发:
/ 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
来看一个更加实际的购物车示例,涉及到调用异步 API 和分发多重 mutation:
actions: {
checkout ({ commit, state }, products) {
// 把当前购物车的物品备份起来
const savedCartItems = [...state.cart.added]
// 发出结账请求,然后乐观地清空购物车
commit(types.CHECKOUT_REQUEST)
// 购物 API 接受一个成功回调和一个失败回调
shop.buyProducts(
products,
// 成功操作
() => commit(types.CHECKOUT_SUCCESS),
// 失败操作
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
Moudle
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
命名空间
默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:
const store = createStore({
modules: {
account: {
namespaced: true,
// 模块内容(module assets)
state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: () => ({ ... }),
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 进一步嵌套命名空间
posts: {
namespaced: true,
state: () => ({ ... }),
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
22:动态class动态style
动态class对象:<div :class="{ 'is-active': true, 'red': isRed }"></div>
动态class数组:<div :class="['is-active', isRed ? 'red' : '' ]"></div>
动态style对象:<div :style="{ color: textColor, fontSize: '18px' }"></div>
动态style数组:<div :style="[{ color: textColor, fontSize: '18px' }, { fontWeight: '300' }]"></div>
23:为什么v-if和v-for不建议用在同一标签?
在Vue2中,v-for优先级是高于v-if的,咱们来看例子
<div v-for="item in [1, 2, 3, 4, 5, 6, 7]" v-if="item !== 3">
{{item}}
</div>
上面的写法是v-for和v-if同时存在,会先把7个元素都遍历出来,然后再一个个判断是否为3,并把3给隐藏掉,这样的坏处就是,渲染了无用的3节点,增加无用的dom操作,建议使用computed来解决这个问题:
<div v-for="item in list">
{{item}}
</div>
computed() {
list() {
return [1, 2, 3, 4, 5, 6, 7].filter(item => item !== 3)
}
}
24:不需要响应式的数据应该怎么处理?
在我们的Vue开发中,会有一些数据,从始至终都未曾改变过,这种死数据,既然不改变,那也就不需要对他做响应式处理了,不然只会做一些无用功消耗性能,比如一些写死的下拉框,写死的表格数据,这些数据量大的死数据,如果都进行响应式处理,那会消耗大量性能。
vue2用下面的方法,vue3直接写
// 方法一:将数据定义在data之外
data () {
this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
return {}
}
// 方法二:Object.freeze() 阻止修改现有属性的特性和值,并阻止添加新属性。
data () {
return {
list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
}
}
25: 直接arr[index] = xxx无法更新视图怎么办?为什么?怎么办?
原因:Vue没有对数组进行Object.defineProperty的属性劫持,所以直接arr[index] = xxx是无法更新视图的
使用数组的splice方法,arr.splice(index, 1, item)
使用Vue.$set(arr, index, value)
26:审查元素时发现data-v-xxxxx,这是啥?
这是在标记vue文件中css时使用scoped标记产生的,因为要保证各文件中的css不相互影响,给每个component都做了唯一的标记,所以每引入一个component就会出现一个新的'data-v-xxx'标记
27:v-model语法糖如何实现?
通过v-on和v-bind实现
<div id="app">
{{msg}}
<!-- 将value属性和msg数据进行绑定,那么msg数据变化时也会影响到value -->
<div><input v-bind:value='msg' v-on:input='handle' type="text"></div>
</div>
<script src="../vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: "#app",
data: {
msg: 'hello'
},
methods: {
handle: function (e) {
//表格的value值发生变化时,msg的值也会变化 ,同时msg的值也是value值
this.msg = e.target.value;
}
}
})
</script>
在组件上使用v-model
在父组件中
因此,需要在子组件中申明自定义属性modelValue,以及发出一个事件update:model-value,以及可以传入值
28:vue2和vue3
✨ 开篇
RT,我们为什么要上 Vue3?使用 Vue3 影响我开法拉利吗?
最近在 Vue3 发布了3.2 大版本之后,掘金上关于 Vue3 的文章越来越多,本来想着让子弹再飞一会,但最近公司上了 Vue3 的项目,自己也跟着学了起来。
昨天还是 Vue2 的 Options API 的忠实信徒,结果今天搞了 Vue3 的 Composition API 之后,直呼 Vue3 真香!
接下来,我们从分析 Vue2 优缺点入手,以及结合图片和用例来了解 Vue3 的优势。
Vue2 ⚔ Vue3
Vue2
优点
不可否认 Vue2 在取得的成功,在 Github 的 Front-End 分类的排行榜上也能看到 Vue 仓库的排名是第一,粉丝足够多,活跃用户足够多
比 React 足足多了 12k+ star
从我的角度来看,最大的功臣莫过于这三个:
- 响应式数据
- Options API
- SFC
除去这三位大将,不可或缺 Vuex/Vue-Router 这两位功臣,以及丰富的周边插件和活跃的社区。
缺点
在一个组件仅承担单一逻辑的时候,使用 Options API 来书写组件是很清晰的。
但是在我们实际的业务场景中,一个父组件总是要包含多个子组件,父组件需要给子组件传值、处理子组件事件、直接操作子组件以及处理各种各样的调接口的逻辑,这时候我们的父组件的逻辑就会变得复杂。
我们从代码维护者的角度出发,假设这个时候有 10 个函数方法,自然我们要把他们放入到methods中,而这个 10 个函数方法又分别操作 10 个数据,这个 10 个数据又分别需要进行Watch操作。
这时候,我们根据Vue2的Options API的写法,就写完了 10 个method、10 个data、10 个watch,我们就将本来 10 个的函数方法,分割在 30 个不同的地方。
这时候父组件的代码,对代码维护者来说是很不友好的。
可能有人说,这么写可以增加代码量,老板夸我牛逼!哈哈哈哈 😂
Vue3
优点
自由,自由,还是 TM 的自由!
- 更强的性能,更好的 tree shaking
- Composition API + setup
- 更好地支持 TypeScript
可能的缺点
Vue3: 让你自由过了火
使用Composition API在setup这个舞台上尽情的表演之后,可能存在一个问题:那就是如何优雅地组织代码?
代码不能优雅的组织,在代码量上去之后,也一样很难维护。
SFC 写法变化
Vue2 一个普通的 Vue 文件
<template>
<div>
<p>{{ person.name }}</p>
<p>{{ car.name }}</p>
</div>
</template>
<script>
export default {
name: "Person",
data() {
return {
person: {
name: "小明",
sex: "male",
},
car: {
name: "宝马",
price: "40w",
}
};
},
watch:{
'person.name': (value) => {
console.log(`名字被修改了, 修改为 ${value}`)
},
'person.sex': (value) => {
console.log(`性别被修改了, 修改为 ${value}`)
}
},
methods: {
changePersonName() {
this.person.name = "小浪";
},
changeCarPrice() {
this.car.price = "80w";
}
},
};
</script>
复制代码
采用 Vue3 Composition API 进行重写
<template>
<p>{{ person.name }}</p>
<p>{{ car.name }}</p>
</template>
<script lang="ts" setup>
import { reactive, watch } from "vue";
// person的逻辑
const person = reactive<{ name: string; sex: string }>({
name: "小明",
sex: "male",
});
watch(
() => [person.name, person.sex],
([nameVal, sexVal]) => {
console.log(`名字被修改了, 修改为 ${nameVal}`);
console.log(`名字被修改了, 修改为 ${sexVal}`);
}
);
function changePersonName() {
person.name = "小浪";
}
// car的逻辑
const car = reactive<{ name: string; price: string }>({
name: "宝马",
price: "40w",
});
function changeCarPrice() {
car.price = "80w";
}
</script>
复制代码
采用 Vue3 自定义 Hook 的方式,进一步拆分
<template>
<p>{{ person.name }}</p>
<p>{{ car.name }}</p>
<p>{{ animal.name }}</p>
</template>
<script lang="ts" setup>
import { usePerson, useCar, useAnimal } from "./hooks";
const { person, changePersonName } = usePerson();
const { car } = useCar();
</script>
复制代码
// usePerson.ts
import { reactive, watch } from "vue";
export default function usePerson() {
const person = reactive<{ name: string; sex: string }>({
name: "小明",
sex: "male",
});
watch(
() => [person.name, person.sex],
([nameVal, sexVal]) => {
console.log(`名字被修改了, 修改为 ${nameVal}`);
console.log(`名字被修改了, 修改为 ${sexVal}`);
}
);
function changePersonName() {
person.name = "小浪";
}
return {
person,
changePersonName,
};
}
复制代码
// useCar.ts
import { reactive } from "vue";
export default function useCar() {
const car = reactive<{ name: string; price: string }>({
name: "宝马",
price: "40w",
});
function changeCarPrice() {
car.price = "80w";
}
return {
car,
changeCarPrice,
};
}
复制代码
对比完之后,我们会发现,Vue3 可以让我们更好组织代码。person和car的逻辑都被单独放置在一块
仅仅是代码组织的优势吗?不不不,我们再看看模板中的一些变化
<template>标签中起始便签可以不用<div>标签,因为Vue3提供了片段的能力,使用Vue-detools中查看,可以看到有fragment的标记Watch也不需要罗列多个了,Vue3中支持侦听多个源- 很自然的使用
TypeScript,类型约束更加简单
性能方面
Vue3 主要在这几个方面进行了提升
- 编译阶段。对 diff 算法优化、静态提升等等
- 响应式系统。
Proxy()替代Object.defineProperty()监听对象。监听一个对象,不需要再深度遍历,Proxy()就可以劫持整个对象 - 体积包减少。Compostion API 的写法,可以更好的进行 tree shaking,减少上下文没有引入的代码,减少打包后的文件体积
- 新增
片段特性。Vue 文件的<template>标签内,不再需要强制声明一个的<div>标签,节省额外的节点开销
29:vue2和vue3劫持数据的不同之处
vue2使用Object.defineProperty(),当我们通过数组的下标去修改数组值,或者向对象添加添加属性时,对应的视图都无法进行更新,这是因为我们所添加的数据不是响应式的,无法响应视图的变化
- Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
- Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
我们可以通过 set(this.arr,0,‘xx’)来去修改数组下标对应的值,对于对象可以通过this.$set(this.obj,‘a’,‘xxx’)向对象新增属性a
vue3使用proxy代理,Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性