[toc]

# 1、更改对象 / 数组类型的 props

当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。

这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件 (opens new window)来通知父组件做出改变。

# 2、prop校验

  // 对象类型的默认值
  propE: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  // 在 3.4+ 中完整的 props 作为第二个参数传入
  propF: {
    validator(value, props) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }

# 3、组件应用v-model

MyInput.vue

默认主要使用 modelValueupdate:modelValue

<template>
  <div>
    <p>default:</p>
    <input :value="modelValue"
           type="text"
           class="my-input"
           @input="onInput">
    <p>lastName:</p>
    <input :value="lastName"
           type="text"
           class="my-input"
           @input="onInput2">
  </div>
</template>
<script lang="ts">
import { ref } from 'vue';
export default {
  name: 'MyInput',
  props: {
    modelValue: {
      type: String,
      default: ''
    },
    lastName: {
      type: String,
      default: ''
    }
  },
  emits: ['update:modelValue', 'update:lastName'],
  // components: {},
  setup(props, context) {
    const onInput = (e) => {
      context.emit('update:modelValue', e.target.value);
    };
    const onInput2 = (e) => {
      context.emit('update:lastName', e.target.value);
    };

    return { onInput, onInput2 };
  }
};
</script>
<style scoped lang='stylus'>
.my-input {
  background-color: #ccc;
}
</style>

如果想使用多个v-mode,就需要使用v-mode的参数,用法如下

<MyInput v-model="inputValue"
         v-model:lastName="lastName"></MyInput>

上面是比较容易理解的写法,封装更多的写法如下, 使用setup语法糖

<template>
  <div>
    XInput: <input type="text"
           v-model="model">
    <div>
      <button @click="add">add</button>
      <p>title={{title}}</p>
      <button @click="setTitle">set title </button>
    </div>
  </div>
</template>
<script>
export default {
  name: 'XInput'
};
</script>
<script setup>
// import { ref } from 'vue';
const model = defineModel({ default: '' });
const title = defineModel('title', {
  default: '',
  required: true
});

// 通过使用 defineModel ,修改model ,会直接更新外部绑定的值
function add() {
  model.value++;
}

function setTitle() {
  title.value = 'title' + Math.random();
}
</script>
<style scoped lang='stylus'></style>

# 3.1修饰符 v-model.capitalize

<template>
  <div>
    <input type="text"
           :value="modelValue"
           @input="onInput">
  </div>
</template>
<script lang="ts">
import { onMounted, ref } from 'vue';
export default {
  name: 'InputCapitalize',
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  // components: {},
  setup(props, context) {
    onMounted(() => {
      console.log(props.modelModifiers); // {capitalize: true}
    });

    const onInput = (e) => {
      let val = e.target.value;
      if (props.modelModifiers.capitalize) {
        val = val.charAt(0).toUpperCase() + val.slice(1);
      }

      context.emit('update:modelValue', val);
    };
    return { onInput };
  }
};
</script>
<style scoped lang='stylus'></style>

用法

<InputCapitalize v-model.capitalize="inputCapitalizeValue"></InputCapitalize>

# setup语法糖

<template>
  <div>
    <input type="text"
           v-model="model">
  </div>
</template>
<script>
import { ref } from 'vue';
export default {
  name: 'InputCapitalize'
};
</script>
<script setup>
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1);
    }
    return value;
  }
});
</script>
<style scoped lang='stylus'></style>

# 4、 禁用 Attributes 继承

如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false

<script setup>
defineOptions({
  inheritAttrs: false
})
// ...setup 逻辑
</script>

在模板里面可以使用$attrs获取属性, 脚本中可以使用

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

或者

export default {
  setup(props, ctx) {
    // 透传 attribute 被暴露为 ctx.attrs
    console.log(ctx.attrs)
  }
}

需要注意的是

虽然这里的 attrs 对象总是反映为最新的透传 attribute,但它并不是响应式的 (考虑到性能因素)。你不能通过侦听器去监听它的变化。如果你需要响应性,可以使用 prop。或者你也可以使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用。

# 5、具名插槽简写

使用#代理v-slot:

<template v-slot:header></template>
<template #header></template>

# 5.1 在render函数里用插槽

<template>
  <main>
    <header>
      <slot name="header"
            count="1"></slot>
    </header>
    <div>
      <slot></slot>
    </div>
    <footer>
      <slot name="footer"
            :testText="testText"></slot>
    </footer>
  </main>
</template>
return () =>
  h('div', {}, [
    h(
      CompA,
      {},
      {
        header: (props) => {
          return h('h1', 'header:' + props.count);
        },
        default: () => {
          return h('p', 'body');
        },
        footer: (props) => {
          return h('div', props.testText);
        }
      }
    )
  ]);

# 6、依赖注入provide/inject

最好使用Symbol作为key值

export const helloKey = Symbol();
import { helloKey } from './symbol';
provide(helloKey, 'helloKey from symbol');
import { helloKey } from './symbol';
const helloValue = inject(helloKey);

# 7、加载异步组件

还是得使用defineAsyncComponent

import { defineAsyncComponent, ref } from 'vue';
export default {
  name: 'AsyncComp',
  // props: {},
  // emits: [],
  components: {
    Child: defineAsyncComponent(() => import('./Child.vue'))
  },
  setup(props, context) {
    return {};
  }
};
<script setup>
// setup 语法糖写法
import { defineAsyncComponent } from 'vue'

const AdminPage = defineAsyncComponent(() =>
  import('./components/AdminPageComponent.vue')
)
</script>

# 8、封装复用有状态逻辑的函数

添加事件,自动移除

import { onMounted, onUnmounted } from 'vue';

function useEvent(target, eventName, listener) {
  onMounted(() => {
    target.addEventListener(eventName, listener);
  });
  onUnmounted(() => {
    target.removeEventListener(eventName, listener);
  });
}

export default useEvent;

封装鼠标移动事件

import { ref } from 'vue';

import useEvent from '@/utils/useEvent.js';
export function useMouse() {
  const x = ref(0);
  const y = ref(0);
  function onMove(e) {
    x.value = e.clientX;
    y.value = e.clientY;
  }
  useEvent(document, 'mousemove', onMove);

  return { x, y };
}

//  const { x, y } = useMouse();

发起请求

即便不依赖于 ref 或 getter 的响应性,组合式函数也可以接收它们作为参数。如果你正在编写一个可能被其他开发者使用的组合式函数,最好处理一下输入参数是 ref 或 getter 而非原始值的情况。可以利用 toValue() (opens new window) 工具函数来实现:

import { ref, toValue } from 'vue';

function useFetch(url) {
  const response = ref(null);
  const error = ref(null);
  // 此处最好使用 toValue 工具函数
  fetch(toValue(url))
    .then(res => res.json())
    .then(data => (response.value = data))
    .catch(err => (error.value = err));

  return { response, error };
}

export default useFetch;

# 9、自定义指令

三种重用代码的方式

  1. 组件是主要的构建模块,
  2. 而组合式函数则侧重于有状态的逻辑
  3. 自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。

注意

只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用 v-bind 这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}
/*
el:指令绑定到的元素。这可以用于直接操作 DOM。

binding:一个对象,包含以下属性。

value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。
oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。
modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。
instance:使用该指令的组件实例。
dir:指令的定义对象。
vnode:代表绑定元素的底层 VNode。

prevNode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
*/

封装一个按钮的权限指令

async function getPermission(key) {
  // 此处假设权限只有A才返回true,同时可能是异步获取权限
  if (key === 'A') {
    return true;
  }
  return false;
}

const vPer = {
  created: (el, binding) => {
    getPermission(binding.arg).then(per => {
      // 此处也可以使用 remove, 直接把DOM移除
      el.style.display = per ? '' : 'none';
    });
  }
};

export { vPer };
import { vPer } from '@/utils/per.js';
const app = createApp(App);
app.directive('per', vPer);

封装一些简单的动画指令

const vFadeIn = {
  created: (el, binding) => {
    el.style.opacity = '0';
    el.style.transform = 'translateX(100px)';
    el.style.transition = 'opacity 1s ease-in, transform 0.3s ease-in';
  },
  mounted(el) {
    el.style.opacity = '1';
    el.style.transform = 'translateX(0)';
    setTimeout(() => {
      el.style.transition = '';
      el.style.transform = '';
    }, 1000);
  }
};

export { vFadeIn };

# 10、插件

插件没有严格定义的使用范围,但是插件发挥作用的常见场景主要包括以下几种: 1、通过 app. component()和 app.directive()注册一到多个全局组件或自定文指父。 2、通过 app.provide()使一介资源可被注入进整个应用。 3、向 app. config.globalproperties 中添加一些全局实例属性或方法 4、一个可能上述三种都包含了的功能库(例如 vue-router)。

我的理解, 插件就是自动帮你调用一次install,然后传入app应用参数,其实这部分完全是可以自己写个函数来实现的, 只不过用app.use的形式, 更符合主流吧,

一个简单的i18n的多语言插件

const i18nPlugin = {
  install(app, options) {
    app.config.globalProperties.$t = function (key) {
      return key.split('.').reduce((o, k) => {
        return o[k];
      }, options);
    };
  }
};

const i18nConfig = {
  zh: {
    greeting: {
      hello: '你好'
    }
  },
  en: {
    greeting: {
      hello: 'hello'
    }
  }
};

function getConfig() {
  const lang = window.localStorage.getItem('lang') || 'zh';
  return i18nConfig[lang] || i18nConfig['zh'];
}

function installI18n(app) {
  const config = getConfig();
  app.use(i18nPlugin, config);
}

export { i18nPlugin, installI18n };

// 只需要在main.js 里面导入 installI18n
// installI18n(app)

# 11、Teleport

是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。

这个操作其实也可以自己通过手动DOM操作来完成, 使用Teleport的有点就是可以更加清楚结构和逻辑

# 12、组合式API下使用TS

# 12.1 定义Props

使用defineProps的泛型申明props类型. 使用withDefaults设置默认值

import { onMounted } from 'vue';

interface Props {
  foo: string;
  bar?: number;
  list?: number[];
}

const props = withDefaults(defineProps<Props>(), {
  foo: 'foo default value',
  bar: 0,
  list: () => [0]
});

onMounted(() => {
  console.log(props);
});

# 12.2 为emits标注类型

/* const emit = defineEmits({
  change: () => {},
  update: (val: number) => true
}); */

const emit = defineEmits<{
  (e: 'change'): void;
  (e: 'update', val: number): void;
}>();

推荐采用泛型的写法,可以对所触发事件的类型进行更细粒度的控制。

emit('update', 1);

# 12.3 为ref()标注类型

建议直接采用自动推论或者泛型

const msg = ref<string>('nihao');

# 12.4 reactive,computed

interface Book {
  title: string;
  year?: number;
}

const book: Book = reactive({ title: '史记', year: 1000 });

const num = ref(1);
const stringifyNum = computed<string>(() => String(num));

# 12.5 事件类型

// <input type="text"           @input="onInput">
function onInput(e: Event) {
  const el = e.target as HTMLInputElement;
  console.log(el.value);
}

# 12.6 provide/inject

如果需要使用Symbol的话借助一下InjectionKey。 如果是字符串, 那就自定义即可

// key.ts
import type { InjectionKey } from 'vue';
const TestKey = Symbol() as InjectionKey<string>;
export { TestKey };


import { TestKey } from './key';
provide(TestKey, 'test provide');


import { TestKey } from './key';
const test = inject(TestKey, '');

# 12.7 ref 模板引用

//  <div ref="TextRef">Test ref dom</div>
const TextRef = ref<HTMLDivElement | null>(null);
onMounted(() => {
  // 需要类型收窄
  if (TextRef.value) {
    TextRef.value.style.color = 'red';
  }
});

# 12.8 为组件模板引用标注类型

// ChildB.vue
const show = ref(false);
const doShow = () => {
  show.value = true;
};
defineExpose({
  doShow
});
/*
    <ChildB ref="ChildBRef">ChildB</ChildB>
    <ChildC ref="ChildCRef"></ChildC>
*/

const ChildBRef = ref<InstanceType<typeof ChildB> | null>(null);
onMounted(() => {
  setTimeout(() => {
    if (ChildBRef.value) {
      ChildBRef.value.doShow();
    }
  }, 2000);
});

const ChildCRef = ref<ComponentPublicInstance | null>(null);
onMounted(() => {
  console.log(ChildCRef.value);
});

# 13、定义组件名字

这个名字主要用在Vue的插件上

<script lang="ts" setup>
defineOptions({
  name: 'ChildAAAA',
});
</script>

# 14、组件调试钩子

我们可以在一个组件渲染时使用 onRenderTracked 生命周期钩子来调试查看哪些依赖正在被使用,或是用 onRenderTriggered 来确定哪个依赖正在触发更新。

onRenderTracked((event) => {
  console.log('onRenderTracked', event);
});

onRenderTriggered((event) => {
  console.log('onRenderTriggered', event);
});

# 15、计算属性,watch watchEffect调试

const doubleCount = computed(() => count.value * 2, {
  onTrack: (e) => {
    // debugger
    console.log('track', e);
  },
  onTrigger: (e) => {
    console.log('trigger', e);
  }
});

watch(source, callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

watchEffect(callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

# 16、模板 vs. 渲染函数

在处理高度动态的逻辑时,渲染函数相比于模板更加灵活,因为你可以完全地使用 JavaScript 来构造你想要的 vnode。

# 17、带编译时信息的虚拟 DOM

编译器可以静态分析模板并在生成的代码中留下标记,使得运行时尽可能地走捷径

在 Vue 中,框架同时控制着编译器和运行时。这使得我们可以为紧密耦合的模板渲染器应用许多编译时优化。编译器可以静态分析模板并在生成的代码中留下标记,使得运行时尽可能地走捷径。与此同时,我们仍旧保留了边界情况时用户想要使用底层渲染函数的能力。我们称这种混合解决方案为带编译时信息的虚拟 DOM

# 18、函数式组件

函数式组件是一种定义自身没有任何状态的组件的方式。它们很像纯函数:接收 props,返回 vnodes。函数式组件在渲染过程中不会创建组件实例 (也就是说,没有 this),也不会触发常规的组件生命周期钩子。

    type FunctionalComponentProps = {
      name: string;
    };
    type FunctionalComponentEvents = {
      click(v: number): void;
    };
    function FunctionalComponent(props: FunctionalComponentProps, context: SetupContext<FunctionalComponentEvents>) {
      return h(
        'div',
        {
          onClick: (e) => {
            context.emit('click', 1);
          }
        },
        '函数式组件-' + props.name
      );
    }
    FunctionalComponent.props = {
      name: {
        type: String,
        default: ''
      }
    };
    FunctionalComponent.emits = {
      click: (v: unknown) => typeof v === 'number'
    };
h(FunctionalComponent, {
  name: '你好',
  onClick: (val: number) => {
    console.log(val);
  }
})
上次更新: 1/22/2025, 9:39:13 AM