【Vue】course_3

13.vue3过渡效果

学习:Vue3过渡效果开发(内置组件 、内置组件 )

Vue 提供了两个内置组件,可以帮助你制作基于状态变化的过渡和动画:

  • <Transition> 会在一个元素或组件进入和离开 DOM 时应用动画。本章节会介绍如何使用它。
  • <TransitionGroup> 会在一个 v-for 列表中的元素或组件被插入,移动,或移除时应用动画。

除了这两个组件,我们也可以通过其他技术手段来应用动画,比如切换 CSS class 或用状态绑定样式来驱动动画。

13.1 <Transition> 组件

<Transition> 是一个内置组件,这意味着它在任意别的组件中都可以被使用,无需注册。它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发:

  • v-if 所触发的切换
  • v-show 所触发的切换
  • 由特殊元素 <component> 切换的动态组件

以下是最基本用法的示例:

<button @click="show = !show">Toggle</button>
<Transition>
  <p v-if="show">hello</p>
</Transition>
/* 下面我们会解释这些 class 是做什么的 */
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}

<Transition> 仅支持单个元素或组件作为其插槽内容。如果内容是一个组件,这个组件必须仅有一个根元素。

12_transition/63_transition.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>动画</title>
  <style>
    .v-enter-from { opacity: 0; transform: translateX(100px);}
    .v-enter-active { transition: all 5s;}
    .v-enter-to { opacity: 1; transform: translateX(0);}
    .v-leave-from { opacity: 1; transform: translateX(0);}
    .v-leave-active { transition: all 5s;}
    .v-leave-to { opacity: 0; transform: translateX(-100px);}
  </style>
</head>
<body>
  <div id="app">
    <button @click="show = !show">切换</button>
    <!-- 
      v-enter-from  从哪里来
      v-enter-active 实现过渡效果
      v-enter-to   去哪里
      v-leave-from  从哪里离开
      v-leave-active 实现离开的过渡效果
      v-leave-to  离开最终状态
     -->
    <Transition>
      <p v-if="show">段落1</p>
    </Transition>
    <Transition>
      <p v-if="show">段落2</p>
    </Transition>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  Vue.createApp({
    data () {
      return {
        show: false
      }
    }
  }).mount('#app')
</script>
</html>

13.2 CSS 过渡 class

一共有 6 个应用于进入与离开过渡效果的 CSS class。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iH3vyOgK-1672137304634)(assets/transition-classes.f0f7b3c9.png)]

  1. v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
  2. v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
  3. v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。
  4. v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
  5. v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
  6. v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。

v-enter-activev-leave-active 给我们提供了为进入和离开动画指定不同速度曲线的能力

13.3 为过渡效果命名

我们可以给 <Transition> 组件传一个 name prop 来声明一个过渡效果名:

<Transition name="fade">
  ...
</Transition>

对于一个有名字的过渡效果,对它起作用的过渡 class 会以其名字而不是 v 作为前缀。比如,上方例子中被应用的 class 将会是 fade-enter-active 而不是 v-enter-active。这个“fade”过渡的 class 应该是这样:

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

可以结合 css3中的 transition以及animation实现动画效果

12_transition/64_transition_name.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>动画</title>
  <style>
    .fade-enter-from { opacity: 0; transform: translateX(100px);}
    .fade-enter-active { transition: all 5s;}
    .fade-enter-to { opacity: 1; transform: translateX(0);}
    .fade-leave-from { opacity: 1; transform: translateX(0);}
    .fade-leave-active { transition: all 5s;}
    .fade-leave-to { opacity: 0; transform: translateX(-100px);}

    .bounce-enter-active {
      animation: bounce-in 5s;
    }

    .bounce-leave-active {
      /* reverse 反向动画 */
      animation: bounce-in 5s reverse;
    }
    @keyframes bounce-in {
      0% {
        opacity: 0;
        transform: translateX(100px);
      }
      90% {
        opacity: 1;
        transform: translateX(100px);
      }
      100% {
        transform: translateX(0);
      }
    }
  </style>
</head>
<body>
  <div id="app">
    <button @click="show = !show">切换</button>
    <!-- 
      fade-enter-from  从哪里来
      fade-enter-active 实现过渡效果
      fade-enter-to   去哪里
      fade-leave-from  从哪里离开
      fade-leave-active 实现离开的过渡效果
      fade-leave-to  离开最终状态
     -->
    <Transition name="fade">
      <p v-if="show">段落1</p>
    </Transition>
    <Transition name="slide">
      <p v-if="show">段落2</p>
    </Transition>

    <!-- animation 动画 -->
    <Transition name="bounce">
      <p v-if="show" style="text-align: center;">
        Hello here is some bouncy text!
      </p>
    </Transition>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  Vue.createApp({
    data () {
      return {
        show: false
      }
    }
  }).mount('#app')
</script>
</html>

更多信息请参考:https://cn.vuejs.org/guide/built-ins/transition.html#the-transition-component

14.Teleport传送门

学习:开发modal组件(内置组件)

vue3 新增,模仿了 react中 Portal

<Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。

有时我们可能会遇到这样的场景:一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。

这类场景最常见的例子就是全屏的模态框。理想情况下,我们希望触发模态框的按钮和模态框本身是在同一个组件中,因为它们都与组件的开关状态有关。但这意味着该模态框将与按钮一起渲染在应用 DOM 结构里很深的地方。这会导致该模态框的 CSS 布局代码很难写。

13_teleport/65_model.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>模态框</title>
  <style>
    .modal {
      position: fixed;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      background-color: rgba(0, 0, 0, 0.4);
    }

    .modal .box {
      width: 50%;
      min-height: 400px;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background-color: #fff;
    }
  </style>
</head>
<body>
  <div id="app">
    <button @click="open=true">打开模态框</button>

    <!-- <my-modal v-if="open" @close="() => open=false"></my-modal> -->
    <!-- 审查元素查看效果 -->
    <teleport to="body">
      <my-modal v-if="open" @close="() => open=false"></my-modal>
    </teleport>
  </div>
</body>
<template id="modal">
  <div class="modal" @click.self="$emit('close')">
    <div class="box">
      <p>hello modal</p>
      <button @click="$emit('close')">关闭</button>
    </div>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Modal = {
    template: '#modal'
  }
  Vue.createApp({
    data () {
      return {
        open: false
      }
    },
    components: {
      MyModal: Modal
    }
  }).mount('#app')
</script>
</html>

通过给组件模板添加 <teleport >标签审查元素查看结果

当父组件有 overflow:hidden 或 z-index样式时,但需要子组件能够在视觉效果跳出其容器,例如对话框,悬浮卡以及提示框。

15.两个重要的实例API

15.1 $forceUpdate()

强制该组件重新渲染。

鉴于 Vue 的全自动响应性系统,这个功能应该很少会被用到。唯一可能需要它的情况是,你使用高阶响应式 API 显式创建了一个非响应式的组件状态。

vue2中对象以及数组的操作 数据改变视图没有更新,可以使用 forceUpdate 进行强制更新视图

14_api/66_forceupdate_vue2.html 不添加 forceUpdate 和添加之后对比查看效果

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>forceupdate</title>
</head>
<body>
  <div id="app">
    <button @click="change(2)">改变arr的数据,给第3个元素添加值</button>
    <button @click="clear">清空list数组</button>
    <button @click="changeObj">改变对象</button>
    <!-- v-for in  《===》 v-for of -->
    <ul>
      <li v-for="(item, index) of list" :key="index">{{ item }}</li>
    </ul>
    {{ arr[2].c }}
    <ul>
      <li v-for="(item, index) of arr" :key="index">{{ item.a }} - {{ item.b }}</li>
    </ul>

    {{ obj.foo }} - {{ obj.bar }}
  </div>
</body>
<script src="../lib/vue.js"></script>
<script>
  new Vue({
    data: {
      list: ['a', 'b', 'c'],
      arr: [
        { a: 1, b: 2 },
        { a: 10, b: 20 },
        { a: 100, b: 200 }
      ],
      obj: {
        foo: 'foo'
      }
    },
    methods: {
      change (index) {
        this.arr[index].c = 300
        console.log(this.arr)
        this.$forceUpdate()
      },
      clear () {
        // 将数组的长度设置为0 清空数组 
        this.list.length = 0
        console.log(this.list)
        this.$forceUpdate()
      },
      changeObj () {
        this.obj.bar = 'bar'
        console.log(this.obj)
        this.$forceUpdate()
      }
    }
  }).$mount('#app')
</script>
</html>

在vue2中如果只是为了数据改变更新视图,还可以使用 $set 将响应式数据中添加额外的属性也变为响应式

14_api/66_set_vue2.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>$set</title>
</head>
<body>
  <div id="app">
    <button @click="change(2)">改变arr的数据,给第3个元素添加值</button>
    <button @click="clear">清空list数组</button>
    <button @click="changeObj">改变对象</button>
    <!-- v-for in  《===》 v-for of -->
    <ul>
      <li v-for="(item, index) of list" :key="index">{{ item }}</li>
    </ul>
    {{ arr[2].c }}
    <ul>
      <li v-for="(item, index) of arr" :key="index">{{ item.a }} - {{ item.b }}</li>
    </ul>

    {{ obj.foo }} - {{ obj.bar }}
  </div>
</body>
<script src="../lib/vue.js"></script>
<script>
  new Vue({
    data: {
      list: ['a', 'b', 'c'],
      arr: [
        { a: 1, b: 2 },
        { a: 10, b: 20 },
        { a: 100, b: 200 }
      ],
      obj: {
        foo: 'foo'
      }
    },
    methods: {
      change (index) {
        // this.arr[index].c = 300
        // 给对象数组添加额外的属性,并且保持响应式
        this.$set(this.arr[index], 'c', 500)
      },
      clear () {
        // 将数组的长度设置为0 清空数组 
        // this.list.length = 0
        this.list = []
      },
      changeObj () {
        // this.obj.bar = 'bar'
        this.$set(this.obj, 'bar', 'bar')
      }
    }
  }).$mount('#app')
</script>
</html>

vue3中的forceUpdate 一般都用不到,在vue2中实现不了的以上案例,vue3全部支持

14_api/66_forceupdate_vue3.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>forceupdate</title>
</head>
<body>
    <div id="app">
       <button @click="change(2)">改变arr的数据,给第3个元素添加值</button>
       <button @click="clear">清空list数组</button>
       <button @click="changeObj">改变对象</button>
       <!-- v-for in  《===》 v-for of -->
       <ul>
         <li v-for="(item, index) of list" :key="index">{{ item }}</li>
       </ul>
       {{ arr[2].c }}
       <ul>
         <li v-for="(item, index) of arr" :key="index">{{ item.a }} - {{ item.b }}</li>
       </ul>
  
    {{ obj.foo }} - {{ obj.bar }}
  </div>
</body>
  <script src="../lib/vue.global.js"></script>
   <script>
     Vue.createApp({
       data () {
         return {
           list: ['a', 'b', 'c'],
           arr: [
             { a: 1, b: 2 },
             { a: 10, b: 20 },
             { a: 100, b: 200 }
           ],
           obj: {
             foo: 'foo'
           }
         }
       },
       methods: {
         change (index) {
           this.arr[index].c = 300
         },
         clear () {
           // 将数组的长度设置为0 清空数组 
           this.list.length = 0
         },
         changeObj () {
           this.obj.bar = 'bar'
        }
    }
    }).mount('#app')
</script>
</html>

vue3中没有了 $set

15.2 $nextTick()

绑定在实例上的 nextTick() 函数。

和全局版本的 nextTick() 的唯一区别就是组件传递给 this.$nextTick() 的回调函数会带上 this 上下文,其绑定了当前组件实例。

14_api/67_nextTick.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>nextTick</title>
</head>
<body>
  <div id="app">
    <div ref="oDiv">{{ count }}</div>
    <button @click="add">加1</button>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  Vue.createApp({
    data () {
      return {
        count: 10
      }
    },
    methods: {
      add () {
        this.count++
        console.log(1, this.count) // 11
        // 获取dom节点真实的值
        console.log(2, this.$refs.oDiv.innerHTML) // 10

        // 借助于 nextTick 函数 获取真实的DOM的值
        // 上啦加载 - 滚动条滚动到一定距离,加载数据
        // 瀑布流布局时,实时获取每一列的高度,将下一个数据插入到高度最低的那一列
        this.$nextTick(() => {
          console.log(3, this.$refs.oDiv.innerHTML) // 11
        })
      }
    }
  }).mount('#app')
</script>
</html>

16.渲染函数

16.1 h()

创建虚拟 DOM 节点 (vnode)。

Vue 提供了一个 h() 函数用于创建 vnodes:

import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  [
    /* children */
  ]
)

h()hyperscript 的简称——意思是“能生成 HTML (超文本标记语言) 的 JavaScript”。这个名字来源于许多虚拟 DOM 实现默认形成的约定。一个更准确的名称应该是 createVnode(),但当你需要多次使用渲染函数时,一个简短的名字会更省力。

h() 函数的使用方式非常的灵活:

创建原生元素:

// 除了类型必填以外,其他的参数都是可选的
h('div')  <div></div>
h('div', { id: 'foo' }) <div id="foo"></div>

// attribute 和 property 都能在 prop 中书写
// Vue 会自动将它们分配到正确的位置
h('div', { class: 'bar', innerHTML: 'hello' }) <div class="bar">hello</div>

// 类与样式可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [this.foo], style: { color: 'red' } }) <div :class="[this.foo]" :style="{color: 'red'}"></div>

// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} }) <div @click=""></div>

// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')<div id="foo" >hello</div>

// 没有 props 时可以省略不写
h('div', 'hello') <div>hello</div>
h('div', [h('span', 'hello')]) <div><span>hello</span></div>

// children 数组可以同时包含 vnodes 与字符串
h('div', ['hello', h('span', 'hello')])<div>hello <span>hello</span></div>

创建组件:

  const Header = {
    template: `<header>1头部</header>`
  }
  const Content = {
    template: `<div>1内容</div>`
  }
  const Footer = {
    template: `<footer>1底部</footer>`
  }
  // h也可以创建组件
  const Container = {
    render () {
      return [
        h('div', { class: 'box' }, [
          h(Header, { class: 'header' }),
          h(Content, { class: 'content' }),
          h(Footer, { class: 'footer' }),
        ])
      ]
    }
  }

15_render_fn/68_h.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>渲染函数</title>
</head>
<body>
  <div id="app">
    <!-- <my-container></my-container> -->
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp, h } = Vue
  const Header = {
    template: '<header>header</header>'
  }
  const Content = {
    template: '<div>content</div>'
  }
  const Footer = {
    template: '<footer>footer</footer>'
  }

  const Container = {
    render () {
      return [
        h('div', { class: 'box' }, [
          h(Header, { class: 'header' }),
          h(Content, { class: 'content' }),
          h(Footer, { class: 'footer' })
        ])
      ]
    }
  }

  createApp({
    // components: {
    //   MyContainer: Container
    // }
    render () {
      return h(Container)
    }
  }).mount('#app')
</script>
</html>

16.2 mergeProps()

合并多个 props 对象,用于处理含有特定的 props 参数的情况。

mergeProps() 支持以下特定 props 参数的处理,将它们合并成一个对象。

  • class
  • style
  • onXxx 事件监听器——多个同名的事件监听器将被合并到一个数组。

如果你不需要合并行为而是简单覆盖,可以使用原生 object spread 语法来代替

import { mergeProps } from 'vue'

const one = {
  class: 'foo',
  onClick: handlerA
}

const two = {
  class: { bar: true },
  onClick: handlerB
}

const merged = mergeProps(one, two)
/**
 {
   class: 'foo bar',
   onClick: [handlerA, handlerB]
 }
 */

15_render_fn/69_mergeProps.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>mergeProps</title>
</head>
<body>
  <div id="app">
    <div :class="propsObj.class" @click="propsObj.onClick">11111</div>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const {createApp, mergeProps} = Vue

  const one = {
    class: 'foo',
    onClick: () => console.log(1)
  }

  const two = {
    class: { bar: true },
    onClick: () => console.log(2)
  }

  const propsObj = mergeProps(one, two)

  console.log(propsObj)

  createApp({
    data () {
      return {
        propsObj
      }
    }
  }).mount('#app')
</script>
</html>

16.3 cloneVNode()

克隆一个 vnode。

返回一个克隆的 vnode,可在原有基础上添加一些额外的 prop。

Vnode 被认为是一旦创建就不能修改的,你不应该修改已创建的 vnode 的 prop,而应该附带不同的/额外的 prop 来克隆它。

Vnode 具有特殊的内部属性,因此克隆它并不像 object spread 一样简单。cloneVNode() 处理了大部分这样的内部逻辑。

import { h, cloneVNode } from 'vue'

const original = h('div')
const cloned = cloneVNode(original, { id: 'foo' })

15_render_fn/70_cloneVNode.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>cloneVnode</title>
</head>
<body>
  <div id="app"></div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp, cloneVNode, h } = Vue

  const  old = h('div')

  const cloned = cloneVNode(old, { id: 'box' })
  console.log('原始的', old)
  console.log('克隆的', cloned)

  // cloneVNode 不可以用来克隆组件,因为返回值不是一个组件
</script>
</html>

cloneVNode 函数不可以克隆组件,返回值不是一个组件

17.自定义过滤器

vue3移除了了自定义过滤器 https://v2.cn.vuejs.org/v2/guide/filters.html

页面展示 男 女 数据库中存 1 0,接口返回为 1 和 0,如何展示

国际化,中国 ¥ 美国 $

16_filter/71_vue2_filters.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue2过滤器</title>
</head>
<body>
  <div id="app">
    {{ sex }} - {{ sex | sexFilter }} <br/>
    {{price}} - {{ price | priceFilter('¥') }} - {{ price | priceFilter('$') }}
  </div>
</body>
<script src="../lib/vue.js"></script>
<script>
  // 全局过滤器
  Vue.filter('sexFilter', (val) => {
    return val === 1 ? '男' : '女'
  })
  new Vue({
    data: {
      sex: 1,
      price: 100
    },
    // 局部过滤器
    filters: {
      priceFilter (val, type) { // type ¥  $
        return type + val
      }
    }
  }).$mount('#app')
</script>
</html>

vue3 没有过滤器

16_filter/72_change_filters.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue2过滤器</title>
</head>
<body>
    <div id="app">
       {{ sex }} - {{ newSex }} <br/>
      {{price}} - {{ zhPrice }} - {{ enPrice }}
  </div>
</body>
<script src="../lib/vue.js"></script>
  <script>
     new Vue({
       data: {
         sex: 1,
         price: 100
       },
       computed: {
         newSex () {
           return this.sex === 1 ? '男' : '女'
         },
         zhPrice () {
           return '¥' + this.price
         },
         enPrice () {
           return '$' + this.price
         }
       }
     }).$mount('#app')
  </script>
</html>

18 混入mixins

一个包含组件选项对象的数组,这些选项都将被混入到当前组件的实例中。

mixins 选项接受一个 mixin 对象数组。这些 mixin 对象可以像普通的实例对象一样包含实例选项,它们将使用一定的选项合并逻辑与最终的选项进行合并。举例来说,如果你的 mixin 包含了一个 created 钩子,而组件自身也有一个,那么这两个函数都会被调用。

Mixin 钩子的调用顺序与提供它们的选项顺序相同,且会在组件自身的钩子前被调用。

在 Vue 2 中,mixins 是创建可重用组件逻辑的主要方式。尽管在 Vue 3 中保留了 mixins 支持,但对于组件间的逻辑复用,Composition API 是现在更推荐的方式。

17_mixin/73_mixins.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>混入</title>
</head>
<body>
  <div id="app">
    <my-child1></my-child1>
    <hr/>
    <my-child2></my-child2>
  </div>
</body>
<template id="child1">
  <div>
    <h1>child1组件</h1>
    {{ count }} <button @click="add">加1</button>
  </div>
</template>
<template id="child2">
  <div>
    <h1>child2组件</h1>
    {{ count }} <button @click="add">加1</button>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp } = Vue
  // 除了生命周期钩子函数外,所有的代码都以组件为优先
  // 生命周期的钩子函数,先执行混入中的,后执行组件的
  const MyMixin = {
    data () {
      return {
        count: 10
      }
    },
    methods: {
      add () {
        this.count++
      }
    },
    mounted () {
      console.log('mixin', this.count)
    }
  }

  const Child1 = {
    template: '#child1',
    mixins: [MyMixin], // 相当于把MyMixin的数据完全拷贝过来了
    data () {
      return {
        count: 20
      }
    },
    methods: {
      add () {
        this.count += 10
      }
    },
    mounted () {
      console.log('child1', this.count)
    }
  }
  const Child2 = {
    template: '#child2',
    mixins: [MyMixin],
    data () {
      return {
        count: 30
      }
    },
    methods: {
      add () {
        this.count += 15
      }
    },
    mounted () {
      console.log('child2', this.count)
    }
  }

  createApp({
    components: {
      MyChild1: Child1,
      MyChild2: Child2
    }
  }).mount('#app')
</script>
</html>

除了生命周期钩子函数外,所有的代码都以组件为优先

生命周期钩子函数先执行 混入的,后执行组件的

三、vue3组合式API

1、组合式API

1.1 什么是组合式API

组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:

  • 响应式 API:例如 ref()reactive(),使我们可以直接创建响应式状态、计算属性和侦听器。
  • 生命周期钩子:例如 onMounted()onUnmounted(),使我们可以在组件各个生命周期阶段添加逻辑。
  • 依赖注入:例如 provide()inject(),使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。

组合式 API 是 Vue 3 及 Vue 2.7 的内置功能。对于更老的 Vue 2 版本,可以使用官方维护的插件 @vue/composition-api。在 Vue 3 中,组合式 API 基本上都会配合 <script setup> 语法在单文件组件中使用。

1.2 为什么使用它

1.2.1 更好的逻辑复用#

组合式 API 最基本的优势是它使我们能够通过组合函数来实现更加简洁高效的逻辑复用。在选项式 API 中我们主要的逻辑复用机制是 mixins,而组合式 API 解决了 mixins 的所有缺陷

组合式 API 提供的逻辑复用能力孵化了一些非常棒的社区项目,比如 VueUse,一个不断成长的工具型组合式函数集合。组合式 API 还为其他第三方状态管理库与 Vue 的响应式系统之间的集成提供了一套简洁清晰的机制,例如 RxJS

1.2.2更灵活的代码组织#

许多用户喜欢选项式 API 的原因是因为它在默认情况下就能够让人写出有组织的代码:大部分代码都自然地被放进了对应的选项里。然而,选项式 API 在单个组件的逻辑复杂到一定程度时,会面临一些无法忽视的限制。这些限制主要体现在需要处理多个逻辑关注点的组件中,这是我们在许多 Vue 2 的实际案例中所观察到的。

我们以 Vue CLI GUI 中的文件浏览器组件为例:这个组件承担了以下几个逻辑关注点:

  • 追踪当前文件夹的状态,展示其内容
  • 处理文件夹的相关操作 (打开、关闭和刷新)
  • 支持创建新文件夹
  • 可以切换到只展示收藏的文件夹
  • 可以开启对隐藏文件夹的展示
  • 处理当前工作目录中的变更

这个组件最原始的版本是由选项式 API 写成的。如果我们为相同的逻辑关注点标上一种颜色,那将会是这样:

folder component before

你可以看到,处理相同逻辑关注点的代码被强制拆分在了不同的选项中,位于文件的不同部分。在一个几百行的大组件中,要读懂代码中的一个逻辑关注点,需要在文件中反复上下滚动,这并不理想。另外,如果我们想要将一个逻辑关注点抽取重构到一个可复用的工具函数中,需要从文件的多个不同部分找到所需的正确片段。

而如果用组合式 API 重构这个组件,将会变成下面右边这样:

重构后的文件夹组件

现在与同一个逻辑关注点相关的代码被归为了一组:我们无需再为了一个逻辑关注点在不同的选项块间来回滚动切换。此外,我们现在可以很轻松地将这一组代码移动到一个外部文件中,不再需要为了抽象而重新组织代码,大大降低了重构成本,这在长期维护的大型项目中非常关键。

1.2.3 更好的类型推导#

近几年来,越来越多的开发者开始使用 TypeScript 书写更健壮可靠的代码,TypeScript 还提供了非常好的 IDE 开发支持。然而选项式 API 是在 2013 年被设计出来的,那时并没有把类型推导考虑进去,因此我们不得不做了一些复杂到夸张的类型体操才实现了对选项式 API 的类型推导。但尽管做了这么多的努力,选项式 API 的类型推导在处理 mixins 和依赖注入类型时依然不甚理想。

因此,很多想要搭配 TS 使用 Vue 的开发者采用了由 vue-class-component 提供的 Class API。然而,基于 Class 的 API 非常依赖 ES 装饰器,在 2019 年我们开始开发 Vue 3 时,它仍是一个仅处于 stage 2 的语言功能。我们认为基于一个不稳定的语言提案去设计框架的核心 API 风险实在太大了,因此没有继续向 Class API 的方向发展。在那之后装饰器提案果然又发生了很大的变动,在 2022 年才终于到达 stage 3。另一个问题是,基于 Class 的 API 和选项式 API 在逻辑复用和代码组织方面存在相同的限制。

相比之下,组合式 API 主要利用基本的变量和函数,它们本身就是类型友好的。用组合式 API 重写的代码可以享受到完整的类型推导,不需要书写太多类型标注。大多数时候,用 TypeScript 书写的组合式 API 代码和用 JavaScript 写都差不太多!这也让许多纯 JavaScript 用户也能从 IDE 中享受到部分类型推导功能。

1.2.4 更小的生产包体积#

搭配 <script setup> 使用组合式 API 比等价情况下的选项式 API 更高效,对代码压缩也更友好。这是由于 <script setup> 形式书写的组件模板被编译为了一个内联函数,和 <script setup> 中的代码位于同一作用域。不像选项式 API 需要依赖 this 上下文对象访问属性,被编译的模板可以直接访问 <script setup> 中定义的变量,无需一个代码实例从中代理。这对代码压缩更友好,因为本地变量的名字可以被压缩,但对象的属性名则不能。

1.3 第一个组合式API的例子

18_composition/74_composition.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>第一个组合式API案例</title>
</head>
<body>
  <div id="app">
    <button @click="add">点击了{{ count }}次</button>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp, ref, onMounted, onUpdated } = Vue

  createApp({
    setup () { // 组合式API标识
      // 定义初始化数据,使用  ref 函数
      const count = ref(10) // 定义了初始化 数据 count 的数值为10

      const add = () => {
        console.log(count)
        count.value += 1 // ref 定义的初始值,修改状态用其 value 属性
      }
      // 生命周期钩子函数的使用
      onMounted(() => { // mounted
        document.title = `点击次数为${count.value}`
      })

      onUpdated(() => { // updated
        document.title = `点击次数为${count.value}`
      })

      // 必须要有返回值,返回数据以及相应的事件
      return {
        count,
        add
      }
    }
  }).mount('#app')
</script>
</html>

体验提取公共部分

18_composition/75_composition_hooks.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第一个组合式API案例</title>
</head>
<body>
    <div id="app">
       <button @click="add">点击了{{ count }}次</button>
    </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
    const { createApp, ref, onMounted, onUpdated } = Vue

    // 自定义hooks 一般以 use 开头
     const useCount = () => {
       const count = ref(10) // 定义了初始化 数据 count 的数值为10
   
       const add = () => {
         console.log(count)
         count.value += 1 // ref 定义的初始值,修改状态用其 value 属性
       }
   
       return {
         count, add
       }
    }

    const useTitle = (count) => {
       // 生命周期钩子函数的使用
       onMounted(() => { // mounted
         document.title = `点击次数为${count.value}`
       })
   
    		onUpdated(() => { // updated
         document.title = `点击次数为${count.value}`
       })
     }
    
    createApp({
       setup () { // 组合式API标识
         const { count, add } = useCount()
            
         useTitle(count)

         // 必须要有返回值,返回数据以及相应的事件
         return {
           count,
           add
      }
       }
    }).mount('#app')
</script>
  </html>

2、setup()函数

setup() 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:

  1. 需要在非单文件组件中使用组合式 API 时。
  2. 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。

其他情况下,都应优先使用 <script setup> 语法。

2.1 基本使用

我们可以使用响应式 API 来声明响应式的状态,在 setup() 函数中返回的对象会暴露给模板和组件实例。其它的选项也可以通过组件实例来获取 setup() 暴露的属性

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    // 返回值会暴露给模板和其他的选项式 API 钩子
    return {
      count
    }
  },

  mounted() {
    console.log(this.count) // 0
  }
}
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

请注意在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。

setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。

18_composition/76_composition_setup_base.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>76_setup的基本使用</title>
</head>
<body>
  <div id="app">
    <!-- 0 -->
    {{ count }}
  </div>
</body>
<script src="lib/vue.global.js"></script>
<script>
  const { createApp, ref, onMounted, onUpdated } = Vue

  const app = createApp({
    setup () { // 组合式API
      // 创建了响应式变量
      const count = ref(0)
     
      // 返回值会暴露给模板和其他的选项式 API 钩子
      return {
        count
      }
    },
    data () { // 虽然设置了同名的变量,但是显示的是 组合式API中的数据
      return {
        count: 100
      }
    },
    mounted () {
      console.log(this.count) // 0
    }
  })

  app.mount('#app')
</script>
</html>

2.2 访问 Prop

setup 函数的第一个参数是组件的 props。和标准的组件一致,一个 setup 函数的 props 是响应式的,并且会在传入新的 props 时同步更新。

{
  props: {
    title: String,
    count: Number
  },
  setup(props) {
    console.log(props.title)
    console.log(props.count)
  }
}

请注意如果你解构了 props 对象,解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx 的形式来使用其中的 props。

如果你确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs()toRef() 这两个工具函数:

{
  setup(props) {
    // 将 `props` 转为一个其中全是 ref 的对象,然后解构
    const { title } = toRefs(props)
    // `title` 是一个追踪着 `props.title` 的 ref
    console.log(title.value)

    // 或者,将 `props` 的单个属性转为一个 ref
    const title = toRef(props, 'title')
  }
}

18_composition/77_composition_setup_props.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>访问props</title>
</head>
<body>
  <div id="app">
    {{ count }} <button @click="count++">加1</button>
    <my-com :count="count"></my-com>
  </div>
</body>
<template id="com">
  <div>
    <h1>子组件</h1>
    {{ count }} -- {{ doubleCount }}
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp, ref, computed, toRef } = Vue

  const Com = {
    template: '#com',
    props: {
      count: Number
    },
    mounted () { // 在选项式API 可以借助this 访问父组件传递过来的数据
      console.log('test', this.count)
    },
    // 在组合式API中, 不能使用this,因为this指向了 Window
    setup (props) {
      // 如果想要访问到父组件传递给子组件的数据,需要通过 props 参数来访问
      console.log(props)

      // const doubleCount = props.count * 2 // ❌
      // const doubleCount = computed(() => props.count * 2) // ✅


      // 使用props内数据时,切记不要解构值,解构后 会丢失响应式
      // const { count } = props // 模版中的 doubleCount 始终为0,
      // const doubleCount = computed(() => count * 2) // ❌

      // toRef 可以保持响应式
      const count = toRef(props, 'count')
      const doubleCount = computed(() => count.value * 2) // ✅


      return {
        doubleCount
      }
    }
  }

  createApp({
    components: {
      MyCom: Com
    },
    setup () {
      const count = ref(0)

      return {
        count
      }
    }
  }).mount('#app')
</script>
</html>

2.3 Setup的上下文

传入 setup 函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup 中可能会用到的值:

{
  setup(props, context) {
    // 透传 Attributes(非响应式的对象,等价于 $attrs)
    console.log(context.attrs)

    // 插槽(非响应式的对象,等价于 $slots)
    console.log(context.slots)

    // 触发事件(函数,等价于 $emit)
    console.log(context.emit)

    // 暴露公共属性(函数)
    console.log(context.expose)
  }
}

该上下文对象是非响应式的,可以安全地解构:

{
  setup(props, { attrs, slots, emit, expose }) {
    ...
  }
}

attrsslots 都是有状态的对象,它们总是会随着组件自身的更新而更新。这意味着你应当避免解构它们,并始终通过 attrs.xslots.x 的形式使用其中的属性。此外还需注意,和 props 不同,attrsslots 的属性都不是响应式的。如果你想要基于 attrsslots 的改变来执行副作用,那么你应该在 onBeforeUpdate 生命周期钩子中编写相关逻辑。

expose 函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose 函数暴露出的内容

18_composition/78_composition_setup_context.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>setup上下文对象</title>
</head>
<body>
  <div id="app">
    <my-com ref="child" class="active" style="color: red" id="box" msg="hello msg" @my-event="getData">
      <template #header>
        <header>header</header>
      </template>
      <div>content</div>
      <template #footer>
        <footer>footer</footer>
      </template>
    </my-com>
  </div>
</body>
<template id="com">
  <div>
    <h1>子组件</h1>
    <button @click="sendData">发送数据</button>
    <slot name="header"></slot>
    <slot></slot>
    <slot name="footer"></slot>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp, ref, onMounted } = Vue

  const Com = {
    template: '#com',
    setup (props, context) {
      // {class: 'active', style: {…}, id: 'box', msg: 'hello msg', onMyEvent: }
      console.log(context.attrs)
      // { header: fn, default: fn, footer: fn}
      console.log(context.slots)

      const sendData = () => {
        // 选项式API中 使用的是 this.$emit('my-event', 参数)
        context.emit('my-event', 1000)
      }

      const a = ref(1)
      const b = ref(2)
      const c = ref(3)

      const fn = () => {
        a.value = 100
      }

      context.expose({ // 父组件通过 ref 获取到子组件实例时,可以访问到的内容
        a, b, fn
      })

      return {
        sendData
      }
    }
  }

  createApp({
    components: {
      MyCom: Com
    },
    setup () {
      const getData = (val) => {
        console.log(val) // 1000
      }
      const child = ref() // child 就是模版中ref="child"
      onMounted(() => {
        console.log('child', child)
        console.log('a', child.value.a) // 1
        console.log('b', child.value.b) // 2
        child.value.fn()
        console.log('a', child.value.a) // 100
      })
      return {
        child,
        getData
      }
    }
  }).mount('#app')
</script>
</html>

在父组件通过ref获取子组件的实例的属性和方法的需求中,需要注意:

1.如果子组件是 选项式API组件,基本不需要做任何操作

2.如果子组件是 组合式API组件,需要通过 context.expose 暴露给父组件需要使用的属性和方法

3.如果父组件使用 选项式API, 可以通过 this.$refs.refName 访问到子组件想要你看到的属性和方法

4.如果父组件使用 组合式API,需要在setup中先创建 refName,然后再访问子组件想要你看到的属性和方法(const refName = ref() refName.value.X)

2.4 与渲染函数一起使用

setup 也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态:

{
  setup() {
    const count = ref(0)
    return () => h('div', count.value)
  }
}

返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说,这样没有问题,但如果我们想通过模板引用将这个组件的方法暴露给父组件,那就有问题

我们可以通过调用 expose() 解决这个问题:

{
  setup(props, { expose }) {
    const count = ref(0)
    const increment = () => ++count.value

    expose({
      increment
    })

    return () => h('div', count.value)
  }
}

18_composition/79_composition_setup_render_function.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>渲染函数</title>
</head>
<body>
  <div id="app">
    <button @click='add'>加1</button>
    <my-com ref="child" ></my-com>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp, ref, onMounted, h } = Vue

  const Com = {
    setup (props, context) {
      const count = ref(0)

      const increment = () => {
        count.value += 1
      }

      context.expose({
        increment
      })

      return () => h('div', { class: 'box' }, count.value)
    }
  }

  createApp({
    components: {
      MyCom: Com
    },
    setup () {
      const child = ref()

      const add = () => {
        child.value.increment()
      }
      return {
        child,
        add
      }
    }
  }).mount('#app')
</script>
</html>

3、响应式核心

3.1ref()

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

ref 对象是可更改的,也就是说你可以为 .value 赋予新的值。它也是响应式的,即所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。

如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。

将一个 ref 赋值给为一个 reactive 属性时,该 ref 会被自动解包

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

18_composition/80_composition_ref.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ref</title>
</head>
<body>
  <div id="app">
    count: {{ count }}
    <button @click="add">加1</button>

    <br/>

    state.count: {{ state.count }}
    <button @click="increment">加1</button>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp, ref, reactive } = Vue
  // ref number string boolean 数组
  // reactive object
  createApp({
    setup () {
      // 1.ref 对象的值是可以更改的,也就是说你可以为 '.value' 赋予新的值
      const count = ref(0)
      const add = () => {
        count.value++
      }

      // 2.如果将ref赋值给 一个 reactive 属性时,该ref会被自动解包 - 了解
      const obj = reactive({}) // reactive 属性
      obj.count = count // ref赋值给 一个 reactive 属性
      // 该ref会被自动解包
      console.log(obj.count) // 0
      console.log(obj.count === count.value) // true

      // 3.如果将一个对象赋值给ref,那么这个对象将通过 reactive() 转为具有深层次响应的对象 - 了解
      const state = ref({ count: 10 })
      const increment = () => {
        state.value.count += 1
      }

      return {
        count,
        add,
        state,
        increment
      }
    }
  }).mount('#app')
</script>
</html>

以后创建 非 对象类型的数据 使用 ref, 创建对象类型的数据建议使用 reactive

3.2computed ()

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象。

创建一个只读的计算属性 ref:

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

创建一个可写的计算属性 ref:

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0

18_composition/81_composition_computed.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>计算属性</title>
</head>
<body>
  <div id="app">
    count: {{ count }} - {{ doubleCount }} - {{ plusOne }}
    <button @click="add">加1</button>

    <br/>
    <button @click="updateComputed">改变计算属性的值</button>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp, ref, computed } = Vue
  createApp({
    setup () {
      const count = ref(0)
      const add = () => {
        count.value++
      }

      const doubleCount = computed(() => { return count.value * 2 })

      const plusOne = computed({ // 可读可写 - 了解
        get () { return count.value + 1 },
        set (val) { count.value = val - 1}
      })

      const updateComputed = () => {
        plusOne.value = 100

      }

      return {
        count, add, doubleCount, plusOne, updateComputed
      }
    }
  }).mount('#app')
</script>
</html>

3.3 reactive()

返回一个对象的响应式代理。

响应式转换是“深层”的:它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。

值得注意的是,当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。

返回的对象以及其中嵌套的对象都会通过 ES Proxy 包裹,因此不等于源对象,建议只使用响应式代理,避免使用原始对象。

创建一个响应式对象:

const obj = reactive({ count: 0 })
obj.count++

ref 的解包:

const count = ref(1)
const obj = reactive({ count })

// ref 会被解包
console.log(obj.count === count.value) // true

// 会更新 `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2

// 也会更新 `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3

注意当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包:

const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)

将一个 ref 赋值给为一个 reactive 属性时,该 ref 会被自动解包:(讲解ref时已经说明)

const count = ref(1)
const obj = reactive({})

obj.count = count

console.log(obj.count) // 1
console.log(obj.count === count.value) // true

18_composition/82_composition_reactive.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>reactive</title>
</head>
<body>
  <div id="app">
    state.count: {{ state.count }}
    <button @click="add">加1</button>
  </div>

</body>
<script src="../lib//vue.global.js"></script>
<script>
  const { createApp, ref, reactive } = Vue
  // ref number string boolean 数组
  // reactive object
  createApp({
    setup () {
      const state = reactive({ count: 10 })

      const add = () => {
        state.count += 1
      }

      // ref的解包
      const count = ref(1)
      const obj = reactive({ count })
      console.log(count.value === obj.count) // true

      count.value++
      console.log(count.value) // 2
      console.log(obj.count) // 2

      count.value++
      console.log(count.value) // 3
      console.log(obj.count) // 3

      // 了解
      // 注意当访问到某个响应式数组或者Map这样的数据原生集合类型中的 ref元素时,不会执行 ref的解包
      const books = reactive([ref('vue3指南')])
      console.log(books[0].value) // vue3指南
      const map = reactive(new Map([['count', ref(0)]]))
      console.log(map.get('count').value) // 0

      return {
        state, add
      }
    }
  }).mount('#app')
</script>
</html>

初始值 对象 reactive 其余用ref

3.4 readonly()

接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。

只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 用来做响应性追踪
  console.log(copy.count)
})

// 更改源属性会触发其依赖的侦听器
original.count++

// 更改该只读副本将会失败,并会得到一个警告
copy.count++ // warning

18_composition/83_composition_readonly.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>readonly</title>
</head>
<body>
  <div id='app'>
    count: {{ count }}
    <button @click="addCount">加1</button>

    <hr>
    copy: {{ copy }}
    <button @click="addCopy">加1</button>

  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, readonly } = Vue
    createApp({
      setup () {
        const count = ref(10)
        const addCount = () => {
          count.value++
        }

        const copy = readonly(count)
        const addCopy = () => { // Set operation on key "value" failed: target is readonly.
          copy.value++
        }

        return {
          count,
          addCount,
          copy,
          addCopy
        }
      }
    }).mount('#app')
</script>
</html>

3.5 watchEffect()

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求。

返回值是一个用来停止该副作用的函数。

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> 输出 0

count.value++
// -> 输出 1

副作用清除:

watchEffect((onInvalidate) => {
    console.log(id.value) 
    const timer = setTimeout(() => {
        console.log('请求成功') // 2秒之内点击列表 只显示一次
        data.value = '数据' + id.value
    }, 2000)
    onInvalidate(() => {
        clearTimeout(timer)
    })
})

停止侦听器:

 const stop = watchEffect(() => {
     console.log(count.value)
 })
 // 当不再需要此侦听器时:
 const stopWatch = () => {
     stop()
 }

18_composition/84_composition_watchEffect.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>watchEffect</title>
</head>
<body>
  <div id='app'>
    <button @click="increment">点击了{{count}}次</button>
    <button @click="stopWatch">停止侦听</button>

    <ul>
      <li @click="id=1">请求第一条数据</li>
      <li @click="id=2">请求第二条数据</li>
      <li @click="id=3">请求第三条数据</li>
    </ul>
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, watchEffect } = Vue
    createApp({
      setup () {
        const count = ref(0)
        const increment = () => {
          count.value++
        }

        const stop = watchEffect(() => {
          console.log('监听到count的数据为:' + count.value)
        })

        const stopWatch = () => {
          stop()
        }

        const id = ref(1)
        // 了解-基本掌握
        watchEffect((onInvalidate) => { // onInvalidate 也是一个函数
          console.log(id.value) // 关键- 引起当前watchEffect的二次执行
          const timer = setTimeout(() => {
            console.log('请求第' + id.value + '条数据')
          }, 3000)
          // 清楚副作用
          onInvalidate(() => {
            console.log('清除')
            clearTimeout(timer)
          })
        })

        return {
          count,
          increment,
          stopWatch,
          id
        }
      }
    }).mount('#app')
</script>
</html>

watchEffect没有具体监听哪一个值的变化,只要内部有某一个状态发生改变就会执行

3.6 watch()

侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

  • watch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。

    第一个参数是侦听器的。这个来源可以是以下几种:

    • 一个函数,返回一个值
    • 一个 ref
    • 一个响应式对象
    • …或是由以上类型的值组成的数组

    第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。

    当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。

    第三个可选的参数是一个对象,支持以下这些选项:

    • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
    • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器

    watchEffect() 相比,watch() 使我们可以:

    • 懒执行副作用;
    • 更加明确是应该由哪个状态触发侦听器重新执行;
    • 可以访问所侦听状态的前一个值和当前值。
  • 示例

    侦听一个 getter 函数:

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

    侦听一个 ref:

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

    当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值:

    watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
      /* ... */
    })
    

    当使用 getter 函数作为源时,回调只在此函数的返回值变化时才会触发。如果你想让回调在深层级变更时也能触发,你需要使用 { deep: true } 强制侦听器进入深层级模式。在深层级模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象。

    const state = reactive({ count: 0 })
    watch(
      () => state,
      (newValue, oldValue) => {
        // newValue === oldValue
      },
      { deep: true }
    )
    

    当直接侦听一个响应式对象时,侦听器会自动启用深层模式:

    const state = reactive({ count: 0 })
    watch(state, () => {
      /* 深层级变更状态所触发的回调 */
    })
    

18_composition/85_composition_watch.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>85_composition_watch</title>
</head>
<body>
  <div id="app">
    <button @click="increment">点击了{{ count }}次</button>
    <button @click="foo='foo!!!'">改变foo</button>
    <button @click="bar='bar!!!'">改变bar</button>
    <button @click="state.obj.a=100">改变state</button>{{ state.obj.a }}

  </div>
</body>
<script src="lib/vue.global.js"></script>
<script>
  const { createApp, ref, watch, reactive } = Vue

  const app = createApp({
    setup () {
      const count = ref(0)
      const increment = () => {
        count.value += 1
      }
      // 函数作为监听源 --- 要的是那个数据 -- 需要value
      watch(() => count.value, (val, oldVal) => {
        console.log('fn', val, oldVal)
      })

      // 侦听ref 不要写value
      watch(count, (val, oldVal) => {
        console.log('ref', val, oldVal)
      })

      // 侦听多个来源
      const foo = ref('foo')
      const bar = ref('bar')
      watch([foo, bar], ([foo, bar], [oldFoo, oldBar]) => {
        console.log('f', foo, oldFoo)
        console.log('b', bar, oldBar)
      })

      // 侦听源为函数,默认无法深度侦听
      const state = reactive({ obj: { a: 10 } })
      // 无法深度侦听
      watch(() => state, (val, oldVal) => {
        console.log('s', val, oldVal)
      } )
      watch(() => state, (val, oldVal) => {
        console.log('d', val, oldVal)
      }, {
        deep: true,
        immediate: true
      } )

      // 直接侦听一个响应式对象时,自动开启深度侦听
      watch(state, (val, oldVal) => {
        console.log('auto', val, oldVal)
      }, {
        immediate: true // 此处不需要写 deep: true
      } )
      return {
        count,
        increment,
        foo,
        bar,
        state
      }

    }
  })

  app.mount('#app')
</script>
</html>

4、工具函数 -了解

4.1 toRef()

基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

18_composition/86_composition_toRef.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>toRef</title>
</head>
<body>
  <div id='app'></div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    // 基于响应式**对象**上的一个属性,创建一个对应的 ref。
    // 这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
    const { createApp, ref, reactive, toRef } = Vue
    createApp({
      setup () {

        // state 即为响应式对象
        // foo 以及 bar 即为 响应式对象上的一个属性
        const state = reactive({
          foo: 1,
          bar: 2
        })

        const count = toRef(state, 'foo')

        console.log(count.value) // 1

        count.value = 10 // 更新ref的值

        console.log(state.foo) // 10
        console.log(count.value) // 10

        state.foo = 100

        console.log(state.foo) // 100
        console.log(count.value) // 100

        return {}

      }
    }).mount('#app')
</script>
</html>

4.2 toRefs()

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

18_composition/87_composition_toRefs.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>toRefs</title>
</head>
<body>
  <div id='app'></div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    // 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。
    // 每个单独的 ref 都是使用 toRef() 创建的。
    const { createApp, ref, reactive, toRefs } = Vue
    createApp({
      setup () {

        const state = reactive({
          foo: 1,
          bar: 2
        })
        state.a = 5
        const newState = toRefs(state)
        state.b = 1000
        console.log(state) // Proxy
        console.log(newState) // 普通对象 - 像ref定义的数据  通过 .value 来获取以及修改数据

        console.log(newState.foo.value) // 1

        newState.foo.value = 10

        console.log(newState.foo.value) // 10
        console.log(state.foo) // 10

        state.foo = 100
        console.log(newState.foo.value) // 100
        console.log(state.foo) // 100

        return {}

      }
    }).mount('#app')
</script>
</html>

toRefs 在调用时只会为源对象上可以枚举的属性创建 ref。如果要为可能还不存在的属性创建 ref,请改用 toRef

5、生命周期钩子

5.1 onMounted()

注册一个回调函数,在组件挂载完成后执行。

组件在以下情况下被视为已挂载:

  • 其所有同步子组件都已经被挂载 (不包含异步组件或 <Suspense> 树内的组件)。
  • 其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。

这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于确保 DOM 相关代码仅在客户端执行。

5.2 onUpdated()

注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。

父组件的更新钩子将在其子组件的更新钩子之后调用。

这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。

5.3 onUnmounted()

注册一个回调函数,在组件实例被卸载之后调用。

一个组件在以下情况下被视为已卸载:

  • 其所有子组件都已经被卸载。
  • 所有相关的响应式作用 (渲染作用以及 setup() 时创建的计算属性和侦听器) 都已经停止。

可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。

5.4 onBeforeMount()

注册一个钩子,在组件被挂载之前被调用。

当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。

5.5 onBeforeUpdate()

注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。

这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。

5.6 onBeforeUnmount()

注册一个钩子,在组件实例被卸载之前调用。

当这个钩子被调用时,组件实例依然还保有全部的功能。

18_composition/88_composition_lifeCycle.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>生命周期</title>
</head>
<body>
  <div id='app'>
    {{ count }}
    <button @click="add">加1</button>
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { 
      createApp, 
      ref,
      onBeforeMount,
      onMounted,
      onBeforeUpdate,
      onUpdated,
      onBeforeUnmount,
      onUnmounted
    } = Vue
    const app = createApp({
      setup () {

        const count = ref(0)

        const add = () => {
          count.value++
          if (count.value === 5) {
            app.unmount()
          }
        }
       
        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 {
          count,
          add
        }
      }
    })

    app.mount('#app')
</script>
</html>

5.7 onActivated

5.8 onDeactivated

5.9 onErrorCaptured

注册一个钩子,在捕获了后代组件传递的错误时调用

6、依赖注入

祖先组件向后代组件传值

6.1 provide()

提供一个值,可以被后代组件注入。

provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。

与注册生命周期钩子的 API 类似,provide() 必须在组件的 setup() 阶段同步调用。

6.2 inject()

注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。

第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。

第二个参数是可选的,即在没有匹配到 key 时使用的默认值。它也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么你必须将 false 作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。

与注册生命周期钩子的 API 类似,inject() 必须在组件的 setup() 阶段同步调用。

18_composition/89_composition_provide_inject.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>依赖注入</title>
</head>
<body>
  <div id='app'>
    <button @click="count++">点击{{count}}次</button>
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    我是父组件 
    <my-child></my-child>
  </div>
</template>
<template id="child">
  <div>
    我是子组件
    -{{ count }}-<button @click="add">加10</button>- {{ test }} - {{ fn() }}
  </div>
</template>
<script src='../lib/vue.global.js'></script>
<script>

    const { createApp, ref, provide, inject } = Vue

    const Child = {
      template: '#child',
      setup () {
        const count = inject('count')
        // 获取祖先组件的数据
        const add = inject('add')
        // 设置默认值
        const test = inject('test', '测试')
        // 如果默认值是一个函数,请设置第三个参数为false
        const fn = inject('fn', () => {
          console.log(10000)
        }, false)

        return {
          count, add, test, fn
        }
      }
    }

    const Parent = {
      template: '#parent',
      components: {
        MyChild: Child
      }
    }

    createApp({
      components: {
        MyParent: Parent
      },
      setup () {

        const count = ref(10)

        const add = () => {
          count.value += 10
        }

        // 数据提供者
        provide('count', count)
        provide('add', add)

        return {count}

      }
    }).mount('#app')
</script>
</html>

7、组合式函数

7.1 什么是“组合式函数”?

在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

也可以叫做自定义hooks

当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。

相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。

如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的

18_composition/90_composition_mouse.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>鼠标跟随</title>
</head>
<body>
  <div id='app'>
    鼠标的位置:({{x}}, {{y}})
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, onMounted, onUnmounted } = Vue
    createApp({
      setup () {
        const x = ref(0)
        const y = ref(0)

        function updatePosition (event) {
          x.value = event.pageX
          y.value = event.pageY
        }

        onMounted(() => {
          window.addEventListener('mousemove', updatePosition, false)
        })

        onUnmounted(() => {
          window.removeEventListener('mousemove', updatePosition, false)
        })

        return {
          x, y
        }
      }
    }).mount('#app')
</script>
</html>

但是,如果我们想在多个组件中复用这个相同的逻辑呢?我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中

7.2 如何定义和使用组合式函数

18_composition/91_composition_mouse_hooks.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>鼠标跟随</title>
</head>
<body>
  <div id='app'>
    鼠标的位置:({{x}}, {{y}})
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, onMounted, onUnmounted } = Vue

    function useMouse () {
      const x = ref(0)
      const y = ref(0)

      function updatePosition (event) {
        x.value = event.pageX
        y.value = event.pageY
      }

      onMounted(() => {
        window.addEventListener('mousemove', updatePosition, false)
      })

      onUnmounted(() => {
        window.removeEventListener('mousemove', updatePosition, false)
      })

      return { x, y }
    }
    
    createApp({
      setup () {
        const { x, y } = useMouse()

        return {
          x, y
        }
      }
    }).mount('#app')
</script>
</html>

如你所见,核心逻辑完全一致,我们做的只是把它移到一个外部函数中去,并返回需要暴露的状态。和在组件中一样,你也可以在组合式函数中使用所有的组合式 API。现在,useMouse() 的功能可以在任何组件中轻易复用了。

更酷的是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。

18_composition/92_composition_mouse_more_hooks.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>鼠标跟随</title>
</head>
<body>
  <div id='app'>
    鼠标的位置:({{x}}, {{y}})
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, onMounted, onUnmounted } = Vue

    function useEventListener (target, event, callback) {
      onMounted(() => {
        target.addEventListener(event, callback, false)
      })

      onUnmounted(() => {
        target.removeEventListener(event, callback, false)
      })
    }

    function useMouse () {
      const x = ref(0)
      const y = ref(0)

      // function updatePosition (event) {
      //   x.value = event.pageX
      //   y.value = event.pageY
      // }

      // onMounted(() => {
      //   window.addEventListener('mousemove', updatePosition, false)
      // })

      // onUnmounted(() => {
      //   window.removeEventListener('mousemove', updatePosition, false)
      // })

      useEventListener(window, 'mousemove', (event) => {
        x.value = event.pageX
        y.value = event.pageY
      })

      return { x, y }
    }
    
    createApp({
      setup () {
        const { x, y } = useMouse()

        return {
          x, y
        }
      }
    }).mount('#app')
</script>
</html>

7.3 异步状态

在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。

18_composition/93_composition_async.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>异步状态</title>
</head>
<body>
  <div id='app'>
    <div v-if="error">{{ error }}</div>
    <div v-else-if="data">
      <ul>
        <li v-for="item of data" :key="item.proid">{{ item.proname }}</li>
      </ul>
    </div>
    <div v-else>加载中....</div>
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, onMounted } = Vue
    createApp({
      setup () {
        const data = ref(null)
        const error = ref(null)

        onMounted(() => {
          // http://121.89.205.189:3000/apidoc/
          // fetch 可以用来做数据的请求,基于promise的
          // 请求到的数据需要转换为json 对象,然后再使用
          fetch('http://121.89.205.189:3000/api/pro/list')
            .then(res => { return res.json() }) // 转换为json格式
            .then(res => {
              console.log(res)
              data.value = res.data
            }).catch(err => {
              error.value = err
            })
        })

        return {
          data, error
        }
      }
    }).mount('#app')
</script>
</html>

如果在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。让我们把它抽取成一个组合式函数:

18_composition/94_composition_async_hooks.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>异步状态</title>
</head>
<body>
  <div id='app'>
    <div v-if="error">{{ error }}</div>
    <div v-else-if="data">
      <ul>
        <li v-for="item of data" :key="item.proid">{{ item.proname }}</li>
      </ul>
    </div>
    <div v-else>加载中....</div>
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, onMounted } = Vue

    function useFetch (url) {
      const data = ref(null)
        const error = ref(null)

        onMounted(() => {
          // http://121.89.205.189:3000/apidoc/
          // fetch 可以用来做数据的请求,基于promise的
          // 请求到的数据需要转换为json 对象,然后再使用
          fetch(url)
            .then(res => { return res.json() }) // 转换为json格式
            .then(res => {
              console.log(res)
              data.value = res.data
            }).catch(err => {
              error.value = err
            })
        })

      return { data, error }
    }

    createApp({
      setup () {
        
        const { data, error } = useFetch('http://121.89.205.189:3000/api/pro/list')
        return {
          data, error
        }
      }
    }).mount('#app')
</script>
</html>

useFetch() 接收一个静态的 URL 字符串作为输入,所以它只执行一次请求,然后就完成了。但如果我们想让它在每次 URL 变化时都重新请求呢?那我们可以让它同时允许接收 ref 作为参数:

95_composition_async_hooks_urls.html - 思考题


7.4 约定和最佳实践

7.4.1 命名

组合式函数约定用驼峰命名法命名,并以“use”作为开头。

7.44.2 输入参数

尽管其响应性不依赖 ref,组合式函数仍可接收 ref 参数。如果编写的组合式函数会被其他开发者使用,你最好在处理输入参数时兼容 ref 而不只是原始的值。unref() 工具函数会对此非常有帮助:

import { unref } from 'vue'

function useFeature(maybeRef) {
  // 若 maybeRef 确实是一个 ref,它的 .value 会被返回
  // 否则,maybeRef 会被原样返回
  const value = unref(maybeRef)
}

如果你的组合式函数在接收 ref 为参数时会产生响应式 effect,请确保使用 watch() 显式地监听此 ref,或者在 watchEffect() 中调用 unref() 来进行正确的追踪。

7.4.3 返回值

你可能已经注意到了,我们一直在组合式函数中使用 ref() 而不是 reactive()。我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:

// x 和 y 是两个 ref
const { x, y } = useMouse()

从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。与之相反,ref 则可以维持这一响应性连接。

如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用 reactive() 包装一次,这样其中的 ref 会被自动解包,例如:

const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
7.4.4 副作用

在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:

  • 如果你的应用用到了服务端渲染 (SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,例如:onMounted()。这些钩子仅会在浏览器中被调用,因此可以确保能访问到 DOM。
  • 确保在 onUnmounted() 时清理副作用。举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在 onUnmounted() 中被移除 (就像我们在 useMouse() 示例中看到的一样)。当然也可以像之前的 useEventListener() 示例那样,使用一个组合式函数来自动帮你做这些事。
7.4.5 使用限制

组合式函数在 <script setup>setup() 钩子中,应始终被同步地调用。在某些场景下,你也可以在像 onMounted() 这样的生命周期钩子中使用他们。

这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例,只有能确认当前组件实例,才能够:

  1. 将生命周期钩子注册到该组件实例上
  2. 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
    常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。

相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。

如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的

18_composition/90_composition_mouse.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>鼠标跟随</title>
</head>
<body>
  <div id='app'>
    鼠标的位置:({{x}}, {{y}})
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, onMounted, onUnmounted } = Vue
    createApp({
      setup () {
        const x = ref(0)
        const y = ref(0)

        function updatePosition (event) {
          x.value = event.pageX
          y.value = event.pageY
        }

        onMounted(() => {
          window.addEventListener('mousemove', updatePosition, false)
        })

        onUnmounted(() => {
          window.removeEventListener('mousemove', updatePosition, false)
        })

        return {
          x, y
        }
      }
    }).mount('#app')
</script>
</html>

但是,如果我们想在多个组件中复用这个相同的逻辑呢?我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中

7.2 如何定义和使用组合式函数

18_composition/91_composition_mouse_hooks.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>鼠标跟随</title>
</head>
<body>
  <div id='app'>
    鼠标的位置:({{x}}, {{y}})
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, onMounted, onUnmounted } = Vue

    function useMouse () {
      const x = ref(0)
      const y = ref(0)

      function updatePosition (event) {
        x.value = event.pageX
        y.value = event.pageY
      }

      onMounted(() => {
        window.addEventListener('mousemove', updatePosition, false)
      })

      onUnmounted(() => {
        window.removeEventListener('mousemove', updatePosition, false)
      })

      return { x, y }
    }
    
    createApp({
      setup () {
        const { x, y } = useMouse()

        return {
          x, y
        }
      }
    }).mount('#app')
</script>
</html>

如你所见,核心逻辑完全一致,我们做的只是把它移到一个外部函数中去,并返回需要暴露的状态。和在组件中一样,你也可以在组合式函数中使用所有的组合式 API。现在,useMouse() 的功能可以在任何组件中轻易复用了。

更酷的是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。

18_composition/92_composition_mouse_more_hooks.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>鼠标跟随</title>
</head>
<body>
  <div id='app'>
    鼠标的位置:({{x}}, {{y}})
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, onMounted, onUnmounted } = Vue

    function useEventListener (target, event, callback) {
      onMounted(() => {
        target.addEventListener(event, callback, false)
      })

      onUnmounted(() => {
        target.removeEventListener(event, callback, false)
      })
    }

    function useMouse () {
      const x = ref(0)
      const y = ref(0)

      // function updatePosition (event) {
      //   x.value = event.pageX
      //   y.value = event.pageY
      // }

      // onMounted(() => {
      //   window.addEventListener('mousemove', updatePosition, false)
      // })

      // onUnmounted(() => {
      //   window.removeEventListener('mousemove', updatePosition, false)
      // })

      useEventListener(window, 'mousemove', (event) => {
        x.value = event.pageX
        y.value = event.pageY
      })

      return { x, y }
    }
    
    createApp({
      setup () {
        const { x, y } = useMouse()

        return {
          x, y
        }
      }
    }).mount('#app')
</script>
</html>

7.3 异步状态

在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。

18_composition/93_composition_async.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>异步状态</title>
</head>
<body>
  <div id='app'>
    <div v-if="error">{{ error }}</div>
    <div v-else-if="data">
      <ul>
        <li v-for="item of data" :key="item.proid">{{ item.proname }}</li>
      </ul>
    </div>
    <div v-else>加载中....</div>
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, onMounted } = Vue
    createApp({
      setup () {
        const data = ref(null)
        const error = ref(null)

        onMounted(() => {
          // http://121.89.205.189:3000/apidoc/
          // fetch 可以用来做数据的请求,基于promise的
          // 请求到的数据需要转换为json 对象,然后再使用
          fetch('http://121.89.205.189:3000/api/pro/list')
            .then(res => { return res.json() }) // 转换为json格式
            .then(res => {
              console.log(res)
              data.value = res.data
            }).catch(err => {
              error.value = err
            })
        })

        return {
          data, error
        }
      }
    }).mount('#app')
</script>
</html>

如果在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。让我们把它抽取成一个组合式函数:

18_composition/94_composition_async_hooks.html

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>异步状态</title>
</head>
<body>
  <div id='app'>
    <div v-if="error">{{ error }}</div>
    <div v-else-if="data">
      <ul>
        <li v-for="item of data" :key="item.proid">{{ item.proname }}</li>
      </ul>
    </div>
    <div v-else>加载中....</div>
  </div>
</body>
<script src='../lib/vue.global.js'></script>
<script>
    const { createApp, ref, onMounted } = Vue

    function useFetch (url) {
      const data = ref(null)
        const error = ref(null)

        onMounted(() => {
          // http://121.89.205.189:3000/apidoc/
          // fetch 可以用来做数据的请求,基于promise的
          // 请求到的数据需要转换为json 对象,然后再使用
          fetch(url)
            .then(res => { return res.json() }) // 转换为json格式
            .then(res => {
              console.log(res)
              data.value = res.data
            }).catch(err => {
              error.value = err
            })
        })

      return { data, error }
    }

    createApp({
      setup () {
        
        const { data, error } = useFetch('http://121.89.205.189:3000/api/pro/list')
        return {
          data, error
        }
      }
    }).mount('#app')
</script>
</html>

useFetch() 接收一个静态的 URL 字符串作为输入,所以它只执行一次请求,然后就完成了。但如果我们想让它在每次 URL 变化时都重新请求呢?那我们可以让它同时允许接收 ref 作为参数:

95_composition_async_hooks_urls.html - 思考题


7.4 约定和最佳实践

7.4.1 命名

组合式函数约定用驼峰命名法命名,并以“use”作为开头。

7.44.2 输入参数

尽管其响应性不依赖 ref,组合式函数仍可接收 ref 参数。如果编写的组合式函数会被其他开发者使用,你最好在处理输入参数时兼容 ref 而不只是原始的值。unref() 工具函数会对此非常有帮助:

import { unref } from 'vue'

function useFeature(maybeRef) {
  // 若 maybeRef 确实是一个 ref,它的 .value 会被返回
  // 否则,maybeRef 会被原样返回
  const value = unref(maybeRef)
}

如果你的组合式函数在接收 ref 为参数时会产生响应式 effect,请确保使用 watch() 显式地监听此 ref,或者在 watchEffect() 中调用 unref() 来进行正确的追踪。

7.4.3 返回值

你可能已经注意到了,我们一直在组合式函数中使用 ref() 而不是 reactive()。我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:

// x 和 y 是两个 ref
const { x, y } = useMouse()

从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。与之相反,ref 则可以维持这一响应性连接。

如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用 reactive() 包装一次,这样其中的 ref 会被自动解包,例如:

const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
7.4.4 副作用

在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:

  • 如果你的应用用到了服务端渲染 (SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,例如:onMounted()。这些钩子仅会在浏览器中被调用,因此可以确保能访问到 DOM。
  • 确保在 onUnmounted() 时清理副作用。举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在 onUnmounted() 中被移除 (就像我们在 useMouse() 示例中看到的一样)。当然也可以像之前的 useEventListener() 示例那样,使用一个组合式函数来自动帮你做这些事。
7.4.5 使用限制

组合式函数在 <script setup>setup() 钩子中,应始终被同步地调用。在某些场景下,你也可以在像 onMounted() 这样的生命周期钩子中使用他们。

这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例,只有能确认当前组件实例,才能够:

  1. 将生命周期钩子注册到该组件实例上
  2. 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值