记录一下vue 3的学习过程

801 阅读8分钟

Vue 3学习笔记

1. 环境搭建

  • 参考资料:官方文档

  • 使用vue-cli搭建脚手架工具 -> 创建一个vue工程 -> 安装vue-next

    yarn global add vue-cli
    vue create vue-test
    cd vue-test
    vue add vue-next
    

2. 基础api使用

1. setup

  • 功能类似于原来的mounted
  • 接受参数 props, context
  • 如果需要使用到pros,需要先在props这个参数中定义对应props的类型, 不要解构赋值(会无法监听到传入参数的变化)
  • context中包含了对于内部组件的代理值包括attrs, slots等(可以解构赋值,一直能拿到最新值)
<template>
  <div>{{ count }} / {{ object.foo }}</div>
</template>

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

export default {
  props: {
    msg: String,
  },
  setup(props) {
    const count = ref(0);
    const object = reactive({ foo: "bar" });
    console.log(props.msg);

    return {
      count,
      object,
    };
  },
};
</script>

2. 响应式api

1. reactive和ref

官方解释:

reactive: 接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()

ref: 接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value

小例子
<template>
  <div>
    <div>{{ count }} / {{ object.foo }}</div>
    <button @click="addNumber">+1</button>
  </div>
</template>

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

export default {
  props: {
    msg: String,
  },
  setup(props) {
    const count = ref(0);
    const object = reactive({ foo: "bar" });
    console.log(props.msg);

    const addNumber = () => {
      count.value++;
      object.foo += count.value;
    };

    return {
      count,
      object,
      addNumber,
    };
  },
};
</script>

<style>
</style>
结果分析与总结

结果: 点击按钮无论是count还是object中的foo 都会同时变化

  • reactive 个人理解应该用在对于引用类型/对象类型的监听上,因为调用对应的值进行响应式变化的时候直接调用object.foo即可
  • ref个人理解,适用于基础类型,因为其引用的时候需要从count.value中获取,这里要注意在模板中会自动调用ref.value
  • ref如果传入的是一个对象,会自动调用reactive进行深层次转换

2. computed

和vue2中的watch类似,但是是一个hooks函数调用的形式

  • 直接传入一个函数为默认定义了一个get方法
  • 手动写入get和set方法定义了一个ref对象,set方法为该computed改变时调用的回调
小例子
<template>
  <div>
    <div>computed -> {{ computeCount }}</div>
    <ul>
      <li>{{ count1 }}</li>
      <li>{{ count2 }}</li>
      <li>{{ reactiveCount.count }}</li>
    </ul>
      
    <div class="con">
      <button @click="addCount">+1</button>
      <button @click="decrCount">-1</button>
    </div>
  </div>
</template>

<script>
import { ref, reactive, computed, watchEffect } from "vue";

export default {
  setup() {
    const count1 = ref(0);
    let count2 = ref(1);
    const reactiveCount = reactive({ count: count2 });
    let computeCount = computed({
      get: () => count2.value + 1,
      set: (val) => (count2.value = val - 1),
    });

    const addCount = () => {
      computeCount.value++;
    };

    const decrCount = () => {
      computeCount.value--;
    };

    return {
      count1,
      count2,
      computeCount,
      reactiveCount,
      addCount,
      decrCount,
    };
  },
};
</script>

<style></style>

结果分析

当通过点击+1,-1 更改computeCount值的时候,count2的值会同时变化

3. watchEffect

官方定义:立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数。类似react中的useEffect

小例子
  • watchEffect 副作用的添加(vue 3会自动识别为副作用添加依赖项,不像react需要手动添加)
  • 调用watchEffect返回值,可以解除副作用
  • watchEffect接受一个onInvalidate的函数,当该副作用被解除或者组件被卸载的时候会调用该函数
<template>
  <div>
    <div>computed -> {{ computeCount }}</div>
    <ul>
      <li>{{ count1 }}</li>
      <li>{{ count2 }}</li>
      <li>{{ reactiveCount.count }}</li>
    </ul>
    <div class="con">
      <button @click="addCount">+1</button>
      <button @click="decrCount">-1</button>
      <button @click="stopEffect">Stop</button>
    </div>
  </div>
</template>

<script>
import { ref, reactive, computed, watchEffect } from "vue";

export default {
  setup() {
    let count1 = ref(0);
    let count2 = ref(1);
    let timer = null;
    const reactiveCount = reactive({ count: count2 });
    let computeCount = computed({
      get: () => count2.value + 1,
      set: (val) => (
        (count2.value = val - 1), console.log("computed改变为", val)
      ),
    });

    const addCount = () => {
      computeCount.value++;
    };

    const decrCount = () => {
      computeCount.value--;
    };

    const stop = watchEffect((onInvalidate) => {
      if (!timer) {
        timer = setInterval(() => {
          count1.value++;
        }, 1000);
      }

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

    watchEffect(() => {
      console.log("computedCount = ", computeCount.value);
      console.log("count1 = ", count1.value);
      console.log("count2 = ", count2.value);
    });
      
    const stopEffect = () => stop();

    return {
      count1,
      count2,
      computeCount,
      reactiveCount,
      addCount,
      decrCount,
      stopEffect,
    };
  },
};
</script>

<style></style>

结果分析与总结

结果

  1. 当computeCount改变的时候,会打印computeCount, count1, count2的值,count1的每1s会增加
  2. computed的console前于watchEfffect
  3. 点击Stop按钮后,count1不再增加且点击+1和-1不会再有console

从上述可以得到几个结论:

  • computed的set响应前于watchEffect
  • watchEffect会自动收集依赖,且异步执行,且在同一个tick中多次执行一个watchEffect会被自动合并
  • 使用onInvalidate可以执行一个函数去取消失效回调,或之前副作用中的失效的异步操作

4. watch

官方说明: watch API 完全等效于 2.x this.$watch (以及 watch 中相应的选项)。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。

小例子
<template>
  <div>
    <div>computed -> {{ computeCount }}</div>
    <ul>
      <li>{{ count1 }}</li>
      <li>{{ count2 }}</li>
      <li>{{ reactiveCount.count }}</li>
    </ul>
    <slot></slot>
    <div class="con">
      <button @click="addCount">+1</button>
      <button @click="decrCount">-1</button>
    </div>
  </div>
</template>

<script>
import { ref, reactive, computed, watchEffect, watch } from "vue";

export default {
  setup() {
    let count1 = ref(0);
    let count2 = ref(1);
    const reactiveCount = reactive({ count: count2 });
    let computeCount = computed({
      get: () => count2.value + 1,
      set: (val) => (
        (count2.value = val - 1), console.log("computed改变为", val)
      ),
    });

    const addCount = () => {
      computeCount.value++;
    };

    const decrCount = () => {
      computeCount.value--;
    };

    watch(
      () => count2.value,
      (newCount2, preCount2) => {
        console.log("单一数据监听: new: ", newCount2, "prev: ", preCount2);
      }
    );

    watch(
      [computeCount, count2],
      ([newComputeCount, newCount2], [prevComputed, prevCount2]) => {
        console.log(
          "多数据监听: computed = ",
          newComputeCount,
          "count2 = ",
          newCount2
        );
        console.log(
          "多数据监听: prev_computed = ",
          prevComputed,
          "prev_count2 = ",
          prevCount2
        );
      }
    );

    return {
      count1,
      count2,
      computeCount,
      reactiveCount,
      addCount,
      decrCount,
    };
  },
};
</script>
结果分析与总结

点击 + 1按钮有如下console:

  • 单一数据监听: new: 2 prev: 1

  • 多数据监听: computed = 3 count2 = 2

  • 多数据监听: prev_computed = 2 prev_count2 = 1

总结

  1. 如果watch监听ref定义的变量需要监听到x.value, 不然无法生效
  2. 功能和watchEffect类似,值变化之后调用回调,但是watchEffect是自动获取依赖,watch需要自己手动绑定依赖

3. vue 3生命周期

vue 2生命周期vue 3对应生命周期备注
beforeCreatesetup
createdsetup现在初始化的工作可以通过setup完成,setup完成数据的初始化
beforeMountonBeforeMount根据初始化的数据挂载对应的页面元素
mountedonMounted元素挂载完成
beforeUpdateonBeforeUpdate页面元素更新之前的回调, 但其实没有现在通过这个回调获取变化之后和现在的state
updatedonUpdated更新完毕后的回调函数,如果针对单一变量的话,我理解可以直接用watchEffect来实现对于单一元素的变化回调
beforeDestoryonBeforeUnmount组件卸载之前的回调
destoryonUnmounted组件卸载之后的回调

1. 执行顺序探究

小例子
// lifeCircle.vue
<template>
  <div>
    <h1>LifeCircle</h1>
    <div>count: {{ count }}</div>
    <button @click="addCount">+1</button>
  </div>
</template>

<script>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  ref,
} from "vue";

export default {
  setup() {
    console.log("setup");
    const count = ref(0);

    const addCount = () => {
      count.value++;
    };

    onBeforeMount(() => {
      console.log("onBeforeMount");
    });

    onMounted(() => {
      console.log("onMounted");
    });

    onBeforeUpdate(() => {
      console.log("onBeforeUpdate");
    });

    onUpdated(() => {
      console.log("updated");
    });

    onBeforeUnmount(() => {
      console.log("onBeforeMount");
    });

    onUnmounted(() => {
      console.log("onUnmounted");
    });

    return {
      count,
      addCount,
    };
  },
};
</script>

<style>
</style>
// Index.vue
<template>
  <div id="app">
    <button @click="showLifeCircle">show life circle</button>
    <life-circle v-if="isShowLife" />
  </div>
</template>

<script>
import LifeCircle from "./components/LifeCircle.vue";

export default {
  name: "App",
  components: {
    LifeCircle,
  },
  data() {
    return {
      isShowLife: false,
    };
  },
  methods: {
    showLifeCircle() {
      this.isShowLife = !this.isShowLife;
    },
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

结果分析和总结
  1. 没有数据更新场景的执行过程
  • 点击show life circle按钮出现lifeCircle组件,console提示
    • setup
    • onBeforeMount
    • onMounted
  • 点击 + 1按钮
    • onBeforeUpdate
    • updated
  • 再次点击show life circle按钮lifeCircle消失,console提示
    • onBeforeMount
    • onUnmounted

结论1

  • 当对于没有在声明周期中更新页面data的过程,在创建组件过程中执行setup,onBeforeMount, onMounted
  • 在数据更新时候更新顺序为**onBeforeUpdate,updated **
  • 当组件卸时的更新顺序: onBeforeMount, onUnmounted
  1. 如果存在数据更新的时候的执行过程
// 在setup中添加下面的代码,来模拟onMounted时候异步载入数据时候的情况

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

onMounted(() => {
    // mounted中加载数据
    console.log("onMounted");
    count.value++;
});
  • 上述当点击show life circle加载LifeCycle组件的时候,页面的更新为
    • setup
    • 0
    • onBeforeMount
    • onMounted
    • onBeforeUpdate
    • 1
    • updated

从结果可以看到:

  • 数据在setup更新后,直接通过watchEffect钩子函数执行(此时不执行update钩子)
  • 在mounted更新后,会进入update的过程
  • watchEffect在更新过程中的执行顺序: onBeforeUpdate -> watchEffect -> updated

4. 依赖注入

provide, inject 类似于React中的Context,我们可以通过Context上下文将一个共同需要用到的状态或者方法直接在深层的子组件中拿到,而不需要通过一直将方法嵌套的模式来完成

调用方法

  1. 在setup中调用,在需要提供全局变量父组件中,直接使用provide提供一个对应的全局变量(可以是响应式的参数)
  2. 在深层需要使用该变量的时候,通过inject拿到对应的变量
  3. 这个变量如果是响应式的,当其变化的时候可以自动更新视图

小例子

  • 在这里我们构造三个嵌套组件最外层Parent、一级子组件Son, 二级嵌套子组件GrandSon
/*Parent.vue*/
<template>
  <div>
    <h2>Parent</h2>
    <Son />
  </div>
</template>

<script>
import { provide, reactive } from "vue";
import Son from "./Son";

export default {
  setup() {
    const userInfo = reactive({
      name: "",
      age: 0,
      right: 0,
    });

    provide("commonInfo", userInfo);

    return {
      userInfo,
    };
  },
  components: {
    Son,
  },
};
</script>

<style>
</style>
/*Son.vue*/
<template>
  <div>
    <h3>Son</h3>
    <ul>
      <li>名字:{{ common.name }}</li>
      <li>年龄:{{ common.age }}</li>
      <li>权利等级: {{ common.right }}</li>
    </ul>
    <GrandSon />
  </div>
</template>

<script>
import GrandSon from "./GrandSon";
import { inject } from "vue";

export default {
  setup() {
    const common = inject("commonInfo", {});
    console.log(common);

    return {
      common,
    };
  },
  components: {
    GrandSon,
  },
};
</script>

<style>
</style>
/*GrandSon*/
<template>
  <div class="wrap">
    <fieldset>
      <legend>GrandSon Modify</legend>
      <div class="item">
        <span>修改姓名</span>
        <input type="text" v-model="common.name" />
      </div>

      <div class="item">
        <span>修改年龄</span>
        <input type="number" v-model="common.age" />
      </div>

      <div class="item">
        <span>修改权利</span>
        <div>
          <input type="radio" id="Manager" value="10" v-model="common.right" />
          <label for="Master">管理员</label>
        </div>
        <div>
          <input type="radio" id="Origin" value="1" v-model="common.right" />
          <label for="Origin">普通人</label>
        </div>
        <div>
          <input type="radio" id="None" value="0" v-model="common.right" />
          <label for="None"></label>
        </div>
      </div>
    </fieldset>
  </div>
</template>

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

export default {
  setup() {
    const common = inject("commonInfo", {});
    console.log(common);

    return {
      common,
    };
  },
};
</script>

<style>
.item {
  text-align: left;
}

.wrap {
  width: 300px;
  margin: 0 auto;
}
</style>

结果

分析

  • 通过provide(key, value)在上层绑定一个全局变量,然后在需要使用的地方通过inject(key)得到对应的绑定结果
  • provide中可以通过传入响应式参数,当深层次组件更新该元素后使上层相关视图也一起变化

5. 模板Refs

在vue 3中ref和原来vue 2中获取元素实体的ref进行了统一,首先来看一个vue3中,通过外部按钮focus对应input的例子

使用ref获取元素实例

<template>
  <div>
    <input type="text" :ref="inputRef" />
    <button @click="focusInput">focus</button>
  </div>
</template>

<script>
import {
  ref,
  reactive,
  onBeforeUpdate,
  unref,
  onMounted,
  toRef
} from "vue";

export default {
  setup() {
    const inputRef = ref(null);
    const focusInput = () => {
      inputRef.value && inputRef.value.focus();
    };

    return {
      inputRef,
      focusInput,
    };
  },
};
</script>

结果1

针对v-for的多ref场景例子

很多时候我们需要绑定多个ref的值,但是v-for根据官方文档解释,其实并不支持多绑定,这个时候官方文档给出了一种v-for的多ref绑定方法,需要注意几个关键点

<template>
<div>
  <div>
    <input type="text" :ref="inputRef" />
    <button @click="focusInput">focus</button>
  </div>
  <div v-for="(item, i) in list" :ref="el => { divs[i] = el }" :key="{i}">{{ item }}</div>
</div>
</template>

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

export default {
  setup() {
    let list = ref([1, 2, 3]);
    const divs = ref([]);

    const getRefListInner = () => {
      const divRef = divs.value;

      divRef.forEach((ref) => {
        console.log(ref.innerHTML);
      });
    };

    onMounted(() => {
      setTimeout(() => {
        list.value = [4, 5, 6];
      }, 500);
    });

    watchEffect(() => {
      getRefListInner();
    });
      
    return {
      list,
      divs,
    };
  },
};
</script>

结果2

随着ref的更新我们可以通过绑定的divs获取得到最新的ref元素

6. 响应式工具集

工具名作用例子备注
unref获取ref.value的值,即unref -> ref.value ?? ref可以换参考下面的例子1如果是简单类型直接返回值,如果是复杂类型是一个包了proxy的复杂类型
toRef复杂类型的reative中的某些属性转换为ref保留响应式 (reactive复杂类型中的单个元素)可以换参考下面的例子2复杂类型的reactive才有效,简单类型无效
toRefs整个复杂类型的reactive变为ref,且保留响应参考下面的例子3返回的是一个对象,但是其中的属性都是ref
isRef判断是不是一个ref类型//
isProxy判断是不是被proxy包了一层//
isReactive判断整体是不是一个reactive//
isReadonly//

1. 例子1

const _ref = ref(1);
const _ref2 = ref({ a: 1 });

console.log("_ref = ", unref(_ref)); // 1
console.log("_ref2 = ", unref(_ref2)); // Proxy{ a: 1, __v_reactive: Proxy }
console.log("isRef = ", isRef(_ref))  // true
console.log("isRef2 = ", isRef(_ref2))

console.log("isRef = ", isRef(_ref)) // true
console.log("isRef2 = ", isRef(_ref2)) // true
console.log("isProxy ref2 = ", isProxy(_ref2)) // false
console.log('isReactive ref = ', isReactive(_ref2)) // false
console.log("isProxy ref2 = ", isProxy(_ref2.value)) // true
console.log("isReactive = ", isReactive(_ref2.value)) // true

结论:如果是通过ref包的不管是基础类型还是引用都是都是ref,但是如果是引用类型,_ref2.value其实是一个reactive也是proxy,就是如果是引用类型的ref,其实是包着ref的壳,内部还是proxy和reactive

2. 例子2

const _reactive1 = reactive(1);
const _reactive2 = reactive({
    a: 1,
});

const _reactive2Ref = toRef(_reactive2, "a");
const _reactive1Ref = toRef(_reactive1);
_reactive2Ref.value++;

console.log("_reactive = ", _reactive1Ref.value); // Proxy {} 啥都没 找不到 1
console.log("_reactive2 = ", _reactive2.a);  // 2 之前的value++影响到了这里
console.log('isReactive reactive1 = ', isReactive(_reactive1)) // true
console.log('isReactive reactive2 = ', isReactive(_reactive2)) // false
console.log('isRef reactive2 = ', isRef(_reactive1)) // false

console.log('toRef single elem = ', isRef(_reactive1Ref)) // true
console.log("toRef total = ", isRef(_reactive1Ref)) // true

结论: reactive用于复杂对象时,才是reactive类型,因此,其实我理解官方在定义这api的时候,其实希望我们能将引用类型传给reactive,然后如果是基础类型,是通过ref来保持响应的。

. 例子3
const _reactive2 = reactive({
    a: 1,
    b: 2
});
const _reactiveRefs = toRefs(_reactive2);
console.log("_refs = ", _reactiveRefs.b.value);

console.log("toRefs = ", isRef(_reactiveRefs))  // false
console.log('toRefs is Proxy = ', isRef(_reactiveRefs)) // false
console.log('toRefs isReactive = ', isReactive(_reactiveRefs)) // false
console.log("toRefs elem = ", isRef(_reactiveRefs.a)) // true

结论toRefs实际上就是将一个引用对象内部拆成多个ref然后再进行组合