【Vue】Vue3 深度总结

Vue3

One Piece

Vue3 新特性

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

性能提升:

  • 打包大小减少 41%
  • 初次渲染快 55%,更新渲染快 133%
  • 内存减少 54%
  • 使用 Proxy 代替 defineProperty 实现数据响应式
  • 重写虚拟 DOM 的实现和 Tree-Shaking

新增特性(变动)

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

创建 Vue3 项目

使用 vue-cli 创建

文档:https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create

# 安装或者升级
npm install -g @vue/cli
# 保证 vue cli 版本在 4.5.0 以上
vue --version
# 创建项目
vue create my-project

使用 vite 创建

文档:https://v3.cn.vuejs.org/guide/installation.html

  • vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发
  • 它做到了本地快速开发启动, 在生产环境下基于 Rollup 打包
    • 快速的冷启动,不需要等待打包操作
    • 即时的热模块更新,替换性能和模块数量的解耦让更新飞起
    • 真正的按需编译,不再等待整个应用编译完成
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

Composition API

文档:https://composition-api.vuejs.org/zh/api.html

setup

  • 新的 option, 所有的组合 API 函数都在此使用, 只在初始化时执行一次
  • 函数如果返回对象, 对象中的属性或方法, 模板中可以直接使用

ref

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

<script>
  import { ref } from 'vue'
  export default {
    /* 在Vue3中依然可以使用data和methods配置, 但建议使用其新语法实现 */
    // data () {
    //   return {
    //     count: 0
    //   }
    // },
    // methods: {
    //   update () {
    //     this.count++
    //   }
    // }

    /* 使用vue3的composition API */
    setup() {
      // 定义响应式数据 ref对象
      const count = ref(1)
      console.log(count)

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

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

reactive

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

<script>
  /*
reactive:
    作用: 定义多个数据的响应式
    const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
    响应式转换是“深层的”:会影响对象内部所有嵌套的属性
    内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
*/
  import { reactive } from 'vue'
  export default {
    setup() {
      /*
    定义响应式数据对象
    */
      const state = reactive({
        name: 'tom',
        age: 25,
        wife: {
          name: 'marry',
          age: 22,
        },
      })
      console.log(state, state.wife)

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

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

比较 Vue2 与 Vue3 的响应式

Vue2 的响应式

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

Vue3 的响应式

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)
  },
})

proxy.name = 'tom'
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Proxy 与 Reflect</title>
  </head>
  <body>
    <script>
      const user = {
        name: 'John',
        age: 12,
      }

      /* 
    proxyUser是代理对象, user是被代理对象
    后面所有的操作都是通过代理对象来操作被代理对象内部属性
    */
      const proxyUser = new Proxy(user, {
        get(target, prop) {
          console.log('劫持get()', prop)
          return Reflect.get(target, prop)
        },

        set(target, prop, val) {
          console.log('劫持set()', prop, val)
          return Reflect.set(target, prop, val) // (2)
        },

        deleteProperty(target, prop) {
          console.log('劫持delete属性', prop)
          return Reflect.deleteProperty(target, prop)
        },
      })
      // 读取属性值
      console.log(proxyUser === user)
      console.log(proxyUser.name, proxyUser.age)
      // 设置属性值
      proxyUser.name = 'bob'
      proxyUser.age = 13
      console.log(user)
      // 添加属性
      proxyUser.sex = '男'
      console.log(user)
      // 删除属性
      delete proxyUser.sex
      console.log(user)
    </script>
  </body>
</html>

setup 细节

  • setup 执行的时机
    • 在 beforeCreate 之前执行(一次), 此时组件对象还没有创建
    • this 是 undefined, 不能通过 this 来访问 data/computed/methods/props
    • 其实所有的 composition API 相关回调函数中也都不可以
  • setup 的返回值
    • 一般都返回一个对象: 为模板提供数据, 也就是模板中可以直接使用此对象中的所有属性/方法
    • 返回对象中的属性会与 data 函数返回对象的属性合并成为组件对象的属性
    • 返回对象中的方法会与 methods 中的方法合并成功组件对象的方法
    • 如果有重名, setup 优先
  • 注意:
    • 一般不要混合使用: methods 中可以访问 setup 提供的属性和方法, 但在 setup 方法中不能访问 data 和 methods
    • 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('--')">更新</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>
<template>
  <div>
    <h3>{{ n }}</h3>
    <h3>{{ m }}</h3>

    <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',
    props: ['msg'],
    emits: ['fn'], // 可选的, 声明了更利于阅读, 且可以对分发的事件数据进行校验
    data() {
      console.log('data', this)
      return {
        // n: 1
      }
    },

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

    methods: {
      // update () {
      //   this.n++
      //   this.m++
      // }
    },

    // setup (props, 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() {
        // console.log('--', this)
        // this.n += 2
        // this.m += 2

        m.value += 2
        n.value += 2

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

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

reactive 与 ref-细节

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

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

  export default {
    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 update() {
        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,
        update,
      }
    },
  }
</script>

计算属性与监视

  • computed:
    • 与 computed 配置功能一致
    • 只有 getter
    • 有 getter 和 setter
  • watch:
    • 与 watch 配置功能一致
    • 监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
    • 默认初始时不执行回调, 但可以通过配置 immediate 为 true, 来指定初始时立即执行第一次
    • 通过配置 deep 为 true, 来指定深度监视
  • watchEffect:
    • 不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
    • 默认初始时就会执行第一次, 从而可以收集需要监视的数据
    • 监视数据发生变化时回调
<template>
  <h2>App</h2>
  fistName: <input v-model="user.firstName" /><br />
  lastName: <input v-model="user.lastName" /><br />
  fullName1: <input v-model="fullName1" /><br />
  fullName2: <input v-model="fullName2" /><br />
  fullName3: <input v-model="fullName3" /><br />
</template>

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

  export default {
    setup() {
      const user = reactive({
        firstName: 'A',
        lastName: 'B',
      })

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

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

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

      const fullName3 = ref('')

      /* 
    watchEffect: 监视所有回调中使用的数据
    */
      /* 
    watchEffect(() => {
      console.log('watchEffect')
      fullName3.value = user.firstName + '-' + user.lastName
    }) 
    */

      /* 
    使用watch的2个特性:
      深度监视
      初始化立即执行
    */
      watch(
        user,
        () => {
          fullName3.value = user.firstName + '-' + user.lastName
        },
        {
          immediate: true, // 是否初始化立即执行一次, 默认是false
          deep: true, // 是否是深度监视, 默认是false
        }
      )

      /* 
    watch一个数据
      默认在数据发生改变时执行回调
    */
      watch(fullName3, value => {
        console.log('watch')
        const names = value.split('-')
        user.firstName = names[0]
        user.lastName = names[1]
      })

      /* 
    watch多个数据: 
      使用数组来指定
      如果是ref对象, 直接指定
      如果是reactive对象中的属性,  必须通过函数来指定
    */
      watch([() => user.firstName, () => user.lastName, fullName3], values => {
        console.log('监视多个数据', values)
      })

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

生命周期

  • 与 Vue2 版本生命周期相对应的组合式 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')
    },

    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>
<template>
  <h2>App</h2>
  <button @click="isShow = !isShow">切换</button>
  <hr />
  <Child v-if="isShow" />
</template>

<script lang="ts">
  import Child from './Child.vue'
  export default {
    data() {
      return {
        isShow: true,
      }
    },

    components: {
      Child,
    },
  }
</script>

自定义 hook

  • 使用 Vue3 的组合 API 封装的可复用的功能函数
  • 自定义 hook 的作用类似于 vue2 中的 mixin 技术
  • 自定义 Hook 的优势: 很清楚复用功能代码的来源, 更清楚易懂
  • 收集用户鼠标点击的页面坐标:
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
自定义hook的作用类似于vue2中的mixin技术
自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂
*/
  import useMousePosition from './hooks/useMousePosition'

  export default {
    setup() {
      const { x, y } = useMousePosition()

      return {
        x,
        y,
      }
    },
  }
</script>
  • 利用 TS 泛型强化类型检查
  • 封装发 ajax 请求的 hook 函数:
import { ref } from 'vue'
import axios from 'axios'

/* 
使用axios发送异步ajax请求
*/
export default function useUrlLoader<T>(url: string) {
  const result = ref<T | null>(null)
  const loading = ref(true)
  const errorMsg = ref(null)

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

  return {
    loading,
    result,
    errorMsg,
  }
}
<template>
  <div class="about">
    <h2 v-if="loading">LOADING...</h2>
    <h2 v-else-if="errorMsg">{{ errorMsg }}</h2>
    <!-- <ul v-else>
    <li>id: {{result.id}}</li>
    <li>name: {{result.name}}</li>
    <li>distance: {{result.distance}}</li>
  </ul> -->

    <ul v-for="p in result" :key="p.id">
      <li>id: {{ p.id }}</li>
      <li>title: {{ p.title }}</li>
      <li>price: {{ p.price }}</li>
    </ul>
    <!-- <img v-if="result" :src="result[0].url" alt=""> -->
  </div>
</template>

<script lang="ts">
  import { watch } from 'vue'
  import useRequest from './hooks/useRequest'

  // 地址数据接口
  interface AddressResult {
    id: number
    name: string
    distance: string
  }

  // 产品数据接口
  interface ProductResult {
    id: string
    title: string
    price: number
  }

  export default {
    setup() {
      // const {loading, result, errorMsg} = useRequest<AddressResult>('/data/address.json')
      const { loading, result, errorMsg } = useRequest<ProductResult[]>('/data/products.json')

      watch(result, () => {
        if (result.value) {
          console.log(result.value.length) // 有提示
        }
      })

      return {
        loading,
        result,
        errorMsg,
      }
    },
  }
</script>

toRefs

  • 把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref
  • 应用: 当从合成函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用
  • 问题: reactive 对象取出的所有属性值都是非响应式的
  • 解决: 利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性
<template>
  <h2>App</h2>
  <h3>foo: {{ foo }}</h3>
  <h3>bar: {{ bar }}</h3>
  <h3>foo2: {{ foo2 }}</h3>
  <h3>bar2: {{ bar2 }}</h3>
</template>

<script lang="ts">
  import { reactive, toRefs } from 'vue'
  /*
toRefs:
  将响应式对象中所有属性包装为ref对象, 并返回包含这些ref对象的普通对象
  应用: 当从合成函数返回响应式对象时,toRefs 非常有用,
        这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用
*/
  export default {
    setup() {
      const state = reactive({
        foo: 'a',
        bar: 'b',
      })

      const stateAsRefs = toRefs(state)

      setTimeout(() => {
        state.foo += '++'
        state.bar += '++'
      }, 2000)

      const { foo2, bar2 } = useReatureX()

      return {
        // ...state,
        ...stateAsRefs,
        foo2,
        bar2,
      }
    },
  }

  function useReatureX() {
    const state = reactive({
      foo2: 'a',
      bar2: 'b',
    })

    setTimeout(() => {
      state.foo2 += '++'
      state.bar2 += '++'
    }, 2000)

    return toRefs(state)
  }
</script>

ref 获取元素

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

输入框自动获取焦点

<template>
  <h2>App</h2>
  <input type="text" />---
  <input type="text" ref="inputRef" />
</template>

<script lang="ts">
  import { onMounted, ref } from 'vue'
  /* 
ref获取元素: 利用ref函数获取组件中的标签元素
功能需求: 让输入框自动获取焦点
*/
  export default {
    setup() {
      const inputRef = ref<HTMLElement | null>(null)

      onMounted(() => {
        inputRef.value && inputRef.value.focus()
      })

      return {
        inputRef,
      }
    },
  }
</script>

shallowReactive 与 shallowRef

  • shallowReactive : 只处理了对象内最外层属性的响应式(也就是浅响应式)
  • shallowRef: 只处理了 value 的响应式, 不进行对象的 reactive 处理
  • 什么时候用浅响应式呢?
    • 一般情况下使用 ref 和 reactive 即可
    • 如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
    • 如果有一个对象数据, 后面会产生新的对象来替换 ===> shallowRef
<template>
  <h2>App</h2>

  <h3>m1: {{ m1 }}</h3>
  <h3>m2: {{ m2 }}</h3>
  <h3>m3: {{ m3 }}</h3>
  <h3>m4: {{ m4 }}</h3>

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

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

  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 } })
      const update = () => {
        // m1.b.c += 1
        // m2.b.c += 1
        // m3.value.a += 1
        m4.value.a += 1
      }
      return {
        m1,
        m2,
        m3,
        m4,
        update,
      }
    },
  }
</script>

readonly 与 shallowReadonly

  • readonly
    • 深度只读数据
    • 获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理
    • 只读代理是深层的:访问的任何嵌套 property 也是只读的
  • shallowReadonly
    • 浅只读数据
    • 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换
  • 应用场景
    • 在某些特定情况下, 可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除
<template>
  <h2>App</h2>
  <h3>{{ state }}</h3>
  <button @click="update">更新</button>
</template>

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

  export default {
    setup() {
      const state = reactive({
        a: 1,
        b: {
          c: 2,
        },
      })

      // const rState1 = readonly(state)
      const rState2 = shallowReadonly(state)

      const update = () => {
        // rState1.a++ // error
        // rState1.b.c++ // error

        // rState2.a++ // error
        rState2.b.c++
      }

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

toRaw 与 markRaw

  • toRaw
    • 返回由 reactivereadonly 方法转换成响应式代理的普通对象
    • 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新
  • markRaw
    • 标记一个对象,使其永远不会转换为代理。返回对象本身
    • 应用场景
      1. 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象
      2. 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能
<template>
  <h2>{{ state }}</h2>
  <button @click="testToRaw">测试toRaw</button>
  <button @click="testMarkRaw">测试markRaw</button>
</template>

<script lang="ts">
  /* 
toRaw: 得到reactive代理对象的目标数据对象
*/
  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
        state.likes = markRaw(likes) // likes数组就不再是响应式的了
        setTimeout(() => {
          state.likes[0] += '--'
        }, 1000)
      }

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

toRef

  • 为源响应式对象上的某个属性创建一个 ref 对象, 二者内部操作的是同一个数据值, 更新时二者是同步的
  • 区别 ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
  • 应用: 当要将 某个 prop 的 ref 传递给复合函数时,toRef 很有用
<template>
  <h2>App</h2>
  <p>{{ state }}</p>
  <p>{{ foo }}</p>
  <p>{{ foo2 }}</p>

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

  <Child :foo="foo" />
</template>

<script lang="ts">
  /*
toRef:
  为源响应式对象上的某个属性创建一个 ref对象, 二者内部操作的是同一个数据值, 更新时二者是同步的
  区别ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
  应用: 当要将某个 prop 的 ref 传递给复合函数时,toRef 很有用
*/

  import { reactive, toRef, ref } from 'vue'
  import Child from './Child.vue'

  export default {
    setup() {
      const state = reactive({
        foo: 1,
        bar: 2,
      })

      const foo = toRef(state, 'foo')
      const foo2 = ref(state.foo)

      const update = () => {
        state.foo++
        // foo.value++
        // foo2.value++  // foo和state中的数据不会更新
      }

      return {
        state,
        foo,
        foo2,
        update,
      }
    },

    components: {
      Child,
    },
  }
</script>
<template>
  <h2>Child</h2>
  <h3>{{ foo }}</h3>
  <h3>{{ length }}</h3>
</template>

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

  const component = defineComponent({
    props: {
      foo: {
        type: Number,
        require: true,
      },
    },

    setup(props, context) {
      const length = useFeatureX(toRef(props, 'foo'))

      return {
        length,
      }
    },
  })

  function useFeatureX(foo: Ref) {
    const lenth = computed(() => foo.value.length)

    return lenth
  }

  export default component
</script>

customRef

  • 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
  • 使用 customRef 实现 debounce 的示例
<template>
  <h2>App</h2>
  <input v-model="keyword" placeholder="搜索关键字" />
  <p>{{ keyword }}</p>
</template>

<script lang="ts">
  /*
customRef:
  创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制

需求: 
  使用 customRef 实现 debounce 的示例
*/

  import { ref, customRef } from 'vue'

  export default {
    setup() {
      const keyword = useDebouncedRef('', 500)
      console.log(keyword)
      return {
        keyword,
      }
    },
  }

  /* 
实现函数防抖的自定义ref
*/
  function useDebouncedRef<T>(value: T, delay = 200) {
    let timeout: number
    return customRef((track, trigger) => {
      return {
        get() {
          // 告诉Vue追踪数据
          track()
          return value
        },
        set(newValue: T) {
          clearTimeout(timeout)
          timeout = setTimeout(() => {
            value = newValue
            // 告诉Vue去触发界面更新
            trigger()
          }, delay)
        },
      }
    })
  }
</script>

provide 与 inject

  • provideinject 提供依赖注入,功能类似 2.x 的 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'
  /* 
- provide` 和 `inject` 提供依赖注入,功能类似 2.x 的 `provide/inject
- 实现跨层级组件(祖孙)间通信
*/

  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>

响应式数据的判断

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

手写组合 API

shallowReactive 与 reactive

const reactiveHandler = {
  get(target, key) {
    if (key === '_is_reactive') return true
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    const result = Reflect.set(target, key, value)
    console.log('数据已更新, 去更新界面')
    return result
  },
  deleteProperty(target, key) {
    const result = Reflect.deleteProperty(target, key)
    console.log('数据已删除, 去更新界面')
    return result
  },
}

/* 
自定义shallowReactive
*/
function shallowReactive(obj) {
  return new Proxy(obj, reactiveHandler)
}

/* 
自定义reactive
*/
function reactive(target) {
  if (target && typeof target === 'object') {
    if (target instanceof Array) {
      // 数组
      target.forEach((item, index) => {
        target[index] = reactive(item)
      })
    } else {
      // 对象
      Object.keys(target).forEach(key => {
        target[key] = reactive(target[key])
      })
    }

    const proxy = new Proxy(target, reactiveHandler)
    return proxy
  }
  return target
}

/* 测试自定义shallowReactive */
const proxy = shallowReactive({
  a: {
    b: 3,
  },
})
proxy.a = { b: 4 } // 劫持到了
proxy.a.b = 5 // 没有劫持到

/* 测试自定义reactive */
const obj = {
  a: 'abc',
  b: [{ x: 1 }],
  c: { x: [11] },
}

const proxy = reactive(obj)
console.log(proxy)
proxy.b[0].x += 1
proxy.c.x[0] += 1

shallowRef 与 ref

/*
自定义shallowRef
*/
function shallowRef(target) {
  const result = {
    _value: target, // 用来保存数据的内部属性
    _is_ref: true, // 用来标识是ref对象
    get value() {
      return this._value
    },
    set value(val) {
      this._value = val
      console.log('set value 数据已更新, 去更新界面')
    },
  }

  return result
}

/* 
自定义ref
*/
function ref(target) {
  if (target && typeof target === 'object') {
    target = reactive(target)
  }

  const result = {
    _value: target, // 用来保存数据的内部属性
    _is_ref: true, // 用来标识是ref对象
    get value() {
      return this._value
    },
    set value(val) {
      this._value = val
      console.log('set value 数据已更新, 去更新界面')
    },
  }

  return result
}

/* 测试自定义shallowRef */
const ref3 = shallowRef({
  a: 'abc',
})
ref3.value = 'xxx'
ref3.value.a = 'yyy'

/* 测试自定义ref */
const ref1 = ref(0)
const ref2 = ref({
  a: 'abc',
  b: [{ x: 1 }],
  c: { x: [11] },
})
ref1.value++
ref2.value.b[0].x++
console.log(ref1, ref2)

shallowReadonly 与 readonly

const readonlyHandler = {
  get(target, key) {
    if (key === '_is_readonly') return true

    return Reflect.get(target, key)
  },

  set() {
    console.warn('只读的, 不能修改')
    return true
  },

  deleteProperty() {
    console.warn('只读的, 不能删除')
    return true
  },
}

/* 
自定义shallowReadonly
*/
function shallowReadonly(obj) {
  return new Proxy(obj, readonlyHandler)
}

/* 
自定义readonly
*/
function readonly(target) {
  if (target && typeof target === 'object') {
    if (target instanceof Array) {
      // 数组
      target.forEach((item, index) => {
        target[index] = readonly(item)
      })
    } else {
      // 对象
      Object.keys(target).forEach(key => {
        target[key] = readonly(target[key])
      })
    }
    const proxy = new Proxy(target, readonlyHandler)

    return proxy
  }

  return target
}

/* 测试自定义readonly */
/* 测试自定义shallowReadonly */
const objReadOnly = readonly({
  a: {
    b: 1,
  },
})
const objReadOnly2 = shallowReadonly({
  a: {
    b: 1,
  },
})

objReadOnly.a = 1
objReadOnly.a.b = 2
objReadOnly2.a = 1
objReadOnly2.a.b = 2

isRef, isReactive 与 isReadonly

/* 
判断是否是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)
}

/* 测试判断函数 */
console.log(isReactive(reactive({})))
console.log(isRef(ref({})))
console.log(isReadonly(readonly({})))
console.log(isProxy(reactive({})))
console.log(isProxy(readonly({})))

Composition API VS Option API

Option API 的问题

  • 在传统的 Vue OptionsAPI 中,新增或者修改一个需求,就需要分别在 data,methods,computed 里修改 ,滚动条反复上下移动

使用 Compisition API

可以更加优雅的组织代码,函数。让相关功能的代码更加有序的组织在一起

新组件

Fragment(片断)

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

Teleport(瞬移)

  • Teleport 提供了一种干净的方法, 让组件的 html 在父组件界面外的特定标签(很可能是 body)下插入显示
<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, 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(不确定的)

  • 它们允许我们的应用程序在等待异步组件时渲染一些后备内容,可以让我们创建一个平滑的用户体验
<template>
  <Suspense>
    <template v-slot:default>
      <AsyncComp />
      <!-- <AsyncAddress/> -->
    </template>

    <template v-slot:fallback>
      <h1>LOADING...</h1>
    </template>
  </Suspense>
</template>

<script lang="ts">
  /* 
异步组件 + Suspense组件
*/
  // import AsyncComp from './AsyncComp.vue'
  import AsyncAddress from './AsyncAddress.vue'
  import { defineAsyncComponent } from 'vue'
  const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))
  export default {
    setup() {
      return {}
    },

    components: {
      AsyncComp,
      AsyncAddress,
    },
  }
</script>

AsyncComp.vue

<template>
  <h2>AsyncComp22</h2>
  <p>{{ msg }}</p>
</template>

<script lang="ts">
  export default {
    name: 'AsyncComp',
    setup() {
      // return new Promise((resolve, reject) => {
      //   setTimeout(() => {
      //     resolve({
      //       msg: 'abc'
      //     })
      //   }, 2000)
      // })
      return {
        msg: 'abc',
      }
    },
  }
</script>

AsyncAddress.vue

<template>
  <h2>{{ data }}</h2>
</template>

<script lang="ts">
  import axios from 'axios'
  export default {
    async setup() {
      const result = await axios.get('/data/address.json')
      return {
        data: result.data,
      }
    },
  }
</script>

其他 API

全新的全局 API

  • createApp()
  • defineProperty()
  • defineAsyncComponent()
  • nextTick()

将原来的全局 API 转移到应用对象

  • app.component()
  • app.config()
  • app.directive()
  • app.mount()
  • app.unmount()
  • app.use()

模板语法变化

  • v-model 的本质变化
    • prop:value -> modelValue
    • event:input -> update:modelValue
    • .sync修改符已移除, 由v-model代替
    • v-if优先v-for解析
posted @ 2020-12-25 16:03  [ABing]  阅读(526)  评论(0编辑  收藏  举报