Vue3_学习总结

179 阅读9分钟

初识 Vue3

简单介绍

相关信息

  • Vue3支持 Vue2的大多数特性
  • 更好的支持 Typescript

性能提升

  • 使用 Proxy代替 defineProperty实现数据响应式
  • 重写虚拟 DOM的实现和 Tree-Shaking

新增特性

  • Composition(组合)API
  • setup
    • ref和 reactive
    • computed和 watch
    • 新的生命周期函数
    • provide与 inject
    • ...
  • 新组件
    • Fragment - 文档碎片
    • Teleport - 瞬移组件的位置
    • Suspense - 异步加载组件的 loading界面
  • 其它 API更新
    • 全局 API的修改
    • 将原来的全局 API转移到应用对象
    • 模板语法变化

创建 Vue3项目

使用 vue-cli创建

## 安装或者升级
npm install -g @vue/cli
## 保证 vue cli 版本在 4.5.0 以上
vue --version
## 创建项目
vue create my-project(项目名称)
  • 选择 Manually select features
  • 添加 TypeScript
  • 余下全部回车即可...

使用 vite创建

Vite 是一个 web 开发构建工具,由于其原生 ES 模块导入方式,可以实现闪电般的冷服务器启动。

  • 快速的冷启动,不需要等待打包操作
  • 即时的热模块更新,替换性能和模块数量的解耦让更新飞起
  • 真正的按需编译,不再等待整个应用编译完成,这是一个巨大的提升

通过在终端中运行以下命令,可以使用 Vite 快速构建 Vue 项目

npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

Composition API(常用部分)

setup 函数

  • 一个组件选项,在创建组件之前执行,
  • 一旦 props被解析,并作为组合式 API的入口点
  • 只在初始化时执行一次
  • 此函数如果返回对象,对象中的属性和方法,模板中可以直接使用

入参:

  • props
  • { attrs, slots, emit } = context

类型声明

interface Data {
  [key: string]: unknown
}

interface SetupContext {
  attrs: Data
  slots: Slots
  emit: (event: string, ...args: unknown[]) => void
}

function setup(props: Data, context: SetupContext): Data

ref

  • 作用:接受一个内部值并返回一个响应式且可变的 ref 对象(定义一个数据的响应式)
  • 语法:const refData = ref(val)
    • js中操作数据:xxx.value
    • 模板中操作数据:xxx,不需要 .value
  • 总结:一般用来定义一个基本类型的响应式数据

类型声明:

interface Ref<T> {
  value: T
}

function ref<T>(value: T): Ref<T>

示例

<template>
  <h2>{{count}}</h2>
  <hr>
  <button @click="handleUpdate">更新</button>
</template>

<script lang='ts>
/**
 * lang='ts':表示这里可以使用 ts代码
 * defineComponent函数:目的是定义一个组件,其内部可传入一个配置对象
 */
import { defineComponent, ref } from 'vue';

export default defineComponent({

  /* 使用vue3的composition API */
  setup () {

    // 定义响应式数据 ref对象
    const count = ref(1)
    console.log(count)

    // 更新响应式数据的函数
    function handleUpdate () {
      // alert('update')
      count.value = count.value + 1
    }
    
    return {
      count,
      handleUpdate
    }
  }
})
</script>

ref 的另一个作用:获取元素

  • 利用 ref函数获取组件中的标签元素
<template>
  <h2>ref的另一个作用:获取页面中的元素</h2>
  <hr />
  input:<input type="text" ref='inputRef' />
</template>

<script lang='ts'>
import { defineComponent, onMounted, ref } from 'vue'

export default defineComponent({
  name: 'App',
  components: {
  },
  setup () {
    // 当页面加载完毕后,页面中的文本框可以自动获取焦点
    // 默认为空,页面加载完毕后再获取文本框元素
    const inputRef = ref<HTMLElement | null>(null)
    console.log('inputRef', inputRef)
    // 在页面挂载后 onMounted,再获取指定的元素
    onMounted(() => {
      inputRef.value && inputRef.value.focus()
    })
    console.log('inputRef', inputRef)
    return {
    // 页面元素绑定此 ref
      inputRef
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

reactive

响应式转换是“深层”的——它影响所有嵌套 property。在基于 ES2015 Proxy 的实现中,返回的 proxy 是不等于原始对象的。建议只使用响应式 proxy,避免依赖原始对象。

  • 作用:接收一个普通对象然后返回该普通对象的响应式代理器对象
  • 语法:const user = reactive(obj)
<template>
  <h2>name: {{state.name}}</h2>
  <h2>age: {{state.age}}</h2>
  <h2>wife: {{state.friend}}</h2>
  <hr>
  <button @click="handleUpdate">更新</button>
</template>

<script lang='ts>
/**
 * lang='ts':表示这里可以使用 ts代码
 * defineComponent函数:目的是定义一个组件,其内部可传入一个配置对象
 */
import { defineComponent, reactive } from 'vue';

export default defineComponent({
  setup () {
    /* 
    定义响应式数据对象
    */
    const state = reactive({
      name: 'tom',
      age: 25,
      friend: {
        name: 'jake',
        age: 22
      },
    })
    console.log(state, state.wife)

    const handleUpdate = () => {
      state.name += '--'
      state.age += 1
      state.wife.name += '++'
      state.wife.age += 2
    }

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

setup细节

setup执行时机

  • 在 beforeCreate之前执行,此时 当前组件对象还没有创建
  • 此时 this是 undefined,不能通过 this来访问 data、computed、methods、props中的数据状态
  • 同时,在所有的 composition API相关回调函数中也都不可以使用

setup返回值

  • 一般都返回一个对象: 为模板提供数据, 也就是模板中可以直接使用此对象中的所有属性/方法
  • setup不能是一个async函数: 因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性数据

setup的参数

  • setup(props, context) / setup(props, { attrs, slots, emit })
  • props: 包含 props配置声明且传入了的所有属性的对象
  • attrs: 包含没有在props配置中声明的属性的对象, 相当于 this.$attrs
  • lots: 包含所有传入的插槽内容的对象, 相当于 this.$slots
  • emit: 用来分发自定义事件的函数, 相当于 this.$emit

示例

// App.vue
<template>
  <h2>App父级组件</h2>
  <h4>msg: {{msg}}</h4>
  <button @click="handleMsg('---')">update</button>
  <hr />
  <child :msg='msg' @handleMsg='handleMsg' />
</template>

<script lang='ts'>
import { defineComponent, ref } from 'vue'
import Child from './components/Child.vue'

export default defineComponent({
  name: 'App',
  components: {
    Child
  },
  setup (props, {attrs, emit, slots}) {
    const msg = ref('what are you no sha lei')
    function handleMsg (content = '123') {
      msg.value += content
    }
    console.log('props', props) // Proxy
    console.log('attrs', attrs) // Proxy
    console.log('emit', emit) // fn
    console.log('slots', slots) // Proxy
    return {
      msg,
      handleMsg
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

// ./components/Child.vue
<template>
  <h3>Child子级组件</h3>
  <h4>msg: {{msg}}</h4>
  <button @click='handleMsgChild'>触发父级事件</button>
</template>

<script lang='ts'>
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'Child',
  props: {
    msg: {
      default: () => ''
    }
  },
  // 父组件传入的 事件、方法
  emits: ['handleMsg'],

  beforeCreate () {
    console.log('beforeCreate', this)
  },

  setup (props, { attrs, emit, slots }) {
  
	console.log('setup', this)
  // setup () {
    console.log('props', props) // Proxy
    // console.log('attrs', attrs) // Proxy
    // attrs:获取没有在 props配置中声明的属性的对象,相当于 this.$attrs
    // console.log('emit', emit) // handleMsg
    // 获取分发事件
    // console.log('slots', slots) // Proxy
    
    // 子组件触发父组件的方法
    const handleMsgChild = () => {
      emit('handleMsg', '+++')
    }
    return {
      handleMsgChild
    }
  }
})
</script>

ref与 reactive 细节

  • 是 vue3中 composition API中两个最重要的响应式 API
  • ref用来处理基本类型数据
  • reactive用来处理对象(递归深度响应式)
  • 如果 ref的初始数据是 对象、数组,内部会自动将 数组、对象使用 reactive进行处理
  • ref内部:通过各 value属性添加 getter/setter来实现对数据的劫持
  • reactive内部:通过使用 Proxy来实现对对象内部所有数据的劫持,并通过Reflect操作对象内部数据
  • ref的数据操作:在 js中要使用 xxx.value,在模板中不需要直接使用 xxx(内部解析模板时会自动添加.value)
<template>
  <h2>App</h2>
  <p>m1: {{m1}}</p>
  <p>m2: {{m2}}</p>
  <p>m3: {{m3}}</p>
  <button @click="handleUpdate">更新</button>
</template>

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

export default defineComponent({

  setup () {
    const m1 = ref('abc')
    const m2 = reactive({x: 1, y: {z: 'abc'}})

    // 使用ref处理对象  ==> 对象会被自动reactive为proxy对象
    const m3 = ref({a1: 2, a2: {a3: 'abc'}})
    console.log(m1, m2, m3)
    console.log(m3.value.a2) // 也是一个proxy对象

    function handleUpdate () {
      m1.value += '--'
      m2.x += 1
      m2.y.z += '++'

      m3.value = {a1: 3, a2: {a3: 'abc---'}}
      m3.value.a2.a3 += '==' // reactive对对象进行了深度数据劫持
      console.log(m3.value.a2)
    }

    return {
      m1,
      m2,
      m3,
      handleUpdate
    }
  }
})
</script>

computed、watch和 watchEffect

computed函数:

  • 功能与 vue2.xxx版的 computed一致
  • 直传一个参数,表示 getter
  • 传入两个参数时,第一个参数表示 getter,第二个参数表示 setter
<template>
  <h2>计算属性和监视</h2>
  <hr />
  <fieldset>
    <legend>姓名操作</legend>
    姓氏:
    <input
      type="text"
      placeholder="请输入姓氏"
      v-model='user.firstName'
    />
    <br />
    名字:
    <input
      type="text"
      placeholder="请输入名字"
      v-model='user.lastName'
    />
  </fieldset>
  <fieldset>
    <legend>计算属性和监视的演示</legend>
    姓名:
    <input
      type="text"
      placeholder="显示姓名"
      v-model='fullName1'
    />
    <br />
    姓名:
    <input
      type="text"
      placeholder="显示姓名"
      v-model='fullName2'
    />
    <br />
</template>

<script lang='ts'>
import { computed, defineComponent, reactive, ref } from 'vue'

export default defineComponent({
  name: 'App',
  setup () {
    const user = reactive({
      firstName: '东方',
      lastName: '不败'
    })
    /**
     * vue3中的计算属性
     * 计算属性的函数中如果只传入一个回调函数,表示的是 get
     * 传入两个参数时,第一个参数表示 getter,第二个参数表示 setter
     */
     
    // 第一个姓名:
    // 返回的是一个 ref类型的对象
    const fullName1 = computed (
      () => {
        return user.firstName + '_' + user.lastName
      }
    )
    // console.log('fullName1', fullName1) // ComputedRefImpl
    
    // 第二个姓名:
    // 当传入一个对象时,参数分别为 get和 set方法
    const fullName2 = computed ({
      get () {
        return user.firstName + '_' + user.lastName
      },
      set (val: string) {
        console.log('val', val)
        const nameArr = val.split('_')
        user.firstName = nameArr[0]
        user.lastName = nameArr[1]
      }
    })

    return {
      user,
      fullName1,
      fullName2
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

watch函数:

  • 与watch配置功能、用法基本一致

watchEffect函数:

  • 不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
  • 默认初始时就会执行第一次, 从而可以收集需要监视的数据
  • 监视数据发生变化时回调
<template>
  <h2>计算属性和监视</h2>
  <hr />
  <fieldset>
    <legend>姓名操作</legend>
    姓氏:
    <input
      type="text"
      placeholder="请输入姓氏"
      v-model='user.firstName'
    />
    <br />
    名字:
    <input
      type="text"
      placeholder="请输入名字"
      v-model='user.lastName'
    />
  </fieldset>
  <fieldset>
    <legend>计算属性和监视的演示</legend>
    姓名:
    <input
      type="text"
      placeholder="显示姓名"
      v-model='fullName1'
    />
    <br />
    姓名:
    <input
      type="text"
      placeholder="显示姓名"
      v-model='fullName2'
    />
    <br />
    姓名:
    <input
      type="text"
      placeholder="显示姓名"
      v-model='fullName3'
    />
  </fieldset>
</template>

<script lang='ts'>
import { computed, defineComponent, reactive, ref, watch, watchEffect } from 'vue'

export default defineComponent({
  name: 'App',
  setup () {
    const user = reactive({
      firstName: '东方',
      lastName: '不败'
    })
    
    /**
     * vue3中的计算属性
     * 计算属性的函数中如果只传入一个回调函数,表示的是 get
     */
    // 第一个姓名:
    // 返回的是一个 ref类型的对象
    const fullName1 = computed (
      () => {
        return user.firstName + '_' + user.lastName
      }
    )
    // console.log('fullName1', fullName1) // ComputedRefImpl
    // 第二个姓名:
    // 当传入一个对象时,参数分别为 get和 set方法
    const fullName2 = computed ({
      get () {
        return user.firstName + '_' + user.lastName
      },
      set (val: string) {
        console.log('val', val)
        const nameArr = val.split('_')
        user.firstName = nameArr[0]
        user.lastName = nameArr[1]
      }
    })

    // 监视指定数据
    watch (
      user,
      ({firstName, lastName}) => {
      fullName3.value = firstName || '' + '_' + lastName || ''
      },
      {
        immediate: true, // 默认初始化时会执行一次
        deep: true // 深度监视
      }
    )
    
    // 第三个姓名:
    const fullName3 = ref('')
    
    // 监视,不需要配置参数 immediate就会在初始化时执行一次
    watchEffect (
       () => {
         fullName3.value = user.firstName + '_' + user.lastName
       }
    )
    // 监视 fullName的数据,改变 firstName和 lastName
    watchEffect (
      () => {
        const nameArr = fullName3.value.split('_')
        user.firstName = nameArr[0] || ''
        user.lastName = nameArr[1] || ''
      }
    )
    // watch (
    //   [user.firstName, user.lastName],
    //   () => {
    //     // 此处代码没有执行,因为 user.firstName和 user.lastName不是响应式的数据
    //     console.log('+++')
    //   }
    // )
    // 当我们需要 watch监视非响应式的数据时,
    // 非响应式数据需要改成回调的方式
    watch (
      [
        () => user.firstName,
        () => user.lastName
      ],
      () => {
        console.log('+++')
      }
    )
    return {
      user,
      fullName1,
      fullName2,
      fullName3
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

生命周期

Vue3 与 Vue2.x.. 版本生命周期对应关系

  • setup() - beforeCreate()
  • setup() - created()
  • onBeforeMount() - beforeMount()
  • onMounted() - mounted()
  • onBeforeUpdate() - beforeUpdate()
  • onUpdated() - updated()
  • onBeforeUnmount() - beforeDestroy()
  • onUnmounted() - destroyed()
  • onErrorCaptured() - errorCaptured()

注意:以上 vue3与 vue2对应的生命周期中,vue3的生命周期执行要早于 vue2

示例

<template>
  <h3>生命周期</h3>
  <div>456</div>
</template>

<script lang='ts'>
import { defineComponent, onBeforeMount, onBeforeUnmount, onMounted, onUnmounted } from 'vue'

export default defineComponent({
  name: 'Child',
  /**
   * vue2.xxx生命周期
   */
  beforeCreate () {
    console.log('beforeCreate')
  },
  created () {
    console.log('created')
  },
  mounted () {
    console.log('mounted')
  },
  beforeUnmount () {
    console.log('beforeUnmount')
  },
  // deprecated
  // beforeUnmount
  beforeDestroy () {
    console.log('beforeDestroy')
  },
  // unMounted
  destroyed () {
    console.log('destroyed')
  }
  /**
   * vue3.xxx生命周期
   * 总结:3.xxx中的生命周期钩子的执行要早于 2.xxx对应的钩子的执行
   */
  /**
   * hook:在不使用 class的情况下管理里边的状态数据,
   * 并且将逻辑思维抽取出来放在一个可复用的功能函数中
   */
  setup () {
    console.log('setup')
    onBeforeMount(() => {
      console.log('onBeforeMount')
    })
    onMounted(() => {
      console.log('onMounted')
    })
    onBeforeUnmount(() => {
      console.log('onBeforeUnmount')
    })
    onUnmounted(() => {
      console.log('onUnmounted')
    })
  }
})
</script>

<style>
</style>

自定义 hook函数

  • 使用 vue3的组合 API封装的可复用的功能函数
  • 自定义 hook函数的作用类似于 vue2中的 mixin技术
  • 自定义 hook函数更加清楚复用功能代码的来源,更清楚易懂

示例

// index.vue
<template>
  <h2>自定义hook函数</h2>
  <h4>x:{{x}}, y:{{y}}</h4>
  <hr />
</template>

<script lang='ts'>
import { defineComponent, ref, watch } from 'vue'
import useMousePosition from './hooks/useMousePosition'
import useUrlLoader from './hooks/useUrlLoader'

// 定义接口,约束对象类型
interface AddressData {
  id: number;
  address: string;
  distance: string;
}
interface ProductsData {
  id: string;
  title: string;
  price: number;
}

export default defineComponent({
  name: 'App',
  components: {
  },
  /**
   * 用户点击页面后,收集 xy坐标数据,使用hook实现
   */
  setup () {
    // 将方法逻辑封装到单独的文件中,进行复用
    const { x, y } = useMousePosition()
    const url = ref('url')
    const { data, loading, errorMsg } = useUrlLoader<AddressData>(url.value)
    const { data, loading, errorMsg } = useUrlLoader<ProductsData[]>(url.value)
    watch(data, () => {
      if (data.value) {
        console.log(data.value.length)
      }
    })
    return {
      x,
      y,
      data,
      loading,
      errorMsg
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

// useMousePosition.ts
import { onBeforeUnmount, onMounted, ref } from 'vue'

export default function () {
  const x = ref(-1)
  const y = ref(-1)
  const getMousePosition = (e: MouseEvent) => {
    x.value = e.pageX
    y.value = e.pageY
  }
  // 页面加载完毕后,再进行点击操作
  onMounted(() => {
    window.document.addEventListener('click', getMousePosition)
  })
  onBeforeUnmount(() => {
    window.document.removeEventListener('click', getMousePosition)
  })
  return {
    x,
    y
  }
}

// useUrlLoader.ts
import axios from 'axios'
import { ref } from 'vue'

// T 泛型
export default function <T>(url: string) {

  const data = ref<T | null>(null)
  const loading = ref(true)
  const errorMsg = ref(null)

  axios.get(url).then(res => {
      loading.value = false
      data.value = res.data
  })
  .catch(e => {
    loading.value = false
    errorMsg.value = e.message || '未知错误'
  })
  return {
    loading,
    data,
    errorMsg
  }
}

toRefs

  • 作用:将一个响应式对象转换成普通对象,并且该普通对象的每个 property都是一个 ref
  • 应用:主要用于 解构、合成函数返回响应式对象时,在不丢失响应式的情况下对返回的对象进行分解使用
<template>
  <h2>toRefs的使用</h2>
  <h4>name:{{name}}</h4>
  <h4>age:{{age}}</h4>
  <h4>name1:{{name1}}</h4>
  <h4>age1:{{age1}}</h4>
  <h4>toRefsState.name:{{toRefsState.name.value}}</h4>
  <h4>toRefsState.age:{{toRefsState.age.value}}</h4>
  <hr />
</template>

<script lang='ts'>
import { defineComponent, reactive, toRefs } from 'vue'

function useFeatureX () {
  const state = reactive({
    name1: 'Jery',
    age1: 12
  })
  return {
    // 对返回的对象使用 toRefs后进行解构处理
    ...toRefs(state)
  }
}

export default defineComponent({
  name: 'App',
  components: {
  },
  setup () {
    const state = reactive({
      name: 'Tom',
      age: 45,
      fim: {
        name: '123',
        info: '123'
      }
    })
    // toRefs
    const toRefsState = toRefs(state)
    // 也可以解构使用
    // const { name, age } = toRefs(state)
    console.log('toRefsState', toRefsState)
    /**
     * toRefsState
     * name: ObjectRefImpl
     */
    
    // hook与 toRefs的结合
    const { name1, age1 } = useFeatureX()

    // 定时器更新数据
    window.setTimeout(() => {
      console.log('setTimeout')
      // state.name += '+-+'
      toRefsState.name.value += '+'
      toRefsState.age.value += 1
      // 解构后的使用
      name1.value += '-'
      age1.value += 1
    }, 1000)
    return {
      // state
      // ...state 此方式处理,对象中解构出的属性不是响应式的了;只能使用 toRefs
      toRefsState,
      ...toRefsState,
      // 解构后的使用
      // name,
      // age
      name1,
      age1
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Composition API(其它)

shallowReactive & shallowRef 的使用

  • shallowReactive:只处理了对象内最外层 property的响应式
  • shallowRef:只处理了 value的响应式,不进行对象的 reactive处理
<template>
  <h2>shallowReactive和shallowRef</h2>
  <hr />
  <h4>m1:{{m1}}</h4>
  <h4>m2:{{m2}}</h4>
  <h4>m3:{{m3}}</h4>
  <h4>m4:{{m4}}</h4>
  <button @click='handleUpdate'>update</button>
</template>

<script lang='ts'>
import { defineComponent, reactive, ref, shallowReactive, shallowRef } from 'vue'

export default defineComponent({
  name: 'App',
  setup () {
    // 深度劫持
    const m1 = reactive({
      name: 'Tom',
      age: 20,
      car: {
        name: 'BMW',
        color: 'red'
      }
    })
    // 浅劫持
    const m2 = shallowReactive({
      name1: 'Tom',
      age1: 20,
      car1: {
        name1: 'BMW',
        color1: 'red'
      }
    })
    // 深度劫持
    const m3 = ref({
      name: 'Tom',
      age: 20,
      car: {
        name: 'BMW',
        color: 'red'
      }
    })
    // 浅劫持
    const m4 = shallowRef({
      name: 'Tom',
      age: 20,
      car: {
        name: 'BMW',
        color: 'red'
      }
    })
    console.log('m1', m1)
    console.log('m2', m2)
    console.log('m3', m3)
    console.log('m4', m4)
    const handleUpdate = () => {
      // m1.name += '+'
      // m1.age += 1
      // m1.car.name += '*'
      // m1.car.color += '-'
      // 这样写时,浅响应会变为深响应
      m2.name1 += '+'
      m2.age1 += 1
      // m2.car1.name1 += '*'
      // m2.car1.color1 += '-'
      // m3.value.name += '+'
      // m3.value.age += 1
      // m3.value.car.name += '*'
      // m3.value.car.color += '-'
      m4.value.name += '+'
      m4.value.age += 1
      // m4.value.car.name += '*'
      // m4.value.car.color += '-'
    }
    return {
      m1,
      m2,
      m3,
      m4,
      handleUpdate
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

readonly & shallowReadonly

readonly:

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

shallowReadonly

  • 浅层只读数据
  • 只处理对象(响应式或单纯对象)或 ref数据的 浅层 property为只读,但不执行嵌套对象的深度只读转换
<template>
  <h2>readonly和shallowReadonly</h2>
  <hr />
  <h4>state:{{state}}</h4>
  <!-- <h4>state2:{{state2}}</h4> -->
  <h4>state3:{{state3}}</h4>
  <button @click='handleUpdate'>update</button>
</template>

<script lang='ts'>
import { defineComponent, reactive, readonly, shallowReadonly } from 'vue'
// import { defineComponent, reactive, shallowReadonly } from 'vue'

interface StateInterface {
  name: string;
  age: number;
  car: {
    name: string;
    color: string;
  };
}

export default defineComponent({
  name: 'App',
  setup () {
    const state = reactive({
      name: 'Tom',
      age: 22,
      car: {
        name: 'BMW',
        color: 'yellow'
      }
    })
    // 只读的数据 -> 深度只读
    // const state2 = readonly<StateInterface>(state)
    // 只读的数据 -> 浅只读
    const state3 = shallowReadonly(state)

    const handleUpdate = () => {
      console.log('handleUpdate')
      // state2.name += '+' // error readonly
      // state2.car.name += '+' // error readonly
      // state3.name += '-' // error readonly
      state3.car.name += '*'
    }
    return {
      state,
      // state2,
      state3,
      handleUpdate
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

toRaw & markRaw

toRaw

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

markRaw

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

  • 使用场景:

    • 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue组件对象
    • 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能

实例

<template>
  <h2>toRaw和markRaw</h2>
  <hr />
  <h4>state:{{ state }}</h4>
  <button @click='handleToRaw'>toRawUpdate</button>
  <button @click='handleMarkRaw'>markRawUpdate</button>
</template>

<script lang='ts'>
// import { defineComponent, reactive, readonly, shallowReadonly } from 'vue'
import { defineComponent, markRaw, reactive, toRaw } from 'vue'

interface UserInfo {
  name: string;
  age: number;
  likes?: string[];
}

export default defineComponent({
  name: 'App',
  setup () {
    const state = reactive<UserInfo>({
      name: 'Tom',
      age: 25
    })
    const handleToRaw = () => {
      // 将代理对象变为普通对象,数据变化,界面不变
      const user = toRaw(state)
      user.name += '*'
    }
    const handleMarkRaw = () => {
      const likes = ['a', 'b', 'c']
      // markRaw标记的对象数据,从此以后不能变为代理对象
      state.likes = markRaw(likes) // likes数组不再是响应式了
      window.setTimeout(() => {
        state.likes[0] += '///'
      }, 1000)
    }
    return {
      state,
      handleToRaw,
      handleMarkRaw
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

toRef

  • 为源响应式对象上的某个属性创建一个 ref对象,二者内部操作的是同一个数据值,更新时二者是同步的
  • 区别 ref:拷贝来了一份新的数据值单独操作,修改时互不影响
  • 应用:当要将某个 prop的 ref传递给复合函数、子组件时,toRef很有用

实例


// App.vue
<template>
  <h2>toRef</h2>
  <hr />
  <h4>state:{{ state }}</h4>
  <h4>foo:{{ foo }}</h4>
  <h4>foo2:{{ foo2 }}</h4>
  <hr />
  <!-- 
    foo:是值传递
   -->
  <child :foo='foo' />
  <button @click='handleUpdate'>handleUpdate</button>
</template>

<script lang='ts'>
// import { defineComponent, reactive, readonly, shallowReadonly } from 'vue'
import { defineComponent, reactive, ref, toRef } from 'vue'

import Child from './components/Child.vue'

interface UserInfo {
  name: string;
  age: number;
  likes?: string[];
}

export default defineComponent({
  name: 'App',
  components: {
    Child
  },
  setup () {
    const state = reactive({
      foo: 1,
      bar: 2
    })
    /**
     * 为 响应式对象上的某个属性创建一个 ref对象,
     * 二者内部操作的是同一个数据值, 更新时二者是同步的
     */
    const foo = toRef(state, 'foo')

    /**
     * 区别ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
     */
    const foo2 = ref(state.foo)
    const handleUpdate = () => {
      /**
       * 以下二者内部操作的是同一个数据值, 更新时二者是同步的
       */
      state.foo++
      // foo.value++

      /**
       * 区别ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
       */
      // foo2.value++
    }
    return {
      state,
      foo,
      foo2,
      handleUpdate
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

// Child.vue
<template>
  <h3>Child</h3>
  <div>foo: {{foo}}</div>
  <div>length: {{length}}</div>
</template>

<script lang='ts'>
import { computed, defineComponent, toRef, Ref } from 'vue'

// import useFeatureX from '../hooks/useFeatureX'
/**
 * 也可以使用上边的钩子函数
 */
function useFeatureX (foo: Ref) {
  console.log('foo: ', foo)
  const len = computed(() => {
    return foo.value.toString().length
  })
  return len
}

export default defineComponent({
  name: 'Child',
  props: {
    /**
     * foo:是值传递
     */
    foo: {
      type: Number,
      required: true
    }
  },
  setup (props) {
    console.log('props: ', props)
    const length = useFeatureX(toRef(props, 'foo'))
    return {
      length
    }
  }
})
</script>

<style>
</style>

customRef

  • 创建一个自定义的 ref,并对其依赖项跟踪和更新触发 进行显式控制
  • 它需要一个工厂函数,该函数接收 track 和 trigger 函数作为参数,并应返回一个带有 get 和 set 的对象
  • 需求:使用 customRef实现 debounce的示例

实例

// App.vue
<template>
  <h2>customRef</h2>
  <hr />
  <input
    type="text"
    v-model='keyword'
    placeholder='search...'
  />
  <h4>keyword: {{ keyword }}</h4>

</template>

<script lang='ts'>
// import { defineComponent, reactive, readonly, shallowReadonly } from 'vue'
import { defineComponent, ref } from 'vue'
/**
 * hooks
 */
import useDebouncedRef from './hooks/useDebouncedRef'

export default defineComponent({
  name: 'App',
  components: {
  },
  setup () {
    // const keyword = ref('')
    const keyword = useDebouncedRef('', 500)
    // console.log('keyword: ', keyword)
    return {
      keyword
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

// './hooks/useDebouncedRef.ts'
import { customRef } from 'vue'

export default function<T> (value: T, delay = 200) {
  let timeId: number
  return customRef((track, trigger) => {
    return {
      // 返回数据
      get() {
        // 告诉 Vue追踪数据
        track()
        return value
      },
      // 修改数据
      set(newValue: T) {
        // console.log('newValue')
        /**
         * 设置定时器
         */
        window.clearTimeout(timeId)
        timeId = window.setTimeout(() => {
          // 新数据传给 value
          value = newValue
          // 告诉 Vue去触发界面更新
          trigger()
        }, delay)
      }
    }
  })
}

provide & inject

  • provide和inject提供依赖注入,功能类似 2.x 的provide/inject
  • 实现跨层级组件(祖孙)间通信

实例

// App.vue
<template>
  <h2>provide和 inject</h2>
  <hr />
  <p>当前颜色:{{ color }}</p>
  <button @click="color = 'red'">red</button>
  <button @click="color = 'blue'">blue</button>
  <button @click="color = 'green'">green</button>
  <son></son>
</template>

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

import Son from './components/Son.vue'

export default defineComponent({
  name: 'App',
  components: {
    Son
  },
  setup () {
    const color = ref('red')
    provide('color', color)
    return {
      color
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

// Son.vue
<template>
  <h3>子组件</h3>
  <hr />
  <grand-son></grand-son>
</template>

<script lang='ts'>
import { defineComponent } from 'vue'

import GrandSon from './GrandSon.vue'
export default defineComponent({
  name: 'Son',
  components: {
    GrandSon
  },
  setup () {
    return {
      //
    }
  }
})
</script>

// GrandSon.vue
<template>
  <h3>子孙组件</h3>
  <hr />
  <p :style="{color: color}">子孙组件:{{ color }}</p>
  <p :style="{color}">子孙组件:{{ color }}</p>
</template>

<script lang='ts'>
import { defineComponent, inject } from 'vue'
export default defineComponent({
  name: 'GrandSon',
  components: {
  },
  setup() {
    const color = inject('color')
    return {
      color
    }
  }
})
</script>

响应式数据的判断

  • isRef:检查一个值是否为一个 ref对象
  • isReactive:检查一个对象是否是由 reactive创建的响应式代理
  • isReadonly:检查一个对象是否是由 readonly创建的只读代理
  • isProxy:检查一个对象是否是由 reactive或 readonly方法创建的代理

实例

<template>
  <h2>is判断</h2>
  <hr />
</template>

<script lang='ts'>
import { defineComponent, ref, reactive, isReactive, isRef, isReadonly, readonly, isProxy } from 'vue'

export default defineComponent({
  name: 'App',
  components: {
  },
  setup () {
    // isRef:检查一个值是否为一个 ref对象
    // isReactive:检查一个对象是否是由 reactive创建的响应式代理
    // isReadonly:检查一个对象是否是由 readonly创建的只读代理
    // isProxy:检查一个对象是否是由 reactive或者 readonly方法创建的代理
    const isRef1 = ref(0)
    console.log('isref: ', isRef(isRef1))
    const isReactive1 = reactive({})
    console.log('isReactive: ', isReactive(isReactive1))
    console.log('isReadonly: ', isReadonly(readonly({})))
    console.log('isProxy: ', isProxy(isReactive1))
    console.log('isProxy: ', isProxy(readonly({})))
    return {
      //
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

其它新组合和 API

新组件

Fragment(片断)

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

Teleport(瞬移)

  • Teleport提供了一种干净的方法,让组件的 html在父组件界面外的特定标签(很可能是body)下插入显示
// ModalButton.vue
<template>
  <button @click="modalOpen = true">
      Open full screen modal! (With teleport!)
  </button>

  <teleport to="body">
    <div v-if="modalOpen" class="modal">
      <div>
        I'm a teleported modal! 
        (My parent is "body")
        <button @click="modalOpen = false">
          Close
        </button>
      </div>
    </div>
  </teleport>
</template>

<script>
import { ref } from 'vue'
export default {
  name: 'modal-button',
  setup () {
    const modalOpen = ref(false)
    return {
      modalOpen
    }
  }
}
</script>


<style>
.modal {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  background-color: rgba(0,0,0,.5);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.modal div {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: white;
  width: 300px;
  height: 300px;
  padding: 5px;
}
</style>

// App.vue
<template>
  <h2>App</h2>
  <modal-button></modal-button>
</template>

<script lang="ts">
import ModalButton from './ModalButton.vue'

export default {
  setup() {
    return {
    }
  },

  components: {
    ModalButton
  }
}
</script>

Suspense(不确定的)

  • 它们允许我们的应用程序在等待异步组件时渲染一些后备内容,可以让我们创建一个平滑的用户体验
// App.vue
<template>
  <h2>Suspense(不确定的):</h2>
  <Suspense>
    <!-- <template v-slot:default> -->
    <template #default>
      <!-- 渲染异步组件 -->
      <async-com></async-com>
    </template>
    <template v-slot:fallback>
      <!-- 临时展示组件 -->
      <h3>LOADING...</h3>
    </template>
  </Suspense>
</template>

<script lang='ts'>
import { defineAsyncComponent, defineComponent } from 'vue'
// import { defineComponent, reactive, shallowReadonly } from 'vue'

/**
 * Vue2中动态引入组件的写法:(在 Vue3中此写法不行)
 */
// const AsyncCom = () => import('./components/AsyncCom.vue')

/**
 * Vue3中动态引入组件的写法:defineAsyncComponent()
 */
// const AsyncCom = defineAsyncComponent(() => import('./components/AsyncCom.vue'))

// 静态引入组件
import AsyncCom from './components/AsyncCom.vue'

export default defineComponent({
  name: 'App',
  components: {
    AsyncCom
  },
  setup () {
    return {
     //
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

// AsyncCom.vue
<template>
  <h3>AsyncCom:异步组件</h3>
  <h3>{{msg}}</h3>
</template>

<script lang='ts'>
import axios from 'axios'
import { defineComponent } from "vue";
// import { defineComponent, reactive, shallowReadonly } from 'vue'

export default defineComponent({
  name: "AsyncCom",
  // setup() {
  //   // return {
  //   //  //
  //   // }
  // 方式一:
  //   return new Promise((resolve, reject) => {
  //     setTimeout(() => {
  //         resolve({
  //             msg: '一往情深,矢志不渝'
  //         })
  //     }, 2000);
  //   });
  // },
  // 方式二:推荐
  // setup () {
  //   return axios.get('/data/address.json').then(res => {
  //     return {
  //       data: res.data
  //     }
  //   })
  // },
  // 方式三:
  async setup () {
    const result = await axios.get('/data/address.json')
    return {
      data: result.data
    }
  }
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>