超详细整理vue3基础知识💥

2,927 阅读13分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

hi, 我是小黄瓜没有刺。一枚菜鸟瓜🥒,期待关注➕点赞,共同成长~

本文为系列文章,其他部分

狂肝半个月!1.3万字深度剖析vue3响应式(附脑图)🎉🎉 

手写mini-vue3第三弹!万字实现渲染器首次渲染流程 🎉 🎉 

更新!更新!实现vue3虚拟DOM更新&diff算法优化🎉 🎉

大结局!实现vue3模版编译功能🎉 🎉

一. 安装vue3

vue3官网提供的方式

// 最新稳定版
$ npm install vue@next

1. 使用vue-cli创建

npm install -g @vue/cli

// 创建项目
vue create my-project

注意:使用vue-cli创建vue3项目时可以选择使用vue2/vue3版本进行项目创建

2. 使用vite创建

npm

// 使用yarn来创建项目

# npm 6.x
$ npm init vite@latest <project-name> --template vue

# npm 7+,需要加上额外的双短横线
$ npm init vite@latest <project-name> -- --template vue

$ cd <project-name>
$ npm install
$ npm run dev

yarn

// 使用yarn来创建项目

$ yarn create vite <project-name> --template vue
$ cd <project-name>
$ yarn
$ yarn dev

pnpm

$ pnpm create vite <project-name> -- --template vue
$ cd <project-name>
$ pnpm install
$ pnpm dev

二. Composition API

因为vue2vue3版本用法上最大的区别就是引入了composition api,所以本文主要整理一下composition各个api的用法。

1. why

为什么vue3要引入composition?这种方式有什么好处?恐怕这是我们刚接触vue3的第一个问题,要回答这个问题,让我们先来看一下vue2版本和vue3版本两种不同的处理代码结构的方式:

Options API

options api是vue2所采用的代码组织方式,它有以下的特点:

  • 包含一个描述组件的选项(datamethodsprops等)的对象
  • options api 开发复杂组件,同一个功能的逻辑的代码被拆分到不同的组件

比如我们经常在vue2中这么写: 实现一个鼠标移动记录坐标的功能

export default {
  data() {
    return {
      position: {
        x: 0,
        y: 0
      }
    }
  },
  
  created() {
    // 在created中注册监听事件
    window.addEventListener('mousemove', this.handle)
  },
  destroyed() {
    // 页面销毁是移除监听
    window.removeEventListener('mousemove', this.handle)
  },
  methods: {
    // 事件
    handle(e) {
      // 保存x,y坐标值
      this.position.x = e.pageX
      this.position.y = e.pageY
    }
  }
}

Composition API

  • 一组基于函数的API
  • 可以更灵活的组织组件的逻辑

下面用vue3的方式来改写上面的功能

// 引入vue函数
import { reactive, onMounted, onUnmounted } from 'vue'

function useMousePosition() {
  // 定义响应式变量
  const position = reactive({
    x: 0,
    y: 0
  })
  
  const update = e => {
    position.x = e.pageX,
    position.y = e.pageY
  }
  
  // 生命周期钩子函数 绑定监听函数
  onMounted(()=>{
    window.addEventListener('mousemove', update)
  })
  
  // 生命周期钩子函数
  onUnmounted(()=>{
    window.removeEventListener('mousemove', update)
  })
  
  // 返回变量
  return position
}

// 导出
export default {
  setup() {
    const position = useMousePosition()
    return {
      position
    }
  }
}

可以看出,使用composition api是代码结构更加集中了,我们可以将某一个功能函数封装在一个函数,而不用在整个页面到处寻找各个option对象,还可以将各个功能的函数拆分文件夹,使整个代码组织形式更加有条理,减轻开发人员的心智负担。 下面的图片可以很好的说明这个问题: WX20220525-115304@2x.png

除此之外vue3带来的还有一系列的性能提升

  • 响应式系统的升级
    • 使用proxy重写响应式系统
    • 可以监听动态新增属性
    • 监听删除属性
    • 监听数组索引和length属性
  • 编译优化
    • 优化diff过程
    • 标记静态节点
    • 缓存事件处理函数
  • 源码体积的优化
    • Tree-shaking
    • 移除不常用的api
  • ........

2. setup

  • 所有的组合API函数都在此使用, 只在初始化时执行一次
  • 函数如果返回对象, 对象中的属性或方法, 模板中可以直接使用
<template>
  // setup返回的值或函数可以直接在template中使用或调用
  <h2>{{count}}</h2>
</template>

<script>
export default {
  // 定义setup函数
  setup () {
    const count = 1

    // 返回值或函数
    return {
      count
    }
  }
}
</script>

setup的细节

  • setup执行的时机

    • beforeCreate之前执行(一次), 此时组件对象还没有创建
    • thisundefined, 不能通过this来访问data/computed/methods / props
    • 其实所有的composition API相关回调函数中也都不可以
  • setup的返回值

    • 一般都返回一个对象: 为模板提供数据, 也就是模板中可以直接使用此对象中的所有属性/方法
    • 返回对象中的属性会与data函数返回对象的属性合并成为组件对象的属性
    • 返回对象中的方法会与methods中的方法合并成功组件对象的方法
    • 如果有重名, setup优先
  • 注意:

    • 一般不要混合使用: methods中可以访问setup提供的属性和方法, 但在setup方法中不能访问datamethods
    • setup不能是一个async函数: 因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性数据
  • setup的参数

    • setup(props, context) / setup(props, {attrs, slots, emit})
    • props: 包含props配置声明且传入了的所有属性的对象
    • attrs: 包含没有在props配置中声明的属性的对象, 相当于 this.$attrs
    • slots: 包含所有传入的插槽内容的对象, 相当于 this.$slots
    • emit: 用来分发自定义事件的函数, 相当于 this.$emit
<template>
  <h2>App</h2>
  <p>msg: {{msg}}</p>
  <button @click="fn('haha')">更新</button>

  <child :msg="msg" msg2="cba" @fn="fn"/>
</template>

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

export default {

  components: {
    child
  },

  setup () {
    const msg = ref('abc')

    function fn (content: string) {
      msg.value += content
    }
    return {
      msg,
      fn
    }
  }
}
</script>

child:

<template>
  <div>
    <h3>msg: {{msg}}</h3>
    <h3>msg2: {{$attrs.msg2}}</h3>

    <slot name="xxx"></slot>

    <button @click="update">更新</button>
  </div>
</template>

<script lang="ts">

import {
  ref,
  defineComponent
} from 'vue'

export default defineComponent({
  name: 'child',

  // {attrs, emit, slots}的写法相当于context的解构
  setup (props, {attrs, emit, slots}) {

    console.log('setup', this)
    console.log(props.msg, attrs.msg2, slots, emit)

    const m = ref(2)
    const n = ref(3)

    function update () {
      m.value += 2
      n.value += 2

      // 分发自定义事件
      emit('fn', '++')
    }

    return {
      m,
      n,
      update,
    }
  },
})
</script>

3. ref

ref用于定义一个数据的响应式,也就是将一个值转换为响应式。 语法:const 变量名 = ref(初始值): 注意:

  • 创建一个包含响应式数据的引用(reference)对象
  • js中操作数据: 变量名.value
  • 模板中操作数据: 不需要.value
  • 一般用来定义一个基本类型的响应式数据
<template>
  // 在template中使用值不需要使用.value来获取
  <h2>{{count}}</h2>
  <hr>
  <button @click="update">更新</button>
</template>

<script>
// vue3用到的api需要手动引入
import {
  ref
} from 'vue'
export default {
  setup () {
    // 定义响应式数据 ref对象
    const count = ref(1)

    // 更新响应式数据的函数
    function update () {
      // 使用ref创建的响应式对象要使用.value访问其值
      count.value = count.value + 1
    }
    
    // 将count和update函数导出
    return {
      count,
      update
    }
  }
}
</script>

如果用ref对象/数组, 内部会自动将对象/数组转换为reactive的代理对象

4. reactive

  • 作用: 定义多个数据的响应式
  • const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
  • 响应式转换是“深层的”:会影响对象内部所有嵌套的属性
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
<template>
  <h2>name: {{state.name}}</h2>
  <h2>age: {{state.age}}</h2>
  <h2>play: {{state.play.name}}</h2>
  <hr>
  <button @click="update">更新</button>
</template>

<script>
    
// 引入 reactive
import {
  reactive,
} from 'vue'
export default {
  setup () {
    // 定义响应式数据对象
    const state = reactive({
      name: '小黄瓜',
      age: 20,
      play: {
        name: '豆豆',
        age: 22
      },
    })

    // 事件处理函数
    const update = () => {
      state.name += '--'
      state.age += 1
      state.play.name += '的小猫'
      state.play.age += 2
    }

    return {
      state,
      update,
    }
  }
}
</script>

5. vue2与vue3响应式核心的变化

vue2

  • 核心:
    • 对象: 通过defineProperty对对象的已有属性值的读取和修改进行劫持(监视/拦截)
    • 数组: 通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
Object.defineProperty(data, 'count', {
    get () {}, 
    set () {}
})
  • 问题
    • 对象直接新添加的属性或删除已有属性, 界面不会自动更新
    • 直接通过下标替换元素或更新length, 界面不会自动更新 arr[1] = {}

vue3

  • 核心:
    • 通过Proxy(代理): 拦截对data任意属性的任意(13种)操作, 包括属性值的读写, 属性的添加, 属性的删除等...
    • 通过 Reflect(反射): 动态对被代理对象的相应属性进行特定的操作
new Proxy(data, {
	// 拦截读取属性值
    get (target, prop) {
    	return Reflect.get(target, prop)
    },
    // 拦截设置属性值或添加新属性
    set (target, prop, value) {
    	return Reflect.set(target, prop, value)
    },
    // 拦截删除属性
    deleteProperty (target, prop) {
    	return Reflect.deleteProperty(target, prop)
    }
})

6. 计算属性

computed函数有两种用法,都是用来监听数据的变化。

只使用getter

// 只有getter的计算属性
const com = computed(() => {
   return user.firstName + '-' + user.lastName
}                      

使用getter和setter

 // 有getter与setter的计算属性
const com = computed({
  get () {
    console.log('get')
    return user.firstName + '-' + user.lastName
  },

  set (value: string) {
    console.log('set')
    const names = value.split('-')
    user.firstName = names[0]
    user.lastName = names[1]
  }
})

7. 监听函数

watch

  • watch配置功能一致
  • 监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
  • 默认初始时不执行回调, 但可以通过配置immediatetrue, 来指定初始时立即执行第一次
  • 通过配置deeptrue, 来指定深度监视
// 深度监听,初始化执行
watch(user, () => {
  name.value = user.firstName + '-' + user.lastName
}, {
  immediate: true,  // 是否初始化立即执行一次, 默认是false
  deep: true, // 是否是深度监视, 默认是false
})

// 监听单个属性
// 指定监听属性,设置监听函数 
// 属性发生变动,就会触发函数
watch(name, (value) => {
   console.log('watch')
   const names = value.split('-')
   user.firstName = names[0]
   user.lastName = names[1]
})

// 监听多个属性
watch([() => user.firstName, () => user.lastName, fullName3], (values) => {
  console.log('监视多个数据', values)
})

watchEffect

  • 不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
  • 默认初始时就会执行第一次, 从而可以收集需要监视的数据
  • 监视数据发生变化时回调
 watchEffect(() => {
   console.log('watchEffect')
   name.value = user.firstName + '-' + user.lastName
 }) 

8. 生命周期

vue2.x生命周期钩子 image.png vue3生命周期钩子 image.png

2.x 版本生命周期相对应的组合式 API beforeCreate -> 使用 setup() created -> 使用 setup() beforeMount -> onBeforeMount mounted -> onMounted beforeUpdate -> onBeforeUpdate updated -> onUpdated beforeDestroy -> onBeforeUnmount destroyed -> onUnmounted errorCaptured -> onErrorCaptured

新增的钩子函数 组合式 API 还提供了以下调试钩子函数:

  • onRenderTracked
  • onRenderTriggered
<template>
<div class="about">
  <h2>msg: {{msg}}</h2>
  <hr>
  <button @click="update">更新</button>
</div>
</template>

<script lang="ts">
// 引入生命周期钩子函数
import {
  ref,
  onMounted,
  onUpdated,
  onUnmounted, 
  onBeforeMount, 
  onBeforeUpdate,
  onBeforeUnmount
} from "vue"

export default {
  
  beforeCreate () {
    console.log('beforeCreate()')
  },

  created () {
    console.log('created')
  },

  beforeMount () {
    console.log('beforeMount')
  },

  mounted () {
    console.log('mounted')
  },

  beforeUpdate () {
    console.log('beforeUpdate')
  },

  updated () {
    console.log('updated')
  },

  beforeUnmount () {
    console.log('beforeUnmount')
  },

  unmounted () {
     console.log('unmounted')
  },
  
  // composition中使用
  setup() {
    
    const msg = ref('abc')

    const update = () => {
      msg.value += '--'
    }

    onBeforeMount(() => {
      console.log('--onBeforeMount')
    })

    onMounted(() => {
      console.log('--onMounted')
    })

    onBeforeUpdate(() => {
      console.log('--onBeforeUpdate')
    })

    onUpdated(() => {
      console.log('--onUpdated')
    })

    onBeforeUnmount(() => {
      console.log('--onBeforeUnmount')
    })

    onUnmounted(() => {
      console.log('--onUnmounted')
    })
    
    return {
      msg,
      update
    }
  }
}
</script>

9. toRef

可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。

应用:如果我们解构一个reactive响应式对象,那么他的响应式可能会丢失。 解决: 利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性。

export default {

  setup () {
    const state = reactive({
      foo: 'a',
      bar: 'b',
    })
    // 使用toRefs进行转换
    const stateAsRefs = toRefs(state)
    
    return {
      // 直接解构原属性,会导致响应式绑定丢失
      // ...state,
      ...stateAsRefs,
      foo2, 
      bar2
    }
  }
}

10. ref获取元素

利用ref函数获取组件中的标签元素。

<template>
  <input type="text" ref="inputRef">
</template>

<script>
import { onMounted, ref } from 'vue'
export default {
  setup() {
    // 定义dom元素
    const inputRef = ref<HTMLElement|null>(null)
    // 自动获取焦点
    onMounted(() => {
      inputRef.value && inputRef.value.focus()
    })

    return {
      inputRef
    }
  },
}
</script>

11. shallowReactive 与 shallowRef

shallowReactive : 只处理了对象内最外层属性的响应式(也就是浅响应式) shallowRef: 只处理了value的响应式, 不进行对象的reactive处理 什么时候用浅响应式呢?

  • 一般情况下使用refreactive即可
  • 如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
  • 如果有一个对象数据, 后面会产生新的对象来替换 ===> shallowRef
export default {
  setup () {
    const m1 = reactive({a: 1, b: {c: 2}})
    const m2 = shallowReactive({a: 1, b: {c: 2}})

    const m3 = ref({a: 1, b: {c: 2}})
    const m4 = shallowRef({a: 1, b: {c: 2}})
  }
}

12. readonly 与 shallowReadonly

readonly

  • 深度只读数据
  • 获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。
  • 只读代理是深层的:访问的任何嵌套 property 也是只读的。

shallowReadonly

  • 浅只读数据
  • 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换

应用场景

  • 在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除
export default {

  setup () {
    const state = reactive({
      a: 1,
      b: {
        c: 2
      }
    })
    // 定义深度只读
    // const rState1 = readonly(state)
    
    // 定义浅度只读
    const p1 = shallowReadonly(state)

    const update = () => {
      // 深度只读,返回错误
      // p1.a++ // error
      // p1.b.c++ // error

      // p1.a++ // error
      // 浅度只读,执行成功
      p1.b.c++
    }
    
    return {
      state,
      update
    }
  }
}
</script>

13. toRaw 与 markRaw

toRaw

  • 返回由reactivereadonly 方法转换成响应式代理的普通对象。
  • 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。

markRaw

  • 标记一个对象,使其永远不会转换为代理。返回对象本身

应用场景:

  • 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。
  • 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。
<template>
  <h2>{{state}}</h2>
  <button @click="testToRaw">测试toRaw</button>
  <button @click="testMarkRaw">测试markRaw</button>
</template>

<script lang="ts">
import {
  markRaw,
  reactive, toRaw,
} from 'vue'
export default {
  setup () {
    const state = reactive<any>({
      name: 'tom',
      age: 25,
    })

    const testToRaw = () => {
      const user = toRaw(state)
      // 界面不会更新
      user.age++  

    }

    const testMarkRaw = () => {
      const likes = ['a', 'b']
      // state.likes = likes
      
      // likes数组就不再是响应式的了
      state.likes = markRaw(likes) 
      setTimeout(() => {
        state.likes[0] += '--'
      }, 1000)
    }

    return {
      state,
      testToRaw,
      testMarkRaw,
    }
  }
}
</script>

14. customRef

创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个函数,该函数接收 tracktrigger 函数作为参数,并且应该返回一个带有 getset 的对象。

使用自定义 ref 通过v-model 实现 debounce 的示例

function useDebouncedRef(value, delay = 200) {
  let timeout
  
  // 返回customRef函数
  return customRef((track, trigger) => {
    return {
      get() {
        // track函数用来告诉Vue追踪数据
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          // trigger函数用来告诉Vue去触发界面更新
          trigger()
        }, delay)
      }
    }
  })
}

export default {
  setup() {
    return {
      text: useDebouncedRef('hello')
    }
  }
}

15. provide 与 inject

实现跨层级组件(祖孙)间通信

<template>
  <h1>父组件</h1>
  <p>当前颜色: {{color}}</p>
  <button @click="color='red'"></button>
  <button @click="color='yellow'"></button>
  <button @click="color='blue'"></button>
  
  <hr>
  <Son />
</template>

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

import Son from './Son.vue'
export default {
  name: 'ProvideInject',
  components: {
    Son
  },
  setup() {
    
    const color = ref('red')
    provide('color', color)

    return {
      color
    }
  }
}
</script>
<template>
  <div>
    <h2>子组件</h2>
    <hr>
    <GrandSon />
  </div>
</template>

<script lang="ts">
import GrandSon from './GrandSon.vue'
export default {
  components: {
    GrandSon
  },
}
</script>
<template>
  <h3 :style="{color}">孙子组件: {{color}}</h3>
  
</template>

<script lang="ts">
import { inject } from 'vue'
export default {
  setup() {
    const color = inject('color')

    return {
      color
    }
  }
}
</script>

16. 自定义hook函数

案例:收集用户鼠标点击的页面坐标 hooks/useMousePosition.ts

import { ref, onMounted, onUnmounted } from 'vue'

// 收集用户鼠标点击的页面坐标
export default function useMousePosition () {
  // 初始化坐标数据
  const x = ref(-1)
  const y = ref(-1)

  // 用于收集点击事件坐标的函数
  const updatePosition = (e: MouseEvent) => {
    x.value = e.pageX
    y.value = e.pageY
  }

  // 挂载后绑定点击监听
  onMounted(() => {
    document.addEventListener('click', updatePosition)
  })

  // 卸载前解绑点击监听
  onUnmounted(() => {
    document.removeEventListener('click', updatePosition)
  })

  return {x, y}
}
<template>
<div>
  <h2>x: {{x}}, y: {{y}}</h2>
</div>
</template>

<script>

import {
  ref
} from "vue"
// 引入hook函数
import useMousePosition from './hooks/useMousePosition'

export default {
  setup() {
    const {x, y} = useMousePosition()
    return {
      x,
      y,
    }
  }
}
</script>

三. 一些api的简易实现

1. reactiveshallowReactive


const reactiveHandler = {
  // 获取触发
  get(target, props){
     // 判断是否为reactive对象
    if(props === '_is_reactive') return true
    return Reflect.get(target, props)
  },
  // 修改或添加属性
  set(target, props, value){
    return Reflect.set(target, props, value)
  },
  deleteProperty(target, props){
    return Reflect.get(target, props)
  }
}

// reactReactive
const shallowReactive = function(target) {
  // 判断是否为对象
  if(target && typeof target === 'object') {
    return new Proxy(target, reactiveHandler)
  }

  // 为对象直接返回
  return target
}


// reactive
const reactive = function(target) {
  // 判断是否为对象
  if(target && typeof target === 'object') {

    if(Array.isArray(target)) {
      target.forEach((item, index)=>{
        target[index] = reactive(item)
      })
    } else {
      Object.keys(target).forEach(key=>{
        target[key] = reactive(target[key])
      })
    }

    return new Proxy(target, reactiveHandler)
  }

  // 为对象直接返回
  return target
}

2. readonlyshallowReadonly

const readonlyHandler = {
  // 获取触发
  get(target, props){
    // 判断是否为readonly对象
    if(props === '_is_readonly') return true
    return Reflect.get(target, props)
  },
  // 修改或添加属性
  set(target, props, value){
    console.log('只读')
  },
  deleteProperty(target, props){
    console.log('只读')
  }
}

// reactReactive
const shallowReadonly = function(target) {
  // 判断是否为对象
  if(target && typeof target === 'object') {
    return new Proxy(target, readonlyHandler)
  }

  // 为对象直接返回
  return target
}


// reactive
const readonly = function(target) {
  // 判断是否为对象
  if(target && typeof target === 'object') {

    if(Array.isArray(target)) {
      target.forEach((item, index)=>{
        target[index] = readonly(item)
      })
    } else {
      Object.keys(target).forEach(key=>{
        target[key] = readonly(target[key])
      })
    }

    return new Proxy(target, readonlyHandler)
  }

  // 为对象直接返回
  return target
}


3. shallowRefref

// 定义shallow函数
const shallowRef = function(target) {

  return {
    _value: target,
    get value() {
      return this._value
    },
    set value(val) {
      this._value = val
    }
  }
}

// 定义ref函数
const ref = function(target) {
  // 转换为响应式对象
  target = reactive(target)
  return {
    // 标识当前对象是否为ref
    _is_ref: true,
    _value: target,
    get value() {
      return this._value
    },
    set value(val) {
      this._value = val
    }
  }
}

4. isRef, isReactiveisReadonly

/* 
判断是否是ref对象
*/
function isRef(obj) {
  return obj && obj._is_ref
}

/* 
判断是否是reactive对象
*/
function isReactive(obj) {
  return obj && obj._is_reactive
}

/* 
判断是否是readonly对象
*/
function isReadonly(obj) {
  return obj && obj._is_readonly
}

/* 
是否是reactive或readonly产生的代理对象
*/
function isProxy (obj) {
  return isReactive(obj) || isReadonly(obj)
}

三. 其他组件

1. Fragment

  • 在Vue2中: 组件必须有一个根标签
  • 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
  • 好处: 减少标签层级, 减小内存占用
<template>
    <h2>内容一</h2>
    <h2>内容二</h2>
</template>

2. Suspense

在正确渲染组件之前进行一些异步请求是很常见的事。组件通常会在本地处理这种逻辑,绝大多数情况下这是非常完美的做法。 该 <suspense> 组件提供了另一个方案,允许将等待过程提升到组件树中处理,而不是在单个组件中。

<template>
  <Suspense>
    <template v-slot:default>
      // 组件
      <AsyncComp/>
    </template>

    <template v-slot:fallback>
      // 自定义等待状态
      <h1>LOADING...</h1>
    </template>
  </Suspense>
</template>

<suspense> 组件有两个插槽。它们都只接收一个直接子节点。default 插槽里的节点会尽可能展示出来。如果不能,则展示 fallback 插槽里的节点

3. 单文件组件

要使用这个语法,需要将 setup attribute 添加到 <script> 代码块上:

<script setup>
  console.log('hello script setup')
</script>

四. 实战:实现todolist

<template>
  <div>
    <input type="text" v-model="val">
    <button @click="addTodo">添加</button>
    <button @click="clearTodo">清理</button>
    <ul v-if="todos.length">
        <li v-for="(item, index) in todos">
            <input type="checkbox" v-model="item.done">
            <span :class="{done: item.done}">{{item.title}}</span>
            <span @click="removeTodo(index)">❌</span>
        </li>
    </ul>
    <div v-else>
        暂无数据
    </div>

    <div>
        <span>全选</span>
        <input type="checkbox" v-model="allDone">
        <span>{{active}}/{{all}}</span>
    </div>

  </div>
</template>

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

interface Todo {
    title: string,
    done: boolean
}

let val = ref<string>('')
let todos = ref<Todo[]>([
        {title: '吃饭', done: true},
        {title: '睡觉', done: false},
        {title: '打自己', done: false},
    ])

// 增加事件
function addTodo() {
    todos.value.push({
        title: val.value,
        done: false
    })
    val.value = ''
}

// 计算属性 computed
let all = computed<number>(()=>todos.value.length)
let active = computed<number>(()=>todos.value.filter(v=>v.done).length)
let allDone = computed<boolean>({
    get() {
        return active.value === todos.value.length
    },
    set(value: boolean) {
        todos.value.forEach(todo=>{
            todo.done = value
        })
    }
})

// 删除
function removeTodo(index: number) {
    todos.value.splice(index, 1)
}

// 清理
function clearTodo() {
    todos.value = todos.value.filter(v=>!v.done)
}
</script>
<style>
span.done {
    text-decoration: line-through;
    color:gray
}
</style>

写在最后

手写mini-vue3第一篇!未来可能会更新实现mini-vue3和js基础知识系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳

本文参考: vue3文档 视频教程