06.Vue3 中的 Composition API

06.Vue3 中的 Composition API,第1张

Vue3 中的 Composition API 为什么要有 Composition API?

当一个组价越来越大的时候,data、methods、computed、directives、mixin 等混杂在一起。里面的功能是混杂的,在一堆混杂的逻辑里面查找如 name 相关的逻辑的话,就会有一定的困难。这样的话会造成可维护性降低。

Composition API 就是解决这个问题的有效方案。

1. setup 函数的使用

setup 函数是在 created 时候(实例被初始化前)执行的。里边返回数据或者方法,插值函数可以检测到。

const app = Vue.createApp({
  template: `
    
      hello world
      name: {{ name }}, age: {{ age }}
    
  `,
  setup(props, context) {
    return {
      name: "sjh",
      age: 28,
      handleClick: () => {
        alert(123)
      }
    }
  }
})

app.mount("#root")

因为是实例初始化前执行,因此 setup 里不能使用 this 访问到实例。

setup 函数里有两个参数,第一个是 props,指组件从父组件那儿接收到的参数,第二个是 context,暴露了 attrs、slot、emit 等实用方法。

上面的数据

2. ref,reactive 响应式引用的用法和原理

上面看着很合理,但是 name 和 age 都是非响应式的,即数据改变并不会触发渲染。

如何解决问题呢?现在需要响应式的引用:通过 proxy 对数据进行封装。当数据变化时,触发模板等内容的更新。

后面的例子都是通过一秒后修改 name 的值来查看是否有响应式。如果页面数据没有改变,说明没有响应式。

2.1 ref:响应式封装基础类型数据

使用 ref 函数,如 ref("sjh"),对基础类型的数据进行封装,变成 proxy({value: "sjh"}) 这样的一个响应式引用。

const { ref } = Vue;

const app = Vue.createApp({
  template: `
  
    name: {{ name }}, age: {{ age }}
  
`,
  setup(props, context) {
    const name = ref("sjh")
    const age = ref(28)
    // 一秒后,name 的值改变
    setTimeout(() => {
      name.value = "hhhhh"
    }, 1000)
    return { name, age }
  }
})

app.mount("#root")

改成响应式引用后,数据变化 ,页面上的内容也会相应改变。

2.2 reactive:响应式封装非基础类型数据

使用 reactive 函数,将非基础类型数据转化为响应式的。一般就是对象或者数组。

原理: reactive({ name: "sjh" }) 转化成 proxy({ name: "sjh" })

const { ref, reactive } = Vue;

const app = Vue.createApp({
  template: `
    
      name: {{ obj.name }}, age: {{ obj.age }}
    
  `,
  setup(props, context) {
    const obj = reactive({ name: "sjh", age: 20 })
    setTimeout(() => {
      obj.name = "hhhhh"
    }, 1000)
    return { obj }
  }
})

app.mount("#root")
2.3 readonly:数据变成只读形式

用 readonly 包裹响应式引用,该响应式数据变成只读,即不可修改该数据。

2.4 toRefs:解构时不丢失响应

使用过对象解构的同学会觉得插值函数里的写法好麻烦啊,所以会用对象解构。但是这里需要注意的一点,如果直接使用结构后的值,这些值不是响应式的。

解构会将对象或数组解构成普通的数据,丢失了响应。因此解构之前需要用 toRefs 包裹一下,使解构后所有值变成响应式的。

原理:toRefs({ name: "sjh", age: 20 }) 转化成 { name: proxy({ value: "sjh" }), age: proxy({ value: 20 }) },将内部的所有数据变成响应式的。现在使用解构后就不会丢失响应式了。

const { ref, reactive, toRefs } = Vue;

const app = Vue.createApp({
  template: `
    
      name: {{ name }}, age: {{ age }}
    
  `,
  setup(props, context) {
    const obj = reactive({ name: "sjh", age: 20 })
    setTimeout(() => {
      obj.name = "hhhhh"
    }, 1000)
    const { name, age } = toRefs(obj)
    return { name, age }
  }
})

app.mount("#root")
3. toRef 以及 context 参数 3.1 toRef:从响应式对象中获取单个响应式数据

有一种使用场景,当解构的时候并非解构响应式对象里所拥有的 key,则解构出来的数据没有响应式,值为 undefined。

如下代码:data 里边只有 name,但是想解构 age 出来,此时前端页面会因为 age 是 undefined 而报错。

const { ref, reactive, toRefs } = Vue;

const app = Vue.createApp({
  template: `
    
      {{ age }}
    
  `,
  setup(props, context) {
    const data = reactive({ name: "sjh" })
    const { age } = toRefs(data)
    setTimeout(() => {
      age.value = 20
    }, 1000)
    return { age }
  }
})

app.mount("#root")

toRef 就可以有效地解决问题。

语法:toRef(data, 'age'),意思是,尝试从 data 里取 age 数据,如果没找到,则返回一个值为 null 的响应式数据。

const { ref, reactive, toRefs, toRef } = Vue;

const app = Vue.createApp({
  template: `
    {{ age }}
  `,
  setup(props, context) {
    const data = reactive({ name: "sjh" })
    const age = toRef(data, "age")
    setTimeout(() => {
      age.value = 20
    }, 1000)
    return { age }
  }
})

app.mount("#root")
3.2 context 参数的意义

在上文已经有稍微讲一下 context 了,在这里深入讲一下。

之前说过,context 里边有三个值,分别为 attrs,slots,emit。

attrs:为父组件传过来的 non-props,即没有通过 props 接收的属性。

slots:父组件传递过来的插槽内容。例如,通过 slots.default() 可以获取默认插槽的虚拟 DOM 形式。

例如,想玩些花的,用 h 函数来渲染通过父组件传过来的默认插槽:

const app = Vue.createApp({
  template: `
    parent
  `,
})

app.component("child", {
  setup(props, context) {
    const data = reactive({ name: "sjh" })
    const { name } = toRefs(data)
    // return { name }
    const { attrs, slots, emit } = context
    return () => Vue.h('div', {}, slots.default())
  }
})

app.mount("#root")

如果不使用 Composition API,就需要用 this.$slots.default() 来获取默认插槽

emit:触发自定义事件的方法。

例如,子组件点击的时候,触发父组件的自定义事件:

const app = Vue.createApp({
  template: `
    parent
  `,
  setup(props) {
    const handleChange = () => alert("change")
    return { handleChange }
  }
})

app.component("child", {
  template: `
    child
  `,
  setup(props, context) {
    const { attrs, slots, emit } = context
    const handleClick = () => emit("change")
    return { handleClick }
  }
})

app.mount("#root")
3. 示例:使用 Composition API 开发 todolist

现在写一个 todolist 来感受一下 Composition API 的优势:

  const { ref, reactive, toRefs, toRef } = Vue;

  // 关于 list  *** 作的内容进行了封装
  const listRelativeEffect = () => {
    const list = reactive([])
    const addItemToList = (item) => {
      list.push(item)
    }
    return { list, addItemToList }
  }

  // 关于输入框 *** 作的内容进行了封装
  const inputRelativeEffect = () => {
    const inputValue = ref("")
    const handleInputValueChange = (e) => {
      inputValue.value = e.target.value
    }
    return { inputValue, handleInputValueChange }
  }

  const app = Vue.createApp({
    template: `
    
      
        
        
      
      
        
          {{ item }}  
        
      
    
  `,
    setup(props) {
      const { list, addItemToList } = listRelativeEffect()
      const { inputValue, handleInputValueChange } = inputRelativeEffect()
      return {
        list,
        addItemToList,
        inputValue,
        handleInputValueChange,
      }
    }
  })

  app.mount("#root")

使用了 Composition API 后,相关的逻辑可以分离出来,使得代码可维护性和可读性都有了很大的提升。

4. computed 方法生成计算属性

之前在 vue 基础中已经学了传统的 computed 语法了,现在讲一下 Composition API 里的计算属性。

4.1 基本使用

实现一个简单的计数器,并且有一行计算属性是计数的结果乘以 2 的结果:

const { ref, computed } = Vue;

const app = Vue.createApp({
  template: `
    
      {{ count }} -- 
      {{ countAddFive }}
    
  `,
  setup(props) {
    const count = ref(0)
    const handleClick = () => {
      count.value += 1
    }
    const countAddFive = computed(() => {
      return 2 * count.value
    })
    return { count, countAddFive, handleClick }
  }
})

app.mount("#root")

可以看到,在 Composition API 中,需要先引入 computed 函数,然后正常使用即可。

4.2 进阶使用

其实更详细的,computed 方法接收一个对象,里面接收 get 和 set 两个属性。

get 和 set 的值均为方法。get 方法返回提供的数据的值,set 方法用来处理属性设置的时候要执行的相应逻辑。

直接上代码,看的更清晰:

const app = Vue.createApp({
  template: `
    
      {{ count }} -- 
      {{ countAddFive }}
    
  `,
  setup(props) {
    const count = ref(0)
    const handleClick = () => {
      count.value += 1
    }
    const countAddFive = computed({
      // 获取到的值时存储的 count 的值加上 5
      get: () => count.value + 5,
      // 设置 count 的值的时候把传进来的值减了 5
      set: (param) => count.value = param - 5,
    })
    return { count, countAddFive, handleClick }
  }
})
5. watch 和 watchEffect 的使用和差异性 5.1 watch 基本用法

Composition API 里提供了 watch 方法,和传统的 watch 方法类似,写在 setup 函数里。

Composition API 中使用 watch 的方式:

watch(name, (currentValue, prevValue) => {
  console.log(currentValue, prevValue)
})

可以看到 watch 方法接收两个参数,第一个参数指的是要监听的数据,第二个参数是监听到数据变化后的回调。回调函数有两个参数,第一个参数是数据改变后的值,第二个参数是数据改变前的值。

可以看出,watch 有两个特性:

具备一定的惰性,只有在数据变化的时候才会执行参数可以拿到改变前和改变后的值 5.2 watch 监听非响应式数据

注意:监听的数据必须具有响应式,或者是个有返回值的函数

如 reactive 返回的响应式对象,里边的属性值时没有响应式的,如果要监听里面的单个属性而非整个响应式对象,就需要写成函数,或者将属性变成响应式。

解法一:写成函数

const { ref, watch, reactive } = Vue;

const app = Vue.createApp({
  template: `
  
    
      Name: 
    
    
      Name is {{ nameObj.name }}
    
  
`,
  setup(props) {
    const nameObj = reactive({ name: "sjh" })
    watch(() => nameObj.name, (currentValue, prevValue) => {
      console.log(currentValue, prevValue)
    })
    return {
      nameObj
    }
  }
})

app.mount("#root")

解法二:将属性变成响应式

const { ref, watch, reactive, toRefs } = Vue;

const app = Vue.createApp({
  template: `
  
    
      Name: 
    
    
      Name is {{ name }}
    
  
`,
  setup(props) {
    const nameObj = reactive({name: "sjh"})
    const { name } = toRefs(nameObj)
    watch(name, (currentValue, prevValue) => {
      console.log(currentValue, prevValue)
    })
    return {
      name
    }
  }
})

app.mount("#root")
5.3 watch 监听多个数据

监听多个数据的时候,没必要重复写,写成数组复用即可。

const { name, age } = toRefs(data)
watch([name, age], ([curName, curAge], [prevName, prevAge]) => {
  console.log([curName, curAge], [prevName, prevAge])
})
5.4 watchEffect 基本用法

watchEffect 侦听器相比于 watch 侦听器,更偏向于 effect。

watchEffect 和 watch 的区别 只要写了 watchEffect,回调就立即执行,没有惰性watchEffect 会自己感知内部的代码数据是否因为相关依赖发生了改变,如果发生了改变就会执行,因此不需要传递很多参数。watch 需要通过参数指定监听哪个数据。watchEffect 无法获取之前的数据和当前的数据。watch 可以获取之前和当前的数据。
watchEffect(() => {
    // 当 nameObj.name 发生改变,会触发 watchEffect 的回调函数
    console.log(nameObj.name)
})
暂停 watchEffect

例如,五秒后结束监听

const stop = watchEffect(() => {
    console.log(nameObj.name)
    setTimeout(() => {
        stop()
    }, 5000)
})

watch 暂停监听的语法和 watchEffect 一模一样

watch 立即执行

设置参数 immediate: true 即可

const stop = watch(name, (currentValue, prevValue) => {
  console.log(currentValue, prevValue)
}, {
  immediate: true
})
6. 生命周期钩子

现在用 Composition API 来写生命周期钩子,直接摘官网的:

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

选项式 API 就是 vue2 的那种 API,因为 vue 组件是通过对象内的配置新建出来的。

因为 setup 函数执行的时间就在 beforeCreate 和 created 之间,因此 Composition API 没有提供响应的钩子。

export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}
7. Provide, inject, 模板 Ref 的用法 provide 和 inject

provide 提供了可跨层级的数据的提供和获取。在 Composition API 里是这样使用的:

const app = Vue.createApp({
  setup(props) {
    const { provide } = Vue
    provide("name", ref("sjh"))
  },
  template: `
    
      
    
  `
})

app.component("child", {
  setup(props) {
    const { inject } = Vue
    // 第二个参数是未接收到 name 时的默认值
    const name = inject("name", "defaultName")
    return {
      name
    }
  },
  template: `{{ name }}`
})
用 provide 和 inject 进行子组件通知父组件

因为单向数据流的原则,子组件不能直接修改父组件的数据,因此是通过子组件通知父组件,然后父组件本身来修改自己的数据。原理和以前的 emit 类似,见下面的代码:

const app = Vue.createApp({
  setup(props) {
    const { provide, ref, readonly } = Vue
    const name = ref("name before change")
    provide("name", readonly(name))
    provide("changeName", (value) => {
      name.value = value
    })
  },
  template: `
    
      
    
  `
})

app.component("child", {
  setup(props) {
    const { inject } = Vue
    const name = inject("name")
    // 获取父组件方法
    const changeName = inject("changeName")
    // 当点击的时候,通知父组件进行修改
    const handleClick = () => {
      changeName("changed name")
    }
    return {
      name,
      handleClick
    }
  },
  template: `{{ name }}`
})
模板 ref 用法

这里的 ref 是获取 dom 节点,和之前的响应式 ref 冲突了。这里有个固定的写法:const nodeName = ref(null),此时会根据声明的变量名去 template 上寻找名称匹配的 ref 属性。

const app = Vue.createApp({
  setup(props) {
    const { ref, onMounted } = Vue
    // 获取 ref 为 hello 节点
    const hello = ref(null)
    onMounted(() => {
      console.log(hello.value)
    })
    return {
      hello
    }
  },
  template: `
    
      hello world
    
  `
})

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/944058.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-05-18
下一篇2022-05-18

发表评论

登录后才能评论

评论列表(0条)

    保存