vue3学习笔记

关于 setup

第一个入参 props

解构props后会丢失响应性,这里可以用props.title来使用,也可以用toRefs或者toRef来避免这一缺点。

需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性

import { toRefs, toRef } from "vue";

export default {
  setup(props) {
    // 将 `props` 转为一个其中全是 ref 的对象,然后解构
    const { title } = toRefs(props);
    // `title` 是一个追踪着 `props.title` 的 ref
    console.log(title.value);

    // 或者,将 `props` 的单个属性转为一个 ref
    const title = toRef(props, "title");
  },
};

第二个入参 context

context也叫做上下文对象,有四个参数attrs,slots,emit,expose。 该上下文对象是非响应式的,可以安全地解构( { attrs, slots, emit, expose })。

export default {
  setup(props, context) {
    // 透传 Attributes(非响应式的对象,等价于 $attrs)
    console.log(context.attrs);

    // 插槽(非响应式的对象,等价于 $slots)
    console.log(context.slots);

    // 触发事件(函数,等价于 $emit)
    console.log(context.emit);

    // 暴露公共属性(函数)
    console.log(context.expose);
  },
};

expose 函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose 函数暴露出的内容

export default {
  setup(props, { expose }) {
    // 让组件实例处于 “关闭状态”
    // 即不向父组件暴露任何东西
    expose();

    const publicCount = ref(0);
    const privateCount = ref(0);
    // 有选择地暴露局部状态
    expose({ count: publicCount });
  },
};

返回

setup() 函数中返回的对象会暴露给模板和组件实例。其他的选项也可以通过组件实例来获取 setup() 暴露的属性。

在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。

在模板渲染上下文中,只有顶级的 ref 属性才会被解包。

setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    // 返回值会暴露给模板和其他的选项式 API 钩子
    return {
      count
    }
  },

  mounted() {
    console.log(this.count) // 0
  }
}
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

setup也可以返回一个渲染函数,该函数可以直接使用同一作用域下声明的响应式状态。

请确保返回的是一个函数而不是一个值!setup() 函数在每个组件中只会被调用一次,而返回的渲染函数将会被调用多次。

这点也是为什么返回 data 是一个函数的原因

值得注意的是,返回一个渲染函数也会阻止返回其他东西,如果需要通过模板引用将该组件方法暴露给父组件,还需要expose的支援。

import { h, ref } from "vue";

export default {
  setup(props, { expose }) {
    const count = ref(0);
    const increment = () => ++count.value;

    // 将局部状态暴露给选项式api或者父组件
    expose({
      increment,
    });
    // 同时返回一个渲染函数
    return () => h("div", count.value);
  },
};

script setup

SFC 中的 setup 和普通的区别

这意味着与普通的 script 只在组件被首次引入的时候执行一次不同,script setup 中的代码会在每次组件实例被创建的时候执行

优势

顶层的绑定会被暴露给模板,import导入的内容也可以直接用,不需要methods来暴露。

响应式

响应式状态需要明确使用响应式 API 来创建。和 setup() 函数的返回值一样,ref 在模板中使用的时候会自动解包:

使用组件

```

这里 MyComponent 应当被理解为像是在引用一个变量。其 kebab-case 格式的 my-component 同样能在模板中使用——不过,我们强烈建议使用 PascalCase 格式以保持一致性。同时这也有助于区分原生的自定义元素。

动态组件

由于组件是通过变量引用而不是基于字符串组件名注册的,在script setup中要使用动态组件的时候,应该使用动态的 :is 来绑定

<*component* :is="someCondition ? Foo : Bar" />

递归组件

相比于导入的组件优先级更低。如果有具名的导入和组件自身推导的名字冲突了,可以为导入的组件添加别名。 一个单文件组件可以通过它的文件名被其自己所引用。

import { FooBar as FooBarChild } from './components'

命名空间组件

可以使用带 . 的组件标签,例如 `` 来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用:

<script setup>
import * as Form from './form-components'
</script>

<template>
  <Form.Input>
    <Form.Label>label</Form.Label>
  </Form.Input>
</template>

使用自定义指令

全局注册的自定义指令将正常工作。本地的自定义指令在 <script setup> 中不需要显式注册,但他们必须遵循 vNameOfDirective 这样的命名规范

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

defineExpose

使用 script setup 的组件是默认关闭的——即通过模板引用或者 $parent 链获取到的组件的公开实例,不会暴露任何在 script setup 中声明的绑定。

可以通过 defineExpose 编译器宏来显式指定在 `` 组件中要暴露出去的属性

<script setup>
  import {ref} from 'vue' const a = 1 const b = ref(2) defineExpose({(a, b)})
</script>

2024 年 5 月 23 日更新

setup 写法

1 普通写法 定义后需要 return 出去

template部分都是一样的,重点在 script 部分

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <p>{{ a }}</p>
  </div>
</template>
<script>
export default {
  setup() {
    const a = 1
    return { a }
  }
}
</script>

2 把 setup 放 template 标签里 不需要 return 直接可用

<script setup lang="ts">
  const a: number = 2
</script>

v- vue 自带指令

v-text 显示文本

v-html 显示富文本

v-if v-else-if 表示v-ifelse if语句块,可以链式调用

v-else 条件语句中的收尾

v-show

v-on 简写@ 给元素添加事件

v-bind 简写: 绑定元素属性 Attr

v-model 双向绑定

v-for

v-on 修饰符

v-once 性能优化 只渲染一次 并且会跳过之后的更新

v-memo 性能优化 条件[]改变后会更新缓存 否则与v-once一致

1 父子事件触发与 v-on的绑定

template部分,触发事件可以是变量。如果上下级都有事件触发,不想点击子也触发父,需要对事件冒泡拦截。

    <div @click="clickFatherBtn">
      <button @[event]="clickBtn">点击会触发</button>
    </div>
const event = "click";
// 子 点击动作
const clickBtn = (e: Event) => {
  console.log("点击按钮");
  e.stopPropagation();
};

// 父 点击动作
const clickFatherBtn = () => {
  console.log("父组件点击触发");
};

在 vue 中可以用stop来简化上面的阻止事件冒泡

      <button @[event].stop="clickBtn">点击会触发</button>

diff 算法

以数组为例,如果没有key,插入流程可以认为是

新旧VNode做对比,新的会把旧的替换-新增-删除

key的情况下

前序对比算法,头和头比-尾序对比算法,尾和尾比-新节点是多,挂载-旧节点多,卸载-乱序特殊处理

当需要进行移动操作时,问题就变成求新旧子树上的【最长递增子序列】

  1. 构建新节点的映射关系 值与序列号

  2. 记录新节点在旧节点中的位置数组 新结点中包含旧节点,要删除,不包含的也要删除

  3. 如果出现交叉,则是移动,说明要去求最长递增子数列

【最长递增子序列算法】

【10,9,2,5,3,7,101,18】-【1,1,1,2,2,3,4,4】

初始数值都是 1,在 5 的情况下,有个 2 比他小,就出现了 1+1=2

到了 3,有个 2 比他小,1+1=2

到 7,前面比他小的中最大数值是 2,(这里要将数字和其对应的数值分开算),2+1=3

以此可以推断出后面的数组

ref 和他的兄弟们 shallowRef triggerRef customRef

经 ref 包裹后返回的是一个 ES6 的class,含有属性value是赋值,要对其修改或者取值,必须加.value

直接修改shallowRef不会更新视图,原因是shallowRef  只有对.value的访问是响应式的(浅层响应式)

triggerRef包裹可以强制更新

需要注意的是,如果对refshallowRef的修改出现在同一函数、同一template内,shallowRef的改动会受到ref的影响,也会更新视图。

官方回答是:在重新渲染期间,所有组件的模板都将更新为最新数据。

customRef的用武之地是自定义赋值,有接口更新,这里可以加入防抖来提高性能

import { ref, shallowRef, customRef } from "vue";

function myRef<T>(value: T) {
  let timer: any;
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        clearTimeout(timer);
        setTimeout(() => {
          console.log("newValue: ", newValue);
          value = newValue;
          trigger();
          timer = null;
        }, 500);
      },
    };
  });
}

const dog3 = myRef < string > "哈士奇";

const changeValue = () => {
  dog3.value = "二哈";
  console.log(dog3);
};

可以通过ref,直接获取界面上的DOM元素

<span ref="spanDom">我是🐕</span>
// 直接求值时机不对 当前dom未渲染
console.log(spanDom ? spanDom.value?.innerText : spanDom.value); // undefined

// 在点击事件中查看
const changeValue = () => {
  console.log(spanDom.value?.innerText); // 我是🐕
};

reactive

ref对入参无限制 reactive必须是引用类型 (对象 数组 Map Set)

ref取值 赋值 都需要.value reactive 不需要

reactiveproxy,不能直接赋值,否则破坏它的响应式结构

Proxy 是 ES6 引入的一个元编程特性,它允许你创建一个代理对象,用于拦截并自定义 JavaScript 对象的基本操作。通过代理对象,你可以拦截并重定义对象的基本操作,比如属性查找、赋值、枚举等。Proxy 的核心思想是在目标对象和代码之间建立一个拦截层,使得可以对目标对象的操作进行拦截和监视。

作者:来颗奇趣蛋
链接:https://juejin.cn/post/7333416709120786468
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

// 1. 数组用push+解构 2. 将数组作为reactive的一个对象
let list = reactive([]);
let list2 = reactive({ arr: [] });

const add = () => {
  setTimeout(() => {
    // list.push('XX') 这个也可以
    const res = ["1", "2", 3]; // list = res x 无效
    list.push(...res);
    list2.arr = ["one", "two", 3];
  }, 2000);
};

readonly

reactive的数值 a,包裹一个readonly后,生成的值 b 便不可再更改。值得注意的是,如果改变数据源的值 a,b 还是会被改动。

import { reactive, readonly } from "vue";
const form = reactive({ name: "妞妞", age: 2 });
const formReadOnly = readonly(form);
console.log("formReadOnly1: ", formReadOnly);

formReadOnly.name = 1;
console.log("formReadOnly2: ", formReadOnly); // 对readonly包裹后的值修改 无效

form.name = "qq";
console.log("formReadOnly3: ", formReadOnly); // 直接修改源数值,生效

shallowReactive(存在和 shallowRef 同款问题)

原理是模板更新时,全部都会更新。

toRef toRefs toRaw

toRef 只能修改响应式对象的值,非响应式视图不会更新

手动实现toRefs

import { toRef, reactive, shallowReactive } from 'vue'
const form = reactive({ name: '妞妞', age: 2 })

// toRefs 取的是所有值
function toRefs<T extends object>(object: T) {
  let map: any = {}

  for (let i in object) {
    map[i] = toRef(object, i)
  }
  return map
}

const forms = toRefs(form)
const { name, age } = forms // name 妞妞 age 2


和下面直接引用toRefs效果一致

import { toRef, reactive, toRefs } from "vue";
const form = reactive({ name: "妞妞", age: 2 });

const forms = toRefs(form);
const { name, age } = forms;

toRef解构出的数值是Proxy一个响应式的数值。如果不用toRef,对解构出来的数值进行改动,数值会更改,但是视图不会更新。

let { name, age } = form; // 视图不会更新 form不受影响

const add = () => {
  name = "小雪飞鸿";
  console.log("name: ", name);
};

能够触发更新的情况

const { name: name2, age: age2 } = toRefs(form); // 可以对age进行操作 会影响到form

const add = () => {
  age2.value = 14;
  console.log("age: ", age2);
};

toRaw去掉响应式的外衣,还原原本的对象

computed 与律师皮肤案例

  1. 选项式写法 传入对象手动实现getset
import { ref, computed } from "vue";
const firstName = ref("谢"); // proxy对象 取值要+.value
const secondName = ref("文东"); // 同上

// 手动实现get与set
const name =
  computed <
  string >
  {
    get() {
      return firstName.value + "-" + secondName.value; // 对proxy对象取值的特殊处理
    },
    set(newValue) {
      console.log("newValue: ", newValue);
      [firstName.value, secondName.value] = newValue.split("-"); // 对proxy对象赋值的特殊处理
    },
  };

const edit = () => {
  name.value = "小雪-飞鸿";
};
  1. 函数式 只支持getter写法,无法修改值

    const name = computed(() => firstName.value + "-" + secondName.value); // 显示同上面选项式的写法
    
    const edit = () => {
      name.value = "小雪-飞鸿"; // 报错:无法为“value”赋值,因为它是只读属性。
    };
    

律师皮肤案例在gitee上的代码库内

watch

watch可以对ref,也可以对reactive使用。监听多个数据源要用数组,如果监听对象要开启第三个选项中的deep,如果是对reactive使用则默认开启deep

因为源码中新旧值的更新,是直接赋值,如果监听源是对象,这里直接指针指向的都是新的值

const secondName = reactive({ foo: 123 });

watch(
  secondName,
  (newValue, oldValue) => {
    console.log("oldValue: ", oldValue);
    console.log("newValue: ", newValue);
  },
  {
    // deep: true // reactive 已默认开启
  }
);

监听对象里的某一数值,可以用箭头函数

const secondName = reactive({ foo: "小律是", age: 23 });

watch(
  () => secondName.age, // 只有age改变能够监听到
  (newValue, oldValue) => {
    console.log("oldValue: ", oldValue);
    console.log("newValue: ", newValue);
  }
);

watch 的配置项

deep 深度监听 布尔值

immediate 立即执行一次 布尔值

flush 监听调用时机 pre 组件更新之前 sync 同步执行 post 组件更新之后执行

watchEffect

// watchEffect返回一个取消监听的函数
const stopWatch = watchEffect(
  (oninvalidate) => {
    console.log('f n', firstName.value)
    console.log('s n', secondName.value)

    const inputDom = document.getElementById('prompt') as HTMLInputElement
    console.log('inputDom: ', inputDom)

    oninvalidate(() => {
      console.log('之前所做的事情')
    })
  },
  // 在加载后触发 来拿到渲染完成的html元素
  { flush: 'post' }

生命周期

注意两个特殊钩子onRenderTrackonRenderTrigger,用来查看effect的变化。DOM元素的获取,注意要在生命周期内,或者是ref绑上。

<template>
  <div>生命周期</div>
  <A></A>
  <input ref="keywordInput" id="keywordInput" type="text" v-model="keyword" />
</template>
const keyword = ref('小雪飞鸿')
const keywordInput = ref<HTMLInputElement>()

// setup最先出 setup语法糖格式没有生命周期
console.log('setup')
onBeforeMount(() => {
  console.log('创建前')
  console.log(keywordInput.value) // null
})
onMounted(() => {
  console.log('创建后')
  console.log(keywordInput.value) // 可以取到input
})

onBeforeUpdate(() => {
  console.log('更新前')
})

onUpdated(() => {
  console.log('更新后')
  console.log(keywordInput.value)
})

onRenderTracked((e) => {
  console.log('调试用的钩子 显示当前的effect onRenderTracked', e)
})
onRenderTriggered((e) => {
  console.log('调试用的钩子 触发依赖后 onRenderTriggered', e)
})
onBeforeUnmount(() => {
  console.log('卸载前')
})
onUnmounted(() => {
  console.log('卸载后')
})

BEM 布局

B block E element M modify

el-input__wrapper    ____ 当前布局内部

el-button--primary    -- 当前标签状态

SASS 变量 $width

LESS 变量 #width

bem.scss

// scss $width less #less
$namespace: 'xx' !default;
$block-sel: '-' !default;
$elem-sel: '__' !default;
$mod-sel: '--' !default;

// .el-input

// 样式混入
@mixin b($block) {
  // #{ } 格式化语法
  $B: #{$namespace + $block-sel + $block};
  .#{$B} {
    // 样式暂存
    @content;
  }
}

// .el-input__wrapper
// .el-input .el-input__wrapper

@mixin e($el) {
  // 获取父级
  $selector: &;
  @at-root {
    $E: #{$selector + $elem-sel + $el};
    #{$E} {
      @content;
    }
  }
}

// el-button--primary
@mixin m($el) {
  // 获取父级
  $selector: &;
  @at-root {
    $M: #{$selector + $mod-sel + $el};
    #{$M} {
      @content;
    }
  }
}

全局样式,要修改配置vite.config.js,不是在入口文件配置

export default defineConfig({
  plugins: [vue(), vueJsx()],
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "./src/bem.scss";`,
      },
    },
  },
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
});

使用

<template>
  <div class="xx-test">
    子组件
    <div class="xx-test__wrapper">组件内容</div>
    <div class="xx-test--disabled">组件内容2</div>
  </div>
</template>

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

<style scoped lang="scss">
@include b(test) {
  color: blue;
  @include e(wrapper) {
    color: orange;
  }
  @include m(disabled) {
    color: gray;
  }
}
</style>

父子组件传值

父->子 defineProps

不用ts,子组件可以直接用defineProps接受从父组件传来的数值,可以直接在template中使用。将defineProps返回,作为props来使用。

ts,可以用特有的withDefaults来包裹defineProps,第二个参数为设定的默认值,注意对象数组一类需要用箭头函数返回,确保数值传递。

// ts模式下的特殊用法
withDefaults(defineProps<{ name: string; lover: string; friends: string[] }>(), {
  name: '原罪2',
  friends: () => ['王牌超人'] // 对象类型必须用箭头函数返回 涉及到指针
})

// const props = defineProps({
//   name: {
//     type: String, // 这里的String必须大写
//     default: '原罪'
//   },
//   lover: {
//     type: String
//   }
// })
// console.log('直接用的传参', name)
// console.log('props: ', props.name)

子->父 defineEmits

    <button @click="send">触发传值</button>

    // 子向父传值
const emit = defineEmits(['deliver-name']) // 还有其他函数,可以加在数组中

const send = () => {
  emit('deliver-name', '亡灵环境')
}

  <A :lover="'丸太'" @deliver-name="getName" :friends="['乌鸦', '蜂鸟']"></A>

  const getName = (childName: string) => {
  console.log('子组件传递来的', childName)
}

ts专用的写法

父中接受写法不变,子组件中要注意的是 接受一个对象,传值是一个方法

// ts专用版本 接受一个对象,传值是一个方法
const emit = defineEmits<{
  (e: 'deliver-name', name: string): void
  // (e: 'deliver-name', name: string): void
}>()
// 还有其他传参,可以加在对象里

子->父 defineExpose

// 子组件暴露值给父组件
defineExpose({
  childName: name,
  value: 114514,
  clickDeliver: () => {
    console.log("child Click");
  },
});

<A ref="deadManRef" :lover="'丸太'" @deliver-name="getName" :friends="['乌鸦', '蜂鸟']"></A>

// 赋值子组件的ref
const deadManRef = ref<InstanceType<typeof A>>()

onMounted(() => {
  // value要等挂载后才能用
  console.log('deadManRef: ', deadManRef, deadManRef.value.childName, deadManRef.value.value)

  const clickDeliverChild = deadManRef.value.clickDeliver
  clickDeliverChild()
})

const getName = (childName: string) => {
  console.log('子组件传递来的', childName) // child Click
}


瀑布流练习题

vue3 小满 zs 教学 瀑布流练习 - 乐盘游 - 博客园 (cnblogs.com)

局部组件、全局组件、递归组件、动态组件

注册全局组件 在入口main.js文件中

import Card from "@/components/Card/index.vue";

const app = createApp(App);

// 注册全局组件 Card
app.component("Card", Card);

递归组件 案例

TreeCom

<template>
  <div><Tree :data="data" /></div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import Tree from './Tree.vue'
export interface TreeType {
  name: string
  checked: boolean
  children?: TreeType[]
}

const data = reactive<TreeType[]>([
  { name: '1', checked: false, children: [{ name: '1-1', checked: true }] },
  { name: '2', checked: false },
  {
    name: '3',
    checked: false,
    children: [{ name: '3-1', checked: true, children: [{ name: '3-1-1', checked: false }] }]
  },
  {
    name: '4',
    checked: false,
    children: [
      { name: '4-1', checked: true },
      { name: '4-2', checked: true }
    ]
  }
])
</script>

<style scoped lang="scss"></style>

Tree

<template>
  <div class="tree" v-for="item in data">
    <input type="checkbox" v-model="item.checked" /><span>{{ item.name }}</span>
    <Tree :data="item.children" v-if="item.children?.length" />
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { TreeType } from './index.vue'

// 以下两种写法都可以 就是TS的校验冒红
// defineProps<{ data: TreeType[] }>()
defineProps({ data: { type: Array, default: {} } })
</script>

<style scoped lang="scss">
.tree {
  background-color: aquamarine;
  margin: 10px;
}
</style>

递归调用的组件可更改名称

这里将当前组件再抛出一个名为TreeNow的,作为递归组件。

<template>
  <div class="tree" v-for="item in data">
    <input type="checkbox" v-model="item.checked" /><span>{{ item.name }}</span>
    <TreeNow :data="item.children" v-if="item.children?.length" />
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { TreeType } from './index.vue'

// 以下两种写法都可以 就是TS的校验冒红
// defineProps<{ data: TreeType[] }>()
defineProps({ data: { type: Array, default: {} } })
</script>

<script lang="ts">
export default {
  name: 'TreeNow'
}
</script>

动态组件

<template>
  <div>ReactiveView</div>
  <div>
    切换组件
    <div class="tabs" v-for="(item, index) in tabs">
      <div class="click-area" @click="changeTab(item, index)">{{ item.name }}</div>
    </div>
    <component :is="selectCom"></component>
  </div>
  <!-- <LifeCycle />
  <TreeCom />
  <Card /> -->
</template>

<script setup lang="ts">
import { ref, reactive, markRaw, shallowRef } from 'vue'
import LifeCycle from '@/components/LifeCycle/index.vue'
import TreeCom from '@/components/TreeCom/index.vue'
import Card from '@/components/Card/index.vue'

const activeId = ref(0)
const selectCom = shallowRef(LifeCycle)
// reactive 会把组件一起缓存,这里不需要缓存组件,直接用markRaw包裹
// 或者换成shallowRef 只到value层
const tabs = reactive([
  { name: '生命周期组件', com: markRaw(LifeCycle) },
  { name: '树组件', com: markRaw(TreeCom) },
  { name: '卡片组件', com: markRaw(Card) }
])

const changeTab = (item, index) => {
  activeId.value = index
  selectCom.value = item.com
}
</script>

<style lang="scss" scoped>
.tabs {
  display: flex;
  border: 1px solid green;
  padding: 10px;
}
.click-area {
  &:hover {
    cursor: pointer;
  }
}
</style>

vue2 的组件注册写法

<script lang="ts" setup>
const tabs = reactive([
  { name: '生命周期组件', com: 'LifeCycle' },
  { name: '树组件', com: 'TreeCom' },
  { name: '卡片组件', com: 'Card' }
])

</script>

<!-- vue2 的 组件注册 -->
<script lang="ts">
import LifeCycle from '@/components/LifeCycle/index.vue'
import TreeCom from '@/components/TreeCom/index.vue'
import Card from '@/components/Card/index.vue'

export default {
  components: {
    LifeCycle,
    TreeCom,
    Card
  }
}
</script>

插槽

插槽 是指在父组件中,插入可以影响子组件的内容。子组件可以通过插槽,传递数值给父组件中的插入内容。

v-slot:header=#header

默认插槽 v-slot=#default

<Card
      ><template #header>脑袋位置</template
      ><template #default="{ data }">我是要插入子组件的父组件插槽{{ data }}</template
      ><template v-slot:footer>脚位置</template></Card
    >

    <slot name="header"></slot>
    我是卡片
    <slot :data="{ tabs }"></slot>
    <slot name="footer"></slot>

    // ...
   // 将tabs通过插槽穿到父中的模板位置
   const tabs = reactive([
  { name: '生命周期组件', com: 'LifeCycle' },
  { name: '树组件', com: 'TreeCom' },
  { name: '卡片组件', com: 'Card' }
])

动态插槽

子不变,父

<template #[dynamicSlotName]>111111111脑袋位置</template>
// ...
const dynamicSlotName = ref('footer')

异步组件

defineAsyncComponent

在组件script中直接写await,无需await,它会变成一个异步组件。

axios Axios 封装,响应拦截操作 - 乐盘游 - 博客园 (cnblogs.com)

父组件 注意异步组件的引入

  <Suspense>
    <template #default><SkeletonContent /></template>
    <template #fallback><Skeleton /></template>
  </Suspense>

  // ...
  import Skeleton from '@/components/Skeleton/index.vue'
// 引入异步组件 1. 箭头函数
// const SkeletonContent = defineAsyncComponent(() => import('@/components/Skeleton/Content.vue'))
// 2. 对象参数
const SkeletonContent = defineAsyncComponent({
  loadingComponent: () => import('@/components/Skeleton/Content.vue')
})


骨架屏 Skeleton

<template>
  <div class="content">
    <div class="header"></div>
    <div class="body"></div>
  </div>
</template>

<script setup lang="ts"></script>

<style scoped lang="scss">
.content {
  display: flex;
  flex-direction: column;
  margin: 10px;
  .header {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    margin: 10px 0;
    background-color: purple;
  }
  .body {
    width: 300px;
    height: 200px;
    background-color: ghostwhite;
  }
}
</style>

骨架屏结束后渲染的内容

<template>
  <div class="content">
    <div class="header">{{ data.id }}</div>
    <div class="body">{{ data.word }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
interface UserType {
  id: string
  word: string
}
let data = reactive<UserType>({})

// 假装是接口返回
const getData = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 'w', word: 'isis' })
    }, 2000)
  })
}

// 个人信息
await getData().then((res) => {
  console.log('res: ', res)
  data = res
})
</script>

<style scoped lang="scss">
.content {
  display: flex;
  flex-direction: column;
  margin: 10px;
  .header {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    margin: 10px 0;
  }
  .body {
    width: 300px;
    height: 200px;
    background-color: ghostwhite;
  }
}
</style>

pkYc5ZT.png

Q: 为什么要用异步组件?

如果不用异步组件,直接用import引入SketelonContent,打包就会包含这个组件,体积比较大。

如果使用异步组件,SketelonContent 就会在被额外打包,能够减小打包体积,提升首屏加载速度

teleport

<Teleport>  接收一个  to prop 来指定传送的目标。to  的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。这段代码的作用就是告诉 Vue“把以下模板片段传送到  body  标签下”。

to 传送目的地 CSS 选择器 DOM 元素对象

disabled 是否禁用传送功能 true 无效果 false 传送开启

<Teleport to="body"> // CSS选择器字符串 标签名
<Teleport to=".middle-house"> // CSS选择器字符串 类名
  <div class="middle-house"></div>
  <div class="big-house">
    <Teleport :disabled="false" to=".middle-house"
      ><span class="little-text">我试试teleport</span></Teleport
    >
  </div>

  // ...
  .middle-house {
  width: 200px;
  height: 200px;
  background-color: #3f501e;
}
.big-house {
  width: 400px;
  height: 400px;
  background-color: #1e87da;
}
.little-text {
  color: wheat;
}

pktVmYq.png

接入 element-plus(专为 vue3)

npm install element-plus --save

keep-alive

组件缓存,可选参数为

include 包括 可接受字符串、正则,或者包含以上两种的数组

exclude 排除 同上

max 最大缓存的实例数 数字 最不经常使用的组件会被销毁

includeexclude的数值如果是正则表达式或者数组,则需要用v-bind或者:

keep-alive会增加两个生命周期钩子onActivatedonDeactivated

一个是在onMount后出现,后一个是在切换组件后出现(相当于取代了onUnmounted

  <el-button type="primary" @click="changeChooseCom">Primary</el-button>
  <KeepAlive :include="['Form']"> <component :is="chosenCom ? Radio : Form"></component></KeepAlive>

Form.vue

import { reactive, onMounted, onActivated, onDeactivated } from "vue";

onMounted(() => {
  console.log("挂载Form");
});
onActivated(() => {
  console.log("keep-alive 挂载Form");
});
onDeactivated(() => {
  console.log("keep-alive 卸载Form");
});

transition 过渡动画效果

provide inject 跨组件传值

GrandPa

<template>
  <div>
    传参老祖
    <el-radio-group @change="changeColor" v-model="color" class="ml-4">
      <el-radio v-for="item in colorList" :value="item.color" size="large">{{
        item.name
      }}</el-radio>
    </el-radio-group>
    <div class="box"></div>
    <Father />
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, provide } from 'vue'
import Father from './Father.vue'

const colorList = reactive([
  { name: '红色', color: 'red' },
  { name: '黑色', color: 'black' },
  { name: '灰色', color: 'grey' }
])
const color = ref(colorList[0].color)

const changeColor = (value) => {
  color.value = value
}
// 将参数传递给下一级 不用props
provide('color', color)
</script>

<style scoped lang="scss">
.box {
  width: 200px;
  height: 200px;
  background-color: v-bind
(color); // 只有vue3能这么用
}
</style>

Father

<template>
  <div>
    父
    <div class="box"></div>
    <Son />
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, inject } from 'vue'
import Son from './Son.vue'

const color = inject('color')
</script>

<style scoped lang="scss">
.box {
  width: 200px;
  height: 200px;
  background-color: v-bind(color); // 只有vue3能这么用
}
</style>

Son

<template>
  <div>
    子
    <button @click="changeColorPink">按下后颜色变为粉色</button>
    <div class="box"></div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, inject } from 'vue'
import type { Ref } from 'vue'
const color = inject<Ref<string>>('color')
// 1.inject的值来一个默认值
// const color = inject('color', ref('red'))

const changeColorPink = () => {
  //   color.value = 'pink' // 注意这里TS提示color类型是unknown 两种解决方法
  // 2. 类型断言 这里string提前给了
  color.value = 'pink'
}
</script>

<style scoped lang="scss">
.box {
  width: 200px;
  height: 200px;
  background-color: v-bind(color); // 只有vue3能这么用
}
</style>
3能这么用
}
</style>

如果不希望子组件能够改动Provide过去的值,可以在Provide时,用readonly包裹数值

provide('color', readonly(color))

bus

vue2 中的eventBus在 vue3 中被取消了

↓ 手写事件总线

// 模拟vue2版本的事件总线
type BusClass = {
  emit: (name: string) => void
  on: (name: string, callback: Function) => void
}

type ParamsKey = string | number | symbol

type List = {
  [key: ParamsKey]: Array<Function>
}

class Bus implements BusClass {
  list: List
  constructor() {
    this.list = {}
  }
  //   先注册,再使用 和provide与inject类似
  //   参数有多个,解构args是为了将所有传参都拿进来
  emit(name: string, ...args: Array<any>) {
    let eventName: Array<Function> = this.list[name]
    eventName.forEach((fn) => {
      fn.apply(this, args)
    })
  }
  on(name: string, callback: Function) {
    // 允许多次注册,如果注册过就直接返回 如果是新的就返回一个空数组
    let fn: Array<Function> = this.list[name] || []
    fn.push(callback)
    this.list[name] = fn
  }
}

export default new Bus()

  <A></A>
  <B></B>

A

注释部分为使用definePropsdefineEimt的写法


<template>
  <div>
    A
    <div>当前flag的值为{{ flagA }}</div>
    <button @click="send">点击按钮穿值给B</button>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import Bus from '@/components/Bus/bus'

const flagA = ref<boolean>(false)

// const emit = defineEmits(['deliverFlag2B'])

const send = () => {
  flagA.value = !flagA.value
  // emit('deliverFlag2B', flagA.value)
  Bus.emit('deliverFlag2B', flagA.value)
}
</script>

<style scoped lang="scss"></style>

B

<template>
  <div>
    B
    <div>在B中传入的flag值{{ flag }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import Bus from '@/components/Bus/bus'

interface BPropsType {
  flag: boolean
}

// defineProps<BPropsType>()
const flag = ref(false)

Bus.on('deliverFlag2B', (flagFromA: boolean) => {
  flag.value = flagFromA
})
</script>

<style scoped lang="scss"></style>

使用 mitt 作为事件总线库

如何把 Mitt 挂载到全局使用 - 掘金 (juejin.cn)

main.js

import mitt from "mitt";

// ts类型
const Mit = mitt();

app.config.globalProperties.$Bus = Mit;

注意全局 ts 类型中增加一个Bus

在 main.js 同级文件夹下,增加一个 index.d.ts

// 自定义全局变量类型
import mitt from 'mitt'

// 在TS中,以.d.ts结尾的文件默认是全局模块,里面声明的类型,或者变量会被默认当成全局性质的。
const Mit = mitt()

declare module 'vue' {
  export interface ComponentCustomProperties {
    $Bus: typeof Mit
  }
}

C

<template>
  <div>
    C
    <div>当前flag的值为{{ flagC }}</div>
    <button @click="send">点击按钮穿值给D</button>
  </div>
</template>

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

const flagC = ref<boolean>(false)

const instance = getCurrentInstance()

const send = () => {
  flagC.value = !flagC.value
  instance.proxy?.$Bus.emit('deliverFlag2D', flagC.value)
  // 多条监听
  instance.proxy?.$Bus.emit('differentDeliver1', flagC.value)
  // 这一条不监听
  instance.proxy?.$Bus.emit('differentDeliver2', flagC.value)
}
</script>

<style scoped lang="scss"></style>

D

<template>
  <div>
    D
    <div>在B中传入的flag值{{ flagD }}</div>
  </div>
</template>

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

interface BPropsType {
  flag: boolean
}

const instance = getCurrentInstance()

const flagD = ref(false)

instance?.proxy?.$Bus.on('deliverFlag2D', (flag: boolean) => {
  flagD.value = flag
})


const getDifferentDeliver2 = (flag: boolean) => {
  console.log('当前是第3个BUS', flag)
}

instance?.proxy?.$Bus.on('differentDeliver1', (flag: boolean) => {
  console.log('当前是第二个BUS', flag)
})

instance?.proxy?.$Bus.on('differentDeliver2', getDifferentDeliver2)
instance?.proxy?.$Bus.off('differentDeliver2', getDifferentDeliver2)
// 全部不监听
instance?.proxy?.$Bus.all.clear()
</script>

<style scoped lang="scss"></style>

书写tsx

配置

  1. 下载tsx插件

    npm i @vitejs/plugin-vue-jsx -D

    在项目中安装包时默认会安装到dependencies中,我们可以通过以下参数来控制这个行为:

    • -P, --save-prod:记录在dependencies
    • -D, --save-dev:记录在devDependencies
    • -O, --save-optional:记录在optionalDependencies
    • --no-save:不会记录在dependencies

    除此之外还有两个额外的参数:

    • -E, --save-exact:版本号不会按照语义化记录,会显示具体的"1.2.3"
    • -B, --save-bundle:依赖也会记录在bundleDependencies

    作者:ineo6
    链接:https://juejin.cn/post/6844903917260636167
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  2. 修改插件

vite.config.js

import vueJsx from '@vitejs/plugin-vue-jsx'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), vueJsx()],
})

配置文件中启用jsx

jsconfig.json

{
  "compilerOptions": {
    // 配置文件中 启用
    "jsx": "preserve",
    },
}

这里jsx的参数值,会影响到VSC对代码正确性的检查

Cannot find name 'React' · Issue #1164 · vuejs/language-tools (github.com)

解决方案:修改 tsconfig.json 配置文件:

  1. 设置 "jsx": "preserve"
  2. 将文件添加到 "include" 字段中。比如 "include": [ "lib/**/*.tsx" ]

使用tsx

  1. 返回一个渲染函数
const FunctionJsx = () => {
  return <div>我只返回一个函数</div>
}

export default FunctionJsx

  1. optionsAPI defineComponent 类似vue2的写法,注意tsx文件中的数值是单括号包裹。
import { defineComponent } from 'vue'

export default defineComponent({
  data() {
    return { price: 10 }
  },
  render() {
    return <div>{this.price}</div>
  }
})

  1. setup 函数模式 setup中返回一个渲染函数

注意template标签包裹的ref自动解包.value,在tsx中需要手动+后缀

v-show可用

不支持 v-if v-for 可以用数组的方式

v-bind 可以直接绑在标签上

props是对象,在setup传参第一个,要规范数值类型就在setup传参后添加

emits是数组类型,在setup传参第二个

slotsv-slots传递,如不确定,可以用

v-model可用

例子

  1. 渲染函数

    const FunctionJsx = () => {
      return <div>我只返回一个函数</div>
    }
    
    export default FunctionJsx
    
    
  2. 选项式

import { defineComponent } from 'vue'

export default defineComponent({
  data() {
    return { price: 10 }
  },
  render() {
    return <div>{this.price}</div>
  }
})

  1. 选项式+setup 以大润发为例
import { defineComponent, ref } from 'vue'

interface SetupComType {
  shoppingMall?: string
}

const A = (_, { slots }) => (
  <>
    <div>{slots.default ? slots.default() : '默认值'}</div>
    <div>{slots.foo ? slots.foo() : slots.default()}</div>
  </>
)

export default defineComponent({
  props: {
    shoppingMall: String
  },
  emits: ['deliverFoods'],
  setup(props: SetupComType, { emit }) {
    const flag = ref(false)
    // 点击选中的食物
    // !reactive无法在tsx中使用
    // let chosenFood = reactive<ChosenFoodType>({})
    const chosenFood = ref<string>()
    const { shoppingMall } = props

    const labelList = [
      { label: '淀粉肠', value: 'sausage' },
      { label: '豆腐', value: 'doufu' },
      { label: '鸡蛋', value: 'egg' }
    ]

    const changeFlag = () => {
      flag.value = !flag.value
    }
    // 将当前被点击的商品标签传递给父组件
    const deliverLabel = (item) => {
      emit('deliverFoods', item.label)
    }
    // 这里写个默认插槽与具名插槽
    const slot = {
      default: () => <div>我是默认插槽</div>,
      foo: () => <div>我是第二个插槽</div>
    }

    return () => (
      <div>
        当前的flag{flag.value}
        <button onClick={() => changeFlag()}>点击改变flag</button>
        <div
          v-show={flag.value}
          style={{ width: '100px', height: '100px', backgroundColor: 'black' }}
          id="小雪飞鸿"
          label={labelList[0].value}
        ></div>
        {labelList.map((item) => {
          return (
            <div onClick={() => deliverLabel(item)}>
              {item.label}-{item.value}
            </div>
          )
        })}
        <div>从外部传入的超市名-{shoppingMall}</div>
        <A v-slots={slot}></A>
        <input type="text" v-model={chosenFood.value} />
        <div>{chosenFood.value}</div>
      </div>
    )
  }
})

注意这里emit传递出的函数

  <SetupCom shopping-mall="大润发" @deliver-foods="getFoods"></SetupCom>

const getFoods = (value) => {
  console.log('父组件接受到的value: ', value)
}

无需引入vue的插件

npm i -D unplugin-auto-import

vite.config.js

import AutoImport from 'unplugin-auto-import/vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    AutoImport({
      imports: ['vue'],
      // 声明文件
      dts: 'src/unplugin-auto-import.d.ts'
    })
  ],
})

v-model的双向绑定

v-modelvue2中只支持单向数据流通,vue3中有改变,还有自定义修饰符

    <Card v-model="canShow" v-model:anotherModel.isOk="fatherAnotherModel">
</Card>

<template>
  <div>
    <!-- 通过父组件传入的v-model值,控制是否显示 -->
    <div v-if="modelValue">父组件中的canShow:{{ modelValue }}</div>
    <hr />
    <button @click="changeVisible">点击按钮改变值</button>
    <div>父组件中的anotherModel:{{ anotherModel }}</div>
    <input type="text" @input="changeInput" :value="anotherModel" />
  </div>
</template>

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

const props = defineProps<{
  modelValue: boolean
  anotherModel: string
  anotherModelModifiers?: { isOk: boolean }
}>()

const emit = defineEmits(['update:modelValue', 'update:anotherModel'])

const changeVisible = () => {
  emit('update:modelValue', !props.modelValue)
}

const changeInput = (e: Event) => {
  const newInput = (e.target as HTMLInputElement).value
  emit('update:anotherModel', newInput)
  // 是否存在自定义修饰符
  if (props.anotherModelModifiers) {
    console.log('anotherModel 存在 isOk')
  }
}

</script>

<style scoped lang="scss"></style>

自定义指令

vue2中指令的钩子函数bind inserted update componentUpdated unbind

vue3中指令的钩子函数created beforeMount mounted beforeUpdate unpdated beforeUnmount unmounted

<template>
  <div>
    <button @click="changeVisible">点击隐藏方块</button>
    <div v-move="{ backgroundColor: 'green' }" v-show="squareVisible" class="square"></div>
  </div>
</template>

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

type DirectiveType = {
  backgroundColor: string
}

const vMove: Directive = {
  created() {
    console.log('创建时')
  },
  beforeMount() {
    console.log('挂载之前')
  },
  // 钩子函数入参 el,dir (可以通过...args打印查看所有参数)
  mounted(el: HTMLElement, dir: DirectiveBinding<DirectiveType>) {
    // dir中存在value
    console.log('挂载后', el, dir)

    el.style.backgroundColor = dir.value.backgroundColor
  },
  beforeUpdate() {
    console.log('更新前')
  },
  updated() {
    console.log('更新之后')
  },
  beforeUnmount() {
    console.log('卸载之前')
  },
  unmounted() {
    console.log('卸载后')
  }
}

const squareVisible = ref(true)
const changeVisible = () => {
  squareVisible.value = !squareVisible.value
}
</script>

<style scoped lang="scss">
.square {
  width: 100px;
  height: 100px;
  background-color: antiquewhite;
}
</style>

实例 按钮权限自定义指令 vPermission

<template>
  <div>
    <button v-permission="'subscribe'">订阅</button>
    <button v-permission="'search'">搜索</button>
    <button v-permission="'setting'">设置</button>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, Directive, DirectiveBinding } from 'vue'

// 假设本地权限有数值
localStorage.setItem('permission', 'syy-search-subscribe')

const vPermission: Directive = (el: HTMLElement, dir: DirectiveBinding) => {
  const userPermission = localStorage.getItem('permission')
  // 检定是否有权限
  const isPermitted = userPermission.includes(dir.value)
  if (!isPermitted) {
    el.style.display = 'none'
  }
}
</script>

<style scoped lang="scss"></style>

实例 拖拽vMove

关于鼠标事件MouseEvent的参数

当点击按下的时候,克隆一个绝对定位的元素,并标识下"拖拽中"的状态,接着在 mousemove 中就可以判断应该执行的具体方法,从而让元素随着鼠标移动起来。

在监听事件的 event 对象中,有几个参数是比较重要的:clientXclientY 标识的鼠标当前横坐标和纵坐标,offsetXoffsetY 表示相对偏移量,可以在 mousedown 鼠标按下时记录初始坐标,在 mouseup 鼠标抬起时判断是否在目标区域中,如果是则用鼠标获取到的当前的偏移量 - 初始坐标得到元素实际在目标区域中的位置。

作者:茶无味的一天
链接:https://juejin.cn/post/7145447742515445791
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

vMove自定义拖拽组件

不+XY的版本鼠标偏移,需要记录鼠标默认位置,再在移动时处理鼠标落点偏移。

<template>
  <div v-move class="box">
    <div class="title">标题</div>
    <div>内容区域</div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, Directive, DirectiveBinding } from 'vue'

const vMove: Directive = (el: HTMLElement, dir: DirectiveBinding) => {
  //   获取可拖拽区域
  const moveableArea = el.firstElementChild as HTMLDivElement
  //   移动方法
  const moveAction = (e: MouseEvent) => {
    // 注意区分 e 按下鼠标后的移动事件 el 挂上自定义指令的方块 eInner 拖拽过程中的鼠标移动事件
    // 初次点击 记录当前鼠标指针在元素内的相对位置 只有第一次点击触发
    let X = e.clientX - el.offsetLeft
    let Y = e.clientY - el.offsetTop

    const move = (eInner: MouseEvent) => {
      //   鼠标新落点的坐标
      const { clientX, clientY } = eInner

      //   监听可拖拽区域,改变整个元素所在的位置
      //   新鼠标落点距离浏览器的宽高 - 此前鼠标的相对位置
      el.style.left = clientX - X + 'px'
      el.style.top = clientY - Y + 'px'
    }
    // 开始对鼠标移动监听
    document.addEventListener('mousemove', move)
    // 监听到鼠标抬起
    document.addEventListener('mouseup', () => {
      // 清除当前对鼠标移动的监听
      document.removeEventListener('mousemove', move)
    })
  }
  // 监听可拖拽区域的动作
  moveableArea.addEventListener('mousedown', moveAction)
}
</script>

<style scoped lang="scss">
.box {
  position: fixed;
  left: 20%;
  top: 10%;
  width: 200px;
  height: 200px;
  border: 2px solid paleturquoise;
  .title {
    background-color: blanchedalmond;
    text-align: center;
    cursor: move;
  }
}
</style>

pk0Ojgg.png

实例 图片懒加载

globEager 弃用可以使用 glob 的第二个参数传入配置 {eager: true}

let imageList = import.meta.globEager('./assets/images/*.*')

glob是懒加载的模式 globEager静态加载

pkBmySe.png

jsconfig.json

{
  "compilerOptions": {
    // vite/client import.meta的类型声明
    "types": ["element-plus/global", "vite/client"],
    // 显示import.meta有误
    "module": "node16"
  },
}

Vite 配置环境变量 import.meta.env 时出现 ts 错误 - Wise.Wrong - 博客园 (cnblogs.com)

图片懒加载的传统写法 图片懒加载技术详解与实战教程 - 掘金 (juejin.cn)

关于观察器 IntersectionObserver:实现滚动动画、懒加载、虚拟列表... - 掘金 (juejin.cn)

<template>
  <div><img v-lazy="item" v-for="item in imageUrlList" :src="item" alt="图片" /></div>
</template>

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

// 获取图片列表
let imageList = import.meta.glob('@/assets/images/*.*')
// 将图片文件中的地址取出
const imageUrlList = Object.values(imageList).map((item) => item.name)
// 注意这里一定要用异步加载
const vLazy: Directive<HTMLImageElement, string> = async (el, dir) => {
  // 赋予默认加载图
  const girlPic = await import('@/assets/girlPic.jpg')
  el.src = girlPic.default
  // 新建观察器
  const observer = new IntersectionObserver((entry) => {
    const { intersectionRatio } = entry[0]
    // 当前是否处于可视区域
    if (intersectionRatio) {
      // 模拟加载过程
      setTimeout(() => {
        el.src = dir.value
      }, 1000)
      observer.unobserve(el)
    }
  })
  // 使用观察器监听当前元素
  observer.observe(el)
}
</script>

<style scoped lang="scss">
img {
  width: 300px;
  height: fit-content;
}
</style>

自定义钩子

图片转base64

Img2Base64.vue

<template>
  <div>
    <img id="girlPic" :src="girlPic" alt="" />
    <img id="girlPic1" :src="base64Url" alt="" />
  </div>
</template>

<script setup lang="ts">
import girlPic from '@/assets/girlPic.jpg'
import useBase64 from './useBase64'

const base64Url = ref()

useBase64({ idName: 'girlPic' }).then((res) => {
  base64Url.value = res
})
</script>

<style scoped lang="scss">
img {
  width: 300px;
}
</style>

useBase64.ts

interface useBase64Type {
  idName: string
}

export default function (props: useBase64Type) {
  const getBase64 = (el: HTMLImageElement) => {
    // 使用canvas画图
    const canvas = document.createElement('canvas')
    // 新建画布。getContext() 方法返回一个用于在画布上绘图的环境。
    const ctx = canvas.getContext('2d')
    canvas.width = el.width
    canvas.height = el.height
    ctx?.drawImage(el, 0, 0, canvas.width, canvas.height)
    // canvas转base64
    return canvas.toDataURL('image/jpg')
  }

  return new Promise((resolve) => {
    const { idName } = props

    onMounted(() => {
      const img = document.getElementById(idName) as HTMLImageElement
      // 待图片加载完成后
      img.onload = () => {
        resolve(getBase64(img))
      }
    })
  })
}

自定义hooks与自定义指令 useResize

小满课堂 小满Vue3( 自定义Hooks 综合案例)_哔哩哔哩_bilibili 包含打包成库、发布到npm上。练习内容就简单化,纯做个自定义钩子useResize

IntersectionObserver 监听元素可视区域

MutationObserver

ResizeObserver 监听元素变化

Resize/index.vue

<template>
  <div class="draggable-area">我是可拖拽区域</div>
</template>

<script setup lang="ts">
import useResize from './useResize'
onMounted(() => {
  const el = document.querySelector('.draggable-area') as HTMLElement
  useResize(el, (change: any) => {
    console.log(change)
  })
})
</script>

<style scoped lang="scss">
.draggable-area {
  overflow: hidden;
  resize: both;
  padding: 10px;
  background-color: aquamarine;
  border: 2px solid pink;
}
</style>

Resize/useResize.ts

export default function (el: HTMLElement, callback: Function) {
  const observer = new ResizeObserver((entry) => {
    callback(entry[0].contentRect)
  })

  observer.observe(el)
}

全局变量 与 全局方法 以envfilter举例

main.js

// 全局变量
app.config.globalProperties.$env = 'dev'
app.config.globalProperties.$filter = {
  add: (str) => {
    return str + '*'
  }
}

注册 index.d.ts

type FilterType = {
  add<T>(str: T): string
}

declare module 'vue' {
  export interface ComponentCustomProperties {
    $env: string
    $filter: FilterType
  }
}

ts写法如下

pkBt1BT.png

Card/index.vue

    <!-- 全局变量 -->
    <div>{{ $env }}</div>
    <div>{{ $filter.add('我是大西瓜') }}</div>
// ...
import { ref, reactive, getCurrentInstance } from 'vue'

const app = getCurrentInstance()

const tsEnv = app.proxy.$env
const tsFilterStr = app.proxy.$filter.add('小甜心')
console.log('tsEnv: ', tsEnv, '---tsFilterStr', tsFilterStr) // tsEnv:  dev ---tsFilterStr 小甜心*


全局加载动画

全局加载组件GlobalLoading/index.vue

<template>
  <div v-show="visible" class="loading-box">等待ing...</div>
</template>

<script setup lang="ts">
const visible = ref(true)
// 控制全局loading的方法
const show = () => (visible.value = true)
const hide = () => (visible.value = false)
// 抛出所需方法
defineExpose({
  show,
  hide,
  visible
})
</script>

<style scoped lang="scss">
.loading-box {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100%;
  color: wheat;
  background-color: black;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

全局加载组件的插件 GlobalLoading/index.ts

import { createVNode, render } from 'vue'
import type { App, VNode } from 'vue'
import GlobalLoading from '@/components/GlobalLoading/index.vue'

export default {
  // 被use之后,自动执行install方法
  install(app: App) {
    // 以静态文件创建一个虚拟节点
    const Vnode: VNode = createVNode(GlobalLoading)
    // 此时里面的component是空值,render塞入页面中
    render(Vnode, document.body)
    // 装在全局变量与全局方法里面
    app.config.globalProperties.$GlobalLoading = {
      show: Vnode.component.exposed.show,
      hide: Vnode.component.exposed.hide,
      visible: Vnode.component.exposed.visible
    }
  }
}

入口main.js

import GlobalLoading from '@/components/GlobalLoading/index.vue'
import GlobalLoadingTSX from '@/components/GlobalLoading/index.ts'

// 1. 注册全局loading组件
app.component('GlobalLoading', GlobalLoading)

// 2. 插件注册 注意是ts或者js结尾的文件
app.use(GlobalLoadingTSX)

全局变量类型 index.d.ts

type GlobalLoadingType = {
  show(): void
  hide(): void
  visible: boolean
}

declare module 'vue' {
  export interface ComponentCustomProperties {
    $GlobalLoading: GlobalLoadingType
  }
}

:deep样式穿透 :global全局

// 没有父级标签 就用global
:global(.need-edit-btn) {
  background-color: #3f501e;
}
.btn-box {
  // 有父级标签可以 用:deep穿透
  :deep(.el-button) {
    color: red;
  }
}

:slotted v-bind module

插槽样式 父

    <Card v-model="canShow" v-model:anotherModel.isOk="fatherAnotherModel"
      ><template v-slot:footer><div class="foot-position">脚位置</div> </template></Card
    >

// ...
// 插槽中的样式 父组件中直接写
.foot-position {
  color: blue;
}

插槽样式 子 :slotted()

:slotted(.foot-position) {
  border: 1px solid pink;
}

动态样式 v-bind

    <!-- 全局变量 -->
    <div class="env-box">{{ $env }}</div>
    <div class="watermelon-box">{{ $filter.add('我是大西瓜') }}</div>
// ...
.env-box {
  padding: 10px;
  background-color: v-bind('envBoxColor.color');
}
.watermelon-box {
  padding: 10px;
  background-color: v-bind(watermelonColor);
}

模块化样式

可书写多个模块样式,注意以[]包裹

    <button :class="[$style['btn-box'], $style['red-font']]" @click="changeVisible">
      点击按钮改变值
    </button>
    <div :class="specialDiv['btn-box']">父组件中的anotherModel:{{ anotherModel }}</div>
   // ...
<!-- 模块样式 -->
<style module>
.btn-box {
  background-color: bisque;
}
.red-font {
  color: red;
}
</style>

<style module="specialDiv">
.btn-box {
  color: bisque;
}
</style>

tailwind

step 1

npm i -D tailwindcss@latest postcss@latest autoprefixer@latest

step 2

生成两个设置文件 postcss.config.js tailwind.config.js

npx tailwindcss init -p

postcss.config.js

export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

tailwind.config.js(注意版本号)

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {}
  },
  plugins: []
}

step 3

手动创建一个tailwind.css

@tailwind base;
@tailwind components;
@tailwind utilities;

main.js中引入

import './assets/tailwind.css'

文档

官网 安装 - TailwindCSS中文文档 | TailwindCSS中文网

小满版本的多加了几个插件,我先用官网提示的看看。官网默认的是TailwindCss Cli不太好用,我用了PostCSS(小满推荐的版本)

VSCode的插件Tailwind css intellisence

入门 听说你还不会 Tailwind CSS(基础·上篇) - 掘金 (juejin.cn)

速查 Tailwindcss Cheat Sheet (muzhifan.top)

@layer+@apply

@tailwind 指令用于将 Tailwind 中的 base、components、utilities 三个层级的样式插入到全局样式中。

  • base:这是最基础的层级,在这个层级上,Tailwind 提供了一些界定基础样式的规则。例如 margin、padding、color、font-size 等等。
  • components:在这个层级可以创建可复用的样式块,例如:按钮、卡片等。默认情况下是空的。
  • utilities:作为工具层级,包括了 Tailwind 的大部分功能,例如: layout、flex、grid、spacing(margin 和 padding)、colors、typography、borders 等等。

.btn 属于 components(组件级别的复用样式),而 .container.center 属于 utilities(更为底层的样式应用

tailwind.css中添加以下

@layer components {
  .btn {
    @apply bg-black text-white min-w-[80px];
  }
}

@layer utilities {
  .container {
    @apply w-[1280px] mx-auto;
  }
  .center {
    @apply flex items-center justify-center;
  }
}

EventLoop 事件循环机制 宏任务与微任务

浏览器中JS代码单线程执行,所有的同步代码在主线程中执行,形成执行栈。那浏览器中的异步机制是如何实现的呢? 首先,异步任务会被依次放入异步任务队列中,当主线程中的同步任务完成以后,浏览器会轮询去异步任务队列中取出异步任务来执行。

JS代码中的异步任务可进一步分为宏任务(macrotask)与微任务(microtask)。 宏任务包括:script代码、setTimeout、setInterval、I/O、UI render 微任务包括:promise.then、Object.observe(已废弃)、MutationObserver

宏任务和微任务会被加入各自的队列中。 当主线程执行完毕后,浏览器会先去清空微任务队列,依次取出微任务队列中的微任务执行,执行过程中如果产生新的微任务,则追加到当前微任务队列的末尾等待执行。 当微任务队列清空后,浏览器会从宏任务队列中取出一个宏任务执行。宏任务执行完毕再去清空微任务队列,微任务队列清空后再取出一个宏任务来执行。如此反复,直至宏任务队列和微任务队列全部清空。

需要注意的是,宏任务和微任务队列中新产生的微任务都会追加到当前微任务队列队尾等待执行,而不是追加到下一个循环的微任务队列中。因此如果微任务队列的清空过程中持续产生新的微任务,会造成微任务卡死

需要注意的是,Promise对象的resolve部分的代码是当前主线程/宏任务的一部分,并不是微任务,Promise对象的then和catch代码段才是微任务。

作者:skFeTeam
链接:https://juejin.cn/post/6844904030678810632
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

练习题0

pkDEPzj.png

练习题1

pkDEFQs.png

练习题2

pkDEVe0.png

nextTick

vue 更新dom异步,更新数据同步 -> 最新发言,但滚动并未滚到最新位置

操作dom的时候发现数据读取的是上次的,或者是有对dom进行操作,注意要用nextTick

ChatRoom.vue

<template>
  <div class="w-100 h-100 flex justify-center flex-col">
    <div ref="chatBox" class="h-40 bg-slate-400 flex flex-col overflow-auto">
      <div v-for="item in chatContent" class="flex">{{ item.name }}说:{{ item.word }}</div>
    </div>
    <div class="h-1/5 bg-slate-50">
      <textarea v-model="inputDom" type="text"></textarea
      ><button
        @click="addChatContent"
        class="bg-slate-600 text-slate-100 relative bottom-0 right-0"
      >
        发送
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
const chatContent = reactive([
  { name: 'alex', word: 'dont want to flee.' },
  { name: 'joy', word: 'Please let me go.' },
  { name: 'joy', word: 'Please let me go.' },
  { name: 'joy', word: 'Please let me go.' },
  { name: 'joy', word: 'Please let me go.' },
  { name: 'joy', word: 'Please let me go.' },
  { name: 'joy', word: 'Please let me go.' }
])

const inputDom = ref<string>()
// 获取聊天框盒子dom
const chatBox = ref<HTMLDivElement>()

const addChatContent = () => {
  chatContent.push({ name: 'xx', word: inputDom.value })
  //   设置当前聊天盒子的滚动高度为最新发言位置
  //   vue 更新dom异步,更新数据同步 -> 最新发言,但滚动并未滚到最新位置
//   1. nextTick回调更新dom的操作
  nextTick(() => {
    chatBox.value.scrollTop = 9999
  })
  inputDom.value = ''
}

// 2. 跟在nextTick后面的都是异步
const addChatContent = async () => {
  chatContent.push({ name: 'xx', word: inputDom.value })
  await nextTick()
  chatBox.value.scrollTop = 9999
  inputDom.value = ''
}
</script>

<style scoped lang="scss"></style>

Q: 为何叫nextTick

A:/60FPS 1000/60 = 16.7ms
1.处理用户的事件,就是event例如click, input change 等。
2.执行定时器任务
3.执行requestAnimationFrame
4.执行dom的回流与重绘
5.计算更新图层的绘制指令
6.绘制指令合并主线程如果有空余时间会执行requestidlecallback

移动端ionic

H5 postcss编写插件 全局字体

<meta name="viewport" content="width=device-width, initial-scale=1.0">

网页适配移动端

html,body,#app{
    height:100%;
    overflow:hidden;
}
*{
    padding:0;
    margin:0
}

百分比 相对于父元素 viewport vw vh相对于视口

双飞翼布局(两侧固定,中间宽度填满)

<template>
  <!-- 双飞翼布局 -->
  <div class="flex h-screen">
    <div class="w-12 bg-slate-400">left</div>
    <div class="flex-1 bg-slate-800 text-slate-200">center</div>
    <div class="w-12 bg-slate-600">right</div>
  </div>
</template>

<script setup lang="ts"></script>

postCss px2Viewport

step 1 src同级文件夹 新建plugins 文件夹内新增postcss-px-2-viewport.ts

step 2 tsconfig.node.json增加对新插件的支持

pkDR5xP.png

step 3 填写postcss-px-2-viewport.ts内容

import { Plugin } from 'postcss'

const Options = {
  viewportWidth: 375
}

// vite.config.js中传入的参数配置
type OptionsType = {
  viewportWidth?: number
}

export const PostcssPx2Viewport = (options: OptionsType = Options): Plugin => {
  const opt = Object.assign({}, options)
  return {
    postcssPlugin: 'postcss-px-2-viewport',
    // 读取所有css的钩子函数
    Declaration(node) {
      // prop-background value-red
      // 如果匹配到px
      if (node.value.includes('px')) {
        // 存在小数的可能
        const num = parseFloat(node.value)
        node.value = `${((num / opt.viewportWidth) * 100).toFixed(2)}vw`
      }
    }
  }
}

step 4 vite.config.ts中加入这个自定义的postcss插件

import { PostcssPx2Viewport } from './plugins/postcss-px-2-viewport'
import tailwindcss from 'tailwindcss'

export default defineConfig({
  css: {
    postcss: {
      plugins: [tailwindcss(), PostcssPx2Viewport({ viewportWidth: 375 })]
    }
  },
})

tailwindcss与postcss冲突问题

vite.config.js中更改

pkDWQde.png

vue 项目中 postcss-px-to-viewport 导致 tailwindcss 失效 · tailwindlabs/tailwindcss · Discussion #12418 · GitHub

全局切换字体

<template>
  <!-- 双飞翼布局 -->
  <div class="flex h-screen">
    <div class="pxWidth bg-slate-400">
      left <button class="w-10 text-slate-600" @click="() => changeFontSize(32)">大</button>
    </div>
    <div class="flex-1 bg-slate-800 text-slate-200">
      center <button class="w-10" @click="() => changeFontSize(24)">中</button>
    </div>
    <div class="pxWidth bg-slate-600 text-slate-100">
      right <button class="w-10" @click="() => changeFontSize(14)">小</button>
    </div>
  </div>
</template>

<script setup lang="ts">
const nowFontSize = ref('12px')

const changeFontSize = (num: number) => {
  nowFontSize.value = num + 'px'
  document.documentElement.style.setProperty('--size', num + 'px')
  // 改变之后的数值
  const afterChangeFontSize = document.documentElement.style.getPropertyValue('--size')
  console.log('afterChangeFontSize: ', afterChangeFontSize)
}
</script>

<style scoped>
.pxWidth {
  width: 100px;
}
</style>

<style>
div {
  /* 注意这里的变量是 动作改变的值 */
  font-size: var(--size);
}
</style>

unoCss

这玩意感觉不如tailwind

函数式编程 h函数

h函数优势 跳过模板编译过程

模板编译过程

parser -> ast -> transform -> js api ->generate ->render

//除类型之外的所有参数都是可选的
h('div')
h('div', { id: 'foo' }) 
//属性和属性都可以在道具中使用
//Vue会自动选择正确的分配方式
h('div',{ class: 'bar', innerHTML: 'hello’ })
// props modifiers such as .prop and .attr can be added
// with”.” and 、^” prefixes respectively
h('div",{ ' .name': ' some-name', '^width': '100’ })
// class 和style 可以是对象或者数组
h('div', { class: [foo, { bar }],style: { color: 'red' } }) 
//定义事件高要加on如onXxx
h('div', { onClick: () =》{} })
//子集可以字符串
h('div",{ id: 'foo’ },"hello' )
//如果没有props是可以省略props的
h('div',"hello') 
h('div', [h(°span", 'hello')])
//子数组可以包含混合的VNode和字符申
h('div', ['hello'", h('span", 'hello')])

<template>
  <div>
    <Btn type="success">成功</Btn>
    <hr />
    <Btn type="fail">失败</Btn>
  </div>
</template>

<script setup lang="ts">
type Props = {
  type: 'success' | 'fail'
}

// props传参
const Btn = (props: Props, ctx: any) => {
  const isSuccess = props.type === 'success'

  return h(
    'button',
    {
      onClick: () => {
        if (isSuccess) {
          console.log('click 1')
        } else {
          console.log('click 0')
        }
      }
    },
    // ctx可以理解为一个组件的设置 也有emit attr slot
    ctx.slots.default()
  )
}
</script>

<style scoped lang="scss"></style>

vue3.3更新对ts的支持

<template>
  <div>{{ name }} <button @click="sendMsg" class="bg-slate-100 w-[100px]">点击传递</button></div>
  <div v-for="(item, index) in name">
    <slot :item="item" :index="index"></slot>
  </div>
</template>

<script generic="T" setup lang="ts">
// defineProps
// const props = defineProps({
//   name: Array as PropType<string[]>
// })
// TS
// const props = defineProps<{ name: string[] }>()
// vue3 update
const props = defineProps<{ name: T[] }>()

// defineEmits
// const emit = defineEmits(['send'])
// TS
// const emit = defineEmits<{ (event: string, name: string | number) }>()
// vue3 update
const emit = defineEmits<{ send: [name: string] }>()

const sendMsg = () => {
  console.log('子组件点击')

  emit('send', '123')
}

defineOptions({
  name: 'update-vue3.3'
})

// 规范插槽的传参
defineSlots<{
  default: {
    props: {
      item: T
      index: number
      ok: boolean // 由于本项目用的js 这里应该报错的但是没报错💧
    }
  }
}>()
</script>

<style scoped lang="scss"></style>

区分生产环境还是开发环境

step 0 打印当前的信息

console.log(import.meta.env)

step 1 新建.env.denelopment.env.production

NODE_ENV=development
VITE_HTTP = "http://www.baidu.com" // 生产环境换个链接

step 2 vite.config.js中对命令做修改

"dev": "vite --mode development",

启动后可以看见新的环境变量VITE_HTTP已经被添加上去

step 3 为看见生产环境的,需要装个http-server插件

在打包文件夹内

npm i http-server -g

修改端口 hs -p 9002 hs是缩写

http-server -a 127.0.0.1 -p 8090

http-server的安装、前端使用http-server启本地服务 - 掘金 (juejin.cn)

posted @ 2024-06-23 10:47  乐盘游  阅读(18)  评论(0编辑  收藏  举报