Vue 3 学习 二、Composition API

Vue 3 官方文档

Composition API 示例

# 安装 vue3
npm i vue@3.0.0

示例内容:实时展示鼠标坐标

createApp

createApp 用于创建 Vue 对象,它可以接收一个(组件)选项对象作为参数,和 Vue 2 中给 Vue 构造函数传入的选项一样。

选项中的 data 应该是一个函数,不同于 Vue 2。这样统一了 data 的写法

<div id="app">
  x: {{ position.x}}<br />
  y: {{ position.y}}
</div>
<script type="module">
  import { createApp } from './node_modules/vue/dist/vue.esm-browser.js'

  const app = createApp({
    // Vue 根实例的 data 也必须是一个函数
    data() {
      return {
        position: {
          x: 0,
          y: 0
        }
      }
    }
  })
  console.log(app)

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

Vue 实例成员

Vue 3 创建的实例对象的成员比 Vue 2 中的少很多,并且没有用 $ 开头,说明未来基本不用给这个对象上新增成员。

  • componentdirectivemixinuse 和 Vue 2 用法一致
  • mount 和 Vue 2 的 $mount 一样
  • unmount 类似 Vue 2 的 $destroy 方法

在这里插入图片描述

setup

Composition API 还是在选项中书写,不过要用到一个新的选项 setup

setup 函数是 Composition API 的入口。

接收两个参数:

  • props 接收外部传入的参数,且 props 是响应式对象,不能被解构
  • context 一个对象:{ attrs, emit, slots }

setup 返回一个对象,这个对象的属性可以在模板、methods、computed 以及 生命周期的钩子函数中直接使用。

setup 执行的时机是在 props 被解析完毕,组件实例被创建之前执行的。

所以在 setup 内部无法通过 this 获取组件的实例。

因为组件实例还未被创建,所以在 setup 中也无法访问到组件中的 datacomputedmethods

setup 内部的 this 此时指向 undefined

<div id="app">
  x: {{ position.x}}
  <br />
  y: {{ position.y}}
</div>
<script type="module">
  import { createApp } from './node_modules/vue/dist/vue.esm-browser.js'

  const app = createApp({
    setup() {
      // 第一个参数 props:接收外部传入的参数
      // 第二个参数 context:对象 {attrs, emit, slots}
      const position = {
        x: 0,
        y: 0
      }

      return {
        position
      }
    },
    mounted() {
      this.position.x = 100
    }
  })
  console.log(app)

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

响应式转化

上面示例在 mounted 中设置了 position.x 的值,但并没有渲染到页面,说明 position 不是响应式的。

过去可以在 data 选项中设置响应式对象。

为了让某一个逻辑的所有代码都能够被封装到一个函数中,Vue 3 中提供了一个新的 API(reactive),用于创建响应式对象。

它就像过去使用的 Vue.observeable, Vue 3 没有使用 observeable 作为函数的名称,是因为避免和另一个函数库 RxJS observables 重名,出现混淆。(官方文档有讲

reactive 函数的作用是把一个对象转换成响应式对象,并且该对象的嵌套属性,也都会转换成响应式对象。

它返回的是一个 Proxy 对象。

<div id="app">
  x: {{ position.x}}
  <br />
  y: {{ position.y}}
</div>
<script type="module">
  import { createApp, reactive } from './node_modules/vue/dist/vue.esm-browser.js'

  const app = createApp({
    setup() {
      // 第一个参数 props:接收外部传入的参数
      // 第二个参数 context:对象 {attrs, emit, slots}
      const position = reactive({
        x: 0,
        y: 0
      })

      return {
        position
      }
    },
    mounted() {
      this.position.x = 100
    }
  })
  console.log(app)

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

小结

目前了解了 Vue 3 中的 3个新增 API。

  • createApp 用于创建 Vue 实例对象
  • setup Composition API 的入口
  • reactive 用于创建响应式对象

生命周期钩子函数

setup 中使用生命周期钩子函数,注册鼠标移动事件 mousemove

当组件被卸载的时候,移除这个鼠标移动事件。

setup 函数中使用生命周期钩子函数,需要在函数名前添加 on 前缀,并且首字母大写。

Options API 和 Composition API 之间的生命周期映射

  • setup 是在组件初始化之前(beforeCreatecreated 之间)执行的,所以 beforeCreatecreated 中的代码,都应该放在 setup 函数中。
  • unmounted 钩子函数类似以前的 destroy
  • renderTrackedrenderTriggered 都是在 render 函数被重新调用的时候触发的
    • 不同的是 renderTracked 在首次调用 render 的时候也会触发,``renderTriggered不会。
Options ApiHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

setup 中使用的生命周期钩子函数,接收一个要注册的处理函数,当生命周期被触发的时候,调用这个函数。

<div id="app">
  x: {{ position.x}}
  <br />
  y: {{ position.y}}
</div>
<script type="module">
  import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'

  const app = createApp({
    setup() {
      // 第一个参数 props:接收外部传入的参数
      // 第二个参数 context:对象 {attrs, emit, slots}
      const position = reactive({
        x: 0,
        y: 0
      })

      // 定义事件处理函数
      const update = e => {
        position.x = e.pageX
        position.y = e.pageY
      }

      // 注册事件
      onMounted(() => {
        window.addEventListener('mousemove', update)
      })

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

      return {
        position
      }
    }
  })
  console.log(app)

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

重构代码

Composition API 的优点就是把同一个功能的逻辑提取到一个地方,使其容易阅读并可以重用。

当前示例的最终目标,是要把 “获取鼠标位置” 这个功能封装到一个函数中。

<div id="app">
  x: {{ position.x}}
  <br />
  y: {{ position.y}}
</div>
<script type="module">
  import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'

  function useMousePosition () {
    const position = reactive({
        x: 0,
        y: 0
      })

      // 定义事件处理函数
      const update = e => {
        position.x = e.pageX
        position.y = e.pageY
      }

      // 注册事件
      onMounted(() => {
        window.addEventListener('mousemove', update)
      })

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

      return position
  }

  const app = createApp({
    setup() {
      // 第一个参数 props:接收外部传入的参数
      // 第二个参数 context:对象 {attrs, emit, slots}

      const position = useMousePosition()

      return {
        position
      }
    }
  })
  console.log(app)

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

现在如果这个函数出现问题,可以很好的定位,并且可以放在一个模块中,将来在任何组件中都可以使用。

reactive/toRefs/ref

reactive / toRefs / ref 3个函数都是用于创建响应式数据的。

  • reactive 用于把对象转化成响应式对象,是一个代理对象
  • ref 用于把基本类型的数据转化成响应式对象
  • toRefs 用于把代理对象中的所有属性都转化成响应式对象
    • toRefs 处理对象中属性的时候,类似 ref
    • 通过 toRefs 处理 reactive 返回的代理对象,可以进行解构

reactive 的问题

上例中如果想简化 x y 的使用,一般来说就会想到去解构 useMousePosition 返回的对象。

<div id="app">
  x: {{ x}}
  <br />
  y: {{ y}}
</div>
<script type="module">
  import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'

  function useMousePosition () {
    const position = reactive({
        x: 0,
        y: 0
      })

      // 定义事件处理函数
      const update = e => {
        position.x = e.pageX
        position.y = e.pageY
      }

      // 注册事件
      onMounted(() => {
        window.addEventListener('mousemove', update)
      })

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

      return position
  }

  const app = createApp({
    setup() {
      // 第一个参数 props:接收外部传入的参数
      // 第二个参数 context:对象 {attrs, emit, slots}

      // const position = useMousePosition()
      const { x, y } = useMousePosition()

      return {
        x,
        y
      }
    }
  })
  console.log(app)

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

但是查看效果,x y并没有实时更新,说明解构出来的 xy 不是响应式对象。

这是因为 useMousePositon 中将 {x:0, y:0} 转化为响应式对象的方式,是调用 reactive 将其转化成 Proxy 代理对象。

此时的 position 是一个 Proxy 对象,将来访问 positionxy 的时候,会调用代理对象的 getter 拦截收集依赖。

x y 变化后,会调用代理对象的 setter 进行拦截触发更新。

const { x, y } = useMousePosition() 把代理对象解构的时候,就相当于定义xy 两个变量来接收 position.xposition.y

这里的 xy 就是两个基本类型的变量,解构的过程就是基本类型的赋值,只是把值在内存中复制一份,和代理对象无关。

当重新给它们赋值的时候,并不会调用代理对象的 setter,无法触发更新的操作。

所以,不能对响应式对象解构

// ES6 解构语法
const {x,y} = position


// babel 降级处理后的结果
"use strict";

var _position = position,
  x = _position.x,
  y = _position.y;

toRefs

如果想要实现上述简化的效果,可以使用 toRefs

toRefs 用于把一个响应式对象中的所有属性也转换成响应式的。

<div id="app">
  x: {{ x}}
  <br />
  y: {{ y}}
</div>
<script type="module">
  import { createApp, reactive, onMounted, onUnmounted, toRefs } from './node_modules/vue/dist/vue.esm-browser.js'

  function useMousePosition () {
    const position = reactive({
        x: 0,
        y: 0
      })

      // 定义事件处理函数
      const update = e => {
        position.x = e.pageX
        position.y = e.pageY
      }

      // 注册事件
      onMounted(() => {
        window.addEventListener('mousemove', update)
      })

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

      return toRefs(position)
  }

  const app = createApp({
    setup() {
      // 第一个参数 props:接收外部传入的参数
      // 第二个参数 context:对象 {attrs, emit, slots}

      // const position = useMousePosition()
      const { x, y } = useMousePosition()

      return {
        x,
        y
      }
    }
  })
  console.log(app)

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

内部实现原理

toRefs() 要求传入的必须是一个代理对象,否则会报警告。

接着它内部会创建一个新的对象,然后遍历传入的这个代理对象的所有属性,把所有属性的值都转化成响应式对象,然后都挂载到新创建的对象上。

最后返回这个新创建的对象。

它内部会为代理对象的每一个属性,创建一个具有 value 属性的对象,该对象是响应式的。

value 属性具有 getter/setter

getter 中返回代理对象中对应属性的值。

setter中给代理对象的属性赋值。

所以返回的每一个属性都是响应式的。

因为toRefs 会将响应式对象中的每个属性都转化成响应式对象,所以可以解构它返回的对象,并且解构的每个属性也都是响应式的。

  • 在模板中使用解构后的属性的时候,可以把 value 省略。

  • 但是在代码中使用的时候,不能省略 value

ref

ref 函数用于把一个普通数据转化成一个响应式数据。

不同于reactive 是把一个对象转化成响应式数据,ref可以把一个基本类型的数据包装成响应式对象。

它内部的原理同 toRefs 一样,也是将数据转化成一个具有 value 属性的对象,value 具有 getter/setter

<div id="app">
  <button @click="increase">按钮</button>
  <span>{{ count }}</span>
</div>
<script type="module">
  import { createApp, ref } from './node_modules/vue/dist/vue.esm-browser.js'

  function useCount () {
    const count = ref(0)
    return {
      count,
      increase: () => {
        // count 在模板中使用可以省略value
        // 在代码中使用不可省略
        count.value++
      }
    }
  }

  const app = createApp({
    setup(){
      return {
        ...useCount()
      }
    }
  }).mount('#app')
</script>

内部实现原理

基本数据类型存储的是值,所以不可能是响应式数据。

ref() 传递的参数

  • 如果是对象,内部会调用 reactive() 返回一个代理对象。
  • 如果是基本类型的值,内部会创建一个只有 value 属性的对象。
    • 该对象的 value 属性具有 getter/setter
    • getter 中收集依赖,在 setter 中触发更新

ref 和 reactive 如何选择

refreactive 的区别:

  • ref 是把原始值添加一层包装,使其变成响应式的引用类型的值
  • reactive是引用类型的值变成响应式的值

所以两者的区别在于是否需要添加一层引用包装,其目的都是对数据添加响应式效果。

需要注意,使用 ref 包装后,需要使用 .value 才能进行取值和赋值操作。所以在创建对象时,如果有一个 value 的属性,容易引起混淆,难以区分是不是 ref 方法加上去的属性。

其他区别参考:https://juejin.cn/post/6860349065742745613

computed

计算属性的作用是简化模板中的代码,可以缓存计算的结果,当数据变化后才会重新计算。

setup 中可以通过 computed 函数创建计算属性。

computed 函数有两种用法:

  • 传入一个获取值的函数
    • 函数内部依赖响应式的数据,当依赖的数据发生变化后,会重新执行该函数获取数据。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2

plusOne.value++ // error
  • 传入一个具有 gettersetter 的对象
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

computed 函数返回一个不可变的响应式对象,类似于使用 ref 创建的对象,只有一个 value 属性,获取计算属性的值要通过 value 属性获取,模板中使用可以忽略 .value

第一种方式返回对象的value不能被修改。

第二种方式返回的对象的value可以修改,会调用计算属性的 setter 方法。

以第一个方式为例:

<div id="app">
  <button @click="push">按钮</button>
  未完成:{{ activeCount }}
</div>
<script type="module">
  import { createApp, reactive, computed } from './node_modules/vue/dist/vue.esm-browser.js'

  const data = [
    {text: '看书', completed: false},
    {text: '敲代码', completed: false},
    {text: '约会', completed: true}
  ]

  createApp({
    setup() {
      const todos = reactive(data)

      const activeCount = computed(() => {
        return todos.filter(item => !item.completed).length
      })

      return {
        activeCount,
        push: () => {
          todos.push({
            text: '开会',
            completed: false
          })
        }
      }
    }
  }).mount('#app')
</script>

watch

setup 函数中可以使用 watch() 创建一个侦听器。

它的使用方式和之前使用的 this.$watch 和 选项中的 watch 一样,监听响应式数据的变化,然后执行一个相应的回调函数。

可以获取到监听数据的新值和旧值。

  • watch() 函数的3个参数:
    • 第一个参数:要监听的数据
      • 可以是获取值的函数、ref 或 reactive 返回的对象、数组
      • 注意:Vue 2 中是一个字符串表达式
    • 第二个参数:监听到数据变化后执行的函数,这个函数有两个参数分别是新值和旧值
    • 第三个参数:选项对象,deep 和 immediate
  • watch() 的返回值
    • 取消监听的函数
<div id="app">
  <p>
    请问一个 yes/no 的问题:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>
<script type="module">
  import { createApp, ref, watch } from './node_modules/vue/dist/vue.esm-browser.js'

  createApp({
    setup(){
      const question = ref('')
      const answer = ref('')

      watch(question, async (newValue, oldValue) => {
        const response = await fetch('https://www.yesno.wtf/api')
        // ref 返回的对象不可变,应该修改它的 value 属性
        const data = await response.json()
        answer.value = data.answer
      })

      return {
        question,
        answer
      }
    }
  }).mount('#app')
</script>

watchEffect

  • watchEffectwatch 函数的简化版本,也用来监听数据的变化
  • 内部实现是和 watch 调用的同一个函数 doWatch
  • 不同的是 watchEffect 没有第二个回调函数的参数
  • watchEffect 接收一个函数作为参数,监听函数内响应式数据的变化
  • 这个函数会立即执行,当响应式数据发生变化,会重新执行
  • 它也返回一个取消监听的函数
<div id="app">
  <button @click="increase">increase</button>
  <button @click="stop">stop</button>
  <br>
  {{ count }}
</div>
<script type="module">
  import { createApp, ref, watchEffect } from './node_modules/vue/dist/vue.esm-browser.js'

  createApp({
    setup(){
      const count = ref(0)

      // watchEffect 接收的函数,初始时会立即执行
      const stop = watchEffect(() => {
        console.log(count.value)
      })

      return {
        count,
        stop,
        increase: () => {
          count.value++
        }
      }
    }
  }).mount('#app')
</script>

在一些监听数据的场景中,当数据变化后,记录数据到 localStorage 中,使用 watchEffect 会非常的方便。

directive 自定义指令

Vue 3 自定义指令不同于 Vue 2.x 地方主要是钩子函数的重命名。

Vue 3 将指令中钩子函数的名称和组件钩子函数的名称保持一致,容易理解。

但是自定义指令的钩子函数和组件的钩子函数执行方式是不一样的。

第二个参数是对象时:

  • Vue 2.x
Vue.directive('editingFocus', {
  bind(el, binding, vnode, prevVnode) {},
  inserted() {},
  update() {}, // remove
  componentUpdated() {},
  unbind() {}
})
  • Vue 3.0
app.directive('editingFocus', {
  // DOM挂载
  beforeMount(el, binding, vnode, prevVnode) {}, // 类似 bind
  mounted() {}, // 类似 inserted
  // DOM更新
  beforeUpdate() {}, // new
  updated() {}, // 类似 componentUpdated
  // DOM卸载
  beforeUnmount() {}, // new
  unmounted() {} // 类似 unbind
})

第二个参数是函数时,Vue 3 和 Vue 2 的用法一样:

// Vue 2 函数会在 bind 和 update 时执行
Vue.directive('editingFocus', (el, binding) => {
  binding.value && el.focus()
})

// Vue 3 函数会在 mounted 和 updated 时执行
app.directive('editingFocus', (el, binding) => {
  binding.value && el.focus()
})
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值