迟到的Vue3学习报告

832 阅读17分钟

前言

继前面讲了 Vue3 搭建环境之后今天就来学习一下它的新特性以及新的 API ,赶紧背起来 ~😆

Componsition API

前情提要:Vue3 没有放弃 Options API 两者可以用在同一个文件夹中,但是不建议,不提倡;

Vue3 相当重大的更新,负责任的讲学完这个 Vue3 就学完了😎,怎么解释?:一套允许灵活组合组件逻辑的附加的 API 函数

Componsition API

Vue3.x

Setup

想要学习 Componsition Api 先从这里看起,setupVue3 中新增的一个选项也是 Componsition 的入口函数;

  • 接收两个参数
  • 返回值可以是对象,函数,JSX

参数 props, context

export default {
  setup(props, context) {
    // props: 就是 Vue2.x 中的接收父组件数据的 props, 切记不要使用结构语法对 props 进行结构,否则会失去 props 的响应式效果;
    // context: 是一个 js 对象,暴露三个属性;slots emit attrs;
    // 在 setup 中应该避免对 this 的使用,因为 setup 的执行时机比较原来的生命周期钩子 beforeCreate 之前,此时的 this 是 undefined
    return {}
  }
}
context

setup 选项的第二个参数,是一个普通的对象,可以对其进行结构,包含三个参数 slots, emit, attrs

以下是摘抄自官网对于 attrs, slots 的描述;

image-20210713110518798.png

  • emit :触发自定义事件使用,后面组件传值时,会有讲到;

  • slots:访问 slots 时,如果是具名插槽可以使用 slots.name() 如果是默认插槽则使用 slots.default() 进行访问,该方法返回一个对象;

  • attrs:值得注意的是,在 Vue3attrs 是可以获取到 classstyle 属性的;

export default {
  props: {
    name: {
      type: String,
      default: ''
    }	
  },
  emits: ['custom-event'], // 此选项可以是最简的数组也可以是对象;
  setup(props, { slots, attrs, emit }) {
    console.log(props.name);
    console.log(slots.default());
    console.log(attrs.style)
    // 注意触发事件于之前有些许不同,需要新增一个 emits 的选项
    emit('custom-event', 'params');
  }
}

对象返回

import { ref } from 'vue'; // 先不要管
export default {
  setup() {
    const msg = ref('hello vue3');
    return { msg } // setup return 出来的值可以直接再模板中使用
  }
}

JSX 或者函数 返回

import { h } from 'vue';
export default {
  setup() {
    return () => {
      // 函数其实就是手写 VNode 也可以使用 jsx 语法,如果是自己搭建的项目需要安装插件
      // @vue/babel-plugin-jsx
      return h('div', {}, 'hello vue3');
    }
  }
}

地址点这里 👉🏿 @vue/babel-plugin-jsx

关于 h 函数和 jsx 可以后面再单独开一篇,这里就不赘述了;

生命周期钩子

本来想把这个往后放一放,毕竟简单,相信大多数用过 Vue2 的人,看一遍就知道了,但是为了照顾大多数人,还是放到这里吧;

什么是生命周期?你能看这个就肯定知道,不解释了;直接看钩子;

强烈建议不要读一遍就完事了,钩子手敲之后感悟很深;

准备前提:parent.vue child.vue ,为了演示会在一个文件中写入 options apicomponsition api 仅用于演示,不建议用于生产环境;

  • Vue3 中去把原来的卸载阶段钩子 beforeDestroy destroyed 更改为 beforeUnmount unmounted

  • Vue3 中所有的同阶段的 Componsition API 执行时机早于 Options API

// child.vue
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from "vue";
export default {
  setup() {
    // setup 可以理解为是之前的 beforeCreate 和 created 钩子
    console.log("childComponsition --- setup");
    onBeforeMount(() => {
      console.log("childComponsition --- onBeforeMount");
    });
    onMounted(() => {
      console.log("childComponsition --- onMounted");
    });
    onBeforeUpdate(() => {
      console.log("childComponsition --- onBeforeUpdate");
    });
    onUpdated(() => {
      console.log("childComponsition --- onUpdated");
    });
    onBeforeUnmount(() => {
      console.log("childComponsition --- onBeforeUnmount");
    });
    onUnmounted(() => {
      console.log("childComponsition --- onUnmounted");
    });

    return () => {
      return <div>this is child</div>;
    };
  },
  beforeCreate() {
    console.log("childOptions --- beforeCreate");
  },
  created() {
    console.log("childOptions --- created");
  },
  beforeMount() {
    console.log("childOptions --- beforeMount");
  },
  mounted() {
    console.log("childOptions --- mounted");
  },
  beforeUpdate() {
    console.log("childOptions --- beforeUpdate");
  },
  updated() {
    console.log("childOptions --- updated");
  },
  beforeUnmount() {
    console.log("childOptions --- beforeUnmount");
  },
  unmounted() {
    console.log("childOptions --- unmounted");
  }
};
<template>
  <Child v-if="isShow" />
  <button @click="isShow = !isShow">toggleChild</button>
</template>
<script>
// parent.vue
import Child from "@/components/child.vue";
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, ref } from "vue";
export default {
  components: {
    Child
  },
  setup() {
    const isShow = ref(true);

    // setup 可以理解为是之前的 beforeCreate 和 created 钩子
    console.log("parentComponsition --- setup");
    onBeforeMount(() => {
      console.log("parentComponsition --- onBeforeMount");
    });
    onMounted(() => {
      console.log("parentComponsition --- onMounted");
    });
    onBeforeUpdate(() => {
      console.log("parentComponsition --- onBeforeUpdate");
    });
    onUpdated(() => {
      console.log("parentComponsition --- onUpdated");
    });
    onBeforeUnmount(() => {
      console.log("parentComponsition --- onBeforeUnmount");
    });
    onUnmounted(() => {
      console.log("parentComponsition --- onUnmounted");
    });

    return {
      isShow
    };
  },
  beforeCreate() {
    console.log("parentOptions --- beforeCreate");
  },
  created() {
    console.log("parentOptions --- created");
  },
  beforeMount() {
    console.log("parentOptions --- beforeMount");
  },
  mounted() {
    console.log("parentOptions --- mounted");
  },
  beforeUpdate() {
    console.log("parentOptions --- beforeUpdate");
  },
  updated() {
    console.log("parentOptions --- updated");
  },
  beforeUnmount() {
    console.log("parentOptions --- beforeUnmount");
  },
  unmounted() {
    console.log("parentOptions --- unmounted");
  }
};
</script>

准备工作完成,首先看创建阶段;

挂载阶段演示

  • 全程所有钩子,同级别 Componsition API 早于 Options API 执行;

  • 父组件优先进入 beforeMount 阶段;然后渲染子组件,子组件渲染完成进入父组件 mounted 阶段;

image-20210713150856885.png

卸载更新阶段演示

  • 父组件进入 beforeUpdate 阶段,直到子组件卸载完成,进入父组件 updated 阶段;

image-20210713151243249.png

常用响应式 API

ref

接收一个值并返回一个 ref 对象;使其成为响应式,

  • 访问时通过 .value 进行访问;如果是在模板中访问不需要 .value
<template>
	{{ msg }}
</template>
<script>
import { ref } from 'vue'
export default {
  setup() {
    const msg = ref('hello vue3');
    return {
      msg
    }
  }
}
</script>
  • 也可以通过给属性绑定 ref 获取 DOM 元素
<template>
	<div ref="rootRef">hello Vue3</div>
</template>
<script>
import { ref, onMounted } from 'ref';
export default {
  setup() {
    const rootRef = ref(null);
    // onMounted Vue3 中的生命周期
    // 注意 setup 执行时机比较早,如果想要访问 DOM 最好等待组件挂载完成之后;
    onMounted(() => {
      // 访问时 .value 后就是原生的 DOM 对象可以使用里面的 属性以及方法
      console.log(rootRef.value.innerText);
    });
    return {
      rootRef
    }
  }
}
</script>

当然 ref 可以获取某一个元素,也可以获取多个元素,具体使用方法可以演示

<template>
  <!-- ref 接收一个函数,参数就是这个元素,并且把他存储起来 -->
	<div v-for="item in lists" :key="item" :ref="el => eles.push(el)"></div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
  setup() {
    const lists = [1, 2, 3, 4, 5];
    const eles = ref([]); // 存储元素的 ref
    
    onMounted(() => {
      console.log(eles);
    });
  }
}
</script>
  • 了解了 ref 的使用,也应该了解一些它的特性;看如下代码的打印信息
import { ref } from 'vue';
export default {
  setup() {
    const msg = ref('hello Vue3');
    const msg1 = ref({
      name: 'zhangsan',
      age: 24
    });
    console.log(msg);
    console.log(msg1);
    return {}
  }
}

image-20210713135115740.png

上面内容可以得出,会返回一个 ref 类型的对象,如果是一个简单数据类型那么就直接存放在 value 中,如果是复杂数据类型的对象,就是一个 proxy 代理对象;

reactive

接收一个对象,返回这个对象的代理对象 基于 ES2015 中的 Proxy 实现,注意代理对象并不等同于对象本身,所以后续操作应当操作这个代理对象,才能保证对象的响应式

  • 注意使用时,只能接收对象,切勿传递普通值;造成错误;
import { reactive } from 'vue';
export default {
  setup() {
    // 正常使用
    const msg = reactive({
      name: 'zangsan',
      age: 24
    });
    // 错误使用。可以正常打印,但是会有 ⚠️
    const msg1 = reactive('111');
    console.log(msg1)
    return {}
  }
}

image-20210713140447639.png

  • 关于类型,它其实就是返回的代理对象 Proxy ;看到这里回过头来看我们给 ref 传递对象时里面的值是一样的;也间接证明了,ref 中的对象其实是由 reactive 进行包装;

readonly

接收一个对象,或者 ref 对象、reactive 代理对象,返回这个对象的只读代理;

看下面代码打印结果,如果修改被 readonly 包裹的对象,会出现警告,并且无法修改;

import { ref, reactive, readonly } from 'vue';
export default {
  setup() {
    const msg = ref("111");
    const msg1 = reactive({ name: "zhangsan" });
    const msg2 = { name: "lisi" };

    const readMsg = readonly(msg);
    const readMsg1 = readonly(msg1);
    const readMsg2 = readonly(msg2);

    readMsg.value = 1;
    console.log(readMsg);
    readMsg1.name = 1;
    console.log(readMsg1);
    readMsg2.name = 1;
    console.log(readMsg2);
  }
}

image-20210713142017403.png

computed 计算属性

传入一个 getter 或者 gettersetter 函数,返回一个 ref 对象;

注意:

  • setter 的前提是一定有 getter ;

  • 返回的是 ref 对象,意味着后续的访问要有 .value

只有 getter 函数时,传递一个回调函数即可;

import { computed, ref } from 'vue';
export default {
  setup() {
    const count = ref(1);
    const counter = computed(() => {
      return ++count.value;
    })
    return {
      counter
    }
  }
}

gettersetter 同时存在时,需要传入一个对象包含 getset 方法;

<template>
	<button @click="add">add</button>
  {{ counter }}
</template>
<script>
import { ref, computed } from 'vue';
export default {
  setup() {
    const count = ref(0);
    // 计算属性
    const counter = computed({
      get: () => {
        return count.value;
      },
      set: (val) => {
        count.value = val;
      }
    });
    // 事件函数
    const add = () => {
      count.value++;
    };
    return {
      add,
      counter
    }
  }
}
</script>

侦听属性

watch

  • 单个监听源:简单理解就是监听一个值(可以是复杂数据类型,也可以是简单数据类型);watch 的第一个参数只会接收一个 getter 函数或者一个 ref 对象;
import { watch, ref, reactive } from 'vue';
export default {
  setup() {
    const count = ref(0);
    const counter = reactive({ val: 0 });
    watch(count, () => console.log('触发了 watch')); // 侦听 ref
    watch(() => counter.val, () => console.log('触发了 watch'));  // 侦听 getter
    
    // 注意:如果不是 ref 对象或者 getter 函数会有警告,但不会报错;
    watch(counter.val, () => console.log('触发了 watch'));
    
    const add = () => {
      count.value++;
      counter.val++;
    };
    return {
      count,
      counter,
      add
    }
  }
}

image-20210713164733666.png

  • 监听多个源,如果我们把值换成数组可以实现监听多条数据,这个功能记得 vue2.x 是不具备的;虽然是多个监听源但是上面的规则同样适用,只要监听对象里面有一个不是 ref 对象就要使用 getter 方法;
import { watch, ref, reactive } from 'vue';
export default {
  setup() {
    const count = ref(0);
    const count1 = ref(1);
    // 注意这个不是 ref 对象
    const count2 = reactive({ val: 2 });
    
    // 正确的侦听方式
    watch(() => [count, count1, count2.val], () => console.log('触发了 watch1 '));
    watch([count, count1], () => console.log('触发了 watch2'));
    // 错误的侦听方式:存在非 ref 对象时,必须使用 getter 函数;
    // 不会报错,但是会触发警告 ⚠️ ;
    watch([count, count1, count2.val], () => console.log('触发了 watch3'));
    
    const add = () => {
      count.value++;
      count1.value++;
      count2.val++;
    }
    return { count, count1, count2, add };
  }
}
  • 这个版本的 watch 同样支持深度监听或者立即触发机制;只需要在第三个参数增加配置即可;
import { watch, reactive } from 'vue';
export default {
  setup() {
    const counter = reactive({
      count: 1
    });
    
    watch(
      () => counter, 
      () => {'触发了深度监听和立即执行'}, 
      { immediate: true, deep: true }
   	);
    
    const add = () => {
      counter.count++;
    }
    return { add, count: counter.count}
  }
}
  • watch 新版本增加了暂停侦听操作;侦听函数会返回一个 stop 函数;
import { watch, ref } from 'vue';
export default {
  setup() {
    const count = ref(0);
    const stop = watch(count, () => console.log('update'));
    
    const add = () => {
      count.value++;
      if (count.value === 5) {
        stop();
      }
    }
  }
}

watchEffect

新增的侦听函数,根据其内部依赖值得变化调用,初始的时候会先调用一次;

  • 非响应式的值是不可以触发二次更新的;默认是深度监听;

  • watch 一样返回一个 stop 可以停止侦听;

  • 如果使用在 setup 或者 生命周期钩子中时,在 unmounted 钩子中会自动停止侦听;

import { watchEffect, ref } from 'vue';
export default {
  setup() {
    const count = ref(0);
    watchEffect(() => {
      console.log(count.value);
    });
    
    setTimeout(() => {
      count.value++;
    }, 1000)
    return {}
  }
}
  • **注意:**如果你想在 watchEffect 中去获取 DOM 或者模板中 refs 的话,一定要放到 onMounted 钩子中去进行;
export default {
  setup() {
    const rootRef = ref(null);
    onMounted(() => {
      watchEffect(() => {
        console.log(rootRef.value.innerText);
      });
    });
  }
}
watchEffect 的第二个参数

watchEffect 中的内容,首次调用的时候,是同步进行调用的,当内容触发更改再次调用时默认回调函数是在组件更新之前调用的;我们可以做一个小小的测试进行校验;以下是完整的测试代码,建议自行复制下来跑一下进行测试;

<template>
	{{ count }}
</template>
<script>
import { ref, watchEffect, onBeforeUpdate } from 'vue';
export default {
  setup() {
    const count = ref(0);
    setTimeout(() => {
      ++count.value;
    }, 2000);
    
    watchEffect(() => {
      console.log('count', count.value);
    });
    onUpdated(() => {
      console.log('onBeforeUpdate');
    });
    
    return { count }
  }
}
</script>

image-20210720141939154.png

那么问题来了,顺序有没有办法改变呢?答案是肯定有的,官网也已经说明 这里 watchEffect 提供了第二个参数;

它接收的第二个参数是一个对象,接收一个键值对;

  • { flush: 'pre'} 默认就是 pre 即我们上面演示的结果(在副作用更新之后执行页面更新操作onBeforeUpdate);
  • { flush: 'post' } 就是在副作用更新之前执行;
  • { flush: 'sync' } 这会将副作用强制进行同步执行,效率比较低下不推荐;
debugger watchEffect

上面说到我们会在第二个参数中新增加一个选项用来控制副作用执行的时机,此外我们还可以使用两个方法来进行调试;

  • onTrack:会在侦听值被追踪时调用;换句话说,就是第一次执行的时候就会被调用;
  • onTrigger:会在侦听值发生改变导致副作用产生时进行调用;是早于 onInvalidate
  • 注意: 这两个方法仅适用于开发环境调试适用;
<template>
  {{ counter }}
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
  setup() {
    const counter = ref(0);
    setTimeout(() => {
      ++counter.value;
    }, 2000);
    
    watchEffect(
      () => {
        console.log('counter', counter.value);
      }, 
      {
        // 首次就会执行,此后每次依赖变更也会执行
        onTrack(e) {
          console.log('onTrack', e);
        },
        // 只有当依赖变更时才会执行,执行时要早于 onTrack
        onTrigger(e) {
          console.log('onTrigger', e);
        }
      }
    );
    return { counter };
  }
}
</script>
清除副作用 onInvalidate

onInvalidatewatchEffect 中回调函数的参数,它本身也是一个函数,接收一个回调函数;

它的调用时机是变化之后调用 watchEffect 调用之前调用,用来清除一些副作用(关于副作用]);它也是早于 stop 函数的;

export default {
  setup() {
    const count = ref(0);
    // 首次执行的时候并不会触发 onInvalidate ;
    // 当 count 的值改变时再次触发 watchEffect 的时候,会优先调用 onInvalidate;
    watchEffect(onInvalidate => {
      console.log('count', count.value);
      onInvalidate(() => {
        console.log('onInvalidate');
      });
    });
  }
}

以官网为示例,假设我们现在需要请求一个异步函数,下次调用之前,我们肯定要清楚这个副作用,此时我们就会用到 onInvalidate 这个回调

export default {
  setup() {
    watchEffect(async onInvalidate => {
      const data = await getData();
      onInvalidate(() => {
        data.cancel();
      });
    });
  }
}

这里可以脱离业务做一个小的案例,比如我们现在有一个定时器,一直在给某一个数自增,如果这个这个数字大于n的时候,我们去停止这个定时器,当然啦实现的方法有很多种,但为了演示,还是使用 onInvalidate 去尝试一下;

export default {
  setup() {
    const count = ref(0);
    
    const timerId = setInterval(() => {
    	++count.value;
    }, 2000);
    
    watchEffect(onInvalidate => {
      console.log('count', count.value);
      onInvalidate(() => {
        if (count.valut >= 5) {
           clearInterval(timerId);
        }
      });
    });
  }
}

响应式工具函数

isRef

检测一个值是否为 ref 对象;返回布尔值;需要注意的后面我会提到的 shallowwRef 包裹的 ref 对象也是会返回为 true 的;以下所有拥有 shallow 系列 api 的函数,享有相同的特性,具体差距会在后面讲到;

import { isRef, ref } from 'vue'
export default {
  setup() {
    const count = ref(0);
    // 注意不要加上.value value访问的是值;
    // 一句话概括就是 vue3.x 中只要用.value访问的对象都是 ref 对象;
    console.log(isRef(count)); // true
  }
}

unref

引用官网的原文:如果传入参数是 ref 这个函数会返回内部值, 否则就返回传入值.

这是一个 val = isRef(val) ? val.value : val 的语法糖.

我个人认为也可以理解为可以将一个 ref 对象变成非 ref 的值或者对象,里面也做了一些容错,比如返回的不是 ref 那么就原样返回

import { ref, unref } from 'vue';
export default {
  setup() {
    const count = ref(0);
    const counter = ref({count: 1});
    
    console.log(unref(count));
    // 注意非 ref 对象不代表不是响应式对象;返回 Proxy{count:1}
    console.log(unref(counter));
  }
}

toRef

引用官网的原文:可以为响应式对象的属性创建 ref, 并保留原属性的响应式.

这句话只是为了强调这个 api 它不会丢失原有的响应式,实际上你也可以给一个普通对象使用;

import { toRef, reactive, isRef } from 'vue';
export default {
  setup() {
    // 按照上面api的解释我们可能写出下面的代码
    const obj = reactive({ name: 'zangsan' });
    const name = toRef(obj, 'name');
    console.log(isRef(name)); // true
  }
}

toRefs

接收一个响应式对象(非响应式也可以)使其内部所有的 property 变成 ref 对象;

这个 api 可是有奇用;比如当你的想给模板一些见得值而非对象的时候可以借助这个函数进行结构;

import { reactive, toRefs } from 'vue';
export default {
  setup() {
    const obj = reactive({name: 'zhangsan', age: 24});
    const {name, age} = toRefs(obj);
    return { name, age }
  }
}

isReactive

用来判断这个对象是否是 reactive 创建的对象,如果是被 readonly 包裹的 reactive 对象也会返回 true ;需要注意的包括我们后面介绍的 shallowReactiveshallowReadonly 包裹的对象也会返回为 true 的;

import { reactive, readonly, isReactive } from 'vue'
export default {
 	setup() {
    const obj = reactive({ name: 'zhangsan', age: 24 });
    const readObj = readonly(obj);
    
    console.log(isReactive(obj)); // true
    console.log(isReactive(readObj)); // true
  }
}

isReadonly

判断是否是一个 readonly 或者 shallowReadonly 包裹的只读代理对象;

import { readonly, isReadonly } from 'vue'
export default {
  setup() {
    const obj = readonly({name: 'zhangsan', age: 24});
    console.log(isReadonly(obj)); // true
  }
}

isProxy

判断是否为 readonly 或者 reactive 创建的代理对象;

import { reactive, readonly, isProxy } from 'vue'
export default {
  setup() {
    const obj = { name: "zhangsan", age: 24 };
    const reactiveObj = reactive(obj);
    const readonlyObj = readonly(obj);

    console.log(isProxy(reactiveObj)); // true
    console.log(isProxy(readonlyObj)); // true
  }
}

依赖注入(provide, inject

vue3provide, inject 同样提供了依赖注入的能力,可以在祖孙(父子)间调用,官网对于这也有示例以及描述,provide, inject

在这咱们也做个假设,加入你有两个组件(父子),但是父组件是用 slot 来封装的,比如一下这样

<!-- /compoennts/map.vue -->
<template>
	<slot></slot>
</template>
<!-- /components/marker.vue -->
<template>
	this is marker
</template>

以上这种情况子组件其实并不一定是marker,虽然例子有些牵强,但相信实际工作中都有碰到过类似情况,如果我们想把 map 中的内容传递下去,此时就可以考虑依赖注入

// /compoennts/map.vue
import { provide } from 'vue'
export default {
  setup() {
    // provide 接收两个值,name: String, value: any
    provide('position', {lg: 100, lt: 105})
  }
}
// /compoennts/marker.vue
import { inject } from 'vue'
export default {
  setup() {
    // inject 接收两个值,name: String, default: 默认值
    const position = inject('position')
    return { position }
  }
}

当然也可以注入响应式数据,就是把 provide 的数据,用 ref 或者 reactive 进行包装,但是如果涉及到修改数据,vue 强烈建议你再父组件中进行修改,为了防止用户修改,我们可以在 refreactive 之外再加一个 readonly 进行包裹;

// /compoennts/map.vue
import { reactive, readonly, provide } from 'vue'
export default {
  setup() {
    const pos = reactive({ lg: 100, lt: 105 });
    const updatePos = () => {
      pos.lg = 101
    };
    
    provide('pos', readonly(pos));
    provide('updatePos', updatePos);
  }
}

高级响应式API

markRaw

标记一个对象,使其不能再转换为代理对象,返回值是它本身

export default {
  setup() {
    const testData = { name: "zhangsan", info: { sex: "boy", hoppy: "football" } };

    console.log(isReactive(markRaw(readonly(testData)))); // false
    console.log(isReactive(markRaw(shallowReadonly(testData)))); // false

    console.log(isReactive(markRaw(reactive(testData)))); // false
    console.log(isReactive(markRaw(shallowReactive(testData)))); // false
  }
};

toRaw

返回被 reactive readonly 代理对象的原始对象,包含 shallow 系列的 API,可以在操作数据时,不触发响应式数据的跟踪;

export default {
  setup() {
    const testData = { name: "zhangsan", info: { sex: "boy", hoppy: "football" } };

    const reactiveData = reactive(testData);
    const shallowReactiveData = shallowReactive(testData);

    const readonlyData = readonly(testData);
    const shallowReadonlyData = shallowReadonly(testData);
    // 修改数据
    toRaw(reactiveData).name = 'lisi'
    console.log(reactiveData)

    const refData = ref(testData);
    const shallowRefData = shallowRef(testData);

    console.log(toRaw(reactiveData) === testData);  // true
    console.log(toRaw(shallowReactiveData) === testData); // true

    console.log(toRaw(readonlyData) === testData); // true
    console.log(toRaw(shallowReadonlyData) === testData); // true

    console.log(toRaw(refData) === testData); // false
    console.log(toRaw(shallowRefData) === testData); // false
  }
};

customRef

顾名思义,创建一个自定义的 ref ,可以自行跟踪其中的依赖变化,

它需要一个工厂函数,这个函数要接收两个函数 track trigger 作为参数,并且这个函数要返回一个带有 get set 的对象;

function useDebounceRef(value) {
  return (track, trigger) => {
    return {
      get() {
        track(); // 告诉Vue这里要追踪依赖变化
        return value;
      },
      set(newVal) {
        newVal = value;
        trigger(); // 通知Vue更新视图
      }
    }
  }
}

export default {
  setup() {
    const data = useDebounceRef("")
    
    // 如果在script标签中访问这个 data 就需要使用 data.value
  }
}

shallowRef

创建一个跟踪自身 .value 变化的 ref,但是不会使值也变成响应式的;

这句话太晦涩了,我个人的理解就是,浅包装,只会对第一层数据有响应式

import { ref, shallowRef, isReactive } from 'vue'
export default {
  setup() {
    // 如果是简单数据类型,响应式仍然存在还没有丢失;
    const data = shallowRef('aaa');
    
    // 如果是复杂数据类型,内部是使用reactive来进行包装的,所以我们可以来使用 isReactive 来测试内部数据是否还存在有响应式
    const data1 = shallowRef({a: 'aaa'});
    const data2 = ref({a: 'aaa'});
    
    console.log(isReactive(data1)); // false
    console.log(isReactive(data2)); // true
  }
}

shallowReactive

创建一个只能跟踪自身属性的响应式数据代理,但是不进行深层跟踪;

简单理解就是只有对象的第一层属性是响应式的,内嵌的所有属性,都没有响应式

import { shallowReactive, reactive, isReactive } from 'vue'
export default {
  setup() {
    const data = shallowReactive({ a: "aaa", info: { b: "bbb" } });
    const data1 = reactive({ a: "aaa", info: { b: "bbb" } });

    const changeValue = () => {
      console.log(isReactive(data.info)); // false
      console.log(isReactive(data1.info)); // true
    };
  }
}

shallowReadonly

创建一个只能跟踪自身属性的响应式只读数据代理,但是不进行深层跟踪;

import { readonly, shallowReadonly, isProxy } from 'vue'
export default {
  setup() {
    const data = shallowReadonly({ a: "aaa", info: { b: "bbb" } });
    const data1 = readonly({ a: "aaa", info: { b: "bbb" } });

    const changeValue = () => {
      console.log(isProxy(data.info)); // false 
      console.log(isProxy(data1.info)); // true
    };
  }
}

getCurrentInstance

获取当前组件实例,不要把他当成 this 慎用,后面有使用场景在进行举例说明,只能在 setup 和 生命周期中使用;

import { getCurrentInstance } from 'vue'
export default {
  setup() {
    const instance = getCurrentInstance();
  }
}

内置组件

teleport

vue 中我们虽然可以随意的创建组件,并且挂载到任意位置,但是我们的组件,始终都是在一个 app 容器中,某些时候特定的组件,放到外部分离会显得更加合适,比如 dialog 我们想让他挂载到和 app 作为兄弟节点;此时我们就可以使用 vue3 新增的组件 teleport

比如在 vue2 中我们想封装一个 dialog 组件可能需要这样,样式什么的我这里就不补充了;

<!-- /components/Dialog.vue -->
<template>
  <div class="dialog" v-if="isShow">
    <div class="dialog-hander">
    </div>
    <div class="dialog-content">
      <slot></slot>
    </div>
    <div class="dialog-footer">
      <div v-if="isFooter">
        <button size="mini" type="primary" @click="confirmHandler">确定</button>
        <button size="mini" @click="cancelHanlder">取消</button>
      </div>
      <div v-else>
        <slot name="footer"></slot>
      </div>
    </div>
  </div>
</template>

封装完成之后,我们如果需要把他放到某个页面中去使用他,就肯定需要导入他,比如

<!-- /views/home.vue -->
<teamplte>
	<div class="home-page">
    <button @click="isShow = !isShow">点击</button>
  	<Dialog :isShow="isShow"></Dialog>
  </div>
</teamplte>
<script>
export default {
  name: 'Home',
  data() {
    return {
      isShow: false
    }
  }
}
</script>

image-20210830111804238.png

但是问题回来了,渲染到这里真的很合适吗?如果我们想把这个DialogAPP 同级放到 body 下有什么方案吗,Vue3teleport 内置组件就是为了解决这个问题出现的;它接收一个属性 to

  • 这个属性必须是有效的查询选择器或 HTMLElement (如果在浏览器环境中使用);

此时只需要在我们封装的Dilaog 标签的外部用 teleport 包裹一下,在增加一个 to 属性即可

<template>
	<teleport to="body">
  	... dialog
  </teleport>
</template>

image-20210830112452570.png

应用配置

这个其实还是有挺多的,这里我就说一个我认为日常开发很重的的方法吧,其他的大家可以自行查阅

应用配置

gloalProperties

Vue2Vue.prototype.xxx 的替代者;

const app = createApp({})
app.config.globalProperties.$http = () => {}

// 此时刚好可以用咱们之前讲到的 currentinstance 来获取这个 $http 方法
// /page/xxx.vue
import { getCurrentInstance } from 'vue'
export default {
  setup() {
    setup() {
    	const instance = getCurrentInstance()
      // $http 方法可以通过以下获得
    	console.log(instance.appContext.config.globalProperties)
  	}
  }
}

指令

个人认为比较人性化的修改,更改的指令的生命周期钩子,与组件的钩子基本相同,这里就不详细说明了,有兴趣的可以去官网看下;

import { createApp } from 'vue'
const app = createApp({})

app.directive('my-directive', {
  // 在绑定元素的 attribute 或事件监听器被应用之前调用
  created(el, binding, vnode) {},
  // 在绑定元素的父组件挂载之前调用
  beforeMount(el, binding, vnode) {},
  // 绑定元素的父组件被挂载时调用
  mounted(el, binding, vnode) {},
  // 在包含组件的 VNode 更新之前调用
  beforeUpdate(el, binding, vnode, preVNode) {},
  // 在包含组件的 VNode 及其子组件的 VNode 更新之后调用
  updated(el, binding, vnode, preVNode) {},
  // 在绑定元素的父组件卸载之前调用
  beforeUnmount(el, binding, vnode) {},
  // 卸载绑定元素的父组件时调用
  unmounted(el, binding, vnode) {}
})
  • el :指令绑定到的元素。这可用于直接操作 DOM。

  • binding:是一个对象

    • arg:指令的参数 例如:v-my-directive:param="11" 其中 param 就是 arg 对应的值
    • dir:一个对象,在注册指令时作为参数传递。

image-20210830134554388.png

  • instance:使用这个指令的组件的实例对象;

  • modifiers:指令的修饰符(修饰符可以写在参数的前后任意位置)例如:v-my-directive.sync1:params.sync="11"

  • oldValue:仅在 beforeUpdate update 中可用

  • value:即给指令传递的值 例如:v-my-directive="11" 其中 11 就是 value

  • vnode :虚拟节点

  • preVNode:仅仅在 beforeUpdate updated 钩子中可以,上一次 的虚拟节点;

除了 el 官方建议我们其他的参数尽量不要修改;应该时刻保持只读的状态;

其他

Vue3 的更新当然不止上面的这些,只是个人认为想要入门 Vue3 这些是必须要掌握的,当然还有很多地方,比如像 组件上的 v-model 修改,$attrs 的修改,去除了 $listener 等等,还有Vue3 中一些实验性的功能 Suspense, 这些大家可以去查阅一下官网,毕竟都是面向百度编程的 😂;

Vue2到Vue3非兼容的变更

Vue3 值得注意的新特性

参考内容:

v3.cn.vuejs.org

Vue Composition Api