跳到主要内容

Vue3.0 所采用的 Composition Api 与 Vue2.x 使用的 Options Api 有什么不同?

开始之前

Composition API 可以说是Vue3的最大特点,那么为什么要推出Composition Api,解决了什么问题?

通常使用Vue2开发的项目,普遍会存在以下问题:

  • 代码的可读性随着组件变大而变差
  • 每一种代码复用的方式,都存在缺点
  • TypeScript支持有限

以上通过使用Composition Api都能迎刃而解

正文

一、Options Api

Options API,即大家常说的选项API,即以vue为后缀的文件,通过定义methodscomputedwatchdata等属性与方法,共同处理页面逻辑

如下图:

image

可以看到Options代码编写方式,如果是组件状态,则写在data属性上,如果是方法,则写在methods属性上...

用组件的选项 (datacomputedmethodswatch) 组织逻辑在大多数情况下都有效

然而,当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解

二、Composition Api

在 Vue3 Composition API 中,组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(更加的高内聚,低耦合)

即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所有 API

image

三、对比

下面对Composition Api Options Api进行两大方面的比较

  • 逻辑组织
  • 逻辑复用
逻辑组织
Options API

假设一个组件是一个大型组件,其内部有很多处理逻辑关注点(对应下图不用颜色)

image

可以看到,这种碎片化使得理解和维护复杂组件变得困难

选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块

Compostion API

Compositon API正是解决上述问题,将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去

下面举个简单例子,将处理count属性相关的代码放在同一个函数了

function useCount() {
let count = ref(10);
let double = computed(() => {
return count.value * 2;
});

const handleConut = () => {
count.value = count.value * 2;
};

console.log(count);

return {
count,
double,
handleConut,
};
}

组件上中使用count

export default defineComponent({
setup() {
const { count, double, handleConut } = useCount();
return {
count,
double,
handleConut
}
},
});

再来一张图进行对比,可以很直观地感受到 Composition API 在逻辑组织方面的优势,以后修改一个属性功能的时候,只需要跳到控制该属性的方法中即可

逻辑复用

Vue2中,我们是用过mixin去复用相同的逻辑

下面举个例子,我们会另起一个mixin.js文件

export const MoveMixin = {
data() {
return {
x: 0,
y: 0,
};
},

methods: {
handleKeyup(e) {
console.log(e.code);
// 上下左右 x y
switch (e.code) {
case "ArrowUp":
this.y--;
break;
case "ArrowDown":
this.y++;
break;
case "ArrowLeft":
this.x--;
break;
case "ArrowRight":
this.x++;
break;
}
},
},

mounted() {
window.addEventListener("keyup", this.handleKeyup);
},

unmounted() {
window.removeEventListener("keyup", this.handleKeyup);
},
};

然后在组件中使用

<template>
<div>
Mouse position: x {{ x }} / y {{ y }}
</div>
</template>
<script>
import mousePositionMixin from './mouse'
export default {
mixins: [mousePositionMixin]
}
</script>

使用单个mixin似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时候

mixins: [mousePositionMixin, fooMixin, barMixin, otherMixin]

会存在两个非常明显的问题:

  • 命名冲突
  • 数据来源不清晰

现在通过Compositon API这种方式改写上面的代码

import { onMounted, onUnmounted, reactive } from "vue";
export function useMove() {
const position = reactive({
x: 0,
y: 0,
});

const handleKeyup = (e) => {
console.log(e.code);
// 上下左右 x y
switch (e.code) {
case "ArrowUp":
// y.value--;
position.y--;
break;
case "ArrowDown":
// y.value++;
position.y++;
break;
case "ArrowLeft":
// x.value--;
position.x--;
break;
case "ArrowRight":
// x.value++;
position.x++;
break;
}
};

onMounted(() => {
window.addEventListener("keyup", handleKeyup);
});

onUnmounted(() => {
window.removeEventListener("keyup", handleKeyup);
});

return { position };
}

在组件中使用

<template>
<div>
Mouse position: x {{ x }} / y {{ y }}
</div>
</template>

<script>
import { useMove } from "./useMove";
import { toRefs } from "vue";
export default {
setup() {
const { position } = useMove();
const { x, y } = toRefs(position);
return {
x,
y,
};

},
};
</script>

可以看到,整个数据来源清晰了,即使去编写更多的 hook 函数,也不会出现命名冲突的问题

小结

  • 在逻辑组织和逻辑复用方面,Composition API是优于Options API
  • 因为Composition API几乎是函数,会有更好的类型推断。
  • Composition API tree-shaking 友好,代码也更容易压缩
  • Composition API中见不到this的使用,减少了this指向不明的情况
  • 如果是小型组件,可以继续使用Options API,也是十分友好的

Vue3.0性能提升主要是通过哪几方面体现的?

一、编译阶段

回顾Vue2,我们知道每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把用到的数据property记录为依赖,当依赖发生改变,触发setter,则会通知watcher,从而使关联的组件重新渲染

image

试想一下,一个组件结构如下图

<template>
<div id="content">
<p class="text">静态文本</p>
<p class="text">静态文本</p>
<p class="text">{{ message }}</p>
<p class="text">静态文本</p>
...
<p class="text">静态文本</p>
</div>
</template>

可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff 和遍历其实都是不需要的,造成性能浪费

因此,Vue3在编译阶段,做了进一步优化。主要有如下:

  • diff算法优化
  • 静态提升
  • 事件监听缓存
  • SSR优化
diff算法优化

vue3diff算法中相比vue2增加了静态标记

关于这个静态标记,其作用是为了会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较

下图这里,已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高

image

关于静态类型枚举如下

export const enum PatchFlags {
TEXT = 1,// 动态的文本节点
CLASS = 1 << 1, // 2 动态的 class
STYLE = 1 << 2, // 4 动态的 style
PROPS = 1 << 3, // 8 动态属性,不包括类名和样式
FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 Fragment
KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
NEED_PATCH = 1 << 9, // 512
DYNAMIC_SLOTS = 1 << 10, // 动态 solt
HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff
BAIL = -2 // 一个特殊的标志,指代差异算法
}
静态提升

Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用

这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用

<span>你好</span>

<div>{{ message }}</div>

没有做静态提升之前

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("span", null, "你好"),
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}

做了静态提升之后

const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_hoisted_1,
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}

// Check the console for the AST

静态内容_hoisted_1被放置在render 函数外,每次渲染的时候只要取 _hoisted_1 即可

同时 _hoisted_1 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff

事件监听缓存

默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化

<div>
<button @click = 'onClick'>点我</button>
</div>

没开启事件监听器缓存

export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
// PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
]))
})

开启事件侦听器缓存后

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "点我")
]))
}

上述发现开启了缓存后,没有了静态标记。也就是说下次diff算法的时候直接使用

SSR优化

当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染

div>
<div>
<span>你好</span>
</div>
... // 很多个静态属性
<div>
<span>{{ message }}</span>
</div>
</div>

编译后

import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"

export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
const _cssVars = { style: { color: _ctx.color }}
_push(`<div${
_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
}><div><span>你好</span>...<div><span>你好</span><div><span>${
_ssrInterpolate(_ctx.message)
}</span></div></div>`)
}

二、源码体积

相比Vue2Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking

任何一个函数,如refreavtivedcomputed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小

import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
setup(props, context) {
const age = ref(18)

let state = reactive({
name: 'test'
})

const readOnlyAge = computed(() => age.value++) // 19

return {
age,
state,
readOnlyAge
}
}
});

三、响应式系统

vue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加gettersetter,实现响应式

vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历

  • 可以监听动态属性的添加
  • 可以监听到数组的索引和数组length属性
  • 可以监听删除属性

关于这两个 API 具体的不同,我们下篇文章会进行一个更加详细的介绍

参考文献

Vue3.0的设计目标是什么?做了哪些优化

一、设计目标

不以解决实际业务痛点的更新都是耍流氓,下面我们来列举一下Vue3之前我们或许会面临的问题

  • 随着功能的增长,复杂组件的代码变得越来越难以维护

  • 缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机制

  • 类型推断不够友好

  • bundle的时间太久了

Vue3 经过长达两三年时间的筹备,做了哪些事情?

我们从结果反推

  • 更小
  • 更快
  • TypeScript支持
  • API设计一致性
  • 提高自身可维护性
  • 开放更多底层功能

一句话概述,就是更小更快更友好了

更小

Vue3移除一些不常用的 API

引入tree-shaking,可以将无用模块“剪辑”,仅打包需要的,使打包的整体体积变小了

更快

主要体现在编译方面:

  • diff算法优化
  • 静态提升
  • 事件监听缓存
  • SSR优化

下篇文章我们会进一步介绍

更友好

vue3在兼顾vue2options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力

这里代码简单演示下:

存在一个获取鼠标位置的函数

import { toRefs, reactive } from 'vue';
function useMouse(){
const state = reactive({x:0,y:0});
const update = e=>{
state.x = e.pageX;
state.y = e.pageY;
}
onMounted(()=>{
window.addEventListener('mousemove',update);
})
onUnmounted(()=>{
window.removeEventListener('mousemove',update);
})

return toRefs(state);
}

我们只需要调用这个函数,即可获取xy的坐标,完全不用关注实现过程

试想一下,如果很多类似的第三方库,我们只需要调用即可,不必关注实现过程,开发效率大大提高

同时,VUE3是基于typescipt编写的,可以享受到自动的类型定义提示

三、优化方案

vue3从很多层面都做了优化,可以分成三个方面:

  • 源码
  • 性能
  • 语法 API

源码

源码可以从两个层面展开:

  • 源码管理
  • TypeScript
源码管理

vue3整个源码是通过 monorepo 的方式维护的,根据功能将不同的模块拆分到packages 目录下面不同的子目录中

image

这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性

另外一些 package(比如 reactivity 响应式库)是可以独立于 Vue 使用的,这样用户如果只想使用 Vue3 的响应式能力,可以单独依赖这个响应式库而不用去依赖整个 Vue

TypeScript

Vue3是基于typeScript编写的,提供了更好的类型检查,能支持复杂的类型推导

性能

vue3是从什么哪些方面对性能进行进一步优化呢?

  • 体积优化
  • 编译优化
  • 数据劫持优化

这里讲述数据劫持:

vue2中,数据劫持是通过Object.defineProperty ,这个 API 有一些缺陷,并不能检测对象属性的添加和删除

Object.defineProperty(data, 'a',{
get(){
// track
},
set(){
// trigger
}
})

尽管 Vue为了解决这个问题提供了 set delete 实例方法,但是对于用户来说,还是增加了一定的心智负担

同时在面对嵌套层级比较深的情况下,就存在性能问题

default {
data: {
a: {
b: {
c: {
d: 1
}
}
}
}
}

相比之下,vue3是通过proxy监听整个对象,那么对于删除还是监听当然也能监听到

同时Proxy 并不能监听到内部深层次的对象变化,而 Vue3 的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归

语法 API

这里当然说的就是composition API,其两大显著的优化:

  • 优化逻辑组织
  • 优化逻辑复用
逻辑组织

一张图,我们可以很直观地感受到 Composition API 在逻辑组织方面的优势

image

相同功能的代码编写在一块,而不像options API那样,各个功能的代码混成一块

逻辑复用

vue2中,我们是通过mixin实现功能混合,如果多个mixin混合,会存在两个非常明显的问题:命名冲突和数据来源不清晰

而通过composition这种形式,可以将一些复用的代码抽离出来作为一个函数,只要的使用的地方直接进行调用即可

同样是上文的获取鼠标位置的例子

import { toRefs, reactive, onUnmounted, onMounted } from 'vue';
function useMouse(){
const state = reactive({x:0,y:0});
const update = e=>{
state.x = e.pageX;
state.y = e.pageY;
}
onMounted(()=>{
window.addEventListener('mousemove',update);
})
onUnmounted(()=>{
window.removeEventListener('mousemove',update);
})

return toRefs(state);
}

组件使用

import useMousePosition from './mouse'
export default {
setup() {
const { x, y } = useMousePosition()
return { x, y }
}
}

可以看到,整个数据来源清晰了,即使去编写更多的hook函数,也不会出现命名冲突的问题

参考文献

Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?

一、Object.defineProperty

定义:Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

为什么能实现响应式

通过defineProperty 两个属性,getset

  • get

属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值

  • set

属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined

下面通过代码展示:

定义一个响应式函数defineReactive

function update() {
app.innerText = obj.foo
}

function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
}
})
}

调用defineReactive,数据发生变化触发update方法,实现数据响应式

const obj = {}
defineReactive(obj, 'foo', '')
setTimeout(()=>{
obj.foo = new Date().toLocaleTimeString()
},1000)

在对象存在多个key情况下,需要进行遍历

function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}

如果存在嵌套对象的情况,还需要在defineReactive中进行递归

function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
}
})
}

当给key赋值为对象的时候,还需要在set属性中进行递归

set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是对象的情况
notifyUpdate()
}
}

上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题

现在对一个对象进行删除与添加属性操作,无法劫持到

const obj = {
foo: "foo",
bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok

当我们对一个数组进行监听的时候,并不那么好使了

const arrData = [1,2,3,4,5];
arrData.forEach((val,index)=>{
defineProperty(arrData,index,val)
})
arrData.push() // no ok
arrData.pop() // no ok
arrDate[0] = 99 // ok

可以看到数据的api无法劫持到,从而无法实现数据响应式,

所以在Vue2中,增加了setdelete API,并且对数组api方法进行一个重写

还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题

小结

  • 检测不到对象属性的添加和删除
  • 数组API方法无法监听到
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

二、proxy

Proxy的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了

ES6系列中,我们详细讲解过Proxy的使用,就不再述说了

下面通过代码进行展示:

定义一个响应式方法reactive

function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
return res
}
})
return observed
}

测试一下简单数据的操作,发现都能劫持

const state = reactive({
foo: 'foo'
})
// 1.获取
state.foo // ok
// 2.设置已存在属性
state.foo = 'fooooooo' // ok
// 3.设置不存在属性
state.dong = 'dong' // ok
// 4.删除属性
delete state.dong // ok

再测试嵌套对象情况,这时候发现就不那么 OK 了

const state = reactive({
bar: { a: 1 }
})

// 设置嵌套对象属性
state.bar.a = 10 // no ok

如果要解决,需要在get之上再进行一层代理

function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return isObject(res) ? reactive(res) : res
},
return observed
}

三、总结

Object.defineProperty只能遍历对象属性进行劫持

function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}

Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的

function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
return res
}
})
return observed
}

Proxy可以直接监听数组的变化(pushshiftsplice

const obj = [1,2,3]
const proxtObj = reactive(obj)
obj.psuh(4) // ok

Proxy有多达13种拦截方法,不限于applyownKeysdeletePropertyhas等等,这是Object.defineProperty不具备的

正因为defineProperty自身的缺陷,导致Vue2在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外setdelete方法)

// 数组重写
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
arrayProto[method] = function () {
originalProto[method].apply(this.arguments)
dep.notice()
}
});

// set、delete
Vue.set(obj,'bar','newbar')
Vue.delete(obj),'bar')

Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9

参考文献

用Vue3.0 写过组件吗?如果想实现一个 Modal你会怎么设计?

一、组件设计

组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式

现在有一个场景,点击新增与编辑都弹框出来进行填写,功能上大同小异,可能只是标题内容或者是显示的主体内容稍微不同

这时候就没必要写两个组件,只需要根据传入的参数不同,组件显示不同内容即可

这样,下次开发相同界面程序时就可以写更少的代码,意义着更高的开发效率,更少的 Bug 和更少的程序体积

二、需求分析

实现一个Modal组件,首先确定需要完成的内容:

  • 遮罩层

  • 标题内容

  • 主体内容

  • 确定和取消按钮

主体内容需要灵活,所以可以是字符串,也可以是一段 html 代码

特点是它们在当前vue实例之外独立存在,通常挂载于body之上

除了通过引入import的形式,我们还可通过API的形式进行组件的调用

还可以包括配置全局样式、国际化、与typeScript结合

三、实现流程

首先看看大致流程:

  • 目录结构

  • 组件内容

  • 实现 API 形式

  • 事件处理

  • 其他完善

目录结构

Modal组件相关的目录结构

├── plugins
│ └── modal
│ ├── Content.tsx // 维护 Modal 的内容,用于 h 函数和 jsx 语法
│ ├── Modal.vue // 基础组件
│ ├── config.ts // 全局默认配置
│ ├── index.ts // 入口
│ ├── locale // 国际化相关
│ │ ├── index.ts
│ │ └── lang
│ │ ├── en-US.ts
│ │ ├── zh-CN.ts
│ │ └── zh-TW.ts
│ └── modal.type.ts // ts类型声明相关

因为 Modal 会被 app.use(Modal) 调用作为一个插件,所以都放在plugins目录下

组件内容

首先实现modal.vue的主体显示内容大致如下

<Teleport to="body" :disabled="!isTeleport">
<div v-if="modelValue" class="modal">
<div
class="mask"
:style="style"
@click="maskClose && !loading && handleCancel()"
></div>
<div class="modal__main">
<div class="modal__title line line--b">
<span>{{ title || t("r.title") }}</span>
<span
v-if="close"
:title="t('r.close')"
class="close"
@click="!loading && handleCancel()"
></span
>
</div>
<div class="modal__content">
<Content v-if="typeof content === 'function'" :render="content" />
<slot v-else>
{{ content }}
</slot>
</div>
<div class="modal__btns line line--t">
<button :disabled="loading" @click="handleConfirm">
<span class="loading" v-if="loading"></span>{{ t("r.confirm") }}
</button>
<button @click="!loading && handleCancel()">
{{ t("r.cancel") }}
</button>
</div>
</div>
</div>
</Teleport>

最外层上通过Vue3 Teleport 内置组件进行包裹,其相当于传送门,将里面的内容传送至body之上

并且从DOM结构上来看,把modal该有的内容(遮罩层、标题、内容、底部按钮)都实现了

关于主体内容

<div class="modal__content">
<Content v-if="typeof content==='function'"
:render="content" />
<slot v-else>
{{content}}
</slot>
</div>

可以看到根据传入content的类型不同,对应显示不同得到内容

最常见的则是通过调用字符串和默认插槽的形式

// 默认插槽
<Modal v-model="show"
title="演示 slot">
<div>hello world~</div>
</Modal>

// 字符串
<Modal v-model="show"
title="演示 content"
content="hello world~" />

通过 API 形式调用Modal组件的时候,content可以使用下面两种

  • h 函数
$modal.show({
title: '演示 h 函数',
content(h) {
return h(
'div',
{
style: 'color:red;',
onClick: ($event: Event) => console.log('clicked', $event.target)
},
'hello world ~'
);
}
});
  • JSX
$modal.show({
title: '演示 jsx 语法',
content() {
return (
<div
onClick={($event: Event) => console.log('clicked', $event.target)}
>
hello world ~
</div>
);
}
});

实现 API 形式

那么组件如何实现API形式调用Modal组件呢?

Vue2中,我们可以借助Vue实例以及Vue.extend的方式获得组件实例,然后挂载到body

import Modal from './Modal.vue';
const ComponentClass = Vue.extend(Modal);
const instance = new ComponentClass({ el: document.createElement("div") });
document.body.appendChild(instance.$el);

虽然Vue3移除了Vue.extend方法,但可以通过createVNode实现

import Modal from './Modal.vue';
const container = document.createElement('div');
const vnode = createVNode(Modal);
render(vnode, container);
const instance = vnode.component;
document.body.appendChild(container);

Vue2中,可以通过this的形式调用全局 API

export default {
install(vue) {
vue.prototype.$create = create
}
}

而在 Vue3 的 setup 中已经没有 this 概念了,需要调用app.config.globalProperties挂载到全局

export default {
install(app) {
app.config.globalProperties.$create = create
}
}

事件处理

下面再看看看Modal组件内部是如何处理「确定」「取消」事件的,既然是Vue3,当然采用Compositon API 形式

// Modal.vue
setup(props, ctx) {
let instance = getCurrentInstance(); // 获得当前组件实例
onBeforeMount(() => {
instance._hub = {
'on-cancel': () => {},
'on-confirm': () => {}
};
});

const handleConfirm = () => {
ctx.emit('on-confirm');
instance._hub['on-confirm']();
};
const handleCancel = () => {
ctx.emit('on-cancel');
ctx.emit('update:modelValue', false);
instance._hub['on-cancel']();
};

return {
handleConfirm,
handleCancel
};
}

在上面代码中,可以看得到除了使用传统emit的形式使父组件监听,还可通过_hub属性中添加 on-cancelon-confirm方法实现在API中进行监听

app.config.globalProperties.$modal = {
show({}) {
/* 监听 确定、取消 事件 */
}
}

下面再来目睹下_hub是如何实现

// index.ts
app.config.globalProperties.$modal = {
show({
/* 其他选项 */
onConfirm,
onCancel
}) {
/* ... */

const { props, _hub } = instance;

const _closeModal = () => {
props.modelValue = false;
container.parentNode!.removeChild(container);
};
// 往 _hub 新增事件的具体实现
Object.assign(_hub, {
async 'on-confirm'() {
if (onConfirm) {
const fn = onConfirm();
// 当方法返回为 Promise
if (fn && fn.then) {
try {
props.loading = true;
await fn;
props.loading = false;
_closeModal();
} catch (err) {
// 发生错误时,不关闭弹框
console.error(err);
props.loading = false;
}
} else {
_closeModal();
}
} else {
_closeModal();
}
},
'on-cancel'() {
onCancel && onCancel();
_closeModal();
}
});
}
};

其他完善

关于组件实现国际化、与typsScript结合,大家可以根据自身情况在此基础上进行更改

参考文献

说说Vue 3.0中Treeshaking特性?举例说明一下?

一、Tree shaking

Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination

简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码

如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去

treeshaking则是一开始就把有用的蛋白蛋黄(import)放入搅拌,最后直接作出蛋糕

也就是说 ,tree shaking 其实是找出使用的代码

Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到

import Vue from 'vue'

Vue.nextTick(() => {})

Vue3源码引入tree shaking特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中

import { nextTick, observable } from 'vue'

nextTick(() => {})

二、如何做

Tree shaking是基于ES6模板语法(importexports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量

Tree shaking无非就是做了两件事:

  • 编译阶段利用ES6 Module判断哪些模块已经加载
  • 判断那些模块和变量未被使用或者引用,进而删除对应代码

下面就来举个例子:

通过脚手架vue-cli安装Vue2Vue3项目

vue create vue-demo

Vue2 项目

组件中使用data属性

<script>
export default {
data: () => ({
count: 1,
}),
};
</script>

对项目进行打包,体积如下图

为组件设置其他属性(comptedwatch

export default {
data: () => ({
question:"",
count: 1,
}),
computed: {
double: function () {
return this.count * 2;
},
},
watch: {
question: function (newQuestion, oldQuestion) {
this.answer = 'xxxx'
}
};

再一次打包,发现打包出来的体积并没有变化

image

Vue3 项目

组件中简单使用

import { reactive, defineComponent } from "vue";
export default defineComponent({
setup() {
const state = reactive({
count: 1,
});
return {
state,
};
},
});

将项目进行打包

image

在组件中引入computedwatch

import { reactive, defineComponent, computed, watch } from "vue";
export default defineComponent({
setup() {
const state = reactive({
count: 1,
});
const double = computed(() => {
return state.count * 2;
});

watch(
() => state.count,
(count, preCount) => {
console.log(count);
console.log(preCount);
}
);
return {
state,
double,
};
},
});

再次对项目进行打包,可以看到在引入computerwatch之后,项目整体体积变大了

image

三、作用

通过Tree shakingVue3给我们带来的好处是:

  • 减少程序体积(更小)
  • 减少程序执行时间(更快)
  • 便于将来对程序架构进行优化(更友好)

参考文献