Vue学习笔记08-Composition_API

setup函数的使用,reactive/ref,readonly等API,computed,watchEffect,watch,生命周期钩子,组件间通信,Hook,h函数,jsx的使用,自定义指令,teleport,vue插件

1. Options API的弊端

  • 在Vue2中,编写组件的方式是Options API:

    • Options API的一大特点就是在对应的属性中编写对应的功能模块;
    • 比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命周期钩子;
  • 但是这种代码有一个很大的弊端:

    • 实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中;
    • 组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散;
    • 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人);
  • 来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分:

    • 这种碎片化的代码使用理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题;
    • 并且处理单个逻辑关注点时,需要不断的跳到相应的代码块中;

2. 大组件的逻辑分散

  • 如果能将同一个逻辑关注点相关的代码收集在一起会更好

  • 这就是Composition API想要做的事情,以及可以帮助完成的事

  • 也有人把Vue Composition API简称为VCA

3. 认识Composition API

  • 在Vue组件中,使用 setup 函数;

  • setup其实就是组件的另外一个选项

    • 只不过这个选项强大到可以用它来替代之前所编写的大部分其他选项;
    • 比如methods、computed、watch、data、生命周期等等;

3.1. setup函数的参数

  • 主要有两个参数

    • props
    • context
  • props就是父组件传递过来的属性被放到props对象中,在setup中如果需要使用,那么就可以直接通过props参数获取:

    • 对于定义props的类型,还是和之前的规则是一样的,在props选项中定义;
    • 并且在template中依然是可以正常去使用props中的属性,比如message;
    • 如果在setup函数中想要使用props,那么不可以通过 this 去获取;
    • 因为props有直接作为参数传递到setup函数中,所以可以直接通过参数来使用即可;
  • 另外一个参数是context,也称之为是一个 SetupContext,它里面包含三个属性:

    • attrs:所有的非prop的attribute;
    • slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用);
    • emit:当组件内部需要发出事件时会用到emit(因为不能访问this,所以不可以通过 this.$emit发出事件);

3.2. setup函数的返回值

  • setup既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?

    • setup的返回值可以在模板template中被使用;
    • 也就是说可以通过setup的返回值来替代data选项;
  • 甚至可以返回一个执行函数来代替在methods中定义的方法:

  • 但是,如果将 counter 在 increment 进行操作时,是否可以实现界面的响应式呢?

    • 答案是不可以;
    • 这是因为对于一个定义的变量来说,默认情况下,Vue并不会跟踪它的变化,来引起界面的响应式操作;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>
<button @click="increment">{{counter}}</button>
</div>
</template>

<script>
export default {
setup(){
let counter = 0;

const increment = ()=>{
counter++;
console.log(counter);
}

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

<style scoped>

</style>

4. this问题

4.1. setup不可以使用this

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

    • https://vuejs.org/api/composition-api-setup.html#basic-usage

    • setup() itself does not have access to the component instance - this will have a value of undefined inside setup(). You can access Composition-API-exposed values from Options API, but not the other way around.

    • 表达的含义是 this并没有指向当前组件实例;

    • 并且在setup被调用之前,data、computed、methods等都没有被解析;

    • 所以无法在setup中获取this;

  • 其实在之前的这段描述是和源码有出入的:

    • 之前的描述大概含义是不可以使用this是因为组件实例还没有被创建出来;
    • 显然是错误的;

4.2. 关于this的描述问题

  • /packages/runtime-core/src/component.ts

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

    • 调用 createComponentInstance 创建组件实例;

      1
      2
      3
      4
      export function createComponentInstance() {
      const instance: ComponentInternalInstance = {}
      return instance
      }
    • 调用 setupComponent 初始化component内部的操作;

      1
      2
      3
      export function setupComponent(
      instance: ComponentInternalInstance
      )
    • 调用 setupStatefulComponent 初始化有状态的组件;

    • 在 setupStatefulComponent 取出了 setup 函数;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      function setupStatefulComponent(
      instance: ComponentInternalInstance,
      isSSR: boolean
      ) {
      const Component = instance.type as ComponentOptions

      // 0. create render proxy property access cache
      instance.accessCache = Object.create(null)
      // 1. create public instance / render proxy
      // also mark it raw so it's never observed
      instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))

      // 2. call setup()
      const { setup } = Component
      if (setup) {
      const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

      setCurrentInstance(instance)
      pauseTracking()
      const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
      )
      resetTracking()
      unsetCurrentInstance()

      }else {
      finishComponentSetup(instance, isSSR)
      }
      }
    • 通过callWithErrorHandling 的函数执行 setup;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      export function callWithErrorHandling(
      fn: Function,
      instance: ComponentInternalInstance | null,
      type: ErrorTypes,
      args?: unknown[]
      ) {
      let res
      try {
      res = args ? fn(...args) : fn()
      } catch (err) {
      handleError(err, instance, type)
      }
      return res
      }
  • 从上面的代码可以看出, 组件的instance肯定是在执行 setup 函数之前就创建出来

5. Reactive API

5.1. 认识reactive api

  • 如果想为在setup中定义的数据提供响应式的特性,可以使用reactive的函数:

  • 那么这是什么原因呢?为什么就可以变成响应式的呢?

    • 当使用reactive函数处理数据之后,数据再次被使用时就会进行依赖收集;
    • 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);
    • 事实上,编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div>
<button @click="increment">{{state.counter}}</button>
</div>
</template>

<script>
import { reactive } from 'vue';
export default {
setup(){
const state = reactive({
counter: 0
});

const increment = ()=>{
state.counter++;
console.log(state.counter);
}

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

<style scoped>

</style>

5.2. Reactive判断的API

  • isProxy

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

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

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

    • 返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用)
  • shallowReactive

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

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

6. Ref API

6.1. 认识ref api

  • reactive API对传入的类型是有限制的,要求必须传入的是一个对象或者数组类型:

    • 如果传入一个基本数据类型(String、Number、Boolean)会报一个警告;
  • Vue3提供了另外一个API:ref API

    • https://vuejs.org/api/reactivity-core.html#ref
    • ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源;
    • 它内部的值是在ref 的 value 属性中被维护的;
  • 这里有两个注意事项:

    • 在模板中引入ref的值时,Vue会自动进行解包操作,并不需要在模板中通过 ref.value 的方式来使用;
    • 但是在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,依然需要使用 ref.value的方式;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div>
<!-- 模板中自动进行解包 -->
<button @click="increment">{{counter}}</button>
</div>
</template>

<script>
import { ref } from 'vue';
export default {
setup(){
let counter = ref(0);

const increment = ()=>{
// setup 函数内部仍为引用,需要使用ref.value
counter.value++;
/**
* RefImpl {__v_isShallow: false,
* dep: Set(1),
* __v_isRef: true,
* _rawValue: 1,
* _value: 1
* }
*/
console.log(counter);
}

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

<style scoped>

</style>

6.2. Ref自动解包

  • 模板中的解包是浅层的解包,如果将 ref 放到一个reactive的属性当中,那么模板中使用时,它会自动解包:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <template>
    <div>
    <button @click="increment">{{info.counter}}</button>
    </div>
    </template>

    <script>
    import { reactive,ref } from 'vue';
    export default {
    setup(){

    let counter = ref(0);

    const info = reactive({
    counter
    });

    const increment = ()=>{
    counter.value++;
    console.log(counter);
    }

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

    <style scoped>

    </style>

6.3. toRefs

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <template>
    <div>
    <input type="text" v-model="name">
    <h2>{{name}}</h2>
    <input type="number" step="1" v-model="age">
    <h2>{{age}}</h2>
    </div>
    </template>

    <script>
    import { reactive } from 'vue';
    export default {
    setup(){
    const state = reactive({
    name: 'test',
    age: 18
    });
    let { name, age } = state;

    return {
    name,
    age
    }
    }
    }
    </script>

    <style scoped>

    </style>
  • 那么有没有办法使解构出来的属性是响应式的呢?

    • Vue提供了一个toRefs的函数,可以将reactive返回的对象中的属性都转成ref;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      import { reactive, toRefs } from 'vue';
      export default {
      setup(){
      const state = reactive({
      name: 'test',
      age: 18
      });
      let { name, age } = toRefs(state);

      return {
      name,
      age
      }
      }
      }
    • 那么再次进行结构出来的 name 和 age 本身都是 ref的;

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

6.4. toRef

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

1
2
let name = toRef(state,'name');
let { age } = state;

6.5. ref其他的API

  • unref

    • 如果想要获取一个ref引用中的value,那么也可以通过unref方法:
      • 如果参数是一个 ref,则返回内部值,否则返回参数本身;
      • 这是 val = isRef(val) ? val.value : val 的语法糖函数;
  • isRef

    • 判断值是否是一个ref对象
  • shallowRef

    • 创建一个浅层的ref对象;
  • triggerRef

    • 手动触发和 shallowRef 相关联的副作用:
1
2
3
4
5
6
7
8
9
10
const info = shallowRef({
name: 'Test'
});

const changeInfo = ()=>{
info.value.name = "coder";
// 手动触发
triggerRef(info);
}

6.6. customRef

  • 创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制:

    • 它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数;
    • 并且应该返回一个带有 get 和 set 的对象;
  • 使用一个的案例:

    • 对双向绑定的属性进行debounce(节流)的操作;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { customRef } from 'vue';

export default function(value,delay=300){
let timer = null;
return customRef((track,trigger)=>{
return{
get(){
track();
return value;
},
set(newValue){
// 清除已有定时器
clearTimeout(timer);
timer = setTimeout(()=>{
value=newValue;
trigger();
},delay);


}
}
});
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div>
<input type="text" v-model="message">
<h2>{{message}}</h2>
</div>
</template>

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

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

return {
message
}
}

}
</script>

<style scoped>

</style>

7. readonly

7.1. 认识readonly

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

    • Vue3提供了readonly的方法;
    • readonly会返回原生对象的只读代理(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持,并且不能对其进行修改);
  • 在开发中常见的readonly方法会传入三个类型的参数:

    • 类型一:普通对象;
    • 类型二:reactive返回的对象;
    • 类型三:ref的对象;

7.2. readonly的使用

  • 在readonly的使用过程中,有如下规则:

    • readonly返回的对象都是不允许修改的;
    • 但是经过readonly处理的原来的对象是允许被修改的;
      • 比如 const info = readonly(obj),info对象是不允许被修改的;
      • 当obj被修改时,readonly返回的info对象也会被修改;
      • 但是不能去修改readonly返回的对象info;
  • 其实本质上就是readonly返回的对象的setter方法被劫持了而已;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<div>
<h2>title</h2>
</div>
</template>

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

export default {
setup(){
// readonly通常会传入三种类型的数据
// 1.传入一个普通对象
const info = {
name: 'test',
age: 18
};
const state = readonly(info);
// Set operation on key "name" failed: target is readonly.
state.name='why';
// 原来的对象是允许被修改的
info.name = 'Test';
console.log(state);

// 2.传入reactive对象
const info2 = reactive({
name: 'test2',
age: 18
});
const state2 = readonly(info2);
// Set operation on key "name" failed: target is readonly
state2.name='why2';
// 原来的对象是允许被修改的
info2.name = 'Test2';
console.log(state2);

// 3.ref对象
const name3 = ref('test3');
const state3 = readonly(name3);
// Uncaught TypeError: "name3" is read-only
// name3='why3';
console.log(name3.value);
}
}
</script>

<style scoped>

</style>

8. computed

  • 计算属性computed:

    • 当某些属性是依赖其他状态时,可以使用计算属性来处理
    • 在Options API中,使用computed选项来完成的;
    • 在Composition API中,在 setup 函数中使用 computed 方法来编写一个计算属性;
  • 如何使用computed呢?

    • 方式一:接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;
    • 方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<template>
<div>
<h2>{{fullName}}</h2>
<button @click="changeName">按钮</button>
</div>
</template>

<script>
import { computed, ref } from 'vue';
export default {
setup(){
const firstName = ref("Kobe");
const lastName = ref('Bryant');

// 方式一:接收一个getter函数
// 返回一个不变的 ref 对象
// const fullName = computed(()=>firstName.value+" "+lastName.value);

// 方式二:接收一个具有 get 和 set 的对象
const fullName = computed({
get(){
return firstName.value+" "+lastName.value
},
set(newValue){
const names = newValue.split(" ");
firstName.value = names[0];
lastName.value = names[1];
}
})

const changeName = ()=>{
// firstName.value="Jame";
fullName.value = "Hello World";
}

return {
fullName,
changeName
}
}
}
</script>

<style scoped>

</style>

9. 侦听数据的变化

  • 在Options API中,可以通过watch选项来侦听data或者props的数据变化,当数据变化时执行某一些操作
  • 在Composition API中,使用 watchEffectwatch 来完成响应式数据的侦听;
    • watchEffect用于自动收集响应式数据的依赖;
    • watch需要手动指定侦听的数据源;

9.1. watchEffect

  • 当侦听到某些响应式数据变化时,希望执行某些操作,这个时候可以使用 watchEffect。

  • 来看一个案例:

    • 首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;
    • 其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行;

9.1.1. watchEffect的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<div>
<button @click="changeName">{{name}}</button>
<button @click="changeAge">{{age}}</button>
</div>
</template>

<script>
import { ref, watchEffect } from 'vue';
export default {
setup(){
const name = ref('Test');
const age = ref(18);

const changeName = ()=>name.value = 'test';
const changeAge = ()=>age.value++;

watchEffect(()=>{
// 传入的函数会被立即执行一次
// 只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行
console.log("name:"+name.value+" age:"+age.value);
});

return {
name,
age,
changeName,
changeAge
}
}
}
</script>

<style scoped>

</style>

9.1.2. watchEffect的停止侦听

  • 如果希望停止侦听,可以获取watchEffect的返回值函数,调用该函数即可
  • 比如在上面的案例中,age达到20的时候就停止侦听:
1
2
3
4
5
6
7
8
9
10
const changeAge = ()=>{
age.value++;
if(age.value>20){
stop();
}
};

const stop = watchEffect(()=>{
console.log("name:"+name.value+" age:"+age.value);
});

9.1.3. watchEffect清除副作用

  • 什么是清除副作用呢?

  • 比如在开发中需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,停止了侦听器,或者侦听器侦听函数被再次执行了

  • 那么上一次的网络请求应该被取消掉,这个时候就可以清除上一次的副作用;

  • 在给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate

    • 副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数;
    • 在传入的回调函数中,执行一些清楚工作;
1
2
3
4
5
6
7
8
9
10
11
12
13
const stop = watchEffect((onInvalidate)=>{
// 传入的函数会被立即执行一次
// 只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行
console.log("name:"+name.value+" age:"+age.value);

const timer = setTimeout(()=>{
console.log("执行操作");
},2000);

onInvalidate(()=>{
clearTimeout(timer);
});
});

9.1.4. watchEffect的执行时机

  • 默认情况下,组件的更新会在副作用函数执行之前:

  • 如果希望在副作用函数中获取到元素,是否可行呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <template>
    <div>
    <h2 ref="titleRef">title</h2>
    </div>
    </template>

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

    setup(){
    // 类比于 this.$refs.title
    const titleRef = ref(null);

    watchEffect(()=>{
    console.log(titleRef.value);
    })

    return{
    titleRef
    }
    }
    }
    </script>

    <style scoped>

    </style>

    打印结果

    1
    2
    null
    <h2>title</h2>
  • 打印结果打印了两次:

    • 这是因为setup函数在执行时就会立即执行传入的副作用函数,这个时候DOM并没有挂载,所以打印为null;
    • 而当DOM挂载时,会给title的ref对象
    • 赋值新的值,副作用函数会再次执行,打印出来对应的元素;

9.1.5. 调整watchEffect的执行时机

  • 如果希望在第一次的时候就打印出来对应的元素呢?

  • 需要改变副作用函数的执行时机;

  • 它的默认值是pre,它会在元素挂载或者更新之前执行;

  • 所以会先打印出来一个空的,当依赖的title发生改变时,就会再次执行一次,打印出元素;

  • 可以设置副作用函数的执行时机:

  • flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setup(){
// 类比于 this.$refs.title
const titleRef = ref(null);

watchEffect(()=>{
console.log(titleRef.value);
},{
flush: "post"
})

return{
titleRef
}
}

9.2. Watch的使用

  • watch的API完全等同于组件watch选项的Property:

  • watch需要侦听特定的数据源,并在回调函数中执行副作用;

  • 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调;

  • 与watchEffect的比较,watch允许:

    • 懒执行副作用(第一次不会直接执行);
    • 更具体的说明当哪些状态发生变化时,触发侦听器的执行;
    • 访问侦听状态变化前后的值;
  • 源码部分

    • /packages/runtime-core/src/apiWatch.ts

    • 接收四种参数 ref,reactive,array,function

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      if (isRef(source)) {
      getter = () => source.value
      forceTrigger = isShallow(source)
      }
      else if (isReactive(source)) {
      getter = () => source
      deep = true
      }
      else if (isArray(source)) {
      isMultiSource = true
      forceTrigger = source.some(s => isReactive(s) || isShallow(s))
      getter = () =>
      source.map(s => {
      if (isRef(s)) {
      return s.value
      }
      else if (isReactive(s)) {
      return traverse(s)
      }
      else if (isFunction(s)) {
      return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
      }
      else {
      __DEV__ && warnInvalidSource(s)
      }
      })

      }
      else if (isFunction(source)) {
      if (cb) {
      // getter with cb
      getter = () =>
      callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
      }
      else {
      // no cb -> simple effect
      getter = () => {
      if (instance && instance.isUnmounted) {
      return
      }
      if (cleanup) {
      cleanup()
      }
      return callWithAsyncErrorHandling(
      source,
      instance,
      ErrorCodes.WATCH_CALLBACK,
      [onCleanup]
      )
      }
      }
      }
      else {
      getter = NOOP
      __DEV__ && warnInvalidSource(source)
      }

9.2.1. 侦听单个数据源

  • watch侦听函数的数据源有两种类型:
    • 一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref);
    • 直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<template>
<div>
<button @click="changeName">{{info.name}}</button>
<button @click="changeAge">{{info.age}}</button>
<button @click="changeRef">{{refName}}</button>
</div>
</template>

<script>
import { ref, reactive, watch } from 'vue';
export default {
setup(){
const info = reactive({
name: 'test',
age: 18
})

const changeName = ()=>info.name = 'Test';
const changeAge = ()=>{
info.age++;
};
// 数据源:一个getter函数
watch(()=>info.name,(newValue,oldVale)=>{
console.log("reactive name:",newValue,oldVale);
});

// 2. 传入一个可响应式的对象,reactive/ref
// 情况一:reactive对象获取到的newValue和oldValue本身都是reactive对象
watch(()=>{
return {...info};
},(newValue,oldValue)=>{
console.log("reactive info:",newValue,oldValue);
})

const refName = ref("Hello");
const changeRef = ()=>{
refName.value = "hello";
}
/**
* 情况二:ref对象获取newValue和oldValue是value值得本身
if (isRef(source)) {
getter = () => source.value
}
*/
watch(refName,(newValue,oldValue)=>{
console.log("ref:",newValue,oldValue);
});


return {
info,
changeName,
changeAge,
changeRef,
refName
}
}
}
</script>

<style scoped>

</style>

9.2.2. 侦听多个数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<div>
<button @click="changeName">{{info.name}}</button>
<button @click="changeAge">{{info.age}}</button>
<button @click="changeRef">{{refName}}</button>
</div>
</template>

<script>
import { ref, reactive, watch } from 'vue';
export default {
setup(){
const info = reactive({
name: 'test',
age: 18
})

const changeName = ()=>info.name = 'Test';
const changeAge = ()=>{
info.age++;
};

const refName = ref("Hello");
const changeRef = ()=>{
refName.value = "hello";
}

// 侦听多个数据源
watch([()=>{
return {...info};
},refName],([newInfo,newName],[oldInfo,oldName])=>{
console.log("array info:",newInfo,oldInfo);
console.log("array ref:",newName,oldName);
})

return {
info,
changeName,
changeAge,
changeRef,
refName
}
}
}
</script>

<style scoped>

</style>

9.2.3. 侦听响应式对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div>
<button @click="pushName">push</button>
</div>
</template>

<script>
import { ref, reactive, watch } from 'vue';
export default {
setup(){

const names = reactive(['a','b','c']);
const pushName = ()=>{
names.push("d");
}
watch(()=>[...names],(newValue,oldValue)=>{
console.log("array: ",newValue,oldValue);
})

return {
pushName
}
}
}
</script>

<style scoped>

</style>

9.2.4. watch的选项

如果希望侦听一个深层的侦听,那么依然需要设置 deep 为true;也可以传入 immediate 立即执行;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div>
<button @click="changeObj">changeObj</button>
</div>
</template>

<script>
import { ref, reactive, watch } from 'vue';
export default {
setup(){
// watch选项
const obj = reactive({
name: 'test',
friends: {
name: 'john'
}
});
watch(()=>{
return {...obj};
},(newObj,oldObj)=>{
console.log(newObj,oldObj);
},{
immediate: true,
deep: true
});
const changeObj = ()=>{
obj.friends.name= 'amy';
}

return {
changeObj
}
}
}
</script>

<style scoped>

</style>

10. 生命周期钩子

  • setup中如何使用生命周期函数呢?
    • 可以使用直接导入的 onX 函数注册生命周期钩子;
    • 因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。
Lifecycle Hooks Options API
onMounted() mounted
onUpdated() updated
onUnmounted() unmounted
onBeforeMount() beforeMount
onBeforeUpdate() beforeUpdate
onBeforeUnmount() beforeUnmount
onErrorCaptured()
onRenderTracked() Dev only
onRenderTriggered() Dev only
onActivated() activated
onDeactivated() deactivated
onServerPrefetch()

11. 组件间通信

11.1. Provide函数

  • 可以通过 provide来提供数据

    • 可以通过 provide 方法来定义每个 Property;

    • provide可以传入两个参数:

      • name:提供的属性名称;
      • value:提供的属性值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div>
<button @click="age++">{{age}}</button>
<inject></inject>
</div>
</template>

<script>
import { provide, reactive, ref } from 'vue';
import Inject from './09.Inject.vue';

export default {
components:{
Inject
},

setup(){
const info = reactive({
name: 'Test',
friends :{
name: 'John'
}
});
const age = ref(18);

provide("info",info);
provide("age",age);

return {
name,
age
}
}
}
</script>

<style scoped>

</style>

11.2. Inject函数

  • 后代组件 中可以通过 inject 来注入需要的属性和对应的值:
    • 可以通过 inject 来注入需要的内容;
    • inject可以传入两个参数:
      • 要 inject 的 name;
      • 默认值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div>
<h2>{{age}}</h2>
<h2>{{info.friends.name}}</h2>
</div>
</template>

<script>
import { inject } from 'vue';


export default {
setup(){
const age = inject("age");
const info = inject("info");
return {
age,
info
}
}
}
</script>

<style scoped>

</style>

12. Hook

12.1. App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<template>
<div class="scroll">
<h2>{{counter}}</h2>
<button @click="decrement">-1</button>
<button @click="increment">+1</button>

<div class="scroll-word">
<h2>scrollX:{{scrollX}}</h2>
<h2>scrollY:{{scrollY}}</h2>
<h2>mouseX:{{mouseX}}</h2>
<h2>mouseY:{{mouseY}}</h2>
</div>

<h2 v-for="item in data" :key="item">{{item}}</h2>
</div>
</template>

<script>
import { useCounter, useTitle,useScrollPostion,useMousePosition, useLocalStorage } from './hooks';

export default {
setup(){
// counter
const { counter,increment,decrement } = useCounter();

// title
const titleRef = useTitle("Test");
setTimeout(()=>{
titleRef.value = 'kobe';
},1000);

// scroll
const { scrollX,scrollY } = useScrollPostion();

// mouse
const { mouseX,mouseY } = useMousePosition(event);

// localStorage
const data= useLocalStorage('0001',{
name: 'test',
age: 18
});


return {
counter,
increment,
decrement,
scrollX,
scrollY,
mouseX,
mouseY,
data
}
}
}
</script>

<style scoped>
.scroll{
width: 4000px;
height: 1000px;
border: 1px solid black;
}
.scroll-word{
position: fixed;
right: 0;
top: 0;
}
</style>

12.2. index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useCounter } from './useCounter';
import { useTitle } from './useTitle';
import { useScrollPostion } from './useScrollPosition';
import { useMousePosition } from './useMousePosition';
import { useLocalStorage } from './useLocalStorage';

export {
useCounter,
useTitle,
useScrollPostion,
useMousePosition,
useLocalStorage
}

12.3. useCounter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ref } from 'vue';

export function useCounter(){
const counter = ref(0);
const increment = ()=> counter.value++;
const decrement = ()=> counter.value--;

return {
counter,
increment,
decrement
}
}

12.4. useTitle.js

修改title,对应网页的 <title><%= htmlWebpackPlugin.options.title %></title>

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ref, watch } from 'vue';

export function useTitle(title='默认的title'){
const titleRef = ref(title);

watch(titleRef,(newValue)=>{
document.title = newValue;
},{
immediate: true
});

return titleRef;
}

12.5. useScrollPosition

完成一个监听界面滚动位置的Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ref } from 'vue';

export function useScrollPostion(){
const scrollX = ref(0);
const scrollY = ref(0);

document.addEventListener("scroll",()=>{
scrollX.value = window.scrollX;
scrollY.value = window.scrollY;
});

return {
scrollX,
scrollY
}
}

12.6. useMousePosition

完成一个监听鼠标位置的Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { ref } from 'vue';

export function useMousePosition(){
const mouseX = ref(0);
const mouseY = ref(0);

document.addEventListener("mousemove",(event)=>{
mouseX.value = event.pageX;
mouseY.value = event.pageY;
});

return {
mouseX,
mouseY
}

}

12.7. useLocalStorage

完成一个使用 localStorage 存储和获取数据的Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { ref, watch } from 'vue';

export function useLocalStorage(key,defaultValue){
const data = ref(defaultValue);

if(defaultValue){
window.localStorage.setItem(key,JSON.stringify(defaultValue));
}
else{
data.value = JSON.parse(window.localStorage.getItem(key));
}

watch(data,(newValue)=>{
window.localStorage.setItem(key,JSON.localStorage(newValue));
});

return data;
}

13. script setup

父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<setup message="哈哈哈" @increment="add" :counter="counter"></setup>
</div>
</template>

<script setup>

import setup from './setup.vue';

import { ref } from "vue";

const counter = ref(0);

const add = ()=> {
counter.value++;
};


</script>

<style scoped>

</style>

子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>
<h2>{{counter}}</h2>
<h2>{{message}}</h2>
<button @click="emitEvent">事件</button>
</div>
</template>

<script setup>

const props = defineProps({
message: {
type: String,
default: 'Hello World'
},
counter: {
type: Number
}
});

const emit = defineEmits(['increment','decrement']);

const emitEvent = ()=>{
emit('increment');
}

</script>

<style scoped>

</style>

14. h函数

  • https://vuejs.org/api/render-function.html

  • Vue推荐在绝大数情况下使用模板来创建HTML,然后一些特殊的场景,需要JavaScript的完全编程的能力,可以使用渲染函数 ,它比模板更接近编译器;

    • Vue在生成真实的DOM之前,会将节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚 拟DOM(VDOM);
    • 事实上,编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode;
    • 那么,如果想充分的利用JavaScript的编程能力,可以编写 createVNode 函数,生成对应的VNode;
  • 应该怎么使用 h()函数:

    • h() 函数是一个用于创建 vnode 的一个函数;
    • 其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数;

14.1. h()函数的使用

  • 它接受三个参数

    1
    2
    3
    4
    5
    function h(
    type: string | Component,
    props?: object | null,
    children?: Children | Slot | Slots
    ): VNode
    • type:一个html标签,一个组件,必须的。
    • props:与attribute,prop和事件相对应的对象,可选的。
    • children:children子VNodes,或有插槽的对象,可选的。
  • 注意事项:

    • 如果没有props,那么通常可以将children作为第二个参数传入;
    • 如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入;
  • h函数可以在两个地方使用:

    • render函数选项中;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <script>
      import { h } from 'vue';
      export default {
      render(){
      return h('h2',{class: 'title'},'Hello World');
      }
      }
      </script>

      <style scoped>

      </style>
    • setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode);

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <script>
      import { h } from 'vue';
      export default {
      setup(){
      return ()=> h('h2',{class: 'title'},'Hello World');
      }
      }
      </script>

      <style scoped>

      </style>

      得到的dom

      1
      2
      3
      <div id="app" data-v-app="">
      <h2 class="title">Hello World</h2>
      </div>

14.2. 计数器案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script>
import { h } from 'vue';
export default {
data(){
return {
counter: 0
}
},
render(){
return h('div',{class: 'app'},[
h('h2',null,`当前计数:`+this.counter),
h('button',{
onclick: ()=> this.counter++
},'+1'),
h('button',{
onclick: ()=>this.counter--
},'-1')
]);
}
}
</script>

<style scoped>

</style>

html转换

1
2
3
4
5
<div class="app">
<h2>当前计数:0</h2>
<button>+1</button>
<button>-1</button>
</div>

14.3. 函数组件和插槽的使用

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
import HelloWorld from './HelloWorld.vue';
import { h } from 'vue';

export default {
render(){
return h(HelloWorld,null,{
default: props => h('span',null,`app传入到和hello中的内容:${props.info}`)
})
}

}
</script>

<style scoped>

</style>

HelloWorld.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
import { h } from 'vue';
export default {
render(){
return h('div',null,[
h('h2',null,"hello world"),

this.$slots.default ? this.$slots.default({info: 'hahaha'}): h('span',null,'默认值')
]);
}
}
</script>

<style scoped>

</style>

15. jsx的babel配置

  • 如果希望在项目中使用jsx,需要添加对jsx的支持:

    • jsx我通常会通过Babel来进行转换(React编写的jsx就是通过babel转换的);
    • 对于Vue来说,只需要在Babel中配置对应的插件即可;
  • 安装Babel支持Vue的jsx插件:

    1
    npm install @vue/babel-plugin-jsx -D
  • 在babel.config.js配置文件中配置插件:

    1
    2
    3
    4
    5
    6
    7
    8
    module.exports = {
    presets: [
    '@vue/cli-plugin-babel/preset'
    ],
    plugins: [
    "@vue/babel-plugin-jsx"
    ]
    }
  • 显示h2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <script>

    export default {
    render(){
    return (
    <h2>Hello Jsx</h2>
    )
    }
    }
    </script>

    <style scoped>

    </style>

15.1. 计数器案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script>
import { ref } from 'vue';

export default {
setup(){
const counter = ref(0);
const increment = ()=>counter.value++;
const decrement = ()=>counter.value--;
return{
counter,
increment,
decrement
}
},
render(){
return (
<div>
<h2>{this.counter}</h2>
<button onClick={this.increment}>+1</button>
<button onClick={this.decrement}>-1</button>
</div>
)
}
}
</script>

<style scoped>

</style>

15.2. jsx组件的使用

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
import HelloWorld from './HelloWorld.vue';

export default {
render(){
return (
<HelloWorld>
{{default: props=> <button>{props.name}</button>}}
</HelloWorld>
)
}

}
</script>

<style scoped>

</style>

HelloWorld.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
export default {
render(){
return (
<div>
<h2>Hello World</h2>
<div class="content">
{this.$slots.default?
this.$slots.default({name:'Test'}):
<span>默认值</span>
}
</div>
</div>
)
}
}
</script>

<style scoped>

</style>

16. 自定义指令

  • https://vuejs.org/guide/reusability/custom-directives.html#directive-hooks

  • 在Vue的模板语法中学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue 也允许自定义自己的指令

    • 注意:在Vue中,代码的复用和抽象主要还是通过组件;
    • 通常在某些情况下,需要对DOM元素进行底层操作,这个时候就会用到自定义指令;
  • 自定义指令分为两种:

    • 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;
    • 自定义全局指令:app的 directive 方法,可以在任意组件中被使用;
  • 比如做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点

    • 实现方式一:使用默认的实现方式;
    • 实现方式二:自定义一个 v-focus 的局部指令;
    • 实现方式三:自定义一个 v-focus 的全局指令;

16.1. 聚焦的默认实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<input type="text" ref="inputRef">
</div>
</template>

<script>
import { onMounted, ref } from 'vue';
export default {
setup(){
const inputRef = ref(null);
onMounted(()=>{
inputRef.value.focus();
});
return {
inputRef
}
}
}
</script>

<style scoped>

</style>

16.2. 局部自定义指令

  • 自定义一个 v-focus 的局部指令
    • 这个自定义指令实现非常简单,只需要在组件选项中使用 directives 即可;
    • 它是一个对象,在对象中编写自定义指令的名称(注意:这里不需要加v-);
    • 自定义指令有一个生命周期,是在组件挂载后调用的 mounted,可以在其中完成操作;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<input type="text" v-focus>
</div>
</template>

<script>

export default {
directives: {
focus: {
mounted(el){
el.focus();
}
}
}
}
</script>

<style scoped>

</style>

16.3. 自定义全局指令

自定义一个全局的v-focus指令可以在任何地方直接使用

1
2
3
4
5
app.directive("focus",{
mounted(el){
el.focus();
}
})

16.4. 指令的生命周期

  • 一个指令定义的对象,Vue提供了如下的几个钩子函数:
    • created:在绑定元素的 attribute 或事件监听器被应用之前调用;
    • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
    • mounted:在绑定元素的父组件被挂载后调用;
    • beforeUpdate:在更新包含组件的 VNode 之前调用;
    • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
    • beforeUnmount:在卸载绑定元素的父组件之前调用;
    • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<div>
<button @click="increment" v-if="counter < 2">{{counter}}</button>
</div>
</template>

<script>
import { ref } from 'vue';
export default {
setup(){
const counter = ref(0);
const increment = ()=> counter.value++;

return{
counter,
increment
}
},
directives: {
test: {
created(el, binding, vnode, prevVnode) {
console.log("created");
},
beforeMount(el, binding, vnode, prevVnode) {
console.log("beforeMount");
},
mounted(el, binding, vnode, prevVnode) {
console.log("mounted");
console.log(el,binding,vnode,prevVnode);
},
beforeUpdate(el, binding, vnode, prevVnode) {
console.log("beforeUpdate");
},
updated(el, binding, vnode, prevVnode) {
console.log("updated");
},
beforeUnmount(el, binding, vnode, prevVnode) {
console.log("beforeUnmount");
},
unmounted(el, binding, vnode, prevVnode) {
console.log("unmounted");
}
}
}
}
</script>

<style scoped>

</style>

16.5. 指令的参数和修饰符

  • 如果指令需要接受一些参数或者修饰符应该如何操作呢?

    • info是参数的名称;
    • aaa-bbb是修饰符的名称;
    • 后面是传入的具体的值;
  • 在生命周期中,可以通过 bindings 获取到对应的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<template>
<div>
<button v-test:info.aaa.bbb="{name:'test',age:18}" @click="increment">{{counter}}</button>
</div>
</template>

<script>
import { ref } from 'vue';
export default {
setup(){
const counter = ref(0);
const increment = ()=> counter.value++;

return{
counter,
increment
}
},
directives: {
test: {
created(el, binding, vnode, prevVnode) {
console.log("created");
/**
{
"dir": {},
"instance": {
"counter": 0
},
"value": {
"name": "test",
"age": 18
},
"arg": "info",
"modifiers": {
"aaa": true,
"bbb": true
},
"oldValue": undefined
}
*/
console.log(binding);
// info
console.log(binding.arg);
// {name: 'test', age: 18}
console.log(binding.value);
// {aaa: true, bbb: true}
console.log(binding.modifiers);
}
}
}
}
</script>

16.6. 自定义指令练习

  • 自定义指令案例:时间戳的显示需求:

    • 在开发中,大多数情况下从服务器获取到的都是时间戳;
    • 需要将时间戳转换成具体格式化的时间来展示;
    • 在Vue2中可以通过过滤器来完成;
    • 在Vue3中可以通过 计算属性(computed) 或者 自定义一个方法(methods) 来完成;
    • 还可以通过一个自定义的指令来完成;
  • 实现一个可以自动对时间格式化的指令v-format-time:

    • 封装了一个函数,在首页中只需要调用这个函数并且传入app即可;

16.6.1. Main.js

1
2
3
4
5
6
import registerDirectives from './directives';
import App from './App.vue'

const app = createApp(App);

registerDirectives(app);

16.6.2. index.js

集合 directives 中所有方法

1
2
3
4
5
import FormatTime from './format-time';

export default function(app){
FormatTime(app);
}

16.6.3. format-time.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import dayjs from "dayjs";
export default function(app){


app.directive("format-time",{
created(el,bindings){
bindings.value = "YYYY-MM-DD HH:mm:ss";

if(bindings.value){
bindings.formatString = bindings.value;
}
},
mounted(el,bindings){

const textContent = el.textContent;

let timestamp = parseInt(textContent);
if(textContent.length == 10){
timestamp *= 1000;
}
el.textContent = dayjs(timestamp).format(bindings.formatString);
}
})
}

16.6.4. App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<h2 v-format-time="'YYYY/MM/DD'">{{timestamp}}</h2>
</div>
</template>

<script>
export default {
setup(){
// https://www.beijing-time.org/shijianchuo/
const timestamp = 1662559895;

return{
timestamp
}
}
}
</script>

<style scoped>

</style>

17. Teleport

17.1. 认识Teleport

  • 在组件化开发中,封装一个组件A,在另外一个组件B中使用:

    • 那么组件A中template的元素,会被挂载到组件B中template的某个位置;
    • 最终应用程序会形成一颗DOM树结构;
  • 但是某些情况下,希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置:

    • 比如移动到body元素上,或者有其他的div#app之外的元素上;
    • 这个时候就可以通过teleport来完成;
  • Teleport是什么呢?

    • 它是一个Vue提供的内置组件,类似于react的Portals;
    • teleport翻译过来是心灵传输、远距离运输的意思;
    • 它有两个属性:
      • to:指定将其中的内容移动到的目标元素,可以使用选择器;
      • disabled:是否禁用 teleport 的功能;
1
2
3
4
5
6
7
<template>
<div class="title">
<teleport to="body">
<h2>Hello World</h2>
</teleport>
</div>
</template>

html

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div id="app" data-v-app="">
<div class="title">
<!--teleport start-->
<!--teleport end-->
</div>
</div>
<!-- built files will be auto injected -->
<div id="forest-ext-shadow-host">
</div>

<h2>Hello World</h2>
</body>

17.2. 和组件结合使用

  • 当然,teleport也可以和组件结合一起来使用:
    • 可以在 teleport 中使用组件,并且也可以给他传入一些数据;

17.2.1. App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="content">
<teleport to="body">
<hello-world message="App中的message"></hello-world>
</teleport>
</div>
</template>
<script>
import HelloWorld from './HelloWorld.vue';

export default {
components: {
HelloWorld
}

}
</script>

<style scoped>

</style>

17.2.2. HelloWorld.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="title">
<h2>{{message}}</h2>
</div>
</template>

<script>
export default {
props:{
message: {
type: String
}
}
}
</script>

<style scoped>

</style>

17.2.3. index.html

生成的html网页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
<div id="app" data-v-app="">
<div class="content">
<!--teleport start-->
<!--teleport end-->
</div>
</div>
<!-- built files will be auto injected -->
<div id="forest-ext-shadow-host">
</div>

<div class="title">
<h2>App中的message</h2>
</div>
</body>

17.3. 多个teleport

  • 如果将多个teleport应用到同一个目标上(to的值相同),那么目标会进行合并

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="content">
<teleport to="body">
<hello-world message="App中的message"></hello-world>
</teleport>
<teleport to="body">
<h2>Hello</h2>
</teleport>
</div>
</template>
<script>
import HelloWorld from './HelloWorld.vue';

export default {
components: {
HelloWorld
}

}
</script>

<style scoped>

</style>

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
<div id="app" data-v-app="">
<div class="content">
<!--teleport start-->
<!--teleport end-->
<!--teleport start-->
<!--teleport end-->
</div>
</div>
<!-- built files will be auto injected -->
<div id="forest-ext-shadow-host">
</div>
<!-- 进行合并 -->
<div class="title">
<h2>App中的message</h2>
</div>
<h2>Hello</h2>
</body>

18. Vue插件

18.1. 认识Vue插件

  • 通常向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:

    • 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;
    • 函数类型:一个function,这个函数会在安装插件时自动执行;
  • 插件可以完成的功能没有限制,比如下面的几种都是可以的:

    • 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现;
    • 添加全局资源:指令/过滤器/过渡等;
    • 通过全局 mixin 来添加一些组件选项;
    • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能;

18.2. 插件的编写方式

18.2.1. 对象类型的写法

plugins_object.js

1
2
3
4
5
6
export default{
install(app,options){
console.log("插件被安装:",app,options);
app.config.globalProperties.$name = "object"
}
}

安装到main.js中

1
2
3
4
5
6
import App from './App.vue';
import pluginsObject from './plugins/plugins_object';

const app = createApp(App);

app.use(pluginsObject);

$name 称为全局变量,可以直接应用,但在composition API中使用需要引入getCurrentInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<h2>{{$name}}</h2>
</div>
</template>

<script>
import { getCurrentInstance } from 'vue';
export default {
setup(){
const instance = getCurrentInstance();
const name = instance.appContext.config.globalProperties.$name;
console.log(name);
}
}
</script>

<style scoped>

</style>

18.2.2. 函数类型的写法

plugins_function.js

1
2
3
4
export default function(app,options){
console.log("插件被安装:",app,options);
app.config.globalProperties.$name = "function"
}

Main.js

1
2
3
4
5
6
import App from './App.vue';
import pluginsFunction from './plugins/plugins_function';

const app = createApp(App);

app.use(pluginsFunction);
本文结束  感谢您的阅读