本文列出Vue3比较重要的几点更新,博客基于Vue3

起步

目录结构:

-——-src
----|--main.js
----|--App.vue
--index.html
--package.json
--vite.config.js

依赖包:

npm install vite @vitejs/plugin-vue -D
npm install vue@next -S

package.json

"scripts": {
    "serve": "vite",
    "build": "vite build"
},

vite.config.js

module.exports = {
    plugins: [require("@vitejs/plugin-vue")()]
}

index.html

<script src="./src/main.js" type="module"></script>

src/main.js

import { createApp } from "vue"
import App from "./App.vue"

createApp(App).mount("#app");

setup

setup运行于beforeCreatecreated之前,这个时候setup里是没有**this**的

如果返回的对象会被全部挂载到组件实例

如果setup返回的是个函数,则会执行这个函数,将其返回值以安全的方式作为vnode返回,此种情况类似于jsx,vite对于vuejs中jsx的插件@vitejs/plugin-vue-jsx中,就有如此要求:1、render函数返回jsx;2、setup返回时以函数进行返回。其本质都是用h函数返回的vnode

注:vue中模板选取有优先级,render > template

<template>
  <div>{{count}}</div>
</template>

<script>
import {
  defineComponent,
  ref,
  h,
} from "vue";
const Comp1 = defineComponent({
  setup() {
    return () => h("span", "asdasd");
  },
});
const Comp2 = defineComponent({
  setup() {
    return {
        count: ref(0)
    };
  },
});
export default Comp2
</script>

组件

Vue3中推荐组件使用defineComponent进行组件的定义,defineComponent接收一个配置对象或函数为参数,配置对象有两种配置方法:

  1. 传统的optional配置方法
  2. setup配置方法

但是定义组件却又多种定义方法,不过推荐第一种

<template>
  <h1>{{ count }}</h1>
</template>

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

// defineComponent传入setup配置的组件
const Comp = defineComponent({
  setup(props, context) {
    const countRef = ref(0);
    return {
      count: countRef.value,
    };
  },
});
  
// 带有setup配置的组件
const SetUpComp = {
  setup(props, context) {
    const countRef = ref(0);
    return {
      count: countRef.value,
    };
  },
};

// 带有defineComponent的optional组件
const OptionalWithDefineComp = defineComponent({
  data() {
    return {
      count: 0,
    };
  },
});

// 没有defineComponent的optional组件
const OptionalWithoutDefineComp = {
  data() {
    return {
      count: 0,
    };
  },
};

// defineComponent传入函数配置的组件
const FunctionComp = defineComponent(function (props, context) {
  const countRef = ref(0);
  return {
    count: countRef.value,
  };
});
export default OptionalWithoutDefineComp;
</script>

异步组件

vue中定义异步组件使用defineAsyncComponent,用于创建一个动态加载的组件。有两种创建方式:

  1. 速写方式

速写方式传入一个Promise的工厂函数。

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => import('./components/AsyncComponent.vue'))

export default AsyncComp
  1. 全写方式

全写方式,传入多种配置:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  // 加载组件的工厂函数
  loader: () => import('./components/AsyncComponent.vue'),
 
  // 加载组件时的loading组件
  loadingComponent: LoadingComponent,
  
  // 加载失败后的error组件
  errorComponent: ErrorComponent,
  
  // 显示loading组件之前的延迟
  delay: 200,
  
  // 加载组件的超时时间,如果超时,显示错误组件,默认值:Infinity
  timeout: 3000,
  
  // 组件是否可挂起,可配合Suspense实现加载状态的控制,脱离组件自身的loading和error
  suspensible: false,
  
  /**
   *
   * @param {*} error 错误信息对象
   * @param {*} retry 重试的函数,当组件加载出错,可调用该函数进行重新加载
   * @param {*} fail  退出加载的函数。fail和retry只可同时调用其一
   * @param {*} attempts 当前重试次数
   */
  onError(error, retry, fail, attempts) {
    if (error.message.match(/fetch/) && attempts <= 3) {
      retry()
    } else {
      fail()
    }
  }
})

render的鸭子类型

对于vue3的所有组件都可写为带有render的鸭子类型的组件,比如下列例子:

// vue3移除了render函数中第一个参数createElement,将其抽离为具名导出的h函数,用于创建vnode
import { defineAsyncComponent, h } from "vue";
const AsyncComp = defineAsyncComponent({
  loader: () =>
    new Promise((resolve, reject) => {
      setTimeout(() => {
        if (Math.random() < 0) {
          resolve({
            render() {
              return h("div", "temp component");
            },
          });
        } else {
          reject("some thing wrong");
        }
      }, 2000);
    }),
  loadingComponent: {
    render() {
      return h("div", "loading");
    },
  },
  errorComponent: {
    render(...args) {
      return h(
        "div",
        {
          style: {
            color: "red",
          },
        },
        "error"
      );
    },
  },
});

属性继承

子组件会将父组件传过来的prop和event中未声明接收的prop和event放置在$attrs,并将$attr所有的属性添加到子组件根节点的attribute,如果组件没有根节点则添加(vue3允许组件有多个根节点,类似React的Fragment,是一个隐式的Fragment

一个非Prop的属性会被子组件的根组件继承,添加到根组件节点的attribute上,这样的规则也同样适用于事件监听(由于该机制,可以弃用.native事件修饰符)。

此操作可以用inheritAttrs进行修改

// Counter.vue
<template>
  <div>
      <h1>counter</h1>
  </div>
</template>

<script>
import { defineComponent } from 'vue'

export default defineComponent({
  emits: ['change'],
  inheritAttrs: true,
})
</script>


// App.vue
<template>
  <Counter style="color: red" @click="alert('111')"></Counter>
</template>

<script>
import {
  defineComponent,
  ref
} from "vue";
import Counter from "./Counter.vue";
const Comp = defineComponent({
  components: {
    Home,
  },
  setup() {
    return {
      alert: (arg) => {
        window.alert(arg);
      },
    };
  },
});
export default Comp;
</script>

指令

Vue3对指令进行了一大波更新,更新包含了生命周期,还有内置指令v-model

指令生命周期

Vue3的指令生命周期和组件的生命周期类似,具有7个生命周期:

createdbeforeMountmountedbeforeUpdateupdatedbeforeMountunmounted,并且都有4参数。

详细查看官方文档:https://v3.cn.vuejs.org/api/application-api.html#directive

<template>
  <div @click="$emit('click', 'asdasd')" v-log.server>
    <h1>sad</h1>
  </div>
</template>

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

export default defineComponent({
  inheritAttrs: false,
  emits: ["change"],
  created() {
    console.log(this.$attrs);
  },
  directives: {
    log: {
      created(el, binding) {},
      beforeMount(el, binding) {},
      mounted(el, binding) {
        if (binding.modifiers.server) {
          console.log("sending log to server");
        }
        else {
          console.log('normal log');
        }
      },
      beforeUpdate(el, binding) {},
      updated(el, binding) {},
      beforeUnmount(el, binding) {},
      unmounted(el, binding) {},
    },
  },
});
</script>

v-model

在Vue2中v-modelsync都用于做双向绑定

但是在Vue3中,sync被移除,只剩下了v-model,所以对于v-model来说,它必须能够传递参数,以保持sync相同的功能。

在之前的博客中有提到,v-model原理就是根据对应的组件合成合适的属性和事件,这里也一样,不过vue3的v-model结合了sync的封装规则,即:属性@update:属性,而且可以对参数进行自定义的modifier传递,下面有个例子:

// App.vue
<tempalte>
	<Editor v-model:checked="checked" v-model:value.trim.cap="value"></Editor>
</tempalte>
<script>
import {
  defineComponent,
  ref
} from "vue";
import Editor from "./components/Editor.vue";
const Comp = defineComponent({
  components: {
    Home,
    Editor,
  },
  setup() {
    return {
      alert: (arg) => {
        window.alert(arg);
      },
      checked: ref(true),
      value: ref(""),
    };
  },
});
</script>

// components/Editor.vue
<template>
  <div class="check-editor">
    <div class="check-editor-inner">
      <div class="checkbox" :class="{ checked }" @click="handleCheck"></div>
      <input type="text" class="editor" :value="value" @input="handleInput" />
    </div>
  </div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
  props: {
    checked: Boolean,
    value: String,
    valueModifiers: { // modifier即组件使用v-model:value.xxx时的参数,规则是`${arg}Modifiers`
      default: {}
    }
  },
  emits: ['update:checked', 'update:value'], // 结合sync修饰符的规则,而且必须在这里进行事件接收,不然会被放到$attrs
  setup(props, context) {
    const handleCheck = () => {
      context.emit("update:checked", !props.checked);
    };
    const handleInput = (e) => {
      let value = e.target.value;
      if (props.valueModifiers.cap) {
        value = value.replace(/(\s+|^)(\w)/g, ($0) => $0.toUpperCase());
      }
      if (props.valueModifiers.trim) {
        value = value.trim();
      }
      context.emit("update:value", value);
    };

    return {
      handleCheck,
      handleInput,
    };
  },
});
</script>

<style scoped>
.check-editor {
  cursor: pointer;
}
.check-editor-inner {
  display: flex;
  align-items: center;
}
.checkbox {
  width: 15px;
  height: 15px;
  border: 1px solid #dcdfe6;
  box-sizing: border-box;
  border-radius: 3px;
  transition: 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
}
.checkbox:hover,
.checkbox.checked {
  border-color: #409eff;
}
.checkbox.checked::after {
  content: "";
  border-radius: 2px;
  width: 9px;
  height: 9px;
  background: #409eff;
}
.editor {
  border: none;
  outline: none;
  margin-left: 5px;
  border-bottom: 1px solid #dcdfe6;
  font-size: inherit;
}
</style>

动态的指令名字

对于**v-onv-bind**,可以存在动态的指令名字,对于v-slot则没有,因为其简写形式的存在是不允许出现这种动态名字的,可以查看我翻译的rfc文档进行详细内容查看:https://github.com/ainuo5213/vuenext-rfcs-translate/blob/main/0003-dynamic-directive-arguments.md, 打个广告(rfc文档我会持续跟进翻译)

<template>
  <div v-if="show">
    <label>账号:</label>
    <input type="text" />
  </div>
  <div v-else>
    <label>密码:</label>
    <input type="text" />
  </div>
  <button @[key]="show = !show">切换</button>
</template>

<script>
import {
  defineComponent,
  ref,
} from "vue";
export default defineComponent({
  setup(props, context) {
    return {
      show: ref(false),
      key: 'click'
    };
  },
});
</script>

自定义指令参数

vue3允许对指令自己封装参数modifier,在如上v-model中有体现。

数据

响应式

vue2中使用Object.defineProperty进行数据响应式,而且需要循环递归每一个对象直到底。这里会有一个问题,如果数据过于庞大,那么就会有大量的事件消耗在响应式数据的递归过程中。

vue3使用Proxy+Reflect进行数据响应式,由于Proxy的特性,不论数据多庞大,我们都只需要new Proxy(data, { ... })进行处理,且不需要递归,非常快速。

定义方式

vue3引入了由于兼容vue2的数据定义方式,现有两种数据来源:datasetup

简单的例子:

import {
  defineComponent,
  ref,
} from "vue";

// 使用data返回响应式数据
const OptionalWithDefineComp = defineComponent({
  components: {
    Home,
  },
  data() {
    return {
      count: 0,
    };
  },
});

// 使用setup返回响应式数据
const SetupComp = {
  setup(props, context) {
    const countRef = ref(0);
    return {
      count: countRef.value,
    };
  },
};

ref

ref常用于声明存储的响应式数据,比如timer、基本类型变量、dom,但是用它去存对象等相对复杂的数据结构也不是不可以。

ref可以用reactive进行封装,区别在于模板中vue对ref有特殊的“拆箱”操作,可以不用取value直接渲染

// App.vue
<template>
    <Counter></Counter>
</template>

<script>
import {
  defineComponent
} from "vue";
import Counter from "./components/Counter.vue";
const Comp = defineComponent({
  components: {
    Counter,
  }
});
export default Comp;
</script>


// components/Counter.vue
<template>
    <div ref="domRef">
        domRef
    </div>
    <button @click="myRef.value++">{{myRef.value}}</button>
</template>

<script>
import { defineComponent, ref, onMounted, onUnmounted } from "vue";
export default defineComponent({
  setup() {
    const timerRef = ref(null);
    onMounted(() => {
      timerRef.value = setInterval(() => {
        console.log("timer calling");
      }, 2000);
    });
    onUnmounted(() => {
      clearInterval(timerRef.value);
      timerRef.value = null;
    });

    const domRef = ref(null);

    onMounted(() => {
        console.log(domRef.value);
    });

    return {
        domRef,
        
        // 使用reactive封装类似的ref,因为在模板中vue编译器对ref有特殊的“拆箱”操作,所以在模板中可以不用取value而直接渲染
        myRef: reactive({
            value: 1
        })
    }
  },
});
</script>

<style></style>

shallowRef

创建浅层比较的响应式数据,只监听其value属性改没改,不会监听value内部的属性值改变与否,所以对于shallowRef如果value内部是一个对象,则必须赋值新的对象或使用triggerRef手动触发副作用才能实现响应式

<template>
  <!-- 注:这里不用.value!!!因为有“拆箱”操作 -->
  <button @click="increase">{{ shaRef.count }}</button>
</template>

<script>
import { defineComponent, shallowRef, triggerRef } from "vue";
export default defineComponent({
  setup() {

    const shaRef = shallowRef({
        count: 1
    });

    const increase = () => {
        shaRef.value = {
            count: shaRef.value.count + 1
        }
        // triggerRef(shaRef);
    }

    return {
      shaRef,
      increase
    };
  },
});
</script>

reactive

返回对象形式的响应式数据。如果传递的属性值是ref会有“拆箱”操作,例子如上

生命周期

Optional

对于Optional配置方式的组件,其常用的生命周期有:

beforeCreate:在setup之后调用,进行数据侦听和事件/侦听器的配置

created:在setup之后调用,此时$el尚不可用,但数据侦听、计算属性、方法和事件回调函数已配置完成

beforeMount:挂载到DOM之前调用

mounted:组件已被挂载到DOM。此时可以用$el$refs获取真实的DOM。

beforeUpdate:组件的数据、props发生改变之后,在更新到DOM之前调用。

updated:真实DOM更新渲染完成调用。

activated:使用keep-alive被缓存到栈里面的组件被激活时调用。

deactivated:使用keep-alive被缓存组件失活(非组件过期,而是被组件切换导致的DOM移除)时调用

beforeUnmount:组件即将卸载前调用,这个阶段组件实例方法仍可用

unmounted:组件卸载完成调用,这个阶段组件实例方法、侦听器、指令、子组件都已被卸载或接触绑定

beforeUnmountunmounted可做一些清楚副作用的事情,不过建议最好在beforeUnmount进行处理

setup

对于setup配置方式的组件,其常用声明周期有:

beforeCreate、created => setup

beforeMount => onBeforeMount

mounted => onMounted

beforeUpdate => onBeforeUpdate

updated => onUpdated

beforeUnmount => onBeforeUnmount

unmounted => onUnmounted

activated => onActivated

deactivated => onDeactivated

对于setup配置方式的组件,其生命周期可以多次声明,类似于React的useEffect等hook,可以做到一个数据依赖对应一系列生命周期函数处理代码段

<script>
import {
  defineComponent,
  ref,
  onMounted,
  onUnmounted,
  reactive,
  shallowRef,
  triggerRef,
} from "vue";
export default defineComponent({
  setup() {
    // handle timer
    const timerRef = ref(null);
    onMounted(() => {
      timerRef.value = setInterval(() => {
        console.log("timer calling");
      }, 2000);
    });
    onUnmounted(() => {
      clearInterval(timerRef.value);
      timerRef.value = null;
    });

    // handle dom ref
    const domRef = ref(null);
    onMounted(() => {
      console.log(domRef.value);
    });

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

声明周期函数执行是同步执行(hook回调同步执行,如果里面有异步代码遵循事件循环规则),所以对于同一种生命周期,写在前面的先执行。

插件

vue3的插件编写和使用有所不同,vue3插件可以是个方法、也可以是个对象,一般来说我们都会写为一个对象,如果是一个对象则需要对象中有一个静态的install方法,一个插件只能安装一次。如果是setup配置方式,由于这个时候没有this,则需要通过getCurrentInstance进行组件实例的获取

// main.js
import { createApp } from "vue"
import App from "./App.vue"
class Plugin {
    static install(app, options) {
        app.config.globalProperties.alert = msg => {
            window.alert(msg);
        }
    }
}
createApp(App).use(Plugin).mount("#app");

// components/Counter.vue
<template>
  <div ref="domRef">domRef</div>
  <button @click="increase">{{ shaRef.count }}</button>
</template>

<script>
import {
  defineComponent,
  ref,
  getCurrentInstance
} from "vue";
export default defineComponent({
  setup() {

    const shaRef = shallowRef({
      count: 1,
    });

    // getCurrentInstance获取当前组件实例,只能在setup或生命周期函数中使用
    const app = getCurrentInstance();
    const increase = () => {
      shaRef.value.count = shaRef.value.count + 1;
      triggerRef(shaRef);
       
      // 使用插件中注入到Vue的实例方法
      app.appContext.config.globalProperties.alert("alert");
    };
    

    return {
      shaRef,
      increase,
    };
  },
});
</script>

方法和计算属性

方法

vue3的方法和Vue2方法一样,都可以用optional配置方式进行配置,不过这里推荐使用setup配置方式,例子如上

计算属性

vue3中计算属性使用computed进行声明,它会返回一个ref类型的响应式数据,有两种写法:

例子来源于官方文档

  1. 速写形式:传递一个回调函数,当依赖项变化的时候,该ref的回调会执行,这个计算属性是只读的
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误
  1. 全写形式:传递一个带有getset函数的对象,用来创建可读写的对象
const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value)

实例属性和方法

在Vue3中,通过optional配置方式,我们依然可以使用this.$xxx获取Vue暴露给外部的API,例如:$emit$el$(app)、$refs$watch$nextTick$props

但是对于setup配置方式的组件,则无法通过实例进行获取,因为在setup里是没有this的,但是可以通过Vue类库暴露给外部的方法进行使用,例如context.emitcontext.slotscontext.attrs,其中context是setup的第二个参数,第一个参数是props

<template>
  <div></div>
</template>

<script>
import {
  defineComponent,
} from "vue";
export default defineComponent({
  setup(props, context) {
      console.log(props, context);
  },
  created() {
    console.log(this);
  },
});
</script>

效率提升

以下代码可以查看请求中对于Vue的编译结果

F12 > 网络 > xxx.vue > 预览

静态提升

静态节点

Vue3的编译器会将没有动态内容的模板抽离出去形成当前作用域可用的静态vnode,避免每次调用render时,重复创建静态的vnode。

// vue2 的静态节点
render(){
  createVNode("h1", null, "Hello World")
  // ...
}

// vue3 的静态节点
const hoisted = createElementVNode("h1", null, "Hello World")
function render(){
  // 直接使用 hoisted 即可
}
静态属性

Vue3对于静态属性,例如类名自定义属性等会将其提升到外部进行公共,避免多次调用render重复创建对象。

<div class="user">
  {{user.name}}
</div>
const hoisted = { class: "user" }

function render(){
  createElementVNode("div", hoisted, user.name)
  // ...
}

预字符串化

当编译器遇到大量连续的静态内容,会将其直接编译为一个普通的字符串节点

<template>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
</template>

<script>
import {
  defineComponent
} from "vue";
export default defineComponent({
  setup(props, context) {
    return {};
  },
});
</script>
const hoisted =
createStaticVNode("<div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div>")

缓存事件处理函数

Vue3中将事件处理函数加入缓存中

<template>
  <div @click="click">2</div>
</template>

<script>
import {
  defineComponent
} from "vue";
export default defineComponent({
  setup(props, context) {
    const handleClick = () => {
        console.log(111);
    }
    return {
        click: handleClick
    };
  },
});
</script>
render(ctx, _cache){
  return createElementVNode("div", {
    onClick: cache[0] || (cache[0] = (...args) => (ctx.click && ctx.click(...args)))
  }, "2")
}

Block Tree

Vue3在通过diff算法比较vnode时,会将静态节点排除在外,只比较动态内容及其节点。

<form>
  <div>
    <label>账号:</label>
    <input v-model="user.loginId" />
  </div>
  <div>
    <label>密码:</label>
    <input v-model="user.loginPwd" />
  </div>
</form>

Patch规则:

vuepress 热更新 vue3更新_javascript

而且对于v-ifv-else会默认生成不重复的key,而这在Vue2是不能办到的。

<template>
  <div v-if="show">
    <label>账号:</label>
    <input type="text" />
  </div>
  <div v-else>
    <label>密码:</label>
    <input type="text" />
  </div>
  <button @click="show = !show">切换</button>
</template>

<script>
import {
  defineComponent,
  ref,
} from "vue";
export default defineComponent({
  setup(props, context) {
    return {
      show: ref(false)
    };
  },
});
</script>

如上例子在切换时,由于v-ifv-else会默认加上key,所以切换show时,会生成新的DOM;而在Vue2中,这段代码在执行diff算法时,被识别为一个虚拟dom,切换时仅仅切换label下的内容,而不会对input进行处理

PatchFlag

Vue3在创建节点时,会给动态节点打个标记,让vue在执行diff算法对比时,清楚哪里是动态的,我就只比较动态部分是否相同,而不必想vue2中那样对比节点类型keyinput中的type等,这里可以通过浏览器中查看createElementVNode最后一个参数体现出来

<div class="user" data-id="1" title="user name">
  {{user.name}}
</div>

vuepress 热更新 vue3更新_vuepress 热更新_02

其他

隐藏的根节点

Vue3在模板中可以存在多个根节点,但实际上这里有一个类似于React的Fragment的隐藏根节点,对应vue中的Fragment

友好的TreeShaking

vue2中绝大部分的API被挂载到Vue或者Vue实例,这是不利于treeshaking的,会导致打包之后体积庞大

vue3中将大部分私有的、共有的API以具名导出的方式导出,依赖于ES模块依赖解析器,这将极大的减少打包后的体积,只打包用到的API

对于treeshaking:

  1. 具名导出,具名导入方式处理模块的导入导出会触发treeshaking
  2. import * as fromimport xxx fromimport()导入模块不会触发treeshaking

v-if和v-for的优先级问题

在vue2中,v-for优先级比v-if要搞,所以能在v-for里面使用v-if,但是vue并不推荐连用,所以在vue3中这一项规则干脆就取消了,v-if优先级比v-for要搞,例如,如下代码将会报错:

<template>
  <ul>
      <!-- 由于v-if优先级比v-for要高,所以user是undefined,访问user.age会报错 -->
      <li v-for="user in users" :key="user.id" v-if="user.age > 18">asdasd</li>
  </ul>
</template>

<script>
import { defineComponent, ref } from "vue";
export default defineComponent({
  setup(props, context) {
    return {
      users: [
        {
          id: 1,
          name: "aaa",
          age: 12
        },
        {
          id: 2,
          name: "mmm",
          age: 20
        },
      ],
    };
  },
});
</script>

Teleport传送门

Teleport在vue3中是一个内置组件,他可以改变一个vnode的渲染目标,常用语模态框、message、notification等组件的渲染,参数to即目标容器的选择器

多个teleport到目标容器时,会追加内容而非替换内容

<template>
  <teleport to="#app"> hello </teleport>
</template>

<script>
import { defineComponent, ref } from "vue";
export default defineComponent({
  setup(props, context) {
    return {
      show: ref(false),
      key: "click",
    };
  },
});
</script>