vue3的知识整理

1. vue3的生命周期

vue3的生命周期一般有2种形式写法,一种是基于vue2options API的写法,一种是vue3特有的Composition API

options API的生命周期
基本同vue2的生命周期基础,只是为了与生命周期beforeCreatecreated对应,将beforeDestroydestroyed更名为beforeUnmountunmounted,使用方法同vue2

<template>
  <p>生命周期</p>
  <p>{{msg}}</p>
  <button @click="changeMsg">修改值</button>
  <button @click="toHome">跳转页面</button>
</template>
<script>
import { useRouter } from 'vue-router'

export default {
  name: 'lifeCycles',
  data() {
    return  {
      msg: 'hello vue3'
    }
  },
  setup() {
    console.log('setup')

    // 在`setup`函数中创建`router`对象,相当于vue2中的`this.router`
    const router = useRouter()
    const toHome = () => {
      router.push({
        path: '/'
      })
    }
    return {toHome}
  },
  beforeCreate() {
    console.log('beforeCreate');
  },
  created() {
    console.log('created');
  },
  beforeMount() {
    console.log('beforeMount');
  },
  mounted() {
    console.log('mounted');
  },
  beforeUpdate() {
    console.log('beforeUpdate');
  },
  updated() {
    console.log('updated');
  },
  // 由vue2 beforeDestroy改名
  beforeUnmount() {
    console.log('beforeUnmount');
  },
  // 由vue2 destroyed改名
  unmounted() {
    console.log('unmounted');
  },
  methods: {
    changeMsg() {
      this.msg = 'after changed'
    }
  }
}
</script>
初次渲染时

点击修改值的生命周期

点击跳转,组件销毁的生命周期

composition API的生命周期
composition API的生命周期钩子函数是写在setup函数中的,它所有生命周期是在vue2生命周期名字前加on,且必须先导入才可使用

在这种写法中,是没有onBeforeCreateonCreated周期的,setup等同于(或者说是介于)这两个生命周期

<template>
  <p>composition API生命周期</p>
  <p>{{msg}}</p>
  <button @click="changeMsg">修改值</button>
  <button @click="toHome">跳转页面</button>
</template>
<script>
import { useRouter } from 'vue-router'
// 必须先导入生命周期
import { 
  onBeforeMount, 
  onMounted, 
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue'
export default {
  name: 'lifeCycles',
  data() {
    return  {
      msg: 'hello vue3'
    }
  },
  setup() {
    console.log('setup')
    onBeforeMount(() => {
      console.log('onBeforeMount');
    })
    onMounted(() => {
      console.log('onMounted');
    })
    onBeforeUpdate(() => {
      console.log('onBeforeUpdate');
    })
    onUpdated(() => {
      console.log('onUpdated');
    })
    onBeforeUnmount(() => {
      console.log('onBeforeUnmount');
    })
    onUnmounted(() => {
      console.log('onUnmounted');
    })

    // 在`setup`函数中创建`router`对象,相当于vue2中的`this.router`
    const router = useRouter()
    const toHome = () => {
      router.push({
        path: '/'
      })
    }
    return {toHome}
  },
  
  methods: {
    changeMsg() {
      this.msg = 'after changed'
    }
  }
}

</script>
初次渲染

点击修改data中的值

页面跳转,组件销毁

2. 如何理解 Composition API 和 options API ?

Composition API带来了什么:

  • 更好的代码组织
  • 更好的逻辑复用,避免mixins混入时带来的命名冲突和维护困难问题
  • 更好的类型推导

options API
使用options API,当代码很多时,即当data, watch, methods等有很多内容时,业务逻辑比较复杂,我们需要修改一部分时,可能要分别到data/methods/模板中对应修改,可能需要我们在页面反复横跳来修改,逻辑块会比较散乱。

Composition API
Composition API则会将业务相关的部分整合到一起,作为一个模块,当要修改,统一到一处修改,代码看起来会更有条理

image.png

它包含的内容包括:

  • reactive
  • ref相关(ref, toRef, toRefs,后面会具体介绍)
  • readonly
  • watch和watchEffect
  • setup
  • 生命周期钩子函数

两者的选择:

  • 不建议共用,否则容易引起混乱(思路、组织方式、写法都不太一样)
  • 小型项目,业务逻辑简单的,建议用options API,对新手也比较友好
  • 中大型项目、逻辑复杂,建议使用Composition API

Composition API它属于高阶技巧,不是基础必会的,有一定的学习成本,是为了解决复杂业务逻辑而设计,就像hooksReact中的地位一样

3. 如何理解ref,toRef,toRefs

ref

  • 通过ref方式创建响应式的值类型,并赋予初始值,并通过.value的方式获取值或修改值
  • 通过reactive方式创建响应式的引用类型,并赋予初始值,修改和获取方式同普通对象一样
  • 除了以上两种用法,还可以使用ref来声明dom元素,也就是类似vue2中的用法
<template>
  <p>ref demo</p>
  <p>{{nameRef}}今年{{ageRef}}岁了</p>
  <p>他喜欢{{hobbies.type}}</p>
</template>

<script>
// 导入ref, reactive, onMounted
<template>
  <p>ref demo</p>
  <p>{{nameRef}}今年{{ageRef}}岁了</p>
  <p>他喜欢{{hobbies.type}}</p>
  <p ref="eleRef">我是refTemplate使用方式的内容</p>
</template>

<script>
import { ref, reactive, onMounted } from 'vue'
export default {
  name: 'ref',
  setup() {
    // ref
    const ageRef = ref(3); // ref创建响应式的值类型,并赋予初始值
    const nameRef = ref('小花')
    console.log(ageRef.value) // 通过.value方式获取值
    ageRef.value = 18 // 通过.value方式修改值

    // reactive
    const hobbies = reactive({
      type: 'basketball'
    })
    console.log(hobbies.type) // basketball,通过obj[key]方式获取值
    hobbies.type = 'aaaaa' // 通过obj[key]=xxx方式修改值

    // refTemplate
    const eleRef = ref(null)
    onMounted(() => {
      // 跟vue2的区别在于,vue2使用this.$refs['eleRef']方式获取dom,这里通过eleRef.value方式获取
      console.log(eleRef.value) // dom
      console.log(eleRef.value.innerHTML) // 我是refTemplate使用方式的内容

    })
    
    // 注意,这些对象都要return出去,否则就不是响应式
    return {
      ageRef,
      nameRef,
      hobbies,
      eleRef
    }
  }
}
</script>

image.png

PS: 小建议,ref定义的数据可以加个Ref后缀,这样就能区分refreactive定义的变量了

toRef

看定义有点绕

  • 针对一个响应式对象(reactive封装)的属性prop
  • 通过toRef创建一个ref对象,这个ref对象和reactive对象的某属性两者保持引用关系

注意,如果toRef是通过普通对象来生成ref对象,那么普通对象和ref对象都将不是响应式的

<template>
  <p>toRef demo</p>
  <p>小花今年 - {{ageRef}}岁 - {{state.age}}岁</p>
</template>

<script>
import { toRef, reactive } from 'vue'
export default {
  name: 'toRef',
  setup() {
    // 定义一个响应式的reactive引用对象
    const state = reactive({
      name: '小花', 
      age: 3
    })

    // 如果是普通对象,使用toRef,那么它们将都不是响应式的
    // 也就是说ageRef和state都不是响应式
    // const state = {
    //   name: '小花', 
    //   age: 3
    // }

    // 通过toRef创建一个响应值类型ageRef, 这个ageRef和state.age属性保持双向引用
    const ageRef = toRef(state, 'age')

    // 修改state.age值时,ageRef也会跟着改
    setTimeout(() => {
      state.age = 25
    }, 1000)

    // 修改ageRef值时,state.age也会跟着改
    setTimeout(() => {
      ageRef.value = 30
    }, 2000)

    return {
      state, ageRef
    }
  }
}
</script>
初始时

1s后,state.age改了,ageRef也跟着改了

2s后,ageRef改了,state.age也跟着改了

toRefs

  • 将响应式对象(reactive)的所有属性prop,转换为对应prop名字的ref对象
  • 两者保持引用关系
<template>
  <p>toRef demo</p>
  <!-- 这样,模板中就不用写state.name, state.age了,直接写name和age即可 -->
  <p>{{name}}今年 - {{age}}岁</p>
</template>

<script>
import { toRefs, reactive } from 'vue'
export default {
  name: 'toRef',
  setup() {
    // 定义一个响应式的reactive引用对象
    const state = reactive({
      name: '小花', 
      age: 3
    })

    // 相当于
    // const age = toRef(state, 'age')
    // const name = toRef(state, 'name')
    // const stateAsRefs = { age, name }
    const stateAsRefs = toRefs(state)

    // 修改state.age值时,就会映射到ref类型的age上
    setTimeout(() => {
      state.age = 25
    }, 1000)

    // return stateAsRefs 等同于:
    // const { age: age, name: name } = stateAsRefs
    // return { age, name }
    return stateAsRefs
  }
}
</script>

应用:
当使用composition API时,抽象出一个模块,使用toRefs返回响应式对象,这样,在接收的时候,我们就可以使用解构的方式获取到对象里面的内容,这也是比较符合我们常用的方式

// 封装一个模块,使用toRefs导出对象
export function useFeature() {
  const state = reactive({
    x: 1,
    y: 2
  })
  // ...
  return toRefs(state)
}
// 导入时,可以使用解构方式
import { useFeature } from './features'
  
export default {
  setup() {
    // 可以在不丢失响应式的情况下解构
    const  { x, y } = useFeature()
    return { x, y }
  }
}

ref, toRef, toRefs 使用小结:

  • reactive做对象的响应式,用ref做值类型的响应式
  • setup中返回toRefs(state),或toRef(state, prop)
  • ref变量命名建议用xxxRef
  • 合成函数返回响应式对象时,使用toRefs

为什么需要 ref ?

  • 如果没有ref,普通的值类型定义,没法做响应式
  • computed,setup,合成函数,都有可能返回值类型,要保证其返回是响应式的
  • 如果vue不定义ref,用户可能会自己造ref,反而更加混乱

为什么需要.value ?

  • ref是一个对象(保证响应式),value用来存储值
  • 通过.value属性的getset实现响应式
  • 用于模板、reactive时,不需要.value,这是因为vue编译会自动识别,其他情况则需要使用

为什么需要 toRef 和 toRefs ?

  • 目的:为了不丢失响应式的情况下,把对象数据分散、扩散(或者说是解构)
  • 前提:针对的是响应式对象(reactive封装的对象)
  • 本质:不创建响应式(创建是ref和reactive的事),而是延续响应式

4. watch和watchEffect的区别

  1. watchwatchEffect都可以监听data的变化
  2. watch需要指定监听的属性,默认初始时不会触发,如果初始要触发,需要配置immediate: true
  3. watchEffect是不需要指定监听的属性,而是自动监听其用到的属性,它初始化时,一定会执行一次,这是为了收集要监听的属性
<template>
  <p>watch 的使用</p>
  <p>numberRef: {{numberRef}}</p>
  <p>{{name}}-{{age}}</p>
</template>

<script>
import { ref, reactive, toRefs, watch, watchEffect } from 'vue'
export default {
  name:'Watch',
  setup() {
    const numberRef = ref(1000)
    const state = reactive({
      name: '小花', 
      age: 3
    })
    // 监听ref变量
    watch(numberRef, (newVal, oldVal) => {
      console.log('watch:', newVal, oldVal);  
      // watch: 1000 undefined
      // watch: 200 1000
    }, {
      immediate: true // 第三个参数可选,是一些配置项,immediate表示初始化时就执行监听
    })

    setTimeout(() => {
      numberRef.value = 200
    }, 1000)

    // 监听对象
    watch(
      // 第一参数是监听对象,如果是对象需要使用函数返回形式
      () => state.age,
      // 第二个参数是监听的变化值
      (newVal, oldVal) => {
        console.log('watch:', newVal, oldVal);  
        // watch: 3 undefined
        // watch: 18 3
      },
      // 第三个参数是配置项
      {
        immediate: true, // 初始变化就监听
        deep: true // 深度监听
      }
    )
    setTimeout(() => {
      state.age = 18
    }, 1000)

    return {
      numberRef,
      ...toRefs(state)
    }
  }
}
</script>
watch
  // watchEffect监听
    watchEffect(() => {
      console.log('watchEffect');
      console.log(numberRef.value);
      console.log(state.age);
    })
watchEffect

5. 在setup中怎么获取组件实例

  • setup和其它compostion API中没有this
  • 如果一定要获取,要使用getCurrentInstance获取,并且在挂载后才可获取数据
  • 如果是options API,则可以像vue2一样正常使用this
<template>
  <p>get instance</p>
</template>

<script>
import { getCurrentInstance, onMounted } from 'vue'
export default {
  name: 'GetInstance',
  data() {
    return  {
      x: 1,
      y: 2
    }
  },
  // composition API
  // 没有this,需要getCurrentInstance来获取组件实例
  // 且setup本身是beforeCreate和Created生命周期间的钩子,拿不到data,所以要在onMounted中获取
  setup() {
    console.log('this', this);  // this undefined

    const instance = getCurrentInstance()
    console.log('instance', instance.data.x); //  instance undefined

    onMounted(() => {
      console.log('instance', instance.data.x); // instance 1
    }) 
  },
  // options API
  mounted() {
    console.log(this.x);  // 1
  }
}
</script>

6. vue3升级了哪些重要的功能

参考官网升迁指南

createApp

// vue2
const app = new Vue({/*options*/})
Vue.use(/*...*/)
Vue.mixin(/*...*/)
Vue.component(/*...*/)
Vue.directive(/*...*/)

// vue3
const app = Vue.createApp({/*options*/})
app.use(/*...*/)
app.mixin(/*...*/)
app.component(/*...*/)
app.directive(/*...*/)

emits属性

  1. setup中可以使用emit向父组件发出事件
  2. 在子组件中,需要emits声明向父组件发出的事件集合
<template>
  parent
  <Child msg="hello" @log="log" />
</template>
<script>
import Child from './child.vue'
export default {
  name: 'emits',
  components:{
    Child
  },
  methods: {
    log() {
      console.log('child emit me!')
    }
  }
}
</script>
<!-- Child.vue -->
<script>
export default {
  name: 'child',
  props: {
    msg: {
      type: String
    }
  },
  emits: ['log'],  // 需要声明接收的父组件传递的方法
  // 在setup方法中,可以使用emit方法与父组件通信
  setup(props, {emit}) {
    emit('log')
  },
  methods: {
    one(e) {
      console.log('one');
    },
    two(e) {
      console.log('two');
    }
  }
}
</script>

多事件处理

<template>
  <!-- 可以同时触发多个事件 -->
  <button @click="one($event), two($event)">触发多事件</button>
</template>

fragment

vue2只允许template中只有一个元素,如果多个元素,必须用一个元素包裹
vue3则允许template中可以直接有多个元素,这样就可以减少dom层级

移除.sync

vue2中的.sync

vue2中使用.sync是对以下语句的语法糖,父组件通过v-bind:xxx.sync='xxx'来向子组件说明这个属性是双向绑定的,子组件中通过$emit('update:xxx', newVal)来更新这个值

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

<!-- sync语法糖 -->
<text-document v-bind:title.sync="doc.title"></text-document>

vue3中,废除了.sync的写法,换成一种更具有语义的写法v-model:xxx,在父组件中使用v-model:xxx方式说明这个属性是双向绑定的,子组件中通过$emit('update:xxx', newVal)来更新这个值

<template>
  <p>{{name}}-{{age}}</p>

  <UserInfo 
    v-model:name="name"
    v-model:age="age"
  />
</template>

<script>
import { reactive, toRefs } from 'vue'
import UserInfo from './userInfo.vue'
export default {
  name: 'vmodel',
  components: {
    UserInfo
  },
  setup() {
    const userInfo = reactive({
      name: '小花',
      age: 3
    })
    return toRefs(userInfo)
  }
}
</script>

<!-- userInfo.vue -->
<template>
  <input type="text" :value="name" @input="$emit('update:name', $event.target.value)">
  <input type="text" :value="age" @input="$emit('update:age', $event.target.value)">
</template> 
<script>
export default {
  props: {
    name: {
      type: String
    },
    age: {
      type: String
    }
  }
}
</script>

异步组件的导入方式不一样

vue2child: () => import('child.vue')
vue3:需要defineAsyncComponent导入,child: defineAsyncComponent(() => import('child.vue'))

teleport

teleport将我们的模板移动到DOMVue app之外的其他位置,比如可以使用teleport标签将组件在body

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

推荐阅读更多精彩内容