Vue3 Composition API

一、响应式基础

前提:你会使用 setup 函数或 <script setup>语法

1.reactive

我们可以使用 reactive() 函数创建一个响应式对象数组

import { reactive } from 'vue'

const state = reactive({ count: 0 })
  • 当我们使用 reactive 函数处理我们的数据之后,数据被使用时就会进行依赖收集;
  • 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);
  • 事实上,在使用Options API时编写的 data 选项,也是在内部交给了 reactive 函数将其变成响应式对象的

注意:

reactive 函数只能包裹对象或数组,包裹基本数据类型会丧失响应式,并且控制会打印出警告

2. ref

reactive() 的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。为此,Vue 提供了一个 ref()方法来允许我们创建可以使用任何值类型的响应式 ref

import { ref } from 'vue'

const countRef = ref(0)
// 使用
console.log(countRef.value)

使用ref()函数定义的响应式数据,是一个带有.value的Ref对象,在<script>中使用该值时,需要.value赋值或取值。

而在template模板中,则会自动解包,取出.value的值。

实例代码:

<template>
  <h2>composition API</h2>
  <p>ref-simple: {{ simple }}</p>
  <p>ref-obj: {{ obj }}</p>
  <div>
    <p><button @click="simple++">更改simple</button></p>
    <p><button @click="obj.name += '1'">更改obj.name</button></p>
    <p><button @click="editObjName">更改obj.name</button></p>
  </div>
  <hr />
  <p>reactive-state: {{ state }}</p>
  <p>reactive-count: {{ count }}</p>
  <div>
    <p><button @click="state.firstName += '1'">更改reactive-state</button></p>
    <p><button @click="count++">更改reactive-count</button></p>
  </div>
</template>

<script>
import { reactive, ref } from 'vue';
export default {
  name: 'App',
  setup() {
    const simple = ref(0);
    const obj = ref({ name: 'fct' });
    // reactive
    const state = reactive({ firstName: 'Steven', secordName: 'Diff' });
    const count = reactive(0);	// 报警告
    function editObjName() {
      obj.value.name += '1';
    }
    return {
      simple,
      obj,
      state,
      count,
      editObjName
    };
  }
};
</script>

2.1 获取HTML元素或组件实例

使用ref()函数,要声明一个同名的 ref。

<template>
  <input ref="inputRef" />
</template>

<script setup>
import { ref, onMounted } from 'vue'

// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const inputRef = ref(null)

onMounted(() => {
  inputRef.value.focus()
})
// 如果不使用 <script setup>,需确保从 setup() 返回 ref:
// export default {
//   setup() {
//     const inputRef = ref(null)
//     // ...
//     return {
//       inputRef
//     }
//   }
// }
</script>

3. readonly

我们通过reactive()或者ref()可以获取到一个响应式的对象,但是某些情况下,我们传入响应式对象给其他地方(组件),希望在另外一个地方(组件)被使用,但是不能被修改,这个时候可以使用readonly()

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

实例代码:

const original = reactive({ count: 0 })

const copy = readonly(original)

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

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

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

二、reactive、ref、readonly相关API

1. reactive判断的API

1.1 isProxy

检查对象是否是由 reactive 或 readonly创建的 proxy

1.2 isReactive

  • 检查对象是否是由 reactive 创建的响应式代理
  • 如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true。

1.3 isReadonly

检查对象是否是由 readonly 创建的只读代理

1.4 toRaw

返回 reactive 或 readonly 代理的原始对象建议保留对原始对象的持久引用。请谨慎使用)。

1.5 shallowReactive

创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。

1.6 shallowReadonly

创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。

2.toRefs

如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive 返回的state对象数据都不再是响应式的:

import { reactive } from 'vue';
const state = reactive({
  msg: 'fct',
  tips: 'vue3'
});
const { msg, tips } = state;
// msg 为非响应式的
  • Vue为我们提供了一个toRefs的函数,可以将reactive返回的对象中的属性都转成ref

  • 那么我们再次进行结构出来的 msg 本身都是 ref的;

import { reactive, toRefs } from 'vue';

const state = reactive({
  msg: 'fct',
  tips: 'vue3'
});
const { msg, tips } = toRefs(state);

这种做法相当于已经在state.msgref.value之间建立了链接,任何一个修改都会引起另外一个变化;

3. toRef

如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法

import { reactive, toRef } from 'vue';

const state = reactive({
  msg: 'fct',
  tips: 'vue3'
});
const tips = toRef(state, 'tips');
// tips 为响应式数据

4. ref其他的API

4.1 unref

如果我们想要获取一个ref引用中的value,那么也可以通过unref方法

  • 如果参数是一个 ref,则返回内部值,否则返回参数本身

  • 这是 val = isRef(val) ? val.value : val 的语法糖函数;

    import { ref, unref } from 'vue';
    
    const fct = ref(999);
    console.log(fct.value, unref(fct));
    

4.2 isRef

判断值是否是一个ref对象

4.3 shallowRef

创建一个浅层的ref对象

// 不能监听 info.name 发生的改变,因为是浅层Ref对象
const info = shallowRef({ name: 'fct' });

// 能监听 num 发生的改变
const num = shallowRef(32);

4.4 triggerRef

手动触发和 shallowRef 相关联的副作用

<template>
  <div>
    <p>info: {{ info }}</p>
    <p>num: {{ num }}</p>
    <button @click="info.name += '1'">更改shallowRef-info中的name</button>
    <button @click="editInfoName">triggerRef-更改shallowRef-info中的name</button>
    <button @click="num++">shallowRef-num++</button>
  </div>
</template>

<script setup>
import { shallowRef, triggerRef } from 'vue';

const info = shallowRef({ name: 'fct' });
const num = shallowRef(32);

function editInfoName(params) {
  info.value.name += '1';
  // 手动触发更新
  triggerRef(info);
}
</script>

三、其他API

1. computed计算属性

1.1 基本使用

与Vue2相同,推荐使用计算属性来描述依赖响应式状态的复杂逻辑。

computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value

<template>
  <div>
    <p>computed: {{ fullName }}</p>
  </div>
</template>

<script>
import { computed, reactive } from 'vue';

export default {
  setup() {
    const state = reactive({
      fName: 'Steven',
      sName: 'Jiff'
    });
    const fullName = computed(() => {
      return state.fName + '--' + state.sName;
    });

    return {
      fullName
    };
  }
};
</script>

1.2 可写可读计算属性

计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 gettersetter 来创建:

<template>
  <div>
    <p>computed-fullName: {{ fullName }}</p>
    <p>computed-fullName-2: {{ fullName2 }}</p>
    <button @click="editFullName2">更改fullname2</button>
  </div>
</template>

<script setup>
import { computed, reactive } from 'vue';
const state = reactive({
  fName: 'Steven',
  sName: 'Jiff'
});
const fullName = computed(() => {
  return state.fName + '--' + state.sName;
});
const fullName2 = computed({
  get() {
    return state.fName + '--' + state.sName;
  },
  set(value) {
    const arr = value.split('--');
    state.fName = arr[0];
    state.sName = arr[1];
  }
});
function editFullName2() {
  fullName2.value = 'go--back';
}
</script>

2. 生命周期钩子

在setup函数中,不再需要beforeCreatecreated两个生命周期函数。

使用前,需要在setup函数中注册生命周期钩子函数。

举例:onMounted钩子,在组件完成初始渲染并创建 DOM 节点后运行代码,

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  console.log(`the component is now mounted.`)
})
</script>

Options API 和 Composition API的生命周期钩子对比:

Options API Composition API
beforeCreate not need
created not need
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onUnmounted
unmounted onUnmounted
activated onActivated
deactivated onDeactivated

3. provide/inject

3.1 provide

使用provide(),提供一个值,可以被后代组件注入。

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

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

<script>
import { reactive, readonly, ref, watchEffect, onMounted, provide } from 'vue';
import Son from './components/Son.vue';
export default {
  name: 'App',
  components: {
    Son
  },
  setup() {
    // provide/inject
    const good = reactive({
      id: '001',
      name: '小米手机'
    });
    const goodNum = ref(100);
    provide('good', good);
    provide('goodNum', goodNum);
    return {
      good,
      goodNum
    };
  }
};
</script>

3.2 inject

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

参数:

  • 第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。
  • 第二个参数是可选的,即在没有匹配到 key 时使用的默认值。它也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么你必须将 false 作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。
  • 第三个参数,用来指定默认值就是一个函数,而不是工厂函数。
<template>
  <div>
    <p>{{ good }}</p>
    <p>{{ goodNum }}</p>
    <button @click="good.name = 'huawei'">更改good.name</button>
    <button @click="goodNum++">更改goodNum</button>
  </div>
</template>

<script setup>
import { inject, watch } from 'vue';

const good = inject('good');
const goodNum = inject('goodNum');
watch(
  good,
  (newValue, oldValue) => {
    console.log(newValue?.name, oldValue?.name);
    console.log(newValue, oldValue);
  },
  { immediate: true }
);
watch(goodNum, (newValue, oldValue) => {
  console.log(newValue, oldValue);
});
</script>

4.侦听器

4.1 watch

在组合式 API 中,我们可以使用 watch() 函数在每次响应式状态发生变化时触发回调函数。

  • watch第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)一个响应式对象一个 getter 函数、或多个数据源组成的数组;
  • 第二个参数是回调函数,回调有两个参数(newValue新值,oldValue旧值);
  • 第三个参数是配置对象
    • immediate 是否立即执行一次
    • deep 深层侦听
<script setup>
import { ref, watch, watchEffect } from 'vue';
const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

</script>
  • 直接给 watch() 传入一个响应式对象作为第一个参数,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发。
const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
})

obj.count++

4.1.1 注意:

你不能直接侦听响应式对象的属性值:

const person = ref({ name: 'fct', age: 18 });

// 错误,因为 watch() 得到的第一个参数是一个 number
watch(person.value.age, () => {
      console.log('age1');
});

// 正确做法:
watch(
  // 提供一个 getter 函数
  () => person.value.age,
  () => {
    console.log('age2');
  }
);

4.2 watchEffect

watch() 是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。就可以使用watchEffect()函数。

watchEffect() 会立即执行一遍回调函数,如果这时函数产生了副作用,Vue 会自动追踪副作用的依赖关系,自动分析出响应源。

<template>
  <div>
    <p>watch-person: {{ person }}</p>
    <p>watch-dog: {{ dog }}</p>
    <button @click="person.age++">person.age++</button>
    <button @click="dog.age++">dog.age++</button>
  </div>
</template>

<script>
import { ref, watchEffect } from 'vue';

export default {
  setup() {
    const person = ref({ name: 'fct', age: 18 });
    const dog = ref({ name: 'lele', age: 3 });
    watchEffect(() => {
      console.log(person.value, person.value.age);
    });
    return {
      person,
      dog
    };
  }
};
</script>
  • 这个例子中,回调会立即执行。在执行期间,它会自动追踪 person.value 作为依赖(和计算属性的行为类似)。每当 person.value 变化时,回调会再次执行。
  • 当改变dog.value中的值时,watchEffect中的回调函数不会执行。

4.2.1 消除副作用

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

watchEffect(async (onCleanup) => {
  const { response, cancel } = doAsyncWork(id.value)
  // `cancel` 会在 `id` 更改时调用
  // 以便取消之前
  // 未完成的请求
  onCleanup(() => {
      // 取消上一次请求
      cancel();
  })
  data.value = await response
})

4.3 停止侦听

setup()<script setup> 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。

但一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。

<script setup>
import { watchEffect } from 'vue'

// 它会自动停止
watchEffect(() => {})

// ...这个则不会!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

手动停止侦听器:

要手动停止一个侦听器,请调用 watchwatchEffect 返回的函数:

const num = ref(0)
const unwatch1 = watch(num, () => {})

const unwatch = watchEffect(() => {})

// ...当该侦听器不再需要时
unwatch()
unwatch1()

4.4 回调触发的时机

当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。

默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。

如果想在侦听器回调中能访问被 Vue 更新之后的DOM,你需要指明 flush: 'post' 选项:

watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

其中后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect()

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})

4.4.1 实例

<template>
  <div>
    <p>watch-person: {{ person }}</p>
    <p>watch-dog: {{ dog }}</p>
    <input ref="inputRef" type="text" v-model.number="person.age" />
    <button @click="person.age++">person.age++</button>
    <button @click="dog.age++">dog.age++</button>
  </div>
</template>

<script>
import { ref, watch, watchEffect, watchPostEffect } from 'vue';

export default {
  setup() {
    const person = ref({ name: 'fct', age: 18 });
    const dog = ref({ name: 'lele', age: 3 });
    const inputRef = ref(null);
    // 回调触发的时机
    // 错误侦听
    // watch(person.value.age, () => {
    //   console.log('age1');
    // });
    watch(
      () => person.value.age,
      (newValue, oldValue) => {
        // if (inputRef.value) {
        console.log('person.value.age--new', newValue);
        console.log('person.value.age--old', oldValue);
        console.log('input.target.value-组件更新前', inputRef.value.value);
        // }
      }
      // {
      //   flush: 'post'
      // }
    );
    watchEffect(
      () => {
        if (person.value.age) {
          console.log('watchEffect-flush:post-组件更新后', inputRef.value.value);
        }
      },
      { flush: 'post' }
    );
    watchPostEffect(() => {
      if (person.value.age) {
        console.log('watchPostEffect-组件更新后', inputRef.value.value);
      }
    });
    return {
      person,
      dog,
      inputRef
    };
  }
};
</script>
posted @ 2022-10-27 17:03  青柠i  阅读(143)  评论(0编辑  收藏  举报