vue3

vue3常见的api使用

# 1.defineProps() 和 defineEmits()

  • 1.1 defineProps()
<script setup>
//props是响应式的不能解构
//方法1:不能设置默认值(使用withDefaults解决)
const props = defineProps({
  foo?: String,
  id: [Number, String],
  onEvent: Function, //Function类型
  metadata: null
})

//方法2
const props = defineProps({ foo: { type: String, required: true, default: '默认值' }})

//方法3-弊端:不能设置默认值(使用withDefaults解决)
interface Props {
    data?: number[]
}
//const props = defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {  data: () => [1, 2] })


const emit = defineEmits(['change', 'delete']) //声明从父组件来的事件
//将事件传递出去
emit('change', 参数);
emit('delete', 参数);
</script>
  • 1.2 props 数据传递
<!-- 父组件 -->
<template>
  <child
    v-model:isIpPanel="isIpPanel"
    v-model:isNetworkPanel="isNetworkPanel"
    @cancel="handleDelete"
  ></child>
</template>

function handleDelete() { }
// 子组件
const { proxy } = getCurrentInstance() //获取当前组件实例,需要放置最前面,不能放到函数里面

// defineModel可以在子组件直接更改父组件的值,不需要使用emit;一个公共组件在多个地方使用时,不需要重复写emit
const ipModel = defineModel('isIpPanel')
const netModel = defineModel('isNetworkPanel', false, {
  event: 'update:isNetworkPanel'
})

const cancelFunc = () => {
  // 方式1:
  proxy.$emit('update:isIpPanel', false)
  proxy.$emit('update:isNetworkPanel', false)

  // 方式2:
  ipModel.value = false
  netModel.value = false

  proxy.$emit('cancel', '传输的数据')
}

# 2. defineExpose()

<!-- <script setup >模式将子组件的属性、方法暴露给父组件;setup()则是return返回 -->
<!--父组件parent.vue -->
<template>
  <child ref="childRef"></child>
  <div ref="divEl"></div>
</template>
<script setup lang="ts">
import child from './components/child.vue'
import type { ComponentInternalInstance } from 'vue'
//import { getCurrentInstance, ComponentInternalInstance } from 'vue'; 我用了自动导入,不需要引getCurrentInstance

//方法一(常用推荐):
//typeof P 是获取到类,InstanceType<类>是拿到类的实例,一个是类一个是实例不一样
//为了获取组件的类型,我们首先需要通过 `typeof` 得到其类型,再使用 TypeScript 内置的 `InstanceType` 工具类型来获取其实例类型:

//获取组件。这个变量名和 DOM 上的 ref 属性必须同名,会自动形成绑定。变量名不能和组件名同名,即chilRef不能命名为child
let childRef = ref<InstanceType<typeof child> | null>(null)

//获取dom
let divEl = ref<HTMLDivElement | null>(null)

//方法二:(不推荐)
//这样写会标红:类型“ComponentInternalInstance | null”上不存在属性“proxy”。使用类型断言
const { proxy } = getCurrentInstance() as ComponentInternalInstance

let parentNum = ref(1)

onMounted(() => {
  console.log(divEl.value) //直接拿到dom本身

  console.log(childRef.value?.msg) //.value的方式调用子组件的数据和方法(defineExpose暴露)
  childRef.value?.open()

  console.log(proxy?.$refs) //proxy对象{childRef: Proxy(Object), divEl: div}
})
defineExpose({
  parentNum
})
</script>
<!--子组件-->
<script setup lang="ts">
import type { ComponentInternalInstance } from 'vue'
let msg: string = '111'
const open = function () {
  console.log(222)
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance
onMounted(() => {
  //标红:类型“ComponentPublicInstance”上不存在属性“parentNum”
  console.log('parent', proxy?.$parent?.parentNum)
})
defineExpose({
  msg,
  open
})
</script>

# 3. useAttrs(), 相当于 vue2 中相当于 listeners)。在 vue2 中,listeners

//parent.vue
<template>
  <child
    foo="222"
    foo2="333"
    class="child"
    :style="{}"
    @test="handleTest"
    @test2="handleTest2"
  ></child>
</template>
<script setup lang="ts">
function handleTest() {}
function handleTest2() {}
</script>
//child.vue
<template>
  <div></div>
</template>
<script setup lang="ts">
const props = defineProps(['foo2'])
const emits = defineEmits(['test2'])
console.log(props) //{foo2: '333'}

const attrs = useAttrs()
console.log(attrs) // {foo: '222', class: 'child', style: {…}, onTest: f handleTest(),onTest: f handleTest2()}
</script>

# 4. 全局注册

//main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)

app.config.globalProperties.name = '猪八戒'
app.mount('#app')
// xx.vue
<script setup lang="ts">
import type { ComponentInternalInstance } from 'vue'
// proxy 就是当前组件实例,可以理解为组件级别的 this,没有全局的、路由、状态管理之类的
const { proxy, appContext } = getCurrentInstance() as ComponentInternalInstance

//global 就是全局实例
const global = appContext.config.globalProperties

console.log(global.name)
console.log(proxy?.name) //会标红
</script>

# 5. 设置组件名称

5.1 在 setup 中定义组件名称

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

5.2 利用插件 vite-plugin-vue-setup-extend-plus vite-plugin-vue-setup-extend

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

# 6. wach

  • 6.1 监听多个
const obj = reactive({
    a: 1,
    b: 2,
    c: {
        d: 1,
        e: 2
    },
    f: []
})
//法一:深层次,当直接侦听一个响应式对象时,侦听器会自动启用深层模式:
//可以监听到obj.f = [1]和obj.c.d ++
watch(obj, (newValue, oldValue) => {
})

//法二:深层次,必须写deep: true,不然浅层的也监听不到
watch(() => obj, (newValue, oldValue) => {
}, { deep: true })

//法三:浅层, 监听不到obj.f = [1]和obj.c.d ++
watch(() => { ...obj }, (newValue, oldValue) => {
})
  • 6.2 监听多个对象
//法一:
watch([() => obj.a, name], ([newA, newName], [oldA, oldName]) => {})
//法二:
watch(
  () => [obj.a, obj.b],
  (newValue, oldValue) => {}
)
  • 6.3 停止监听
watch(id, async (newId, oldId, onCleanup) => {
  const { response, cancel } = doAsyncWork(newId)
  // 当 `id` 变化时,`cancel` 将被调用,
  // 取消之前的未完成的请求
  onCleanup(cancel)
  data.value = await response
})
  • 6.4 深度监听
watch: {
  bookList: {
    handler(val, oldVal) {
      console.log('book list changed')
    },
    deep: true,  //不会立即执行,只有值变化才会更新
    // immediate:true, //会立即执行
  },
}
  • 6.5 监听 props 的数据变化 (重要)
import { watch, toRefs } from 'vue'
const props = defineProps({
  visible: {
    type: Boolean,
    default: () => {
      return false
    }
  }
})

// 方案一:
const visible = toRefs(props).visible //将数据转成响应式
watch(visible, (newValue, oldValue) => {
  console.log(newValue)
})

// 方案二:
watch(
  () => props.visible,
  (newValue, oldValue) => {
    console.log(newValue)
  }
)
  • 6.6 监听多个
watch([() => user.name, () => user.sex], (newValue, oldValue) => {})

// 添加配置参数
watch(
  () => user.hobbies,
  (newValue, oldValue) => {},
  {
    immediate: true,
    deep: true,
    // 回调函数的执行时机,默认在组件更新之前执行,更新之后执行参数为‘post’
    flush: 'pre'
  }
)

# 7. provie 和 inject

//父级
const name = ref('猪八戒');
const changeName = (newName: string) => {
    name.value = newName;
};
provide('name', name);
provide('changeName', changeName);  //更改name的方法

//子级孙级
const name = inject('name') as string;  //使用类型断言,不然会有红色波浪线
const changeName = inject('changeName') as Fn;

# 8. 组件属性定义添加校验

const props = defineProps({
  status: {
    type: String,
    required: true,
    validator: value => {
      return ['created', 'loading', 'loaded'].includes(value)
    }
  }
})

# 9. 基础 jsx/tsx 使用

<script lang="tsx">
import { defineComponent, ref, Transition } from 'vue'
export default defineComponent({
  setup() {
    const count = ref(0);
    const handleClick = () => {
      count.value ++;
    }
    return () => (
      <div>
        <button onClick={handleClick}>click me!</button>
        <Transition name="slide-fade">
          {count.value % 2 === 0 ?
            <h1>count: {count.value}</h1>
          : null}
        </Transition>
      </div>
    )
  }
})
</script>

# 10. 插槽使用

https://github.com/fengcms/vue3-demo

  • 10.1 父组件
<template v-slot:licon>
  <img src="@/assets/img/help.png" />
</template>
  • 10.2 子组件
<div>
    <slot name="licon"/>
</div>
  • 10.3 插槽示例
<template>
    <Son>
        <template #default><div>默认插槽内容</div></template>
        <template #slotName><div>具名插槽内容</div></template>
        <template #propsSlot="scope">
            <div>
                作用域插槽内容:name,{{scope.data.name}};age,{{scope.data.age}}
            </div>
        </template>
    </Son>
</template>

<script setup>
import Son from './Son.vue'
<script>

# 11. @input 在移动端不生效

@input在移动端不生效

@change 代替或者使用 @blur

# 12. provide、inject、wacth

// A.vue
<script>
import { provide } from 'vue'
export default {
  setup() {
    const obj = {
      name: '前端印象',
      age: 22
    }

    // 向子组件以及子孙组件传递名为info的数据
    provide('info', obj)
  }
}
</script>

// B.vue
<script>
import { inject } from 'vue'
export default {
  setup() {
    // 接收A.vue传递过来的数据
    inject('info') // {name: '前端印象', age: 22}
  }
}
</script>

// C.vue
<script>
import { inject } from 'vue'
export default {
  setup() {
    // 接收A.vue传递过来的数据
    inject('info') // {name: '前端印象', age: 22}

    const state = reactive({ count: 0 })

    // watch监听多个值
    const state = reactive({ count: 0, name: 'zs' })

    watch(
      [() => state.count, () => state.name],
      ([newCount, newName], [oldvCount, oldvName]) => {
        console.log(oldvCount) // 旧的 count 值
        console.log(newCount) // 新的 count 值
        console.log(oldName) // 旧的 name 值
        console.log(newvName) // 新的 name 值
      }
    )
  }
}
</script>

# 13. 动态引入组件

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
)

app.component('async-component', AsyncComp)

# 14. Suspense 是有两个 template 插槽的,第一个 default 代表异步请求完成后,显示的模板内容。fallback 代表在加载中时,显示的模板内容。

<template>
  <div>
    <Suspense>
      <template #default> //此处编写你的组件或者异步请求回来的数据 </template>

      <template #fallback>
        <h1>Loading...</h1>
      </template>
    </Suspense>
  </div>
</template>

# 15. teleport 介绍

teleport 能使 Dom 不在#app 的跟节点下

  • 15.1. 解决 Dialog 被包裹在其它组件之中,容易被干扰
  • 15.2. 解决样式也在其它组件中,容易变得非常混乱
<template>
  <teleport to="#modal">  //需要渲染的DOM的id在。
    <div id="center">
      <h2>JSPang11</h2>
    </div>
  </teleport>
</template>
<script lang="ts">
export default {};
</script>
<style>
#center {
  width: 200px;
  height: 200px;
  border: 2px solid black;
  background: white;
  position: fixed;
  left: 50%;
  top: 50%;
  margin-left: -100px;
  margin-top: -100px;
}
</style>
  • 15.3 在页面中使用
<template>
  <div>点餐完毕</div>
  <modal/>

</template>

import modal from "./components/Modal.vue";
const app = {
  name: "App",
  components: {
    modal,
  }
}
  • 15.4 打开/public/index.html,增加一个 model 节点。
<div id="app"></div>
<div id="modal"></div>

# 16. bus 使用

//bus.js
import mitt from 'mitt'

const bus = {}

const emitter = mitt()

bus.$on = emitter.on
bus.$off = emitter.off
bus.$emit = emitter.emit
bus.$all = emitter.all.clear()

export default bus
import bus from '@/bus.js'

app.config.globalProperties.$bus = bus

proxy.$bus.$emit('dr-workOrd-change', 11)

proxy.$bus.$on("dr-workOrd-change", (val) => {
   console.log('你的逻辑',val)
})

# 17. vite template 中使用本地图片

<img :src="getAssetsFile('images/home-0.png')" />
export const getAssetsFile = (url = '') => {
  return new URL(`../assets/${url}`, import.meta.url).href
}

# 18. 父组件调用子组件方法

<template>
  <div>
    <button @click="callChildMethod">调用子组件的 increment 方法</button>
    <ChildComponent ref="childComponentRef" />
  </div>
</template>

<script setup>
// 子组件
defineExpose({ acceptParams, increment }) //暴露两个方法
function acceptParams(param) {
  console.log('acceptParams ', param)
}
function increment(param) {
  console.log('acceptParams ', param)
}
// 子组件

// 父组件
const childComponentRef = ref(null)
const callChildMethod = () => {
  nextTick(() => {
    console.log('---', childComponentRef.value)
    childComponentRef.value.acceptParams()
  })
}
// 父组件
</script>

# 19. jsx/tsx 使用

  • 19.1 定义组件
<template>
  <Comp1></Comp1>
  <Comp2></Comp2>
</template>

<script setup lang="tsx">
import {
  type FunctionalComponent,
  h,
  ref,
  defineComponent,
  Fragment
} from 'vue'

const count = ref(0)
const increment = () => count.value++

// 定义渲染函数
const renderContent = () => {
  return h('div', [
    h('p', `计数: ${count.value}`),
    h('button', { onClick: increment }, '增加')
  ])
}

const Comp1 = defineComponent({
  props: {
    age: {
      type: Number,
      default: 18
    }
  },
  setup(props) {
    return () => {
      // return renderContent()
      return (
        <>
          <div onClick={increment}>333:{count.value}</div>
        </>
      )
    }
  }
})

const Comp2: FunctionalComponent = () => {
  return h('div', { onClick: increment }, `Comp2: ${count.value}`)
}
</script>
  • 19.2 配置解决报错
npm i @vue/babel-plugin-jsx @vitejs/plugin-vue-jsx -D
//vite.config.js
import vueJsx from '@vitejs/plugin-vue-jsx'

plugins: [vueJsx()]
//.babelrc
"plugins": [ "@vue/babel-plugin-jsx"]
// tsconfig.json
"compilerOptions": {
  "strict": true,
  "jsx": "preserve",
  "jsxFactory": "h",
  "jsxFragmentFactory": "Fragment"
},
上次更新: