Vue3.0之初体验

上周,Vue3.0 正式版发布,相对之前的 RC 版,变化不是很大,这几天我也体验了一下 3.0 的正式版,这里就以最近的一个小 demo 看一下 Vue3.0 带来的新特性。

这里实现一个搜索框的效果。

demo效果

模板部分,和 Vue2.x 无太大差别

<template>
  <div class="search-wrapper" ref="searchRef">
    <input
      v-model="searchText"
      :class="{ 'has-result': matchList.length }"
      @keydown.down.prevent="handleKeydown"
      @keydown.up.prevent="handleKeyUp"
      @keydown.enter.prevent="handleSelect"
      @keydown.esc.prevent="handleExit"
      @focus="handleFocus"
    />
    <ul v-if="canShowResult" class="result-wrapper">
      <li
        v-for="(item, index) in matchList"
        :key="index"
        :class="{ active: activeIndex === index }"
        @mouseover="activeIndex = index"
      >
        <span v-html="highlight(item.code, searchText)" class="fund-code" />
        <span v-html="highlight(item.name, searchText)" />
        <span class="fund-type" :style="{ color: fundTypeColors.get(item.type) || '#3e3a3a' }">{{ item.type }}</span>
      </li>
    </ul>
  </div>
</template>

逻辑部分,我使用 Vue3.0 带来的 composition api 新特性来实现,下面是逻辑部分的全部代码

import { defineComponent, ref, watch, watchEffect, computed, onUnmounted } from 'vue';
import useDebounceRef from './useDebounceRef';
import usePage from './usePage';
import data from './data';

type MatchItem = { code: string; shortcut: string; name: string; type: string };

export default defineComponent({
  setup() {
    const searchText = useDebounceRef('');
    const matchList = ref<MatchItem[]>([]);
    const showResult = ref(false);
    const searchRef = ref<HTMLDivElement>();

    watch(searchText, () => {
      const preList = searchText.value
        ? data.filter((d) => d.some((input) => input.includes(searchText.value.toUpperCase()))).slice(0, 8)
        : [];
      matchList.value = preList.map((item) => {
        const [code, shortcut, name, type] = item;
        return { code, shortcut, name, type };
      });
    });

    watchEffect(() => {
      if (searchText.value) {
        showResult.value = true;
      }
    });

    const handleClick = (e: any) => {
      if (e.target !== searchRef.value && !searchRef.value?.contains(e.target)) {
        showResult.value = false;
      }
    };

    document.body.addEventListener('click', handleClick, false);

    onUnmounted(() => {
      document.body.removeEventListener('click', handleClick, false);
    });

    const canShowResult = computed(() => showResult.value && matchList.value.length);

    const { prev, next, page } = usePage(matchList, true);

    const fundTypeColors = new Map([
      ['混合型', '#d08a31'],
      ['债券型', '#318fbb'],
      ['股票型', '#ef0505'],
      ['货币型', '#67bf43'],
      ['定开债券', '#32af86'],
      ['联接基金', '#8e26a7'],
      ['QDII', '#d03077'],
      ['QDII-指数', '#f562a4'],
      ['股票指数', '#ce2222'],
      ['混合-FOF', '#317929'],
    ]);

    const highlight = (text: string, keyword: string) =>
      text.replace(new RegExp(keyword, 'ig'), `<span class='active'>${keyword}</span>`);

    const handleSelect = () => {};

    const handleExit = () => {
      showResult.value = false;
    };

    const handleFocus = () => {
      showResult.value = true;
    };

    return {
      searchText,
      matchList,
      fundTypeColors,
      highlight,
      handleKeydown: next,
      handleKeyUp: prev,
      handleExit,
      handleSelect,
      handleFocus,
      showResult,
      activeIndex: page,
      canShowResult,
      searchRef,
    };
  },
});

为了让 typescript 更好的类型推断,这里使用defineComponentapi 来定义组件,如果你在 Vue3.0 的源码中查看这个函数的定义,你会发现它其实接近一个空的函数定义,如下所示,主要目的就是为了 ts 推断。

// Vue3.0源码
export function defineComponent(options: unknown) {
  return isFunction(options) ? { setup: options, name: options.name } : options;
}

继续向下看。

const searchText = useDebounceRef('');

这里使用useDebounceRef自定义 hook,它用来给 input 添加防抖功能。这也是 Vue3.0 带来的新的代码书写方式,将可以复用的逻辑抽离到自定义 hook 中。

useDebounceRef 实现代码如下

import { customRef } from 'vue';

export default function useDebounceRef<T = unknown>(value: T, delay = 200) {
  let timeout: number;

  /**
   * 创建具有自定义ref,显式控制它的依赖跟踪(track)和触发更新(trigger)
   * 它需要一个工厂函数customRef,该函数接收track和trigger作为参数,返回带有get和的set对象
   */
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return value;
      },
      set(newValue: T) {
        clearTimeout(timeout);
        timeout = window.setTimeout(() => {
          value = newValue;
          trigger();
        }, delay);
      },
    };
  });
}

useDebounceRef使用了 Vue3.0 中的customRefapi 来实现效果。通过使用customRef,你可以手动的控制依赖的收集和更新的触发时机。当输入框在输入的时候,v-model触发set操作,此时通过setTimeout定时器可以很容易做到输入防抖。

继续向下

const matchList = ref<MatchItem[]>([]);
const showResult = ref(false);
const searchRef = ref<HTMLDivElement>();

这里用到了ref这个新的 api,可以看一下官方的介绍:https://v3.vuejs.org/api/refs-api.html

通过ref将值包裹,Vue3.0 就可以追踪值的变化,不论值是引用类型还是基本数据类型,不过,ref包装后的值都在value属性下。

const number = ref(0);

// 访问的时候应该为number.value
const number1 = number.value + 1;

继续向下看

watch(searchText, () => {
  const preList = searchText.value
    ? data.filter((d) => d.some((input) => input.includes(searchText.value.toUpperCase()))).slice(0, 8)
    : [];
  matchList.value = preList.map((item) => {
    const [code, shortcut, name, type] = item;
    return { code, shortcut, name, type };
  });
});

watchEffect(() => {
  if (searchText.value) {
    showResult.value = true;
  }
});

这里用到了watchwatchEffect两个 api,它们有什么区别?

watch和 Vue2.x 中区别不大,都是用来watch属性值的变化执行一些操作,有一点不同是它的第一个参数,第一个参数表示要watch的值,当值变化的时候,执行watch的回调,它可以传递 Vue3.0 中的可响应对象或者一个函数,在 Vue2.x 中watch可以是字符串,但是在这里则不行。

export default defineComponent({
  props: {
    propValue: String,
  },
  setup(props) {
    const refValue = ref(0);
    const state = reactive({ count: 0 });

    watch(refValue, () => {
      // bala bala...
    });

    watch(
      () => state.count,
      () => {
        // bala bala...
      },
    );

    watch(
      () => props.propValue,
      () => {
        // bala bala...
      },
    );
  },
});

watchEffect则不用传递第一个参数,它会自动追踪它的回调函数中的响应式数据的变化,当数据变化的时候,它的回调会自动执行。这和 Vue2.x 中的computed有点像,但是computed无法执行副作用,而watchEffect可以。

// 官网例子
const count = ref(0);

watchEffect(() => console.log(count.value));
// -> logs 0

setTimeout(() => {
  count.value++;
  // -> logs 1
}, 100);

继续向下看

const handleClick = (e: any) => {
  if (e.target !== searchRef.value && !searchRef.value?.contains(e.target)) {
    showResult.value = false;
  }
};

document.body.addEventListener('click', handleClick, false);

onUnmounted(() => {
  document.body.removeEventListener('click', handleClick, false);
});

Vue3.0 中提供了一系列的生命周期函数,具体查看:https://v3.vuejs.org/api/options-lifecycle-hooks.html,这里onMounted表示页面初始渲染完毕时,onUnmounted表示页面卸载时。

最后,将上述定义的函数在setupreturn出去,之后在模板中就可以访问到了。

// ...
return {
  searchText,
  matchList,
  fundTypeColors,
  highlight,
  handleKeydown: next,
  handleKeyUp: prev,
  handleExit,
  handleSelect,
  handleFocus,
  showResult,
  activeIndex: page,
  canShowResult,
  searchRef,
};

结语

Vue3.0 带来的 composition api 让页面组织代码更灵活,也解决了 Vue2.0 中mixin代码复用命名空间的问题。

相对于 React 而言,使用 Vue3.0,你不用去考虑依赖的问题,也不用去考虑 React 中组件每次传参重复传染的问题,减少了不少的心智负担。不过呢,对于使用者来说,针对不同的项目,选择合理的框架才是关键。

如果您觉得本文对您有用,欢迎捐赠或留言~
微信支付
支付宝

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注