新的api和常见问题

  1. context
    1). expose
    如果我们的 setUp 直接返回了一个 jsx ,这时我么还想同时返回一个方法让父组件使用我们就可以使用 expose,不需要 return 直接可以暴露给父组件使用
  • 子组件
import { h, ref } from 'vue'
export default {
  setup(props, { expose }) {
    const count = ref(0)
    const increment = () => ++count.value

    expose({
      increment
    })

    return () => h('div', count.value)
  }
}
  • 父组件
const testRef = ref()
const onClick = () => {
  testRef.value.increment?.()
}
return (
  <div onClick={onClick}>
    <Parent ref={testRef}/>
  </div>
)

2). context.emit
使用 context.emit 触发更新的时候要更新的对应的 props 必须是可以直接被赋值的一个属性,(也就是说不能是最外层的 reactive,可是是 reactive 里的某个属性值,也可以是 ref) 否则不会更新 ui ,因为 emit 触发更新传出一个新的值的本质是在父组件拿到这个新的value,然后直接赋值给对应的属性 a = newValue 这种
比如:

  • 直接使用 reactive 最外层不会更新 ui
const Parent = defineComponent({
  setup() {
    const fileList = reactive([])
    oMounted(() => {
      Object.assign(fileList, ["a", "b"])
    })
  return () => (
    <Child v-model:fileList={fileList}/>
   )
  }
// 触发 emit 后相当于 fileList = newValue,因为这里的 reactive 是最外层的只能通过 Object.assign 来修改,所以失败不会更新 ui
export const Child = defineComponent({
    props: {
        fileList: {
            type: Array as PropType<string[]>,
            default: () => []
        }
    },
    emits: ["update:fileList"],
    setup(props, context) {
        const handleClick = () => {
            console.log(123)
            context.emit("update:fileList", ["c", "d", "e"])
        }
        return () => (
            <>
                <button onClick={handleClick}>点我</button>
                <div>
                    {props.fileList.map(list => (
                        <span>{list}</span>
                    ))}
                </div>
            </>
        )
    }
})
  • ui 可以更新
    我们把上面的 reactive 改成一个 ref 或者里面再加一个属性就可以
    方式1:使用 ref
const Parent = defineComponent({
  setup() {
    const fileList = ref([])  // 这里可以直接被赋值,所以可以更新 ui
    oMounted(() => {
      fileList.value = ["a", "b"]
    })
  return () => (
    <Child v-model:fileList={fileList.value}/>
   )
  }
// 触发 emit 后相当于 fileList.value = newValue

方式2:reactive 里加一个属性

const Parent = defineComponent({
  setup() {
    const fileList = reactive({list: []}) 
    oMounted(() => {
      fileList.list = ["a", "b"]
    })
  return () => (
    <Child v-model:fileList={fileList.list}/>
   )
  }
// 触发 emit 后相当于 fileList.list = newValue
  1. Provide / Inject
    Provide 提供一个属性或者方法给其他子孙组件用(要确保Provide 传递的数据是只读的),其他组件可以通过 Inject 使用,注意:Provide 只能在父组件中提供方法,然后inject 在子组件中注入
  • symbol.ts
import { InjectionKey, Ref } from "vue"
export const PersonInfoKey: InjectionKey<Ref<PersonId>> = Symbol("PersonId")
  • Parent.tsx
import { provide, reactive, readonly, ref } from 'vue'
import {PersonInfoKey} from "./symbol"
setup() {
  const personIdRef = ref({personId: ""})
   provide(PersonInfoKey, personIdRef)
   onMounted(() => personIdRef.value.personId = "111")
    return () => (<Child />)
  }
  • Child.tsx
import { inject } from 'vue'
import {PersonInfoKey} from "./symbol"
setup() {
  const personIdRef = inject(PersonInfokey)
  console.log(personIdRef.value)
}

注意 provide 和 inject 都要在setup 内部直接使用,不能放在声明周期和异步里使用,

  1. watchEffect
    只有回调里使用了的ref变了才会执行
const testRef = ref(0)
const xRef = ref(0)
watchEffect(() => {
   console.log(xRef.value, 'iii')
})

// 等价于
useEffect(() => {}, [xRef.value])

上面的代码只有 xRef.value 变了才会执行

  1. watch
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

// 侦听多个
const firstName = ref('')
const lastName = ref('')

watch([firstName, lastName], (newValues, prevValues) => {
  console.log(newValues, prevValues)
})

firstName.value = 'John' // logs: ["John", ""] ["", ""]
lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]

默认情况下 watch 初始化页面的时候不会执行,如果想要 watch 一开始就执行,需要设置 immediate: true

    watch(
            a,
            newValue => {
                console.log(newValue, "ddd")
            },
            {
                immediate: true
            }
        )
  1. reactive
    对象类型的 ref,可以直接通过变量.属性访问不需要通过 value
const testRef = reactive({number: 1})
console.log(restRef.number)
  1. setup
    setup 对于普通表达式和语句如果不是在 return 里只会执行一次,setup 就相当于 created 声明周期
    比如:
setup: (props, context) => {
    const current = ref(0)
    const array = [
      {name: 'lifa'},
      {name: 'hh'}
    ]
    // 这里因为我们的value只是一个普通的变量所以只会在初始化的时候执行一次
//也就是初始值 lifa,所以下面我们点击切换的时候,即使点击 hh,value 值也不会变化
    const value = array[current.value].name
    const onClick = (index) => {
      current.value = index
    }
    
     // 上面这部分代码只会在初始化的时候执行一次

    // return 里面的代码每次重新 render 都会重新执行
    return () => (
      <div>
        <div style={{marginBottom: '20px'}}>{array.map((item, index) => <div onClick={() => onClick(index)} style={{marginBottom: '10px', fontSize: '16px'}}>{item.name}</div>)}</div>
        <div>{value}</div>
      </div>
    )
  }

如果想要我们的value变化有两种方式
1). 写在 return 里(不推荐)

setup: (props, context) => {
    const current = ref(0)
    const array = [
      {name: 'lifa'},
      {name: 'hh'}
    ]
    const onClick = (index) => {
      current.value = index
    }
    return () => {
      const value = array[current.value].name
      return (
        <div>
          <div style={{marginBottom: '20px'}}>{array.map((item, index) => <div onClick={() => onClick(index)} style={{marginBottom: '10px', fontSize: '16px'}}>{item.name}</div>)}</div>
          <div>{value}</div>
        </div>
      )
    }   
  }

2). 使用 computed

setup: (props, context) => {
    const current = ref(0)
    const array = [
      {name: 'lifa'},
      {name: 'hh'}
    ]
    const value = computed(() => {
      return array[current.value].name
    })
    const onClick = (index) => {
      current.value = index
    }
    return () => (
      <div>
        <div>{array.map((item, index) => <div onClick={() => onClick(index)}>{item.name}</div>)}</div>
        <!--注意这里得用.value-->
        <div>{value.value}</div>
      </div>
    )
  }
  1. toRaw
    将 proxy 数据转化为原始数据
const formData = reactive({
      name: '',
      sign: '',
    })
    const onSubmit = (event: Event) => {
      console.log(formData, 'fff')
      event.preventDefault()
    }

上面不加 toRaw 结果如下

加 toRaw 就是原始对象

console.log(toRaw(formData), 'fff') // {name: '', sign: ''}
  1. v-model 的类型声明
    vue3 默认的v-model 绑定的是modelValue 属性,也就是我们需要再props里定义一个 modelValue
exoprt const Test = defineComponent({
  props: {
    modelValue: string as PropType<String>
  },
  setup: (props, context) => {
     cons onChange = () => {
        context.emit('update:modelValue')
      }
  }
})

// 父组件中使用
<Test v-modle={test.value} />

不使用 v-model 语法糖的形式

exoprt const Test = defineComponent({
  props: {
    modelValue: string as PropType<String>
  },
  // 在 emits 里写的update方法可以直接在父组件中通过onUpdate:属性的方法来监听
  emits: ['update:modelValue']
  setup: (props, context) => {
     cons onChange = () => {
        context.emit('update:modelValue')
      }
  }
})

// 父组件使用
<Test modelValue={test.value} onUpdate:modelValue={(value) => test.value = value}

onUpdate:modelValue和 v-model 可以一起使用

  1. useHooks
    用来代替 vue2 的 mixin
    适用场景:有重复的js代码,然后里面用了vue的相关api,我们可以把这些重复的代码封装成 hooks

  2. shallowReactive
    相对于 reactive 来说它只是浅层的响应式代理,也就是只有第一层对象在我们改变时是响应式的,其他情况下都不会重新渲染 ui,可以提升页面性能

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

state.foo = 2 // 响应式
state.nested.bar = 3 // 非响应式,数据变了但是 ui 不会变

这里需要注意如果在修改深层的同时紧接着改了其他可以响应式的数据,那么这个深层数据也会跟着在 ui 上变化

const handleClick = () => {
  state.foo = 2
  state.nested.bar = 3
}
<div>{state.foo} {state.nested.bar}</div>
<div onClick={handleClick}>按钮</div>

上面的代码当我们触发 click 事件后页面上展示的state.foo 变成了2,state.nested.bar 也变成了 3

  1. 路由懒加载
    使用路由懒加载必须导出一个默认的组件,所以我们不能直接 export const,而要使用 export default
  • Home/index.tsx
const Home = defineComponent({})
export default Home
  • router.ts
{
        path: "/",
        name: "Dashboard",
        meta: {
            title: "首页",
            icon: "Home"
        },
        component: () => import(/* webpackChunkName: "about" */ "@/views/Home")
    },
  1. Teleport 传送门
    将元素添加到指定dom下
<Teleport to="body">
      <div>aaaa</div>
 </Teleport>

上面的代码我们的Teleport 里的内容就会出现在 body 下面,to 后面还可以跟类名、id

  1. 自定义指令
    注意如果你使用tsx写法只能使用全局指令
  • directive/index.ts
import { App, DirectiveBinding } from "vue";
import { ElLoading } from "element-plus";
type loadingOption = ReturnType<typeof ElLoading.service>

let loadingInstance: loadingOption = {} as loadingOption

export default {
  install(app: App) {
    app.directive(
        "loading",
        (
            el: HTMLElement,
            binding: DirectiveBinding<boolean>,
            vnode: VNode<unknown, HTMLElement>
        ) => {
            const { oldValue, value } = vnode.dirs![0]
            if (oldValue !== value) {
                if (binding.value) {
                    loadingInstance = ElLoading.service({ target: el })
                } else {
                    loadingInstance.close?.()
                }
            }
        }
    )
  }
}
  • main.ts
import directives from '../src/directive'
app.use(directives)
  • 使用
const loading = ref(false)
onMounted(() => {
      setTimeout(() => {
        loading.value = true
      }, 6000)
      setTimeout(() => {
        loading.value = false
      }, 8000)
    })
<div v-loading={loading.value} >
aaa
</div>
  1. 使用扩展运算符解构props
const newProps = computed(() => {
     const {age, ...reset} = props
     return reset
   })
   return () => (
     <div>
       <div>{props.age}</div>
       <Child {...newProps.value}></Child>
     </div>
   );
  1. KeepAlive 的使用
    注意如果有多层路由嵌套 router-view,必须得在最底层使用 KeepAlive
  • App.tsx
        <RouterView>
          {({
            Component,
            route,
          }: {
            Component: VNode
            route: RouteLocationNormalizedLoaded
          }) => {

            return (
              <>
                <KeepAlive>{route.meta.keepAlive ? Component : null}</KeepAlive>
                {!route.meta.keepAlive ? Component : null}
              </>
            )
          }}
        </RouterView>

注意 KeepAlive 组件移动要自始至终渲染,错误写法如下

            <RouterView>
                {({
                    Component,
                    route
                }: {
                    Component: VNode
                    route: RouteLocationNormalizedLoaded
                }) => {
                    console.log(route.meta.keepAlive, "kekke")
                    if (route.meta.keepAlive) {
                        return <KeepAlive>{Component}</KeepAlive>
                    } else {
                        return Component
                    }
                }}
            </RouterView>
  • router.ts
import { RouteRecordRaw } from "vue-router"
import Home from "@/views/Home"
// import { Child } from "@/views/Home/Child";
import { Test } from "@/views/Test"
import About from "@/views/About"
// import { TestChild } from "@/views/Home/TestChild";
export const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "home",
    component: Home,
    meta: {
      keepAlive: true,
    },
    beforeEnter: (to, from) => {
      if (from.name === "test" || from.name === undefined) {
        to.meta.keepAlive = true
      } else {
        to.meta.keepAlive = false
      }
    },
  },
  {
    path: "/test",
    component: Test,
    name: "test",
    meta: {
      keepAlive: false,
    },
  },
  {
    path: "/about",
    component: About,
    name: "about",
    meta: {},
  },
]

  1. 使用 slot 插槽
import { defineComponent } from 'vue';
import s from './First.module.scss';
export const First = defineComponent({
  setup: (props, {slots}) => {
    return () => (
      <div class={s.wrapper}>
        <div class={s.card}>
          {slots.icon?.()}
          {slots.title?.()}
        </div>
        <div class={s.actions}>
          {slots.buttons?.()}
        </div>
      </div>
    )
  }
})
// demo
import { WelcomeLayout } from './WelcomeLayout';
export const First = defineComponent({
  setup: (props, context) => {
    const slots = {
      icon: () => <span>icon</span>,
      title: () => 'hi',
      buttons: () => <><button>+1</button></>
    }
    return () => (
     <WelcomeLayout v-slots={slots} />
    )
  }
})
或者
export const First = defineComponent({
  setup: (props, context) => {
    return () => (
     <WelcomeLayout>
      {{
        icon: () => <span>icon</span>,
        title: () => 'hi',
        buttons: () => <><button>+1</button></>
      }}
      </WelcomeLayout>
    )
  }
})
  1. refs 的使用
import { ComponentPublicInstance, defineComponent, onMounted, reactive, ref } from 'vue';
export const Test = defineComponent({
  setup: (props, context) => {
    const arr = reactive([{
      name: "立发",
      age: "19"
    }, {
      name: "帅哥",
      age: "18"
    }])
    const refs = ref<Record<string, Element | ComponentPublicInstance | null>>({})
    onMounted(() => {
      const a = refs.value["18"] as Element
      a.scrollIntoView()
      
    })
    return () => (
      <>
        {
          arr.map((item, index) => (
            <div key={item.name} ref={el => refs.value[item.age] = el}>{item.name}</div>
          ))
        }
      </>
    )
  }
});
  1. 在 tsx 中使用 v-if 和 v-show 必须在普通元素标签上,不能在组件上使用
// 正确
const visible = ref(false)
<div v-show={visible.value}>aaa</div>

// 错误
<Child v-show={visible.value} />
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容