熟练使用vue3?这一篇就够了~(非标题党!)
Composition API
vue2的Options API
vue3的由多个hook
组合起来的形成的Composition API
框架设计模式
setup
setup
和name
,components
一样实际上也是vue的一个配置项
但是组合式API是在setup
中调用的
所以说vue3的模式从本质上来说是由options+composition两种模式结合的
-
在初始化属性之后,在
beforeCreate
之前调用所以this === undefined,不让使用this的主要原因是:
避免在
options
和composition
两种模式混用时出现的不必要的this冲突这违背了vue开箱即用的初衷
-
需要return出模板中需要的数据
return出的对象中的属性会被
merge
到模板编译的render context
中return也可以返回一个
render
函数const count = ref(0) const obj = reactive({foo:'bar'}) return ()=> h('h1',[count.value,obj.foo])
会直接编译到模板成为
h1
的DOM
节点 -
响应式数据可以有
reactive
,ref
两种声明方式ref
声明的数据在模板中被引用时会被拆解开不必使用
.value
的形式获取 -
setup(props,ctx)
的第一个参数是经过解析的父组件传来的属性1.props已经是被proxy代理过的响应式,不必对其再进行包装 2.当某个props中的属性有改变的话会对props整个进行更新 3.可以同时被watchEffect和watch观测到 4.不要去解构props中的属性否则会失去响应式
-
第二个参数
ctx
是一个对象,包含了在vue2.x
中的this
上暴露出的一些属性ctx.attrs / ctx.slots / ctx.emit attrs/slots也是在当前组件实例中的响应式代理过的值 总是抛出最新的值,即使是在更新之后,可以去解构 setup(props,{attrs,slots,emit}) 1.slots即当前组件的插槽 2.attrs即当前组件的属性 3.emit即vue2.x的this.$emit()
-
setup语法糖
setup默认是和return搭配使用,是同步的 setup语法糖省略组件注册 defineProps、defineEmits代替props和ctx.emit defineProps接收父组件传递属性名赋默认值并拿到props: import {defineProps,withDefaults} from 'vue' const props = withDefaults({ defineProps<{ width?:string, otherVals?:Array<ISth> }>(), { width:'100%', otherVals:[] || ISth[] } })
Reactivity APIs
reactive
接收一个对象返回原对象的响应式proxy
对象,是深度的代理
其实vue2.x也有一个Vue.observable()
和其功能相同
isReactive
检测一个对象是否由reactive
创建出来的对象
注意以下情况
const state = reactive({
name:'zs'
})
const test = readonly({
name:'zs'
})
const copyState = readonly(state)
isReactive(test) === false
isReactive(copyState) === true
shallowReactive
不会对嵌套的对象执行深度响应
ref
1.接收一个inner value
返回一个reactive
并且可变的ref
对象
这个对象有唯一的一个.value
属性指向inner value
2.如果接收一个对象,会默认调用reactive
进行处理
3.如果reactive
某个属性值是ref
:
const state = reactive({
count:ref(1)
})
//访问和修改
state.count
4.如果传入的是个Array
或者Map
对象或者其他原生集合类型包含的ref
,默认不会对ref
自动展开
const arr = reactive([ref(0)])
const map = reactive(new Map([['foo',ref(0)]]))
//访问和修改
arr[0].value
map.get('foo').value
unref/isRef
如果参数是一个 ref
,则返回内部值,否则返回参数本身。
isRef
检测值是否是一个 ref
对象
即 unref === isRef(val) ? val.value : val
function useFoo(x: number | Ref<number>) {
const unwrapped = unref(x)
// unwrapped 现在一定是数字类型
}
toRef
为一个reactive
对象的某个属性新创建一个ref
这个新创建的ref
对象会和源property
进行响应式连接
经常用于只需要使用响应式对象的某个property
的情况
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
toRefs
将响应式对象转为普通对象, 结果对象的每个property都指向原始对象相应property的ref
通常用于在不丢失响应式的前提下对返回的对象进行解构/展开
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
//stateAsRefs 的类型:
{
foo: Ref<number>,
bar: Ref<number>
}
//在template中可以直接使用foo / bar
return{
...toRefs(stateAsRefs)
}
customRef
本质是一个自定义的composition API
,接收一个工厂函数
该函数接收track
和trigger
两个函数,返回一个带get
和set
的对象
track
即追踪依赖,trigger
即触发更新
通常用于图片的懒加载和防抖或节流
//使用customRef的实现debounce
function useDebounce(value,delay=200){
let t=null;
return customRef((track,trigger)=>{
return {
get(){
track();
return value
},
set(newVal){
clearTimeout(t)
t = setTimeout(()=>{
value = newVal
trigger()
},delay)
}
}
})
}
const text = useDebounce('',500)
return {
text
}
shallowRef
可以追踪自身的.value
的变化但是不会使其变为响应式
triggerRef
手动执行与shallowRef
关联的任何作用(effect)
1.正常使用ref声明的变量
const info = ref({name:'zs'})
info.value = {name:'ls'}
//info.value===Proxy{name:'ls'}
2.使用shallowRef声明的变量
const info = shallowRef({name:'zs'})
info.value = {name:'ls'}
//info.value==={name:'ls'}
//此时watchEffect追踪不到info.value.name的变化
triggerRef(info) //执行之后可以追踪到info.value.name的变化
computed
1.可以接收一个getter
函数并返回一个不可更改的响应式对象
2.可以接收一个set
和get
函数的对象,创建一个可写的ref
对象
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
readonly
接收一个对象(reactive
或plain
或ref
)
返回一个只读的readonly
的original
源对象的proxy
代理
而且是深度代理,任何访问都可以被watchEffect监测到
const original = reactive({ count: 1 })
const copy = readonly(original)
watchEffect(() => {
console.log('copy.count', copy.count)
})
original.count++ // 先后打印1 2
isReadonly
检测是否是由readonly
创建的proxy
代理的对象
shallowReadonly
const state = readonly({
info: {
age: 23,
},
})
const shallowState = shallowReadonly({
info: {
age: 23,
},
})
console.log(isReadonly(state), isReadonly(state.info)) //true true
console.log(isReadonly(shallowState), isReadonly(shallowState.info)) //true false
isProxy
检测是否是一个由reactive
或readonly
创建的proxy
代理的对象
注意以下情况
const state = new Proxy({
a:'属性a'
},{})
//原生Proxy代理的这种情况
isProxy(state)===false
watchEffect
- 默认在
onBeforeUpdate
前被调用,若观测DOM
的ref
注册节点
需要在onMounted
hook里面执行watchEffect
- 接收一个回调函数并响应式追踪其依赖(数据)
- 当依赖改变会
re-run
- 返回一个
stop handle
,调用可以明确停止其监听
在组件卸载时会自动停止
const count = ref(0)
setTimeout(() => {
count.value = 1
}, 1000)
const stop = watchEffect(async (onInvalidate) => {
onInvalidate(() => {
//在此处进行拦截来消除副作用 需要在Promise被resolve前被注册
//此外,Vue依赖这个返回的Promise来自动处理Promise链的潜在错误
console.log('onInvalidate is triggered')
})
console.log(count.value)
//在此处有可能是异步操作 为了取消异步操作的副作用(失败的回调)
//类似data.value = await fetchData(props.id)
},
{ //接收第二个参数 是个对象 对象内两个函数只有在开发模式下可以使用
onTrack(e){
console.log('trick') //追踪 一开始就会执行
},
onTrigger(e){
//通常在这里放置debugger 交互式的调试检查
console.log('trigger') //依赖 以来改变就会执行
},
flush:'post' //默认是`pre`在beforeUpdate执行,
//可以通过设置为`post`使其在beforeUpdate之后执行,但是肯定是在updated之前执行
})
setTimeout(() => {
stop()
console.log('watchEffect is closed')
}, 2000)
/**先后打印为:
* 0
* onInvalidate is triggered
* 1
* onInvalidate is triggered
* watchEffect is closed
**/
watch
传入一个返回需要观测的明确的数据的回调(
getter
的方式)传入一个执行副作用的回调
对比watchEffect:
- 可以懒执行副作用
- 更加明确了什么时候执行副作用函数
- 可以访问
newVal
和oldVal
- 同样是在
onBeforeUpdate
前执行- 同样返回
stop
具柄函数,可以调用手动关闭watch
监听- 同样接收第三个回调,里面可以调用
onTrack
和onTrigger
回调watch
可接受的回调和回调中常用的方法和属性如下
<template>
<h2>count: {{ count }}</h2>
<button @click="changeBtn" type="button">更新count</button>
</template>
<script lang="ts" setup>
const count = ref(1)
const changeBtn = () => {
count.value++
}
onBeforeUpdate(() => {
console.log('onBeforeUpdate') //点击后4
})
const stop = watch(
() => count.value,
(newVal, oldVal, onInvalidate) => {
console.log('newVal', newVal) //初始化 2 点击后 3
onInvalidate(() => {
console.log('onInvalidate', onInvalidate) //点击后 2
})
},
{
onTrack() {
console.log('onTrack') // 初始化 1
},
onTrigger() {
console.log('onTrigger') //点击后 1
},
immediate: true,
deep: true,
}
)
</script>
toRaw
raw
可以理解为不是reactive
或readonly
创建的proxy
代理的原始对象
是一个逃生舱
,用于临时读取数据不承担跟踪的开销
视图不进行更新的话,比如列表数据仅展示,可以使用此属性
markRaw
标记
一个对象,此对象不会被转化成proxy
代理对象,而会返回其本身
会给标记的对象添加一个_v_skip:true
的属性,在响应式代理时会被跳过
在另外的响应式对象中的某个属性使用markRaw
仍然是有效的
注意以下情况
//情况1
const foo = markRaw({
nested: {},
})
const bar = reactive({
nested: foo.nested,
})
console.log(foo.nested === bar.nested) //false 因为前者是原始对象,后者是Proxy代理对象
//情况2
const foo = markRaw({
nested: markRaw({
a: 1,
}),
})
const bar = reactive({
nested: foo.nested,
})
bar.nested.a++ //a:2 表明指向的仍是同一个内存对象
console.log(foo.nested === bar.nested) //true 表明markRaw也是表层标注
markRaw小结
markRaw
和shallowXXX
都是允许不参与深度reactive/readonly
转换
通常见于Vue component object
或第三方的class instance
当渲染庞大且不可变的
数据图表跳过响应可以提高性能
即手动对嵌套层
进行markRaw
或者直接在最外层使用shallowXXX
生命周期函数
vue2.x
生命周期流程如下
new Vue()
初始化Events
和lifecycle
进入
beforeCreate
钩子:初始化一些
injections
注入内容和reactivity
响应式进入
created
钩子:判断是否有
el
属性
如果有
el
属性判断是否有template
配置项如果有
template
配置项,将模板编译到渲染函数中,即compile template into render function
如果没有
template
配置项,将el outerHTML
即#app
节点的全部模板作为模板编译如果没有
el
属性,当进入到vm.$mount(el)
会被回调进入
beforeMount
钩子:创建
vm.$el
并且替换掉el
,即进行模板编译过程模板转成AST树--->AST树转成render函数-->render函数创建出虚拟DOM--->虚拟DOM再打到真实节点
进入
mounted
钩子:
- 初始化时可以写一些逻辑
- 当数据更改时,虚拟DOM重新渲染并打补丁,即
beforeUpdate
和update
钩子在此时也会被调用当
vm.$destroy()
回调触发时,进入beforeDestroy
钩子:卸载掉监听器,子组件和事件的监听等
进入
destroyed
钩子,组件销毁
vue3.x
生命周期钩子都是以onX
为名称,同步使用在setup
配置项中
下表包含如何在 setup () 内部调用生命周期钩子:
选项式 API Hook inside setup
beforeCreate
Not needed* created
Not needed* beforeMount
onBeforeMount
mounted
onMounted
beforeUpdate
onBeforeUpdate
updated
onUpdated
beforeUnmount
onBeforeUnmount
unmounted
onUnmounted
errorCaptured
onErrorCaptured
renderTracked
onRenderTracked(dev环境使用)
renderTriggered
onRenderTriggered(dev环境使用)
activated
onActivated
deactivated
onDeactivated
onErrorCaptured
表示在父组件里面可以捕获子组件的错误信息onRenderTracked
追踪render
的行为,通常在此debugger
调试onRenderTriggered
当re-render
时触发onActivated
被 keep-alive 缓存的组件激活时调用onDeactivated
被 keep-alive 缓存的组件失活时调用
依赖注入
Provide/Inject
当你仅需要
祖级
组件的某些数据
祖级
组件不需知道哪些子组件使用了它provide
的property
子组件不需要知道
inject
的property
来自哪里可以使用这一对属性
处理响应式:
provide
时使用computed
进行处理数据;或者在定义数据的时候使用
ref
或reactive
修改响应式数据:
祖级
组件直接provide
一个方法给子组件,仍然在祖级
组件进行操作如果不想让子组件修改的话,用
readonly
进行数据修饰
节点引用
vue2.x
中为了直接访问到子组件,可以通过ref
属性给子组件或者DOM节点注册一个reference ID
,可以在refs
访问到
vue3.x
的template
中使用ref
注册组件或者DOM节点时,在setup
可以定义同名ref
代理的变量,会指向同一个引用reference
如果一个
VNode ref
的在渲染函数中和setup
定义的ref
的变量的key
是对应一致的,虚拟节点对应的元素或组件实例会被指定为这个变量的值,并且在VDOM
的挂载和更新进程中起作用,所以在初始化render
之后才会设置这个值
v-for的情况下使用refs
<template>
<ul>
<li
v-for="(item, index) in students"
:key="index"
:ref="
(el) => {
if (el) lists[index] = el
}
"
>
{{ item.name }}
</li>
</ul>
</template>
const students = reactive([{ name: 'zs' }, { name: 'ls' }])
const lists = reactive([])
onMounted(() => {
console.log('lists', lists)
})
应用配置项
globalProperties
添加一个可以再应用的任何组件实例中访问的全局property
此属性命名优先级高于某个组件setup
声明的变量
const app = createApp({})
app.config.globalProperties.$util = ()=>{}
//等同于vue2.x的
Vue.prototype.$util = () => {}
compilerOptions(3.1+)
配置运行时编译器的选项,设置在这个对象上的值会被传入浏览器内的模板编译器,并影响应用内的每个组件
只有在完整的构建版本中生效,通常用vue-cli
的配置项或vite
的@vitejs/plugin-vue
选项传入
compilerOptions.isCustomElement
指定一个方法识别Vue以外 (小程序的语法) 定义的自定义元素
compilerOptions.whitespace
默认情况下,Vue 会移除/压缩模板元素之间的空格以产生更高效的编译结果:
- 元素内的多个开头/结尾空格会被压缩成一个空格
- 元素之间的包括折行在内的多个空格会被移除
- 文本结点之间可被压缩的空格都会被压缩成为一个空格
将值设置为 'preserve'
可以禁用 (2) 和 (3)。
compilerOptions.comments
- 类型:
boolean
- 默认值:
false
- 用法:
app.config.compilerOptions.comments = true
默认情况下,Vue 会在生产环境下移除模板内的 HTML 注释。将这个选项设置为 true
可以强制 Vue 在生产环境下保留注释。而在开发环境下注释是始终被保留的。
这个选项一般会用于依赖 HTML 注释的其它库和 Vue 配合使用。
compilerOptions.delimiters
- 类型:
Array<string>
- 默认值:
['{{', '}}']
- 用法:
// 将分隔符设置为 ES6 模板字符串风格
app.config.compilerOptions.delimiters = ['${', '}']
用于配置模板内文本插值的分隔符。
这个选项一般会用于避免和同样使用双大括号语法的服务端框架发生冲突
optionMergeStrategies
仅针对于options api
为自定义选项定义合并策略
合并策略是在Mixin
选项中被定义的
performance
组件性能相关的配置,通常进行前端性能优化时会用到此配置进行调试
仅在开发模式下可用并且支持performance.mark()
的浏览器
设置为 true
以在浏览器开发工具的 performance/timeline 面板中启用对组件初始化、编译、渲染和更新的性能追踪。
复用性
Mixin
vue2.x中,mixin是将部分组件逻辑抽象成可重用块的主要工具,但是仍然存在不足:
1.Mixin很容易发生变量名冲突
2.可重用性有限,不能传递参数改变其逻辑
3.隐式依赖,
mixin
和使用它的组件没有层次关系
组合式API
vue3.x中添加了一种通过逻辑关注代码组织的方法:
组合式API进行代码重用,通常被称为hooks
//js文件中定义
import { ref, computed } from "vue";
export default function () {
const count = ref(0);
const double = computed(() => count.value * 2)
function increment() {
count.value++;
}
return {
count,
double,
increment
}
}
// vue文件中使用
import useCounter from "./useCounter.js";
export default {
setup() {
const { count, double, increment } = useCounter();
return {
count,
double,
increment
}
}
}
内置组件
内内置组件可以直接在模板使用,不需要注册
<keep-alive> <transition> <transition-group>和新增的<teleport>
组件都可以被打包工具tree-shake
即需要使用的时候需要将其显性导入
import { KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'
component
渲染一个元组件
为动态组件
根据is
的值,决定哪个组件被渲染出来,通常和keep-alive
结合使用
<!-- 失活的组件将会被缓存!还可以和下面的transition连用-->
<transition>
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
</transition>
transition
为单个元素/组件提供过渡效果
传入name
属性和其他配置项属性进行过渡效果定义
<!-- 动态组件 -->
<transition name="fade" mode="out-in" appear>
<component :is="view"></component>
</transition>
slot
使用非常频繁的内置组件,简单来说
父组件使用子组件的结构,但是子组件的内容是由父组件控制并传入
<slot>
即为我们想要插入内容的占位符
teleport
移动实际的DOM节点到特定的节点目录下,比如根节点
是移动而不是销毁!
特殊属性
共包含包含ref key is
三个特殊属性
key
主要用做Vue的虚拟DOM算法的提示,以在比对新旧节点组时辨识 VNodes。
如果不使用 key,Vue 会使用一种算法来最小化元素的移动并且尽可能尝试就地修改/复用相同类型元素。
使用 key 时,它会基于 key 的顺序变化重新排列元素,并且那些使用了已经不存在的 key 的元素将会被移除/销毁。
有相同父元素的子元素必须有唯一的 key。重复的 key 会造成渲染错误
应用API
vue3.x中是在const app = createApp()
上进行应用API的挂载,整个组件树都会共享这个app的上下文,类似于vue2.x的const vm = new Vue()
directive
对视图上一些逻辑的抽离封装通常利用directive
来操作
// 注册 (函数指令)
app.directive('my-directive', () => {
// 这将被作为 `mounted` 和 `updated` 调用
})
// 也可以把第二个参数写成对象写法
app.directive('my-directive', {
created(el, binding, vnode) {},
beforeMount(el, binding, vnode) {
el.style.background = binding.value
},
mounted() {},
beforeUpdate(a,b,c,prevNode) {
//第四个参数 prevNode 只在beforeUpdate和updated才有!
a.style.background = b.value
},
updated() {},
beforeUnmount() {
// 当指令绑定的元素 的父组件销毁前调用。
},
unmounted() {},// 当指令与元素解除绑定且父组件已销毁时调用。
})
// getter, 如果已注册,则返回指令定义
const myDirective = app.directive('my-directive')
component
在app
实例进行全局注册组件
mount
所提供 DOM 元素的 innerHTML
将被替换为应用根组件的模板渲染结果
返回值是根组件实例
unmount
卸载应用实例的根组件
use
安装插件时,如果插件是一个对象,则必须暴露一个install
方法
如果插件本身是一个函数,则会被视为install
方法
返回值是当前应用实例,所以可以进行链式调用
config
参考应用配置项
provide
参考composition API
里的依赖注入
实例属性
通俗理解就是*.vue
单页面组件可以使用的属性
实例方法
在vue3中想要得到当前组件实例,需要使用getCurrentInstance
方法
但是此方法在生产模式下是不生效的
所以实例方法通常是在vue2.x中有this
的情况下使用较多
全局API
简单来说在任何地方都可以访问到,以下是常用的几个api
defineComponent
defineComponent是从vue中导入,传入对象返回另一个对象,返回的对象实际上是对该组件的描述
--->可以看做是一个类,表明在父组件调用子组件方法时实际上是生成了新的子组件实例进行操作的
具体如下:
子组件不能直接改变 props 的数据,需要改变的时候使用 v-model
resolveDirective
返回一个Directive
,如果没有找到返回undefined
//App.vue
const app = createApp({})
app.directive('highlight', {})
//component.vue
import { resolveDirective } from 'vue'
render () {
const highlightDirective = resolveDirective('highlight')
}
withDirectives
把指令应用VNode,返回一个包含应用指令的VNode
import { withDirectives, resolveDirective } from 'vue'
const foo = resolveDirective('foo')
const bar = resolveDirective('bar')
return withDirectives(h('div'), [
[foo, this.x],
[bar, this.y]
])
nextTick
将回调推迟到下一个DOM更新周期之后执行
在更改了一些数据之后等待DOM更新的时机立即使用它
转载自:https://juejin.cn/post/7083108986962051079