Composition API 与 Options API 有什么不同
分析
Vue3
最重要更新之一就是Composition API
,它具有一些列优点,其中不少是针对Options API
暴露的一些问题量身打造。是Vue3
推荐的写法,因此掌握好Composition API
应用对掌握好Vue3
至关重要
What is Composition API?(opens new window)
体验
Composition API
能更好的组织代码,下面用composition api
可以提取为useCount()
,用于组合、复用
compositon api 提供了以下几个函数:
setup
ref
reactive
watchEffect
watch
computed
toRefs
生命周期的hooks
回答范例
Composition API
是一组API
,包括:Reactivity API
、生命周期钩子
、依赖注入
,使用户可以通过导入函数方式编写vue
组件。而Options API
则通过声明组件选项的对象形式编写组件
Composition API
最主要作用是能够简洁、高效复用逻辑。解决了过去Options API
中mixins
的各种缺点;另外Composition API
具有更加敏捷的代码组织能力,很多用户喜欢Options API
,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API
则可以将它们有效组织在一起。最后Composition API
拥有更好的类型推断,对 ts 支持更友好,Options API
在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API
时获得类型推断,然而还是没办法用在mixins
和provide/inject
上
Vue3
首推Composition API
,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API
仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API
会获得更大收益
可能的追问
Composition API
能否和Options API
一起使用?
可以在同一个组件中使用两个script
标签,一个使用 vue3,一个使用 vue2 写法,一起使用没有问题
<!-- vue3 -->
<script setup>
// vue3写法
</script>
<!-- 降级vue2 -->
<script>
export default {
data() {},
methods: {}
}
</script>
复制代码
v-if 和 v-show 区别
v-show
隐藏则是为该元素添加css--display:none
,dom
元素依旧还在。v-if
显示隐藏是将dom
元素整个添加或删除
编译过程:v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show
只是简单的基于css
切换
编译条件:v-if
是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
v-show
由false
变为true
的时候不会触发组件的生命周期
v-if
由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子,由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法
性能消耗:v-if
有更高的切换消耗;v-show
有更高的初始渲染消耗
v-show 与 v-if 的使用场景
v-if
与 v-show
都能控制dom
元素在页面的显示
v-if
相比 v-show
开销更大的(直接操作dom节
点增加与删除)
如果需要非常频繁地切换,则使用 v-show 较好
如果在运行时条件很少改变,则使用 v-if
较好
v-show 与 v-if 原理分析
v-show
原理
不管初始条件是什么,元素总是会被渲染
我们看一下在 vue 中是如何实现的
代码很好理解,有transition
就执行transition
,没有就直接设置display
属性
// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display
if (transition && value) {
transition.beforeEnter(el)
} else {
setDisplay(el, value)
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el)
}
},
updated(el, { value, oldValue }, { transition }) {
// ...
},
beforeUnmount(el, { value }) {
setDisplay(el, value)
}
}
复制代码
v-if
原理
v-if
在实现上比v-show
要复杂的多,因为还有else
else-if
等条件需要处理,这里我们也只摘抄源码中处理 v-if
的一小部分
返回一个node
节点,render
函数通过表达式的值来决定是否生成DOM
// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// ...
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
})
}
)
复制代码
v-if 和 v-for 哪个优先级更高
实践中不应该把v-for
和v-if
放一起
在vue2
中,v-for
的优先级是高于v-if
,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件,哪怕我们只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表,这会比较浪费;另外需要注意的是在vue3
中则完全相反,v-if
的优先级高于v-for
,所以v-if
执行时,它调用的变量还不存在,就会导致异常
通常有两种情况下导致我们这样做:
为了过滤列表中的项目 (比如 v-for="user in users" v-if="user.isActive"
)。此时定义一个计算属性 (比如 activeUsers
),让其返回过滤后的列表即可(比如users.filter(u=>u.isActive)
)
为了避免渲染本应该被隐藏的列表 (比如 v-for="user in users" v-if="shouldShowUsers"
)。此时把 v-if
移动至容器元素上 (比如 ul
、ol
)或者外面包一层template
即可
文档中明确指出永远不要把 v-if
和 v-for
同时用在同一个元素上,显然这是一个重要的注意事项
源码里面关于代码生成的部分,能够清晰的看到是先处理v-if
还是v-for
,顺序上vue2
和vue3
正好相反,因此产生了一些症状的不同,但是不管怎样都是不能把它们写在一起的
vue2.x 源码分析
在 vue 模板编译的时候,会将指令系统转化成可执行的render
函数
编写一个p
标签,同时使用v-if
与 v-for
<div id="app">
<p v-if="isShow" v-for="item in items">
{{ item.title }}
</p>
</div>
复制代码
创建vue
实例,存放isShow
与items
数据
const app = new Vue({
el: "#app",
data() {
return {
items: [
{ title: "foo" },
{ title: "baz" }]
}
},
computed: {
isShow() {
return this.items && this.items.length > 0
}
}
})
复制代码
模板指令的代码都会生成在render
函数中,通过app.$options.render
就能得到渲染函数
ƒ anonymous() {
with (this) { return
_c('div', { attrs: { "id": "app" } },
_l((items), function (item)
{ return (isShow) ? _c('p', [_v("\n" + _s(item.title) + "\n")]) : _e() }), 0) }
}
复制代码
<div id="app">
<template v-if="isShow">
<p v-for="item in items">{{item.title}}</p>
</template>
</div>
复制代码
再输出下render
函数
ƒ anonymous() {
with(this){return
_c('div',{attrs:{"id":"app"}},
[(isShow)?[_v("\n"),
_l((items),function(item){return _c('p',[_v(_s(item.title))])})]:_e()],2)}
}
复制代码
这时候我们可以看到,v-for
与v-if
作用在不同标签时候,是先进行判断,再进行列表的渲染
我们再在查看下 vue 源码
源码位置:\vue-dev\src\compiler\codegen\index.js
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
...
}
复制代码
在进行if
判断的时候,v-for
是比v-if
先进行判断
最终结论:v-for
优先级比v-if
高
watch 原理
watch
本质上是为每个监听属性 setter
创建了一个 watcher
,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deep
和 immediate
,对应原理如下
deep
:深度监听对象,为对象的每一个属性创建一个 watcher
,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象 setter
,因此引入 deep
能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。
immediate
:在初始化时直接调用回调函数,可以通过在 created
阶段手动调用回调函数实现相同的效果
用过 pinia 吗?有什么优点?
1. pinia 是什么?
2. 为什么要使用 pinia?
Vue2
和Vue3
都支持,这让我们同时使用Vue2
和Vue3
的小伙伴都能很快上手。
pinia
中只有state
、getter
、action
,抛弃了Vuex
中的Mutation
,Vuex
中mutation
一直都不太受小伙伴们的待见,pinia
直接抛弃它了,这无疑减少了我们工作量。
pinia
中action
支持同步和异步,Vuex
不支持
良好的Typescript
支持,毕竟我们Vue3
都推荐使用TS
来编写,这个时候使用pinia
就非常合适了
无需再创建各个模块嵌套了,Vuex
中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia
中每个store
都是独立的,互相不影响。
体积非常小,只有1KB
左右。
pinia
支持插件来扩展自身功能。
支持服务端渲染
3. pinna 使用
pinna文档(opens new window)
准备工作
我们这里搭建一个最新的Vue3 + TS + Vite
项目
npm create vite@latest my-vite-app --template vue-ts
复制代码
pinia
基础使用
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.mount("#app");
复制代码
2.1 创建store
//sbinsrc/store/user.ts
import { defineStore } from 'pinia'
// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
// 其它配置项
})
复制代码
创建store
很简单,调用 pinia
中的defineStore
函数即可,该函数接收两个参数:
我们可以定义任意数量的store
,因为我们其实一个store
就是一个函数,这也是pinia
的好处之一,让我们的代码扁平化了,这和Vue3
的实现思想是一样的
2.2 使用store
<!-- src/App.vue -->
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
console.log(store);
</script>
复制代码
2.3 添加state
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
});
复制代码
2.4 读取state
数据
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>性别:{{ sex }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const name = ref<string>(store.name);
const age = ref<number>(store.age);
const sex = ref<string>(store.sex);
</script>
复制代码
上段代码中我们直接通过store.age
等方式获取到了store
存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点
import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的
复制代码
2.5 修改state
数据
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>性别:{{ sex }}</p>
<button @click="changeName">更改姓名</button>
</template>
<script setup lang="ts">
import child from './child.vue';
import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store);
const changeName = () => {
store.name = "张三";
console.log(store);
};
</script>
复制代码
2.6 重置state
<button @click="reset">重置store</button>
// 重置store
const reset = () => {
store.$reset();
};
复制代码
当我们点击重置按钮时,store
中的数据会变为初始状态,页面也会更新
2.7 批量更改state
数据
如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store
的$patch
方法,修改app.vue
代码,添加一个批量更改数据的方法
<button @click="patchStore">批量修改数据</button>
// 批量修改数据
const patchStore = () => {
store.$patch({
name: "张三",
age: 100,
sex: "女",
});
};
复制代码
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
复制代码
2.8 直接替换整个state
pinia
提供了方法让我们直接替换整个state
对象,使用store
的$state
方法
store.$state = { counter: 666, name: '张三' }
复制代码
上段代码会将我们提前声明的state
替换为新的对象,可能这种场景用得比较少
getters
属性
3.1 添加getter
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 10,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return state.age + 100;
},
},
})
复制代码
上段代码中我们在配置项参数中添加了getter
属性,该属性对象中定义了一个getAddAge
方法,该方法会默认接收一个state
参数,也就是state
对象,然后该方法返回的是一个新的数据
3.2 使用getter
<template>
<p>新年龄:{{ store.getAddAge }}</p>
<button @click="patchStore">批量修改数据</button>
</template>
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
// 批量修改数据
const patchStore = () => {
store.$patch({
name: "张三",
age: 100,
sex: "女",
});
};
</script>
复制代码
上段代码中我们直接在标签上使用了store.gettAddAge
方法,这样可以保证响应式,其实我们state
中的name
等属性也可以以此种方式直接在标签上使用,也可以保持响应式
3.3 getter
中调用其它getter
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return state.age + 100;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
});
复制代码
3.3 getter
传参
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
});
复制代码
<p>新年龄:{{ store.getAddAge(1100) }}</p>
复制代码
actions
属性
前面我们提到的state
和getter
s 属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data
数据和computed
计算属性一样。
那么,如果我们有业务代码的话,最好就是卸载actions
属性里面,该属性就和我们组件代码中的methods
相似,用来放置一些处理业务逻辑的方法。
actions
属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法
4.1 添加actions
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
actions: {
// 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store
saveName(name: string) {
this.name = name;
},
},
});
复制代码
4.2 使用actions
使用actions
中的方法也非常简单,比如我们在App.vue
中想要调用该方法
const saveName = () => {
store.saveName("poetries");
};
复制代码
总结
pinia
的知识点很少,如果你有 Vuex 基础,那么学起来更是易如反掌
pinia 无非就是以下 3 个大点:
Vue3.0 为什么要用 proxy?
在 Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶
不需用使用 Vue.$set
或 Vue.$delete
触发响应式。
全方位的数组变化检测,消除了 Vue2 无效的边界情况。
支持 Map,Set,WeakMap 和 WeakSet。
Proxy 实现的响应式原理与 Vue2 的实现原理相同,实现方式大同小异∶
参考 前端进阶面试题详细解答
$route
和$router
的区别
delete 和 Vue.delete 删除数组的区别?
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[0]
console.log(a) //[empty,2,3,4]
this.$delete(b,0)
console.log(b) //[2,3,4]
复制代码
Vue 修饰符有哪些
vue 中修饰符分为以下五种
表单修饰符
事件修饰符
鼠标按键修饰符
键值修饰符
v-bind
修饰符
1. 表单修饰符
在我们填写表单的时候用得最多的是input
标签,指令用得最多的是v-model
关于表单的修饰符有如下:
在我们填完信息,光标离开标签的时候,才会将值赋予给value
,也就是在change
事件之后再进行信息同步
<input type="text" v-model.lazy="value">
<p>{{value}}</p>
复制代码
自动过滤用户输入的首空格字符,而中间的空格不会过滤
<input type="text" v-model.trim="value">
复制代码
自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat
解析,则会返回原来的值
<input v-model.number="age" type="number">
复制代码
2. 事件修饰符
事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符
<div @click="shout(2)">
<button @click.stop="shout(1)">ok</button>
</div>
//只输出1
复制代码
<form v-on:submit.prevent="onSubmit"></form>
复制代码
<div @click.capture="shout(1)">
obj1
<div @click.capture="shout(2)">
obj2
<div @click="shout(3)">
obj3
<div @click="shout(4)">
obj4
</div>
</div>
</div>
</div>
// 输出结构: 1 2 4 3
复制代码
<div v-on:click.self="doThat">...</div>
复制代码
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self
会阻止所有的点击,而 v-on:click.self.prevent
只会阻止对元素自身的点击
<button @click.once="shout(1)">ok</button>
复制代码
在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll
事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll
事件整了一个.lazy
修饰符
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
复制代码
<my-component v-on:click.native="doSomething"></my-component>
<!-- 使用.native修饰符来操作普通HTML标签是会令事件失效的 -->
复制代码
3. 鼠标按钮修饰符
鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:
.left
左键点击
.right
右键点击
.middle
中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>
复制代码
4. 键盘事件的修饰符
键盘修饰符是用来修饰键盘事件(onkeyup
,onkeydown
)的,有如下:
keyCode
存在很多,但 vue 为我们提供了别名,分为以下两种:
<!-- 只有按键为keyCode的时候才触发 -->
<input type="text" @keyup.keyCode="shout()">
复制代码
还可以通过以下方式自定义一些全局的键盘码别名
Vue.config.keyCodes.f2 = 113
复制代码
5. v-bind 修饰符
v-bind
修饰符主要是为属性进行操作,用来分别有如下:
//父组件
<comp :myMessage.sync="bar"></comp>
//子组件
this.$emit('update:myMessage',params);
复制代码
以上这种方法相当于以下的简写
//父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){
this.bar = e;
}
//子组件js
func2(){
this.$emit('update:myMessage',params);
}
复制代码
使用async
需要注意以下两点:
使用sync
的时候,子组件传递的事件名格式必须为update:value
,其中value
必须与子组件中props
中声明的名称完全一致
注意带有 .sync
修饰符的 v-bind
不能和表达式一起使用
prop 设置自定义标签属性,避免暴露数据,防止污染 HTML 结构
<input id="uid" title="title1" value="1" :index.prop="index">
复制代码
<svg :viewBox="viewBox"></svg>
复制代码
应用场景
根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景:
如果让你从零开始写一个 vuex,说说你的思路
思路分析
这个题目很有难度,首先思考vuex
解决的问题:存储用户全局状态并提供管理状态 API。
回答范例
官方说vuex
是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex
实现Store
时,可以定义Store
类,构造函数接收选项options
,设置属性state
对外暴露状态,提供commit
和dispatch
修改属性state
。这里需要设置state
为响应式对象,同时将Store
定义为一个Vue
插件
commit(type, payload)
方法中可以获取用户传入mutations
并执行它,这样可以按用户提供的方法修改状态。 dispatch(type, payload)
类似,但需要注意它可能是异步的,需要返回一个Promise
给用户以处理异步结果
实践
Store
的实现:
class Store {
constructor(options) {
this.state = reactive(options.state)
this.options = options
}
commit(type, payload) {
this.options.mutations[type].call(this, this.state, payload)
}
}
复制代码
vuex 简易版
/**
* 1 实现插件,挂载$store
* 2 实现store
*/
let Vue;
class Store {
constructor(options) {
// state响应式处理
// 外部访问: this.$store.state.***
// 第一种写法
// this.state = new Vue({
// data: options.state
// })
// 第二种写法:防止外界直接接触内部vue实例,防止外部强行变更
this._vm = new Vue({
data: {
$$state: options.state
}
})
this._mutations = options.mutations
this._actions = options.actions
this.getters = {}
options.getters && this.handleGetters(options.getters)
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}
get state () {
return this._vm._data.$$state
}
set state (val) {
return new Error('Please use replaceState to reset state')
}
handleGetters (getters) {
Object.keys(getters).map(key => {
Object.defineProperty(this.getters, key, {
get: () => getters[key](this.state)
})
})
}
commit (type, payload) {
let entry = this._mutations[type]
if (!entry) {
return new Error(`${type} is not defined`)
}
entry(this.state, payload)
}
dispatch (type, payload) {
let entry = this._actions[type]
if (!entry) {
return new Error(`${type} is not defined`)
}
entry(this, payload)
}
}
const install = (_Vue) => {
Vue = _Vue
Vue.mixin({
beforeCreate () {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store
}
},
})
}
export default { Store, install }
复制代码
验证方式
import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)
export default new Vuex.Store({
state: {
counter: 0
},
mutations: {
// state从哪里来的
add (state) {
state.counter++
}
},
getters: {
doubleCounter (state) {
return state.counter * 2
}
},
actions: {
add ({ commit }) {
setTimeout(() => {
commit('add')
}, 1000)
}
},
modules: {
}
})
复制代码
怎么缓存当前的组件?缓存后怎么更新
缓存组件使用keep-alive
组件,这是一个非常常见且有用的优化手段,vue3
中keep-alive
有比较大的更新,能说的点比较多
思路
回答范例
开发中缓存组件使用keep-alive
组件,keep-alive
是vue
内置组件,keep-alive
包裹动态组件component
时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM
<keep-alive>
<component :is="view"></component>
</keep-alive>
复制代码
结合属性include
和exclude
可以明确指定缓存哪些组件或排除缓存指定组件。vue3
中结合vue-router
时变化较大,之前是keep-alive
包裹router-view
,现在需要反过来用router-view
包裹keep-alive
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
复制代码
缓存后如果要获取数据,解决方案可以有以下两种
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},
复制代码
activated(){
this.getData() // 获取数据
},
复制代码
keep-alive
是一个通用组件,它内部定义了一个map
,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component
组件对应组件的vnode
,如果该组件在map
中存在就直接返回它。由于component
的is
属性是个响应式数据,因此只要它变化,keep-alive
的render
函数就会重新执行
Vue 为什么没有类似于 React 中 shouldComponentUpdate 的生命周期
考点: Vue
的变化侦测原理
前置知识: 依赖收集、虚拟DOM
、响应式系统
根本原因是Vue
与React
的变化侦测方式有所不同
当 React 知道发生变化后,会使用Virtual Dom Diff
进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要 shouldComponentUpdate
进行手动操作来减少diff
,从而提高程序整体的性能
Vue
在一开始就知道那个组件发生了变化,不需要手动控制diff
,而组件内部采用的diff
方式实际上是可以引入类似于shouldComponentUpdate
相关生命周期的,但是通常合理大小的组件不会有过量的 diff,手动优化的价值有限,因此目前Vue
并没有考虑引入shouldComponentUpdate
这种手动优化的生命周期
Vue 中的过滤器了解吗?过滤器的应用场景有哪些?
过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数
Vue 允许你自定义过滤器,可被用于一些常见的文本格式化
ps: Vue3
中已废弃filter
如何用
vue 中的过滤器可以用在两个地方:双花括号插值和 v-bind
表达式,过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:
<!-- 在双花括号中 -->
{ message | capitalize }
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
复制代码
定义 filter
在组件的选项中定义本地的过滤器
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
复制代码
定义全局过滤器:
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
new Vue({
// ...
})
复制代码
注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器
过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize
过滤器函数将会收到 message
的值作为第一个参数
过滤器可以串联:
{ message | filterA | filterB }
复制代码
在这个例子中,filterA
被定义为接收单个参数的过滤器函数,表达式 message
的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB
,将 filterA
的结果传递到 filterB
中。
过滤器是 JavaScript
函数,因此可以接收参数:
{{ message | filterA('arg1', arg2) }}
复制代码
这里,filterA
被定义为接收三个参数的过滤器函数。
其中 message
的值作为第一个参数,普通字符串 'arg1'
作为第二个参数,表达式 arg2
的值作为第三个参数
举个例子:
<div id="app">
<p>{{ msg | msgFormat('疯狂','--')}}</p>
</div>
<script>
// 定义一个 Vue 全局的过滤器,名字叫做 msgFormat
Vue.filter('msgFormat', function(msg, arg, arg2) {
// 字符串的 replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则
return msg.replace(/单纯/g, arg+arg2)
})
</script>
复制代码
小结:
应用场景
平时开发中,需要用到过滤器的地方有很多,比如单位转换
、数字打点
、文本格式化
、时间格式化
之类的等
比如我们要实现将30000 => 30,000
,这时候我们就需要使用过滤器
Vue.filter('toThousandFilter', function (value) {
if (!value) return ''
value = value.toString()
return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')
})
复制代码
原理分析
使用过滤器
{{ message | capitalize }}
复制代码
在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过parseFilters
,我们放到最后讲
_s(_f('filterFormat')(message))
复制代码
首先分析一下_f
:
_f
函数全名是:resolveFilter
,这个函数的作用是从this.$options.filters
中找出注册的过滤器并返回
// 变为
this.$options.filters['filterFormat'](message) // message为参数
复制代码
关于resolveFilter
import { indentity,resolveAsset } from 'core/util/index'
export function resolveFilter(id){
return resolveAsset(this.$options,'filters',id,true) || identity
}
复制代码
内部直接调用resolveAsset
,将option
对象,类型,过滤器id
,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;
resolveAsset
的代码如下:
export function resolveAsset(options,type,id,warnMissing){ // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西
if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回
return
}
const assets = options[type] // 将我们注册的所有过滤器保存在变量中
// 接下来的逻辑便是判断id是否在assets中存在,即进行匹配
if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤器
// 没有找到,代码继续执行
const camelizedId = camelize(id) // 万一你是驼峰的呢
if(hasOwn(assets,camelizedId)) return assets[camelizedId]
// 没找到,继续执行
const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢
if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]
// 如果还是没找到,则检查原型链(即访问属性)
const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
// 如果依然没找到,则在非生产环境的控制台打印警告
if(process.env.NODE_ENV !== 'production' && warnMissing && !result){
warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options)
}
// 无论是否找到,都返回查找结果
return result
}
复制代码
下面再来分析一下_s
:
_s
函数的全称是 toString
,过滤器处理后的结果会当作参数传递给 toString
函数,最终 toString
函数执行后的结果会保存到Vnode
中的 text 属性中,渲染到视图中
function toString(value){
return value == null
? ''
: typeof value === 'object'
? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距
: String(value)
}
复制代码
最后,在分析下parseFilters
,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式
function parseFilters (filter) {
let filters = filter.split('|')
let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组
let i
if (filters) {
for(i = 0;i < filters.length;i++){
experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数
}
}
return expression
}
// warpFilter函数实现
function warpFilter(exp,filter){
// 首先判断过滤器是否有其他参数
const i = filter.indexof('(')
if(i<0){ // 不含其他参数,直接进行过滤器表达式字符串的拼接
return `_f("${filter}")(${exp})`
}else{
const name = filter.slice(0,i) // 过滤器名称
const args = filter.slice(i+1) // 参数,但还多了 ‘)’
return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')'
}
}
复制代码
小结:
在编译阶段通过parseFilters
将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数)
编译后通过调用resolveFilter
函数找到对应过滤器并返回结果
执行结果作为参数传递给toString
函数,而toString
执行后,其结果会保存在Vnode
的text
属性中,渲染到视图
Vue 中常见性能优化
编码优化 :
使用v-show
复用DOM
:避免重复创建组件
<template>
<div class="cell">
<!-- 这种情况用v-show复用DOM,比v-if效果好 -->
<div v-show="value" class="on">
<Heavy :n="10000"/>
</div>
<section v-show="!value" class="off">
<Heavy :n="10000"/>
</section>
</div>
</template>
复制代码
合理使用路由懒加载、异步组件,有效拆分App
尺寸,访问时才异步加载
const router = createRouter({
routes: [
// 借助webpack的import()实现异步组件
{ path: '/foo', component: () => import('./Foo.vue') }
]
})
复制代码
keep-alive
缓存页面:避免重复创建组件实例,且能保留缓存组件状态
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
复制代码
v-once
和v-memo
:不再变化的数据使用v-once
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
复制代码
按条件跳过更新时使用v-momo
:下面这个列表只会更新选中状态变化项
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
<p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
<p>...more child nodes</p>
</div>
复制代码
长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
<recycle-scroller
class="items"
:items="items"
:item-size="24"
>
<template v-slot="{ item }">
<FetchItemView
:item="item"
@vote="voteItem(item)"
/>
</template>
</recycle-scroller>
复制代码
防止内部泄漏,组件销毁后把全局变量和事件销毁:Vue
组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件
export default {
created() {
this.timer = setInterval(this.refresh, 2000)
},
beforeUnmount() {
clearInterval(this.timer)
}
}
复制代码
图片懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载
<!-- 参考 https://github.com/hilongjw/vue-lazyload -->
<img v-lazy="/static/img/1.png">
复制代码
滚动到可视区域动态加载
https://tangbc.github.io/vue-virtual-scroll-list(opens new window)
第三方插件按需引入:(babel-plugin-component
)
像element-plus
这样的第三方组件库可以按需引入避免体积太大
import { createApp } from 'vue';
import { Button, Select } from 'element-plus';
const app = createApp()
app.use(Button)
app.use(Select)
复制代码
服务端渲染:SSR
如果SPA
应用有首屏渲染慢的问题,可以考虑SSR
以及下面的其他方法
用户体验
app-skeleton
骨架屏
pwa
serviceworker
SEO 优化
打包优化
基础的 Web 技术的优化
请说出 vue cli 项目中 src 目录每个文件夹和文件的用法
assets
文件夹是放静态资源;
components
是放组件;
router
是定义路由相关的配置;
view
视图;
app.vue
是一个应用主组件;
main.js
是入口文件
为什么 Vue 采用异步渲染
Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。核心思想 nextTick
源码相关
dep.notify()
通知 watcher
进行更新, subs[i].update
依次调用 watcher
的 update
, queueWatcher
将watcher
去重放入队列, nextTick
( flushSchedulerQueue
)在下一tick
中刷新watcher
队列(异步)
update () { /* istanbul ignore else */
if (this.lazy) {
this.dirty = true
}
else if (this.sync) {
this.run()
}
else {
queueWatcher(this); // 当数据发生变化时会将watcher放到一个队列中批量更新
}
}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id // 会对相同的watcher进行过滤
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue) // 调用nextTick方法 批量的进行更新
}
}
}
复制代码
Vue.set 的实现原理
基本使用
以下方法调用会改变原始数组:push()
, pop()
, shift()
, unshift()
, splice()
, sort()
, reverse()
,Vue.set( target, key, value )
调用方法:Vue.set(target, key, value )
target
:要更改的数据源(可以是对象或者数组)
key
:要更改的具体数据
value
:重新赋的值
<div id="app">{{user.name}} {{user.age}}</div>
<div id="app"></div>
<script>
// 1. 依赖收集的特点:给每个属性都增加一个dep属性,dep属性会进行收集,收集的是watcher
// 2. vue会给每个对象也增加一个dep属性
const vm = new Vue({
el: '#app',
data: { // vm._data
user: {name:'poetry'}
}
});
// 对象的话:调用defineReactive在user对象上定义一个age属性,增加到响应式数据中,触发对象本身的watcher,ob.dep.notify()更新
// 如果是数组 通过调用 splice方法,触发视图更新
vm.$set(vm.user, 'age', 20); // 不能给根属性添加,因为给根添加属性 性能消耗太大,需要做很多处理
// 修改肯定是同步的 -> 更新都是一步的 queuewatcher
</script>
复制代码
相关源码
// src/core/observer/index.js 44
export class Observer { // new Observer(value)
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep() // 给所有对象类型增加dep属性
}
}
复制代码
// src/core/observer/index.js 201
export function set (target: Array<any> | Object, key: any, val: any): any {
// 1.是开发环境 target 没定义或者是基础类型则报错
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// 3.如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val // 直接修改属性值
return val
}
// 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 5.如果不是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val
return val
}
// 6.将属性定义成响应式的
defineReactive(ob.value, key, val)
// 通知视图更新
ob.dep.notify()
return val
}
复制代码
我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组 ,直接使用数组的 splice
方法触发相应式;
如果目标是对象 ,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive
方法进行响应式处理( defineReactive
方法就是 Vue
在初始化对象时,给对象属性采用 Object.defineProperty
动态添加 getter
和 setter
的功能所调用的方法)
构建的 vue-cli 工程都到了哪些技术,它们的作用分别是什么
vue.js
:vue-cli
工程的核心,主要特点是 双向数据绑定 和 组件系统。
vue-router
:vue
官方推荐使用的路由框架。
vuex
:专为 Vue.js
应用项目开发的状态管理器,主要用于维护vue
组件间共用的一些 变量 和 方法。
axios
( 或者 fetch
、ajax
):用于发起 GET
、或 POST
等 http
请求,基于 Promise
设计。
vuex
等:一个专为vue
设计的移动端 UI 组件库。
创建一个emit.js
文件,用于vue
事件机制的管理。
webpack
:模块加载和vue-cli
工程打包器。
实现双向绑定
我们还是以Vue
为例,先来看看Vue
中的双向绑定流程是什么的
new Vue()
首先执行初始化,对data
执行响应化处理,这个过程发生Observe
中
同时对模板执行编译,找到其中动态绑定的数据,从data
中获取并初始化视图,这个过程发生在Compile
中
同时定义⼀个更新函数和Watcher
,将来对应数据变化时Watcher
会调用更新函数
由于data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep
来管理多个Watcher
将来 data 中数据⼀旦发生变化,会首先找到对应的Dep
,通知所有Watcher
执行更新函数
流程图如下:
先来一个构造函数:执行初始化,对data
执行响应化处理
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 对data选项做响应式处理
observe(this.$data);
// 代理data到vm上
proxy(this);
// 执行编译
new Compile(options.el, this);
}
}
复制代码
对data
选项执行响应化具体操作
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return;
}
new Observer(obj);
}
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
复制代码
编译Compile
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
复制代码
依赖收集
视图中会用到data
中某key
,这称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知
实现思路
defineReactive
时为每⼀个key
创建⼀个Dep
实例
初始化视图时读取某个key
,例如name1
,创建⼀个watcher1
由于触发name1
的getter
方法,便将watcher1
添加到name1
对应的Dep
中
当name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新
// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
// 置空
Dep.target = null
}
// 未来执行dom更新函数,由dep调用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
复制代码
声明Dep
class Dep {
constructor() {
this.deps = []; // 依赖管理
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
复制代码
创建watcher
时触发getter
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
复制代码
依赖收集,创建Dep
实例
function defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例
return val;
},
set(newVal) {
if (newVal === val) return;
dep.notify(); // 通知dep执行更新方法
},
});
}
复制代码
Vue-router 除了 router-link 怎么实现跳转
声明式导航
<router-link to="/about">Go to About</router-link>
复制代码
编程式导航
// literal string path
router.push('/users/1')
// object with path
router.push({ path: '/users/1' })
// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'test' } })
复制代码
回答范例
vue-router
导航有两种方式:声明式导航和编程方式导航
声明式导航方式使用router-link
组件,添加to
属性导航;编程方式导航更加灵活,可传递调用router.push()
,并传递path
字符串或者RouteLocationRaw
对象,指定path
、name
、params
等信息
如果页面中简单表示跳转链接,使用router-link
最快捷,会渲染一个 a 标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航
实际上内部两者调用的导航函数是一样的
评论