Appearance
基础教程
setup
setup 函数只会执行一次,如果是if语句中创建的部分配置,可能无法访问。组合式函数需要同步调用,但是可以使用await语句。setup 单文件下如果语句使用了await 会自动将setup设置为async
模板中表达式的作用域是ctx,在模板中使用赋值语句如下
html
<div class="about">
<button @click="increaseCount">{{ (a = count) }}</button>
<button>{{ a }}</button>
</div>会被转换如下
ts
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", { class: "about" }, [
_createElementVNode("button", { onClick: _ctx.increaseCount }, _toDisplayString((_ctx.a = _ctx.count)), 9 /* TEXT, PROPS */, ["onClick"]),
_createElementVNode("button", null, _toDisplayString(_ctx.a), 1 /* TEXT */)
]))
}
// Check the console for the AST挂载到this data上的值全部变成响应式
js
export default {
data() {
return {
value: null
}
}
created() {
const test1 = {
value: 1
}
this.value = test1;
// this.value 已经是一个proxy对象
console.log(this.value === test1 ) // false
}
}:style 支持数组的样式对象,vue会自动补齐前缀
html
<div :style="[styleObject1, styleObject2]"></div>ref 和reactive
ref用于对非引用对象创建响应式
reactive 对数组或者对象创建响应式
对一个响应式对象调用响应式依旧是响应式。。。。。
对reactive 原对象修改值,不会引起响应式变动,但会影响值。
js
const obj = {
test: 1
}
const state = reactive(obj);
obj.test = 2 // 不会导致响应式监听
state.test === 2 // 但是读取值时会变动,可以用于手动控制视图更新ref的解包
- 声明在顶层作用域的ref在模板中会自动解包,:count="count" 相当于:count="count.value" 子组件收到的是原始值, 不是ref对象。 state.count 就不能自动解包, 因为只有state是顶层对象
- 模板计算值如果是一个ref,会自动解包 不需要使用count.value
- 如果一个ref是reactive 对象(不包括数组和其他集合类型set,map)的property, 那么和普通的property表现相同, 会自动解包
js
const count = ref(9);
const state = reactive({
count
})
state.count++ // 不需要使用 state.count.value++
const state1 = reactive([count])
state[0].value++ // 数组需要计算属性
计算属性返回一个ref(将返回值用ref()包装起来, 如果返回的是一个reactive对象,就是ref(reactive(xxxx)), 如果是其他响应式对象,处理方式同上),支持set,get形式。可以用于v-model。
js
computed({
get() {
return props.modelValue
}
set(value) {
emits('update:modelValue', value)
}
})style 和 class 绑定
- 内联式
html
<div :class="{'is-error': isError, active: data.active.value, success: state.success}"></div>js
const isError = ref(false);
const data = {
active: ref(true)
}
const state = reactive({
success: true
})- 对象式和计算属性 对象内部通过属性取值获取值(存疑),所以可以传入一个reactive对象。对象的键名作为class名称,属性值表示是否渲染
html
<div :style="styleObject"></div>js
const styleObject = reactive({
active: false,
'is-error': true
})
// 同样可以使用计算属性
const active = ref(false)
const styleObject1 = computed(()=> ({
active: active.value
}))数组式
:class="[active, isError ? 'is-error' : '']"可以传入一个空字符串不绑定class,数组每一项计算值直接作为class名称style绑定对象时,对象的键作为属性,对象键值作为属性值。绑定数组时,每一项是一个样式对象。同理也支持字符串(空字符表示不渲染),可以通过alt html属性查看定义
html
<div :style="['color: red', '', [{color: 'blue '}]]"></div> // blue- 样式绑定是自动添加浏览器前缀
v-if 和 v-for
vue3 中 同时使用v-if 会先执行,vue2 中 v-for 会先执行,如果同时使用,建议使用计算属性或者将v-for 提升到template上面
vue3中的v-model
vue3 中v-model 语法糖有所变化
- 在原生html 表单控件中, 通过添加.lazy 修饰符可以将input 替换成change事件
html
<div :value="xxx" @input="event => xxx = event.target.value"></div>- 在组件中使用的是:modelValue 和 @update:modelValue实现,替代了.sync 功能
- 同时组件中支持v-model:arg1="xxx" 这种参数。展开来是 :arg1="xxx" @update:arg1。所以v-model是v-model:modelValue的简写。完全替代了.sync 修饰符
生命周期
- 组合式api的生命周期始终在选项式前面执行
- 组合式生命周期少了created和beforeCreate
- setup在beforeCreate 前面执行
watch 和 watchEffect
watch 用于副作用操作,支持同步和异步的方式,能对ref、reactive对象(属性变化时监听,浅监听,支持第三个参数设置 { deep: true })、getter函数(同计算属性),或者数组监听(任何一个变化都触发)。
watchEffect 时一种自动监听的方法,会默认执行一次回调函数,收集依赖(同计算属性),同时也支持异步的方式, 不过只会收集前面同步的依赖(比如第一个await 前的属性)。
watch
如果一个getter函数返回的是响应式对象, 默认是浅监听,需要设置deep: true 才能生效。reactive对象直接作为监听项默认是深度监听 ,如果需要浅监听需要使用getter函数。
js
const url = ref('https://www.xx.com');
const state = reactive({url: 'xxx'})
const state1 = reactive({ // 作为监听项时, 默认是深层监听,如果只想是浅,用 getter 函数, deep设置为false也无效。
data: {
url: 'xxx'
}
})
const data = ref('')
const stopRequest = watch(url, async (newValue, oldValue, cleanFn) => {
const res = await request(newValue);
data.value = res.data;
})
// 上述监听值还可以为 ()=> url.value 、 [()=>url.value, url]、 () => state.url state。- watch函数会返回一个stop函数,通过调用stop可以停止监听。
- watch回调函数支持第三个参数用于清理之前的正在执行的副作用,比如每次回调时清理前一个未完成的回调。cleanFn(() => { // 执行清理逻辑, 比如放弃上一次请求, 始终使用最新请求 }),会在每次回调前调用(第一次回调不调用)
js
watch(count, (_, __, clearFn) => {
const timer = setTimeout(() => {
console.log(count.value);
}, 2000);
clearFn(() => clearTimeout(timer));
});- watch函数第三个参数时选项,{deep: boolean, immediate: boolean, flush: boolean}, 其中flush默认时pre 表示在dom更新前调用,post表示更新后(和设置pre然后nexttick()相同)。flush中pre,post都有缓冲队列机制,在同步操作中多次调用会放入到队列中,只会执行一次。这个机制同vue的默认机制。同时可以设置flush: sync 取消缓冲队列, 而且会立刻同步的调用的回调
js
state.data.url = 123; // 语句后立刻执行一次回调
console.log("xxx"); // 然后打印 xxx
state.data.url = 1234; // 语句又立刻执行一次回调
// 如果上面是post或者pre 则会打印 xxx 然后只执行一次回调- watch 同步创建watch监听器会在组件销毁时自动停止, 如果是异步创建的监听器,需要手动设置停止,尽量少创建异步监听器,可以使用判断语句异步转同步。
ref
把一个ref绑定到模板ref属性上,会自动绑定ref,同react。使用起来同ref对象一样,需要通过ref.value 来获取。
组件
defineProps和defineEmits
都是sfc setup下面的宏。在sfc编译的时候回解析到相应的位置。
defineProps 详解
defineProps定义的属性会解析成选项式中props属性。defineProps的返回值解析成对setup(prop)中prop的引用。由于宏原因,defineProps中不能访问setup内部声明的变量,但是可以访问引用进来的。因为引用语句在总是上层
props如果接受到undefind值会触发自动default,如果未设置default,值为undefined。Boolean类型会自动转换成对应boolean值
props 是一个响应式对象,需要避免对齐解构,如果需要解构使用toRefs 或者toRef(props, xxx),但是在defineProps 解构是安全的,因为vue编译器会忽略这个解构
js
const props = definedProps({
count: number,
info: null, // 或者undefined,设置这两个值时会跳过类型检查
data: {
type: String // 支持对象式的配置
}
})
// 会转换成
export default {
props: {
count: number,
info: null, // 或者undefined,设置这两个值时会跳过类型检查
data: {
type: String // 支持对象式的配置
}
},
setup(_props) {
const props =_ props;
}
}<CustomComp count />实际是<CustomComp count='' />的缩写,所以除了boolean类型总是会得到一个空字符串,boolean 类型在收到一个''或者undefined时会触发default模板中可以自动使用prop属性,比如上面的count, 可以直接,实际上也是宏的作用,在解析模板时,如果是变量是setup定义的,会使用ctx.count, 如果是宏定义的会使用宏的props.count, 如果都没定义使用ctx.count
defineEmits
同defineProps一样会到解析到{emits: [] }中,支持对象语法实现拦截器或者hook,可以在每次触发的通过返回boolean进行校验。但是不支持混合书写。defineEmits返回的结果是setup(props, {emit: customEmit})中emit的别名,在模板中如果直接使用$emit,使用的是ctx.$emit。也可以把emit暴露到模板中直接使用customEmit('xxxx');
组件内部实现了事件机制,对组件v-on或者onXxx的prop 会添加订阅,通过emit或者once(vue3 没得once)触发订阅。vue3不能取消订阅,可以通过触发拦截器实现。事件在beforeCreate前初始化,所谓在生命周期任何适合都可以用,可以通过@hook:created 实现对声明周期的监听
透传attribute
任何未在组件porps或者emit 声明的都是attribute,默认情况下attribute会绑定到组件根元素上,如果根元素不存在需要手动绑定$attr到制定元素,可以添加inheritAttrs: false 选项取消默认绑定。$attrs对象包含了所有的未声明$attrs, 会将v-on转换成onXxx形式的prop值传入(所以v-on 也可以用v-bind来实现)。通过这个机制可以实现事件的分发,比如可以把某个事件分布到多个子组件上,或则批量的设置样式。
关于事件的思考
对于组件v-bind:onXxx和直接v-on:xxx 相同(:onAdd = @add), 针对原生元素@click 相当于v-bind:onClick。
针对以上特性所以在透传attrbute时,可以直接用v-bind 绑定事件,因为事件在处理成onXxx的prop属性值 这个特性可以用h函数解释,h函数的第二参数,以on开头的事件会被处理成事件
vue2 特性待研究
插槽
直接第三个参数设置为对象传入,每个键是slot name,值是一个函数包括子组件传递的作用域。同理支持动态的slot name。指定插槽选项式使用的是this.$slot,setup() 函数用二参数中的slot属性
js
h(CustomComponent, {
onClick: () => state.count++;
}, {
default: (user) => h('span', [user.name, user.code]) // <CustomComponent><template #default="user">{{user.name}}{{user.code}}</template></CustomComponent>
})插槽的函数如下,每一个slot都是一个函数,通过传递函数参数实现作用域插槽
ts
interface ComponentPublicInstance {
$slots: { [name: string]: Slot }
}
type Slot = (...args: any[]) => VNode[]因为render函数中,插槽是在子组件中调用的。所以子组件可以控制插槽延迟渲染,可以实现一个组件版本的v-if。这样可以升级权限指令为权限组件,实现更高级的功能。
插槽的生命周期如下,parent init -> parent beforeMounte -> child init -> child beforeMounte -> child mounted -> parent mounted
provide 和 inject
在vue3 provide 和 inject 的功能可以通过第三方store覆盖比如声明一个store 文件,里面简单的存放响应式对象即可实现。
js
const count = ref(0)
provide('count', count) // 提供
inject('count', ref(2)) // 注入实际可以用一个store.js实现, 然后在provide和inject的地方引入这个变量进行操作
js
// store.js
export const count = ref(0)组合式函数
- 组合式函数需要同步调用,因为需要感知当前的组件的实例,也可以在await 后调用(仅局限于sfc 的setup),编译器会自动添加保存await前的实例。组合函数是逻辑复用的银弹,但不是唯一银弹,无渲染组件可以解决组件复用和逻辑复用。
- 组合式函数仅会调用一次,如果是有副作用的操作,可以使用watch、effect等操作。
- 约定组合式函数使用use命名开头
js
import { onMounted, onUnmounted, ref } from "vue";
export const useEventListener = (target, name, callback) => {
onMounted(() => { // 可以感知当前绑定组件的实例
target.addEventListener(name, callback);
});
onUnmounted(() => {
target.removeEventListener(name, callback);
});
};
export const useMouse = () => {
const x = ref(0);
const y = ref(0);
useEventListener(document, "mousemove", (event) => {
x.value = event.pageX;
y.value = event.pageY;
});
return {
x,
y,
};
};指令
- 不建议在自定义组件上使用自定义指令,组件会透传指令。
- 自定义组件还是有多个根元素,指令无法正确识别而且$attrs 也无法识别
- 可以看到内置指令和自定义指令是两回事,自定义指令更多的是对dom操作。通过生成的vnode实现,但是不能改变创建的vnode。
teleporr
支持to和disable prop。to变更实际上会导致el的变化,并不会导致重新生命周期。 to支持元素或者选择器
路由
- 使用params和path不能同时使用,path需要改成name
- redirect 支持函数,参数是to,可以动态的重定向。重定向中相对路径相对于当前路由
- 别名是多个path对应一个component, 别名的path也支持相对于当前路由的相对路劲,同时支持params
- 支持路由组件传参的方式,将params 变成props,导航时设置props: true 即可。
渲染函数和jsx
在setup() 函数或者函数组件中返回vnode,参数都是setup(props, { emits, attrs, expose, slots})
在render函数或者setup返回函数中,参数形式如下render(ctx),其中ctx指的是vue实例, 也就是this。
setup暴露的出的属性方法默认不能通过ref查询到,可以通过expose暴露。
插槽
slot需要通过slots.xxx() 渲染,而且如果是自定义组件需要通过函数或者对象函数的形式传入
js
{
render(ctx) {
console.log(ctx === this) // true
return h('span', /** 没得选项,第二个参数直接变成slot **/, 123) // 不需要使用插槽
return h(CustomComp, () => 123) // 传入default插槽 123
return h(CustomComp, { default() {return 123}, header() {return 'header'} }) // 使用具名插槽,同时也可以给函数传入参数,
}
// jsx中可以用
}指令
withDirectives(vnode, [directives, params, modifier, data])。通过把vnode包装,实现一个有指令的vnode,可以通过resolvedirective找到指令。
jsx中自定义指令和模板相同,v-model也是相同, 在sfc中需要满足vmDerect
事件
使用props传入,变成onXxx形式,如果需要添加修饰符,需要将事件响应函数使用withModifiers(fun, ['self'])这种形式包裹传入。
函数式组件
函数组件返回一个vnode。参数同setup相同,需要手动声明函数的props和emits。可以偷懒不声明,到attrs属性中获取,不过就不能获的devtools支持。
声明方式在函数的的静态属性上, 同选项式api相同
插件
插件是一个具有install属性的对象(或者就是一个install函数),通过app.use 调用。会传入app和options
全局对象
vue3中不能通过vue.prototype.xx 来全局添加属性,需要通过app.config.globalProperties.xxx 来添加全局属性。
响应式基础
ref 和 shadowRef
当ref的参数是一个对象时,参数会被reactive调用,意味着对state.value.count.xxx 进行修改也会触发state.value 的变化,可以通过shadowRef避免这个问题。仅当对state.value进行修改时才触发。
reactive、shallowReactive
对第一层reactive创建响应式,reactive对递归的将所有的子属性设置为reactive对象
js
const _state = {
user: {
name: 'xxx'
}
}
const state = reactive(_state)
isReactive(state.user) // truereadonly和shallowReadonly
创建一个只读的响应式对象,接受一个对象、响应式对象或者ref。使用场景,在做提供一个原值拷贝。
CustomRef
创建一个自定义行为的ref,官方内置两种ref,可以通过CustomRef来实现自定义行为的ref。
js
const debounceRef = (value, delay) => {
let timer = null;
CustomRef((track, trigger) => {
return {
get() {
track()
return value
}
set(v) {
if(!timer) {
value = v;
track();
timer = setTimeout(() => {
timer = null;
}, delay);
}
}
}
})
}强制触发ref的变化
对一个shallowRef对象使用triggerRef(ref)可以触发ref所有相关的副作用。不能触发computed
js
const count = shallowRef(0);
watch(count, console.log);
count.value++;
triggerRef(count) // 1 --- 2工具api
toRef、toRefs、unref
将一个响应式对象(包括readonly)转换成每个子属性都是ref的,对象属性值的变更会导致ref也更新。
unref将一个值(如果是非ref)转化成ref并返回
js
const state = reactive({
count: 1
})
const count = state.count
state.count++ // count 不会变化,已经丢失响应式
const { count } = toRefs(state) // 这样不会丢失响应式,对
const count = toRef(state, 'count')
isRef(count) // trueisRef、isProxy、isReactive、isReadonly
isProxy检查 reactive、shallowReactive、readonly、shallowReadonly
toRaw、markRaw
toRaw 返回一个响应对象的原始对象的引用。markRaw将一个普通对象锁住,使其不能被转换成响应式对象。
js
const state = {}
console.log(toRaw(reactive(state)) === state) // true
console.log(isReactive(reactive(markRaw(state)))) // false
state.state = markRaw({})
console.log(isReactive(reactive(state))) // false 子属性也是能锁住其他
支持多个组件引用同一个模板
html
<template src="./template.html"></template>
<style src="./style.css"></style>
<script src="./script.js"></script>