Vue3面试真题

作者: jie 分类: Vue,面试 发布时间: 2024-03-16 14:59

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

1.1 开始之前

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

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

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

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

1.2 正文

1.2.1 Options Api

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

如下图:

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

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

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

1.2.2 Composition Api

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

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

1.2.3 对比

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

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

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

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

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

的选项块

1.2.3.1.2 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在逻辑组织方面的优势,以后修改一个属性功能的时候,只需要跳到控制该属性的方法中即可

1.2.3.2 逻辑复用

在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函数,也不会出现命名冲突的问题

1.3 小 结

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

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

2.1 设计目标

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

  • 随着功能的增长,复杂组件的代码变得越来越难以维护
  • 缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机制
  • 类型推断不够友好
  • bundle的时间太久了

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

我们从结果反推

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

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

2.1.1 更小

Vue3移除一些不常用的API

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

2.1.2 更 快

主要体现在编译方面:

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

2.1.3 更友好

vue3在兼顾vue2的options 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);
}

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

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

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

2.2 优化方案

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

  • 源码
  • 性能
  • 语法API

2.2.1 源码

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

  • 源码管理
  • TypeScript
2.2.1.1 源码管理

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

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

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

2.2.1.2 TypeScript

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

2.2.2 性能

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

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

这里讲述数据劫持:

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

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

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

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

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

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

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

2.2.3 语法API

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

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

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

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

2.2.3.2 逻辑复用

在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函数,也不会出现命名冲突的问题

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

3.1 组件设计

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

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

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

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

3.2 需求分析

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

  • 遮罩层
  • 标题内容
  • 主体内容
  • 确定和取消按钮

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

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

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

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

3.3 实现流程

首先看看大致流程:

  • 目录结构
  • 组件内容
  • 实现API形式
  • 事件处理
  • 其他完善

3.3.1 目录结构

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目录下

3.3.2 组件内容

首先实现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>
		);
	}
});

3.3.3 实现 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
	}
}

3.3.4 事件处理

下面再看看看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();
			}
		});
	}
};

3.3.5 其他完善

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

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

4.1 编译阶段

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

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

<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优化

4.1.1 diff 算 法 优 化

vue3在diff算法中相比vue2增加了静态标记

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

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

关于静态类型枚举如下

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 // 一个特殊的标志,指代差异算法
}

4.1.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

4.1.3 事件监听缓存

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

<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算法的时候直接使用

4.1.4 SSR优化

当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static些静态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 _ssrInterpol
	ate
} 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>`)
}

4.2 源码体积

相比Vue2,Vue3整体体积变小了,除了移出一些不常用的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
		}
	}
});

4.3 响应式系统

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

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

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

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

5.1 Object.defineProperty

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

5.1.1 为什么能实现响应式

通过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中,增加了set、delete APl, 并且对数组api方法进行一个重写

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

5.1.2 小 结

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

5.2 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
}

5.3 总结

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不具备的

正因为definePropert自身的缺陷,导致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,  也没有polyfilldefineProperty能支持到IE9

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

6.1 是什么

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(() => {})

6.2 如何做

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

Tree shaking无非就是做了两件事

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

下面就来举个例子:

通过脚手架vue-cli安装Vue2与Vue3项目

vue create vue-demo

6.2.1 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'
		}
	}
};

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

6.2.2 Vue3项目

组件中简单使用

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

将项目进行打包

在组件中引入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之后,项目整体体积变大了

6.3 作用

通过Tree shaking,Vue3给我们带来的好处是:

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

发表回复