Composition API(一)

本文整理来自深入Vue3+TypeScript技术栈-coderwhy大神新课,只作为个人笔记记录使用,请大家多支持王红元老师。

Composition API其实就是用来替代Mixin的,我们先来学习一下Mixin。

认识Mixin

目前我们是使用组件化的方式在开发整个Vue的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。

在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成,Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能,一个Mixin对象可以包含任何组件选项,当组件使用Mixin对象时,所有Mixin对象的选项将被混合进入该组件本身的选项中。

Mixin的基本使用

组件中使用Mixin一般通过一个数组,因为组件中可能使用不止一个Mixin对象。

Mixin的合并规则

如果Mixin对象中的选项和组件对象中的选项发生了冲突,那么Vue会如何操作呢?这里分成不同的情况来进行处理:

  • 情况一:如果是data函数的返回值对象
    返回值对象默认情况下会进行合并,如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据。
  • 情况二:生命周期钩子函数
    生命周期的钩子函数会被合并到数组中,都会被调用。
  • 情况三:值为对象的选项
    例如 methods、components 和 directives,将被合并为同一个对象,比如都有methods选项,并且都定义了方法,那么它们都会生效,但是如果对象的key相同,那么会取组件对象的键值对。

全局混入Mixin

如果组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的mixin。全局的Mixin可以使用应用app的方法 mixin 来完成注册,一旦注册,那么全局混入的选项将会影响每一个组件。使用全局mixin之后我们就不用在组件中一个一个写mixins: [sayHelloMixin]了。

import { createApp } from 'vue';
import App from './01_mixin和extends/App.vue';

const app = createApp(App);

app.mixin({
  data() {
    return {}
  },
  methods: {
  },
  created() {
    console.log("全局的created生命周期");
  }
});

app.mount("#app");

extends

另外一个类似于Mixin的方式是通过extends属性,extends是继承的意思,继承只会把组件的export default{}对象继承过去,组件的html结构不会继承过去。

如下代码,左边是BasePage.vue,里面有个message属性,在右边组件中我们引入BasePage.vue,然后指定extends: BasePage,就可以使用message属性了。

在实际开发中ext ends用的非常少,在Vue2中比较推荐大家使用Mixin,而在Vue3中推荐使用Composition API。

Options API的弊端

在Vue2中,我们编写组件的方式是Options API,Options API的一大特点就是在对应的属性中编写对应的功能模块,比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命周期钩子。但是这种代码有一个很大的弊端:当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中。当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散,尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的。

下面我们来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分,这种碎片化的代码使用理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题,并且当我们处理单个逻辑关注点时,需要不断的跳到相应的代码块中。

大组件的逻辑分散

Options API:

Composition API:

如果我们能将同一个逻辑关注点相关的代码收集在一起会更好,这就是Composition API想要做以及可以帮助我们完成的事情,所以也有人把Vue Composition API简称为VCA。

认识Composition API

为了开始使用Composition API,我们需要有一个可以实际使用它(编写代码)的地方,在Vue组件中,这个位置就是 setup 函数。setup其实就是组件的另外一个选项,只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项,比如methods、computed、watch、data、生命周期等等。

setup函数的参数

我们先来研究一个setup函数的参数,它主要有两个参数:props和context。

props非常好理解,它其实就是父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可以直接通过props参数获取:

  • 对于定义props的类型,我们还是和之前的规则是一样的,在props选项中定义,并且在template中依然是可以正常去使用props中的属性,比如message。
  • 如果我们在setup函数中想要使用props,那么不可以通过 this 去获取(后面我会讲到为什么),因为props有直接作为参数传递到setup函数中,所以我们可以直接通过参数来使用即可,比如:props.message。

另外一个参数是context,我们也称之为是一个SetupContext,它里面包含三个属性:

  • attrs:所有的非prop的attribute;
  • slots:父组件传递过来的插槽(这个在以渲染函数返回时才会有用,后面会讲到,用的不多);
  • emit:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过 this.$emit发出事件);
// setup(props, context), 下面是对象解构
setup(props, {attrs, slots, emit}) {
  console.log(props.message);
  console.log(attrs.id, attrs.class);
  console.log(slots);
  console.log(emit);
} 

setup函数的返回值

setup函数的返回值可以在模板template中被使用,也就是说我们可以通过setup的返回值来替代data选项。

// setup(props, context), 下面是对象解构
setup(props, {attrs, slots, emit}) {
  console.log(props.message);
  console.log(attrs.id, attrs.class);
  console.log(slots);
  console.log(emit);

  //返回数据对象
  return { 
    title: "Hello Home",
    counter: 100
  }
},

甚至是我们可以返回一个执行函数来代替在methods中定义的方法,如下计数器的案例:

<template>
  <div>
    Home Page
    <h2>{{message}}</h2>

    <h2>{{title}}</h2>
    <h2>当前计数: {{counter}}</h2>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
  export default {
    props: {
      message: {
        type: String,
        required: true
      }
    },
    setup() {
      let counter = 100;

      // 局部函数
      const increment = () => {
        counter++;
        console.log(counter);
      }

      return {
        title: "Hello Home",
        counter,
        increment
      }
    }
  }
</script>

<style scoped>
</style>

但是,如果我们将 counter 进行 increment 操作时,是否可以实现界面的响应式呢?
答案是不可以。这是因为对于一个定义的变量 (let counter = 100) 来说,默认情况下,Vue并不会跟踪它的变化来引起界面的响应式操作。我们以前在data()函数中定义的是响应式的,那是因为Vue内部通过reactive()函数包裹了一下。

setup不可以使用this

官方关于this有这样一段描述:

表达的含义是this并没有指向当前组件实例,并且在setup被调用之前,data、computed、methods等都没有被解析,所以无法在setup中获取this。

其实之前的这段描述是和源码有出入的,coderwhy向官方提交了PR做出了描述的修改,后来coderwhy的PR也有被合并到官方文档中。之前的描述大概含义是不可以使用this是因为组件实例还没有被创建出来。

其实Vue源码是在调用createComponentInstance()方法之后再调用的setup()函数,所以调用setup()函数的时候组件实例肯定已经创建出来了,只不过在setup()函数中没有进行任何this的绑定,所以不可以使用this。

coderwhy之前关于this的描述问题

coderwhy是如何发现官方文档的错误的呢?

在阅读源码的过程中,代码是按照如下顺序执行的:

  1. 调用 createComponentInstance 创建组件实例;
  2. 调用 setupComponent 初始化 component 内部的操作;
  3. 调用 setupStatefulComponent 初始化有状态的组件;
  4. 在 setupStatefulComponent 取出了 setup 函数;
  5. 通过 callWithErrorHandling 的函数执行 setup;

从上面的代码我们可以看出,组件的instance肯定在执行setup函数之前就已经创建出来了,只不过在setup()函数中没有进行任何this的绑定,所以不可以使用this。

Reactive API

接着上面计数器的案例,如果想要为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive函数。

<template>
  <div>
    Home Page
    <h2>{{message}}</h2>
    <h2>当前计数: {{state.counter}}</h2>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
  //先导入函数
  import { reactive } from 'vue';

  export default {
    props: {
      message: {
        type: String,
        required: true
      }
    },
    setup() {
      const state = reactive({
        counter: 100
      })

      // 局部函数
      const increment = () => {
        state.counter++;
        console.log(state.counter);
      }

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

<style scoped>
</style>

那么这是什么原因呢?为什么就可以变成响应式的呢?
这是因为当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集,当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面)。事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的。

上面的reactive函数要求传入的必须是对象或数组,所以上面,即使我们只有counter: 100,也需要包裹成对象,就显得很麻烦,这时候我们可以使用Ref API。

Ref API

reactive函数对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型,如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告:

这个时候Vue3给我们提供了另外一个Ref API,ref 会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值,这就是 ref 名称的来源,它内部的值是在 ref 的 value 属性中被维护的。

let counter = ref(100);

这里有两个注意事项:

  1. 在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用。
  2. 但是在 setup 函数内部,它依然是一个 ref 引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式。
<template>
  <div>
    Home Page
    <h2>{{message}}</h2>
    <!-- 当我们在template模板中使用ref对象, 它会自动进行解包 -->
    <h2>当前计数: {{counter}}</h2>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
  //先导入函数
  import { ref } from 'vue';

  export default {
    props: {
      message: {
        type: String,
        required: true
      }
    },
    setup() {
      // counter编程一个ref的可响应式的引用
      let counter = ref(100);

      // 局部函数
      const increment = () => {
        // 在setup中就要通过.value访问
        counter.value++;
        console.log(counter.value);
      }

      return {
        counter,
        increment
      }
    }
  }
</script>

<style scoped>
</style>

Ref的浅层解包

模板中的解包是浅层解包,比如:我们用ref包裹一个"Hello World",赋值给message,再把message放到一个普通info对象里面,这时候在模板中我们只能使用info.message.value来获取"Hello World",直接使用info.message获取的就只是一个包裹对象,而不是包裹对象里面的"Hello World"。

如果我们把上面的info对象用reactive函数包裹一下,那么在模板中使用时,它会自动解包:

总结:

  • 如果ref对象没被其他对象包裹,在模板中使用会自动解包。
  • 如果ref对象有被其他对象包裹,如果包裹的是普通对象,在模板中使用时,不会自动解包,如果是reactive函数包裹的对象,则会自动解包。
  • 但是在 setup 函数内部,它依然是一个 ref 引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式。

认识readonly

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

Vue3为我们提供了 readonly 的方法,readonly会返回原生对象的只读代理(也就是它依然是一个Proxy,这个 proxy 的 set 方法被劫持,并且不能对其进行修改)。

在开发中常见的 readonly 方法会传入三个类型的参数:
类型一:普通对象;
类型二:reactive返回的对象;
类型三:ref的对象;

readonly的使用

readonly返回的对象都是不允许修改的,但是经过readonly处理的原来的对象是允许被修改的。比如 const info = readonly(obj),info对象是不允许被修改的,obj可以被修改,当obj被修改时,readonly返回的info对象也会被修改,所以一般我们会把info传递给其他组件使用。

<template>
  <div>
    <button @click="updateState">修改状态</button>
  </div>
</template>

<script>
  // 导入函数
  import { reactive, ref, readonly } from 'vue';

  export default {
    setup() {
      // 1.普通对象
      const info1 = {name: "why"};
      const readonlyInfo1 = readonly(info1);

      // 2.响应式的对象reactive
      const info2 = reactive({
        name: "why"
      })
      const readonlyInfo2 = readonly(info2);

      // 3.响应式的对象ref
      const info3 = ref("why");
      const readonlyInfo3 = readonly(info3);

      const updateState = () => {
        // info1.name = "coderwhy"; 可修改
        // info2.name = "coderwhy"; 可修改
        // info3.value = "coderwhy"; 可修改

        // readonlyInfo1.name = "coderwhy" 不可修改 非响应式
        // readonlyInfo2.name = "coderwhy" 不可修改 响应式
        // readonlyInfo3.value = "coderwhy" 不可修改 响应式
      }

      return {
        updateState,
      }
    }
  }
</script>

<style scoped>
</style>

在我们传递给其他组件数据时,往往希望其他组件使用我们传递的内容,但是不允许它们修改,这时就可以使用readonly了,而且我们希望子组件使用的数据是响应式的,所以我们可以使用reactive函数或者ref。

Reactive判断的API

  • isProxy:检查对象是否是由 reactive 或 readonly创建的 proxy。
  • isReactive:检查对象是否是由 reactive创建的响应式代理,如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true。
  • isReadonly:检查对象是否是由 readonly 创建的只读代理。
  • toRaw:返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用,请谨慎使用)。
  • shallowReactive:翻译过来就是浅层响应式,创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换(深层还是原生对象)。比如:对象里面还有对象,如果我们希望外面的属性改变才是响应式的,里面深层对象的改变不是响应式的,这时候可以用shallowReactive。
  • shallowReadonly:浅层只读,创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读可写的)。

toRefs

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

这是因为解构其实就相当于重新声明变量然后赋值,如下:

//解构
const { name, age } = state;
//相当于如下
const name = "why";
const age = 18;

那么有没有办法让我们解构出来的属性是响应式的呢?
Vue为我们提供了一个toRefs函数,可以将reactive返回的对象中的属性都转成ref,那么我们再次进行结构出来的 name 和 age 本身都是 ref的。

然后在模板中我们直接使用name、age就行,因为模板中会自动解构,但是在下面的逻辑代码中我们还是要使用 .value 来修改值:

const changeAge = () => {
  age.value++;
}

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

toRef

上面的toRefs是将reactive对象中的所有属性都转成ref,建立链接,但是有时候有些属性我们用不到,这就额外增加了不必要的开销。

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

//普通的解构
let { name } = state;
//解构age,并建立连接,第一个参数是对象,第二个参数是对象的属性名
let age = toRef(state, "age");

ref其他的API

  • unref:如果我们想要获取一个ref引用中的value,那么也可以通过unref方法。如果参数是一个 ref,则返回内部值,否则返回参数本身,这是val = isRef(val) ? val.value : val的语法糖函数。
  • isRef:判断值是否是一个ref对象。
  • shallowRef:创建一个浅层的ref对象。
  • triggerRef:手动触发和 shallowRef 相关联的副作用。

默认情况下,不管是reactive还是ref创建的响应式对象都是深层次的,如下:

const info = ref({name: "why"})

const changeInfo = () => {
  //通过value拿到原对象,再修改原对象的值,这时候界面的数据也会改变,这就是深层次的响应式
  info.value.name = "james";
}

如果我们不希望深层次的响应式,只希望改变外面大的对象的时候才是响应式的,改变里面的属性值不是响应式的,我们就可以使用shallowRef。
如果我们又想触发响应式了,就可以调用triggerRef,来手动触发相关联的副作用,这时候界面就又变成响应式的了。

//浅层次的响应式
const info = shallowRef({name: "why"})

const changeInfo = () => {
  //这时候修改里面的属性值,界面就不会变化了
  info.value.name = "james";
  //如果我们又想触发响应式,就可以调用triggerRef,来触发相关联的副作用,这时候界面就又变成响应式的了
  triggerRef(info);
}

customRef

下面代码,我们在输入框中输入文字,下面显示的文字会立马更新:

<template>
  <div>
    <input v-model="message"/>
    <h2>{{message}}</h2>
  </div>
</template>

<script>
  import ref from 'vue';

  export default {
    setup() {
      const message = ref("Hello World");

      return {
        message
      }
    }
  }
</script>

<style scoped>
</style>

如果我们不想更新这么频繁,比如输入后300ms才更新,那么使用ref就做不到了,所以我们需要自定义ref。

创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制。它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数,并且应该返回一个带有 get 和 set 的对象。

自定义ref的useDebounceRef.js文件代码如下:

import { customRef } from 'vue';

// 自定义ref
export default function(value, delay = 300) {
  let timer = null;
  return customRef((track, trigger) => {
    return {
      get() {
        //收集依赖
        track();
        return value;
      },
      set(newValue) {
        //如果在300ms内又输入值了,就把定时器清空,也就是取消触发更新
        clearTimeout(timer);
        timer = setTimeout(() => {
          value = newValue;
          //触发更新
          trigger();
        }, delay);
      }
    }
  })
}

然后使用我们自定义的ref,就可以达到我们想要的延迟效果了,这样做可以提升一点点性能。

<template>
<div>
  <input v-model="message"/>
  <h2>{{message}}</h2>
</div>
</template>

<script>
import debounceRef from './hook/useDebounceRef';

export default {
  setup() {
    const message = debounceRef("Hello World");

    return {
      message
    }
  }
}
</script>

<style scoped>
</style>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容