• 组合式API(composition api) 选项式API(Options API)

# setup 语法糖

setup是Vue3.0推出的语法糖,并且在Vue3.2版本进行了大更新,像写普通JS一样写Vue组件,对于开发者更加友好了;

按需引入computed、watch、directive等选项,一个业务逻辑可以集中编写在一起,让代码更加简洁便于浏览

# 一、基本用法

只需在<script>里添加一个setup属性,编译时会把<script setup></script>里的代码编译成一个setup函数

<script setup>
    console.log('hello script setup');
</script>

普通<script>只会在组件被首次引入的时候执行一次,<script setup>里的代码会在每次组件实例被创建的时候执行。

# 二、data和methods

<script setup>里生命的变量和函数,不需要return暴露出去,就可以直接在template使用

<script setup>
    import {ref, reactive} from 'vue';
    // 普通变量
    const msg = 'Hello';

    // 响应式变量
    let num = ref(111); // ref声明基本类型变量
    const obj = reactive({ // reactive声明对象类型变量,如Object、Array、Date...
        key: 'this is a object'
    })

    // 函数
    function log() {
        console.log(msg); // hello
        console.log(num.value); // 111(可根据inpu输入值而改变)
        console.log(obj.key); // this is a object
    }
</script>

<template>
    <h1>{{ msg }}</h1>
    <p>{{ obj.key }}</p>
    <input v-model="num" type="text"/>
    <button @click="log">打印日志</button>
</template>

# 三、计算属性computed

<script setup>
    import { ref, computed } from 'vue';

    let count = ref(0);
    const countPlus = computed(() => {
        return count.value + 1;
    })
</script>

<template>
    <h1>计数: {{ countPlus }}</h2>
</template>

# 四、监听器watch、watchEffect

  1. watch监听器除了使用方式有区别之外,其他的与vue2.0没啥变化

    <script setup>
        import { ref, reactive, watch } from 'vue';
    
        // 监听ref
        let count = ref(0);
        watch(count, (newVal, oldVal) => {
            console.log('修改后', newVal);
            console.log('修改前', oldVal);
        })
    
        // 监听reactive属性
        const obj = reactive({
            count: 0
        })
        watch(
            () => obj.count, // 一个函数,返回监听属性
            (newVal, oldVal) => {
                console.log('修改后', newVal);
                console.log('修改前', oldVal);
            }, {
                immediate: true, // 立即执行,默认为false
                deep: true // 深度监听,默认为false
            }
        )
    
        const onChange = function() {
            count.value++;
            obj.count++;
        }
    </script>
    
    <template>
        <button @click="onChange">改变count</button>
    </template>
    
  2. watchEffect

    watchEffect是Vue3.0新增的一个监听属性的方法,它与watch的区别在于watchEffect不需要指定监听对象,回调函数里可直接获取到修改后的值

    <script setup>
        import { ref, reactive, watchEffect } from 'vue';
    
        let count = ref(0);
        const obj = reactive({
            count: 0
        })
        setTimeout(() => {
            count.value++;
            obj.count++;
        }, 1000);
    
        watchEffect(()=> {
            console.log('修改后的count', count.value);
            console.log('修改后的obj', obj, obj.count);
        })
    </script>
    

# 五、自定义指令directive

以 vNameOfDirective 的形式来命名本地自定义指令,可以直接在模板中使用

<script setup>
    // 导入指令可重命名
    // import  { myDirective as vMyDirective } from './MyDirective.js';

    // 自定义指令
    const vMyDirective = {
        beforeMount: (el) => {
            // 在元素上做些操作
        }
    }
</script>
<template>
    <h1 v-my-directive>This is a Heading</h1>
</template>

# 六、import 导入的内容可直接使用

  1. 导入的模块内容,不需要通过methods来暴露它

    // utils.js
    export const onShow = function(name) {
        return 'my name is' + name;
    }
    
    <!-- show.vue -->
    <script setup>
        import { onShow } from './utils.js';
    </script>
    <template>
        <div>{ onShow('jack') }</div>
    </template>
    
  2. 导入外部组件,不需要通过components注册使用

    <!-- Child.vue -->
    <template>
        <div>I am a child</div>
    </template>
    
    
    <!-- Parent.vue -->
    <script setup>
        import Child from './Child.vue';
    </script>
    <template>
        <child></child>
    </template>
    

# 7、声明props和emits

使用 defineProps 和 defineEmits API 来声明 props 和 emits

<!-- Child.vue -->
<script setup>
    import { defineProps, defineEmits } from 'vue';

    // 声明 props
    const props = defineProps({
        info: {
            type: String,
            default: ''
        }
    })

    // 声明emits
    const $emit = defineEmits(['change']);

    const onChange = function() {
        $emit('change', 'child返回值');
    }
</script>
<template>
    <h1>信息: {{info}}</h1>
    <button @click="onChange">点击我</button>
</template>


<!-- Parent.vue -->
<script setup>
    import { ref } from 'vue';
    import Child from './Child.vue';
    
    const msg = ref('hello setup!');

    const onAction = function(event) {
        console.log(event);
    }
</script>
<template>
    <child :info="msg" @change="onAction"></child>
</template>

demo

const props = withDefaults( // 设置默认值withDefaults
    defineProps<{
        modelValue?: string | number;
        placeholder?: string;
        type?: FieldType;
        prefixIcon?: string;
        maxlength: number;
        minLimit?: number;
        disabled?: boolean;
        autosize?: boolean;
        showWordLimit?: boolean;
        row?: number;
        fieldRef?: FieldInstance;
        autofocus?: boolean;
        addVisible?: boolean;
        noLineBreak?: boolean; // 不允许换行
    }>(),
    {
        autofocus: location.href.indexOf('mod') || location.href.indexOf('role') ? env.isAndroid : false
    }
);

# 八、父组件获取子组件的数据

父组件要想通过ref获取子组件的变量或函数,子组件须用defineExpose暴露出去

<!-- Child.vue -->
<script setup>
    import { ref, defineExpose } from 'vue';

    const info = ref('I am child');
    const onChange = function() {
        console.log('Function of child');
    }

    defineExpose({
        info, 
        onChange
    })
</script>
<template>
    <h1>信息: {{ info }}</h1>
    <button @click="onChange">点击我</button>
</template>

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
 
const childRef = ref()
const onAction = function() {
    console.log(childRef.value.info)    // I am child
    console.log(childRef.value.onChange())    // Function of child
}
</script>
 
<template>
    <child ref="childRef"></child>
    <button @click="onAction">获取子值</button>
</template>

# 九、provide 和 inject 传值

无论组件层次结构有多深,父组件都可以通过 provide 选项来给其所有子组件提供数据,子组件通过 inject 接收数据

<!-- Parent.vue -->
<script setup>
    import { ref, provide } from 'vue';
    import Child from './Child.vue';

    const msg = ref('Hello, my son');
    const onShow = function() {
        console.log('I am your parent');
    }

    provide('myProvide', {
        msg, 
        onShow
    })
</script>
<template>
    <child></child>
</template>

<!-- Child.vue -->
<script setup>
    import { inject } from 'vue';

    const provideState = inject('myProvide');

    const getData = function() {
        console.log(provideState.msg); // Hello, my son
        console.log(provideState.onShow()); // I am your parent
    }
</script>
<template>
    <button @click="getData">获取父值</button>
</template>

# 十、路由 useRoute 和 useRouter

<script setup>
    import { useRoute, useRouter } from 'vue-router';

    const $route = useRoute();
    const $router = useRouter();

    // 路由信息
    console.log($route.query);

    // 路由跳转
    $router.push('/login');
</script>

# 十一、对await异步的支持

<script setup>中可以使用顶层 await。结果代码会被编译成 async setup();

<script setup>
    const post = await fetch('/api/post/1').then(r => r.json());
</script>

# 十二、nextTick

<!-- 方式一 -->
<script setup>
    import { nextTick } from 'vue';

    nextTick(() => {
        console.log('DOM已更新')
    })
</script>


<!-- 方式二 -->
<script setup>
    import { nextTick } from 'vue';

    await nextTick(); // nextTick是一个异步函数,返回一个promise实例
    // console.log('Dom已更新!')
</script>

# 十三、全局属性 globalProperties

// main.js定义 
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);

// 定义一个全局属性$global
app.config.globalProperties.$global = 'This is a global property';

app.mount('#app');
<!-- 组件内使用 -->
<script setup>
    import { getCurrentInstance } from 'vue';

    // 获取Vue实例
    const { proxy } = getCurrentInstance();
    // 输出
    console.log(proxy.$global); // This is a global property
</script>

# 十四、生命周期

setup()里访问组件的生命周期需要在生命周期钩子钱加上"on",并且没有beforeCreate和created生命周期钩子

TIP

因为setup是围绕 beforeCreate 和 created 声明周期钩子运行的,所以不需要显式的定义它们。换句话说,在这些钩子中编写的任何代码都应该在 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
renderTriggered onRenderTriggered
activated onActivated
deactivated onDeactivated
<!-- 使用方式 -->
<script setup>
    import { onMounted } from 'vue';

    onMounted( => {
        console.log('onMounted')
    })
</script>

# 十五、与普通的script标签一起使用

<script setup>可以和普通的<script>一起使用。普通的<script>在有这些需要的情况下或许会被使用到:

  • 无法在<script setup>声明的选项,例如 inheritAttrs 或通过插件启用的自定义的选项;
  • 声明命名导出,<script setup >定义的组件默认以组件文件的名称作为组件名;
  • 运行副作用或创建只需要执行一次的对象。
<script>
    // 普通 script,在模块范围下执行(只执行一次)
    runSideEffectOnce();
    // 声明额外的选项
export default {
  name: 'ComponentName',    // 组件重命名
  inheritAttrs: false,
  customOptions: {}
}
</script>
 
<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>

# 十六、v-memo新指令

该指令与v-once类似,v-once是只渲染一次之后的更新不在渲染,而v-memo是根据条件渲染.该指令接收一个固定长度的数组作为依赖值进行记忆比对,如果数组中的每个值都和上次渲染的时候相同,则该元素(含子元素)不刷新

  1. 应用于普通元素或者组件

    <template>
        <!-- 普通元素 -->
        <div v-memo="[valueA, valueB]">
            <!-- ... -->
        </div>
    
    <!-- 组件 -->
    <component v-memo="[valueA, valueB]"></component>
    </template>
    
    <script setup>
        import component from '../components/component.vue'
    </script>
    

    当组件重新渲染的时候,如果valueA 与 valueB 都位置不变,那么对这个<div>以及它的所有自己诶单的更新豆浆杯跳过

  2. 结合 v-for使用

    v-memo仅提供性能敏感场景的针对性优化,会用到的场景应该很少。渲染v-for长列表(长度大于1000)可能是他最有用的场景

    <template>
    <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>
    </template>
    

    selected发生变化时,只有item.id===selected的该项重新渲染,其余不刷新。

# 十七、Hooks封装

// useCounter.js
import { ref } from 'vue';

export function useCounter(initial = 0) {
    const count = ref(initial);
    function increment() {
        count.value++;
    }
    return {count, increment}
}

// used
import { useCounter } from './useCounter';
const {count, increment} = useCounter(10);

# 十八、watchEffect

watchEffect 相比于 watch,它能自动帮我们收集依赖进行监听响应时变化

<script setup lang="ts">
    import { ref, watchEffect } from 'vue';

    const count = ref(0);
    const message = ref('');

    watchEffect(() => {
        message.value = `count 的值为:${count.value}`
    })
</script>

# 十九、provide+inject

当你在封装一些比较复杂的组件时,设计到的组件层级比较多的时候,使用props进行数据传递比较麻烦,所以可以使用 provide + inject 来进行数据传递共享

<!-- Parent.vue -->
<script setup lang="ts">
    import { provide, ref } from 'vue';

    const theme = ref('light');
    provide('theme', theme);
</script>

<!-- DeepChild.vue -->
<script setup lang="ts">
    import { inject } from 'vue';

    const theme = inject('theme');
    console.log(theme.value);
</script>

# 二十、shallowRef

当有数据量打的数组或者对象时,我们如果只需要监听渐层响应式变化的话,可以不需要使用ref,而是使用shallowRef,这样的话只会监听浅层变化,而不会监听深层变化,提升了性能

<script setup lang="ts">
    import { shallowRef } from 'vue';

    const largeObject = shallowRef({/**海量属性 */})function updateObject() {
        largeObject.value = {/**大型对象 */}
    }
</script>

# 二十一、defineExpose

如果你想要在使用组件的页面中,去调用组件中的方法,可以使用 defineExpose 将组件内的方法暴露出来,供外部使用

<!-- ParentComponent.vue -->
<script setup lang="ts">
    const modal = ref();
</script>

<template>
    <button @click="modal.open()">打开模拟框</button>
    <AppModal ref="modal"/>
</template>

# 二十二、effectScope全局状态管理

Pinia 相信大家都用过吧,它其实原理就是基于 effectScope 实现的, effectScope 可以用来做全局或者局部状态管理,Vueuse 中的状态管理 Hooks createGlobalState 也是基于 effectScope 实现的

import { effectScope } from 'vue';

export function createGlobalState<Fn extends (...args: any[]) => any>(stateFactory:Fn): Fn {
    let initialized = false;
    let state : any;
    const scope = effectScope(true);

    return ((...args: any[]) => {
        if(!initialized) {
            state = scope.run(() =>  stateFactory(...args))!;
            initialized = true
        }
        return state;
    }) as Fn
}

# 其他

# unref、isRef、toRef、toRefs

  • unref 如果参数是一个ref 则返回它的value,否则返回参数本身

    unref(val)相当于val = isRef(val) ? val.value : val

    function initialCount(value: number | Ref<number>) {
        // Make the output be true
        console.log(value === 10)
    }
    const initial = ref(10);
    
    initialCount(unRef(initial)); // 必然返回true
    
  • isref 检查一个值是否是一个ref对象

  • toRef、toRefs的本质是引用,修改响应式数据,会影响到原始数据,视图不会更新

    • toRef 一次仅能设置一个数据,接收两个参数,第一个参数是哪个对象,第二个参数是对象的那个属性
    • toRefs 作用是将响应式对象中所有属性转换为单独的响应式数据,将对象中的每个属性都做一次ref操作,使每个属性都具有响应式
    const state = reactive({
        foo:1,
        bar: 2
    })
    // const fooRef = toRef(state, 'foo');
    const fooRef = toRefs(state).foo
    
    fooRef.value++;
    console.log(state.foo === 2);
    
    state.foo++;
    console.log(fooRef.value === 2)
    
    <template>
        <div>
            <p>
                <span @click="update(count - 1)">-</span>
                {{count}}
                <span @click="update(count + 1)">-</span>
            </p>
        </div>
    </template>
    <script setup lang="ts">
        import { reactive, toRefs } from 'vue';
    
        function useCount() {
            const state = reacitve({
                count: 0
            })
            function update(value: number) {
                state.count = value;
                console.log(state.count)
            }
    
            return {
                state: toRefs(state),
                update,
            }
        }
        const {state: {count}, update} = useCount();
    </script>
    
  • 展开运算符

    响应式对象的处理,是加给对象的,如果对对象做了展开操作,那么就会丢失响应式效果。

    <template>
        <button @click="name='张三'">修改名字</button>
        {{name}}
    </template>
    <script lang="ts">
        import { reactive, toRef } form 'vue';
        
        export default  {
            setup() {
                const user = reactive<any>({
                    name: '小明',
                    age: 10,
                    addr: {
                        province: '山东',
                        city: '青岛'
                    }
                })
    
                return {
                    ...toRefs(user);
                }
            }
        }
    </script>
    

# 资料

原文 (opens new window)

SFC 语法规范 | Vue.js (opens new window)

Vue3.2 setup语法糖、Composition API、状态库Pinia归纳总结 (opens new window)

Vue3.2 setup语法糖、Composition API、状态库Pinia归纳总结 --pinia (opens new window)