Skip to content

基础教程

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的解包

  1. 声明在顶层作用域的ref在模板中会自动解包,:count="count" 相当于:count="count.value" 子组件收到的是原始值, 不是ref对象。 state.count 就不能自动解包, 因为只有state是顶层对象
  2. 模板计算值如果是一个ref,会自动解包 不需要使用count.value
  3. 如果一个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 绑定

  1. 内联式
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
})
  1. 对象式和计算属性 对象内部通过属性取值获取值(存疑),所以可以传入一个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 
}))
  1. 数组式 :class="[active, isError ? 'is-error' : '']" 可以传入一个空字符串不绑定class,数组每一项计算值直接作为class名称

  2. style绑定对象时,对象的键作为属性,对象键值作为属性值。绑定数组时,每一项是一个样式对象。同理也支持字符串(空字符表示不渲染),可以通过alt html属性查看定义

html
<div :style="['color: red', '', [{color: 'blue '}]]"></div>  // blue
  1. 样式绑定是自动添加浏览器前缀

v-if 和 v-for

vue3 中 同时使用v-if 会先执行,vue2 中 v-for 会先执行,如果同时使用,建议使用计算属性或者将v-for 提升到template上面

vue3中的v-model

vue3 中v-model 语法糖有所变化

  1. 在原生html 表单控件中, 通过添加.lazy 修饰符可以将input 替换成change事件
html
<div :value="xxx" @input="event => xxx = event.target.value"></div>
  1. 在组件中使用的是:modelValue 和 @update:modelValue实现,替代了.sync 功能
  2. 同时组件中支持v-model:arg1="xxx" 这种参数。展开来是 :arg1="xxx" @update:arg1。所以v-model是v-model:modelValue的简写。完全替代了.sync 修饰符

生命周期

  1. 组合式api的生命周期始终在选项式前面执行
  2. 组合式生命周期少了created和beforeCreate
  3. 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。
  1. watch函数会返回一个stop函数,通过调用stop可以停止监听。
  2. watch回调函数支持第三个参数用于清理之前的正在执行的副作用,比如每次回调时清理前一个未完成的回调。cleanFn(() => { // 执行清理逻辑, 比如放弃上一次请求, 始终使用最新请求 }),会在每次回调前调用(第一次回调不调用)
js
watch(count, (_, __, clearFn) => {
    const timer = setTimeout(() => {
    console.log(count.value);
    }, 2000);

    clearFn(() => clearTimeout(timer));
});
  1. 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 然后只执行一次回调
  1. watch 同步创建watch监听器会在组件销毁时自动停止, 如果是异步创建的监听器,需要手动设置停止,尽量少创建异步监听器,可以使用判断语句异步转同步。

ref

把一个ref绑定到模板ref属性上,会自动绑定ref,同react。使用起来同ref对象一样,需要通过ref.value 来获取。

组件

defineProps和defineEmits

都是sfc setup下面的宏。在sfc编译的时候回解析到相应的位置。

defineProps 详解

  1. defineProps定义的属性会解析成选项式中props属性。defineProps的返回值解析成对setup(prop)中prop的引用。由于宏原因,defineProps中不能访问setup内部声明的变量,但是可以访问引用进来的。因为引用语句在总是上层

  2. props如果接受到undefind值会触发自动default,如果未设置default,值为undefined。Boolean类型会自动转换成对应boolean值

  3. 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;
	}
}
  1. <CustomComp count /> 实际是<CustomComp count='' />的缩写,所以除了boolean类型总是会得到一个空字符串,boolean 类型在收到一个''或者undefined时会触发default

  2. 模板中可以自动使用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)  // true

readonly和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) // true

isRef、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>