IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Vue Composition API · 看不见我的美 · 是你瞎了眼

    馬腊咯稽发表于 2020-04-23 00:00:00
    love 0
    Vue Composition API

    与 Hooks 的区别

    1. 更符合 JS 习惯;
    2. 对调用顺序不敏感;
    3. 在每次渲染的过程中不会重复调用,对垃圾回收机制的压力更小;
    4. 避免了内联处理程序导致子组件重复渲染的问题(在 React 中需要使用 useCallback);
    5. 不需要像 useEffect 和 useMemo 那样需要正确的设置依赖,Vue 会自动的追踪。

    值传递与引用传递

    新的 API 非常依赖引用传递,这样 Vue 才能及时捕获数据变化;这也是为什么 toRef 会将普通类型数据包装成对象的原因;reactive 包装的对象在解构时为了保持响应性需要用 toRefs 包装也是这个原因。

    setup

    作为 Composition API 在 Vue 组件中的入口;它会在组件实例被创建,props 完成初始化之后调用;和模板一起使用时,返回的对象会自动合并到渲染上下文中。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    <template>
     <div>{{ count }} {{ object.foo }}</div>
    </template>
    <script>
     import { ref, reactive } from 'vue';
     export default {
     setup() {
     // ref 在 setup 中返回并在模板中使用时会自动解构,不需要通过 count.value 访问
     const count = ref(0);
     const object = reactive({ foo: 'bar' });
     return {
     count,
     object
     };
     }
     };
    </script>
    

    setup 也可以返回一个渲染函数,渲染函数可以直接使用所在作用域中的响应式状态。

    1
    2
    3
    4
    5
    6
    7
    8
    
    import { h, ref, reactive } from 'vue';
    export default {
     setup() {
     const count = ref(0);
     const object = reactive({ foo: 'bar' });
     return () => h('div', [count.value, object.foo]);
     }
    };
    

    setup 的第一个参数接收 props。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    export default {
     props: {
     name: String
     },
     setup(props) {
     // props 对象是响应式的并且是不可变(immutable)对象,不要尝试去修改它
     console.log(props.name);
     watchEffect(() => {
     console.log(`name is: ` + props.name);
     });
     }
    };
    

    不要对 props 进行解构,这样就不再是响应式对象了。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    export default {
     props: {
     name: String
     },
     setup({ name }) {
     watchEffect(() => {
     // props 变化时,不会重新执行
     console.log(`name is: ` + name);
     });
     }
    };
    

    第二个参数是 context 对象,context 中包含着一些属性;context 可以被解构。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    const OneComponent = {
     setup(props, context) {
     context.attrs;
     context.slots;
     context.emit;
     }
    };
    const TwoComponent = {
     setup(props, { attrs }) {
     /**
     * 之所以没有把 props 作为 context 的一个属性的原因:
     * 1. props 更常用
     * 2. 可以更好的用 TS 对其类型做限制
     */
     function onClick() {
     console.log(attrs.foo);
     // setup 中无法访问 this
     this && console.log(this);
     }
     }
    };
    

    setup 的类型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    interface Data {
     [propsName: string]: unknown;
    }
    interface SetupContext {
     attrs: Data;
     slots: Slots;
     emit: (event: string, ...args: unknown[]) => void;
    }
    function setup(props: Data, context: SetupContext): Data;
    

    reactive

    reactive 接受一个对象并返回一个响应式代理,相当于 2.x 版本的 Vue.observable({})。

    1
    2
    3
    
    // 响应式转换是深层的,会影响所有嵌套的属性
    function reactive<T extends object>(raw: T): T;
    const obj = reactive({ count: 0 });
    

    ref

    ref 的返回值是一个响应式的可变对象,对象只有一个属性 value;如果传入一个对象,这个对象会被 reactive 方法进行响应式转换。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    const count = ref(0);
    const otherCount = ref(2);
    // 如果 ref 作为响应式对象的一个属性,也会自动展开内部的 value,就像普通属性一样
    const state = reactive({ count });
    console.log(state.count); // 0
    state.count = 1;
    console.log(count.value); // 1
    // count 被替换了,两个 ref 之间并没有建立任何联系
    state.count = otherCount;
    console.log(state.count); // 2
    console.log(count.value); // 1
    

    ref 在 setup 中返回并在模板中使用时会自动解构,不需要通过 count.value 访问;如果从响应式数组或其他响应式的数据类型中获取 ref 的值时,还是需要访问 value 属性。

    1
    2
    3
    4
    5
    
    const arr = reactive([ref(0)]);
    const map = reactive(new Map([['foo', ref(0)]]));
    // 需要访问 value 属性
    console.log(arr[0].value);
    console.log(map.get('foo').value);
    

    ref 类型:

    1
    2
    3
    4
    5
    6
    
    interface Ref<T> {
     value: T;
    }
    function ref<T>(value: T): Ref<T>;
    const foo = ref<string | number>('foo');
    foo.value = 123;
    

    computed

    传入一个 getter 函数并返回一个不可变的响应式的 ref 对象。

    1
    2
    3
    4
    
    const count = ref(1);
    const plusOne = computed(() => count.value + 1);
    console.log(plusOne.value); // 2
    plusOne.value++; // 报错
    

    如果传入一个有 getter 和 setter 函数的对象,则返回一个可写的 ref 对象。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    const count = ref(1);
    const plusOne = computed({
     get() {
     return count.value + 1;
     },
     set(val) {
     count.value = val - 1;
     }
    });
    plusOne.value = 1;
    console.log(count.value); // 0
    

    computed 的类型:

    1
    2
    3
    4
    5
    
    function computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>;
    function computed<T>(options: {
     get: () => T;
     set: (value: T) => void;
    }): Ref<T>;
    

    readonly

    返回一个原始对象的只读代理,代理中所有嵌套的属性都将是只读的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    const original = reactive({ count: 0 });
    const copy = readonly(original);
    watchEffect(() => {
     console.log(copy);
    });
    // 改变原始对象会触发 watcher 的执行,因为 watcher 依赖了原始对象的只读代理
    original.count++;
    // 改变只读对象会报警告并失败
    copy.count++;
    

    watchEffect

    在追踪响应式依赖时会立即执行,当依赖发生变化时会重新运行;就像 React.useEffect 的自动设置依赖的版本。

    1
    2
    3
    4
    5
    6
    7
    
    const count = ref(0);
    watchEffect(() => {
     console.log(count.value); // 0, 1, 2, ...
    });
    setTimeout(() => {
     count.value++;
    }, 1000);
    

    当 watchEffect 在 setup 或生命周期函数中调用时,watcher 会被关联到组件的生命周期上,当组件被卸载时会自动停止执行。当然它会返回一个函数,调用这个函数也可以停止 watcher。

    1
    2
    3
    4
    
    const stopWatcher = watchEffect(() => {
     console.log('...');
    });
    stopWatcher();
    

    有时 watchEffect 中会进行一些异步的副作用操作,当副作用失效时需要进行一些清理工作(比如,在副作用执行完毕之前,依赖发生了变化);副作用函数可以接受一个 onInvalidate 函数,这个函数会在这些情况下被调用:

    • 副作用将被重新执行的时候;
    • watcher 被停止的时候。

    鹅,好像 React.useEffect 的返回函数。

    1
    2
    3
    4
    5
    6
    7
    
    watchEffect(onInvalidate => {
     const token = performAsyncOperation(id.value);
     onInvalidate(() => {
     // 终止之前正在进行的异步操作
     token.cancel();
     });
    });
    

    Vue 的响应式系统会缓冲失效的副作用并且异步地释放掉他们来避免在一个事件循环中进行太多非必要的重复调用;在 Vue 内部,组件更新函数也是一个被观察的操作,当一个用户操作被加入到队列中时,在组件更新之后,它总是会被调用。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    <template>
     <div>{{ count }}</div>
    </template>
    <script>
     export default {
     setup() {
     const count = ref(0);
     watchEffect(() => {
     /**
     * 将在初始化时同步运行,初始化运行会在组件挂载之前
     * 当 count 改变时,回调函数会在组件更新之后调用
     */
     console.log(count.value);
     });
     return {
     count
     };
     }
     };
    </script>
    

    watchEffect 的初始化运行发生在组件挂载之前,如果想要在 watchEffect 中访问 DOM/refs,需要把 watchEffect 放在 onMounted 中。

    1
    2
    3
    4
    5
    
    onMounted(() => {
     watchEffect(() => {
     // 这里可以访问 DOM
     });
    });
    

    watchEffect 可以接受第二个参数来控制副作用的执行时机。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    // 同步调用
    watchEffect(() => {}, {
     flush: 'sync' // 默认值是 post
    });
    // 在组件更新之前
    watchEffect(() => {}, {
     flush: 'pre'
    });
    // 调试
    watchEffect(() => {}, {
     // 这两个函数只在开发环境会被执行
     onTrack() {
     // 当响应式属性或者 ref 作为一个依赖被追踪到时调用
     },
     onTrigger() {
     // 当依赖更新 watcher 回调被调用后调用
     debugger;
     }
    });
    

    watchEffect 的类型:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    type StopHandle = () => void;
    type InvalidateCbRegistrator = (invalidate: () => void) => void;
    interface DebuggerEvent {
     effect: ReactiveEffect;
     target: any;
     type: OperationTypes;
     key: string | symbol | undefined;
    }
    interface WatchEffectOptions {
     flush?: 'pre' | 'post' | 'sync';
     onTrack?: (event: DebuggerEvent) => void;
     onTrigger?: (event: DebuggerEvent) => void;
    }
    function watchEffect(
     effect: (onInvalidate: InvalidateCbRegistrator) => void,
     options?: WatchEffectOptions
    ): StopHandle;
    

    watch

    和 2.x 的 this.$watch 一模一样,用来观察数据并(惰性的)执行对应的副作用;惹,和 watchEffect 傻傻分不清:

    1. watch 是惰性的;
    2. 可以同时获取状态改变前后的值;
    3. 更加有针对性,也就是说可以明确指定要追踪的依赖。

    watch 可以监视 getter、一个或多个 ref:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    const state = reactive({
     count: 0
    });
    const count = ref(0);
    watch(
     () => state.count,
     (count, prevCount) => {}
    );
    watch(count, (count, prevCount) => {});
    watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
     /* ... */
    });
    

    取消观察、取消副作用、时机控制和调试方面,watch 的行为和 watchEffect 行为一致。

    watch 的类型:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    type WatcherSource<T> = Ref<T> | (() => T);
    type MapSources<T> = {
     [K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never;
    };
    interface WatchEffectOptions {
     flush?: 'pre' | 'post' | 'sync';
     onTrack?: (event: DebuggerEvent) => void;
     onTrigger?: (event: DebuggerEvent) => void;
    }
    interface WatchOptions extends WatchEffectOptions {
     immediate?: boolean; // 默认是 false
     deep?: boolean;
    }
    function watch<T>(
     source: WatchSource<T>,
     callback: (
     value: T,
     oldValue: T,
     onInvalidate: InvalidateCbRegistrator
     ) => void,
     options?: WatchOptions
    ): StopHandle;
    function watch<T extends WatcherSource<unknown>[]>(
     source: T,
     callback: (
     values: MapSources<T>,
     oldValues: MapSources<T>,
     onInvalidate: InvalidateCbRegistrator
     ) => void,
     options?: WatchOptions
    ): StopHandle;
    

    生命周期

    生命周期函数只能用在 setup 里,因为它们依赖内部的全局状态来定位当前活跃的组件实例,否则会报错。

    对比 2.x 的生命周期函数:

    • beforeCreate -> setup
    • created -> setup
    • beforeMount -> onBeforeMount
    • mounted -> onMounted
    • beforeUpdate -> onBeforeUpdate
    • updated -> onUpdated
    • beforeDestroy -> onBeforeUnmount
    • destroyed -> onUnmounted
    • errorCaptured -> onErrorCaptured
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    import { onMounted, onUpdated, onUnmounted } from 'vue';
    const MyComponent = {
     setup() {
     onMounted(() => {
     console.log('mounted!');
     });
     onUpdated(() => {
     console.log('updated!');
     });
     onUnmounted(() => {
     console.log('unmounted!');
     });
     }
    };
    

    3.0 还新赠送了两个钩子它们都接收 DebuggerEvent 作为参数,用来检查是哪些依赖导致组件重新渲染的:

    • onRenderTracked
    • onRenderTriggered
    1
    2
    3
    4
    5
    
    export default {
     onRenderTriggered(e) {
     debugger;
     }
    };
    

    依赖注入

    行为和 2.x 的 provide/inject 一样;两者需要在 setup 中使用。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    import { provide, inject } from 'vue';
    const ThemeSymbol = Symbol();
    const Ancestor = {
     setup() {
     provide(ThemeSymbol, 'dark');
     }
    };
    const Descendent = {
     setup() {
     // inject 的第二个参数是一个缺省值
     const theme = inject(ThemeSymbol, 'light');
     return { theme };
     }
    };
    

    可以注入一个 ref 来保持响应性,也可以传入一个响应式对象。

    1
    2
    3
    4
    5
    6
    7
    8
    
    // provider
    const themeRef = ref('dark');
    provide(ThemeSymbol, themeRef);
    // comsumer
    const theme = inject(ThemeSymbol, ref('light'));
    watchEffect(() => {
     console.log(`theme set to: ${theme.value}`);
    });
    

    provide/inject 的类型:

    1
    2
    3
    4
    
    interface InjectionKey<T> extends Symbol {}
    function provide<T>(key: InjectionKey<T> | string, value: T): void;
    function inject<T>(key: InjectionKey<T> | string): T | undefined;
    function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T;
    

    Vue 提供一个继承于 Symbol 的 InjectionKey 接口:

    1
    2
    3
    4
    5
    6
    
    import { InjectionKey, provide, inject } from 'vue';
    const key: InjectionKey<string> = Symbol();
    // provider
    provide(key, 'foo');
    // comsumer
    const foo = inject(key, 'bar');
    

    refs

    在新 API 中,响应式 refs 和 模板 refs 的概念是统一的:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    <template>
     <div ref="root"></div>
    </template>
    <script>
     import { ref, onMounted } from 'vue';
     export default {
     setup() {
     const root = ref(null);
     // 在初始化时,DOM 元素会赋值给 ref
     onMounted(() => {
     console.log(root.value); // <div/>
     });
     return { root };
     }
     };
    </script>
    

    在 VirtualDOM 的 patch 算法中,如果一个 VNode 的 ref 和渲染上下文中的 ref 对应,那么 VNode 对应的元素或组件实例会赋值给这个 ref;这发生在 VDOM 挂载/更新的过程中,所以 template refs 只会在渲染时被赋值。

    和 JSX/渲染函数 一起使用:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    export default {
     setup() {
     const root = ref(null);
     // 渲染函数
     return () =>
     h('div', {
     ref: root
     });
     // JSX
     return () => <div ref={root} />;
     }
    };
    

    在 v-for 中使用:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    <template>
     <div v-for="(item, i) in list" :ref="el => { divs[i] = el }">{{ item }}</div>
    </template>
    <script>
     import { ref, reactive, onBeforeUpdate } from 'vue';
     export default {
     setup() {
     const list = reactive([1, 2, 3]);
     const divs = ref([]);
     onBeforeUpdate(() => {
     divs.value = [];
     });
     return {
     list,
     divs
     };
     }
     };
    </script>
    

    unref

    如果传入的是 ref,返回 ref.value;否则返回参数自身;val = isRef(val) ? val.value : val 的语法糖。

    1
    2
    3
    
    function useFoo(x: number | Ref<number>) {
     const unwrapped = unref(x); // 黑人问号,这是为了保持 Vue API 巨多的传统吗?
    }
    

    toRef

    使用响应式对象中的某个属性来创建 ref,这个 ref 会和那个属性保持响应式的连接。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    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
    

    toRef 这个时候最有用:

    1
    2
    3
    4
    5
    
    export default {
     setup(props) {
     useSomeFeature(toRef(props, 'foo'));
     }
    };
    

    toRefs

    将响应式对象转化为普通对象,这个对象的每个属性都是一个 ref,指向原来的对象。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    const state = reactive({
     foo: 1,
     bar: 2
    });
    /**
     * Type of stateAsRefs:
     * {
     * foo: Ref<number>,
     * bar: Ref<number>
     * }
     */
    const stateAsRefs = toRefs(state);
    // ref 和原对象中对应的属性连接
    state.foo++;
    console.log(stateAsRefs.foo); // 2
    stateAsRefs.foo.value++;
    console.log(state.foo); // 3
    

    toRefs 这个时候最有用:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    function useFeatureX() {
     const state = reactive({
     foo: 1,
     bar: 2
     });
     return toRefs(state);
    }
    export default {
     setup() {
     // 可以解构而不会丢失响应性
     const { foo, bar } = useFeatureX();
     return {
     foo,
     bar
     };
     }
    };
    

    其他

    • isRef,判断值是否为 ref 对象
    • isProxy,判断对象是否是 reactive/readonly 创建的一个代理;
    • isReactive,判断对象是否是 reactive 创建的一个响应式代理;
    • isReadonly,判断对象是否是 readonly 创建的一个只读代理。


沪ICP备19023445号-2号
友情链接