Vue3产生原因
更快
1. 响应式系统的改进
- 创建实例时采用Proxy对于一个大规模数据的监听减少性能开销,虽然对于深层级对象,仍然需要递归实现,但是它是在proxy的getter操作中赋予响应式。意味着只有访问到这个层级的属性才会建立响应;而不是像Vue2一样直接给整个对象都加上响应,因为需要预先知道要拦截的key是什么,所以并不能检测对象属性的添加和删除。
// 创建响应式对象
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 当访问属性时,进行依赖收集
track(target, key);
// 使用 Reflect.get 确保操作的一致性
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 当设置属性时,触发更新
const result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
}
});
}
关键点
- 延迟响应式连接:Vue 3 的
Proxy实现只在实际访问属性时建立响应式连接,而不是在初始化时对整个对象进行处理。这减少了不必要的性能开销。 - 细粒度的依赖收集:通过
Proxy,Vue 3 能够更细粒度地追踪依赖,只对访问过的属性进行响应式处理。 - 动态属性支持:与
Object.defineProperty不同,Proxy能够拦截对象属性的添加和删除操作,使得 Vue 3 的响应式系统更加灵活。
2. 虚拟 DOM 的优化
Vue 3 对虚拟 DOM 算法进行了重构,引入了静态树提升和动态块的概念。这意味着 Vue 3 可以更高效地处理静态和动态内容,减少不必要的渲染操作。
- 虚拟DOM算法重构,模版分析对静态节点生成优化提示,编译时跳过不需要编译部分;
<template>
<div>
<h1>这是一个静态标题</h1>
<p>{{ dynamicContent }}</p>
<button @click="handleClick">点击我</button>
</div>
</template>
<script>
export default {
data() {
return {
dynamicContent: '这是动态内容'
};
},
methods: {
handleClick() {
this.dynamicContent = '内容已更新';
}
}
};
</script>
编译后的渲染函数
import { createVNode, render, toDisplayString } from 'vue';
export default {
setup() {
const dynamicContent = ref('这是动态内容');
// 静态节点被提升到渲染函数外部,并标记为 `-1`(`HOISTED`)。这意味着这些节点在首次渲染时会被创建一次,并在后续的更新中直接复用,不会参与后续的 `diff` 操作。
const staticH1 = createVNode('h1', null, '这是一个静态标题', -1 /* HOISTED */);
// 被缓存到 `staticButton` 的 `onClick` 属性中。这样在后续的渲染中,事件处理函数不会被重新创建,而是直接复用。
const staticButton = createVNode('button', {
onClick: handleClick
}, '点击我', -1 /* HOISTED */);
return () => {
// 动态内容的 VNode 被包装在一个 `VNode` 中,并标记为 `1`(`TEXT_CHILDREN`)。这意味着这个节点是动态的,需要在每次更新时重新渲染。
const dynamicP = createVNode('p', null, toDisplayString(dynamicContent.value), 1 /* TEXT_CHILDREN */);
// 创建根节点的 VNode
const vnode = createVNode('div', null, [staticH1, dynamicP, staticButton]);
return vnode;
};
}
};
关键点
- 渲染函数的优化:
- 静态节点被提升到渲染函数外部,只在组件首次渲染时创建一次。
- 动态节点在每次组件更新时重新生成,但仅限于动态部分。
问题:setup函数只会初始化调用?组件更新时只会调用setup函数返回的函数?
setup 函数的执行机制
-
setup函数只在组件初始化时调用一次:setup是 Vue 3 中组合式 API 的入口函数,它只会在组件初始化时被调用一次。它的主要作用是初始化组件的状态、方法和响应式逻辑。- 在
setup函数中定义的响应式状态(如ref、reactive)和方法(如computed、watch)会被返回,并在组件的渲染函数中使用。
-
组件更新时,会调用
setup函数返回的渲染函数:setup函数返回的是一个渲染函数(或一个对象,其中包含render函数)。这个渲染函数会在组件的响应式状态发生变化时被调用,用于重新渲染组件。- 渲染函数负责生成虚拟 DOM(VNode),并将其传递给 Vue 的渲染器,渲染器会根据新的 VNode 更新实际的 DOM。
3. 细粒度更新
-
在 Vue 3 中,
PatchFlags是一个关键的优化机制,它通过标记虚拟 DOM 节点的动态特性,使得渲染器在更新时能够更高效地处理变化。这种机制将虚拟 DOM 的更新性能与动态内容的数量相关联,而不是模板的整体大小。 -
将vdom更新性能由与模板整体大小相关提升为与动态内容的数量相关,这个区块的大小,甚至可以小到class的名字;新增PatchFlag标记,只比较有PatchFlag节点,PatchFlag有枚举值(TEXT/动态class/动态style/动态属性);
PatchFlags 的实现原理
PatchFlags是一个枚举类型
export enum PatchFlags {
TEXT = 1, // 动态文本节点
CLASS = 1 << 1, // 动态类名
STYLE = 1 << 2, // 动态样式
PROPS = 1 << 3, // 动态属性
// 其他标志...
}
- 编译阶段的优化
在编译阶段,Vue 3 会分析模板中的动态和静态内容,并为动态节点生成相应的
PatchFlags。编译后,动态节点会被标记为PatchFlags.TEXT或PatchFlags.CLASS,而静态节点则会被提升到渲染函数外部。
<template>
<div>
<p>{{ msg }}</p> <!-- 动态文本 -->
<span :class="classObj"></span> <!-- 动态类名 -->
</div>
</template>
- 运行时的优化
在运行时,Vue 3 的渲染器会根据
PatchFlags来决定如何更新节点。例如,当检测到PatchFlags.TEXT时,渲染器会直接更新文本内容,而不会重新渲染整个节点。这种细粒度的更新机制显著减少了不必要的 DOM 操作。
const patchElement = (n1, n2) => {
const el = (n2.el = n1.el);
const { patchFlag } = n2;
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children);
}
}
if (patchFlag & PatchFlags.CLASS) {
if (n1.props.class !== n2.props.class) {
hostPatchProp(el, 'class', null, n2.props.class);
}
}
// 其他动态特性的处理...
};
- render阶段对不参与更新的vnode做静态提升,只创建一次,后面数据变化reRender直接复用;同理,事件监听也缓存;
1. 静态节点提升(Static Node Hoisting)
将模板中的静态部分(不会改变的部分)提前提升到渲染函数外部。这样在组件更新时,这些静态节点可以直接复用,而无需重新创建。
<template>
<div>
<span>静态文本</span>
<p>{{ dynamicContent }}</p>
</div>
</template>
编译后,静态节点会被提升到渲染函数外部:
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "静态文本", -1 /* HOISTED */); // 静态节点,被提升到渲染函数外部,只在首次渲染时创建一次。- 静态节点的 `PatchFlag` 被标记为 `-1`,表示该节点是静态的,不会参与后续的 `diff` 操作。
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_createVNode("p", null, _toDisplayString(_ctx.dynamicContent), 1 /* TEXT */)
]));
}
2. 事件监听缓存(Event Listener Caching)
通过缓存事件处理函数,避免在每次渲染时重新创建事件监听器。
<template>
<button @click="onClick">点击我</button>
</template>
编译后,事件处理函数会被缓存:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("button", {
// `_cache[1]` 是缓存的事件处理函数,首次渲染时会被创建,并在后续渲染时复用。
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "点击我"));
}
- vue3把所有的slot统一编译一个函数,而调用这个函数,是子组件的行为,只需要重新渲染子组件,而不必再去渲染父组件了;这个函数在子组件中被调用时,会根据父组件传递的插槽内容生成对应的虚拟 DOM。
插槽函数在子组件中的工作原理
关键点:
-
插槽内容的编译:
- 在编译阶段,Vue 3 会将父组件中定义的插槽内容编译为一个函数。这个函数在运行时会被子组件调用,以生成对应的虚拟 DOM。
-
插槽函数的传递:
- 父组件将编译后的插槽函数传递给子组件。这些函数被存储在子组件的
slots对象中。
- 父组件将编译后的插槽函数传递给子组件。这些函数被存储在子组件的
-
子组件中的插槽渲染:
- 子组件在渲染时,会调用这些插槽函数来生成插槽内容的虚拟 DOM,并将其插入到子组件的渲染树中。
-
性能优化:
- 由于插槽内容的生成是通过函数动态完成的,只有当插槽内容发生变化时,子组件才会重新渲染插槽部分,而不会影响整个父组件的渲染。
// 父组件
<template>
<ChildComponent>
<template #header>
<h1>这是头部</h1>
</template>
<p>这是默认插槽的内容</p>
<template #footer>
<footer>这是尾部</footer>
</template>
</ChildComponent>
</template>
// 子组件
<template>
<div>
<slot name="header"></slot>
<div>子组件的内容</div>
<slot></slot>
<slot name="footer"></slot>
</div>
</template>
编译后的插槽函数
// 编译后的父组件渲染函数
import { createVNode, render } from 'vue';
export default {
setup() {
return () => {
const headerSlot = () => createVNode('h1', null, '这是头部');
const defaultSlot = () => createVNode('p', null, '这是默认插槽的内容');
const footerSlot = () => createVNode('footer', null, '这是尾部');
return createVNode(ChildComponent, null, {
header: headerSlot,
default: defaultSlot,
footer: footerSlot
});
};
}
};
// 编译后的子组件渲染函数
export default {
setup() {
return () => {
const slots = {
header: this.$slots.header,
default: this.$slots.default,
footer: this.$slots.footer
};
return createVNode('div', null, [
slots.header ? slots.header() : null,
createVNode('div', null, '子组件的内容'),
slots.default ? slots.default() : null,
slots.footer ? slots.footer() : null
]);
};
}
};
插槽内容变化时的更新流程
-
响应式数据变化:
- 在父组件中,
headerContent、defaultContent和footerContent是响应式数据。当这些数据发生变化时,Vue 的响应式系统会触发更新。
- 在父组件中,
-
父组件的渲染函数重新执行:
- 父组件的渲染函数会重新执行,生成新的虚拟 DOM。
- 在渲染函数中,插槽内容被编译为函数(如
headerSlot、defaultSlot、footerSlot),这些函数会根据最新的响应式数据生成新的虚拟 DOM 节点。
-
子组件的插槽函数重新调用:
- 子组件接收到新的插槽函数后,会调用这些函数来生成新的插槽内容的虚拟 DOM。
- 子组件的渲染函数会将新的插槽内容插入到子组件的渲染树中。
-
渲染器的
diff算法:- Vue 的渲染器会比较新旧虚拟 DOM 的差异,并只更新发生变化的部分。
- 例如,如果只有
headerContent发生了变化,渲染器只会更新<h1>标签的内容,而不会影响其他部分。
更小
1. 按需引入
- Vue3.0中,采取了ES module imports按需引入的方式。如果我们没有用到一些内置组件时,在编译时就不会去加载这些内容。而之前Vue的整个代码都是直接将整个Vue对象放进来,所以一些没用到的东西,也无法通过tree-shaking将其扔掉。
ES Module 按需引入的核心机制
-
ES Module 的静态结构:
- ES Module 的
import和export语法是静态的,这意味着它们在代码中是固定的,不能动态改变。这种静态结构使得工具(如 Webpack 和 Vite)能够在编译时分析代码,找出未被使用的模块。
- ES Module 的
-
Tree-Shaking:
-
Tree-shaking 是一种代码优化技术,它通过静态分析移除未使用的代码。Vue 3 的模块化设计使得 Tree-shaking 更加有效。当使用 ES Module 时,未被引用的模块不会被打包,从而优化了最终产物的大小。
-
通过以下步骤实现 Tree-shaking:
- 静态分析:在编译阶段,构建工具会分析项目中的所有模块及其依赖关系,构建一个依赖图(Dependency Graph),明确每个模块的输入和输出。
- 死代码消除:通过静态分析,识别出哪些模块或变量未被使用或引用,然后将这些未使用的模块或变量从最终的输出文件中移除。
-
为了确保 Tree-shaking 的效果,开发者需要避免在模块中使用副作用(如全局变量、定时器等)。副作用可能会影响 Tree-shaking 的效果,因为构建工具可能无法确定这些代码是否可以安全移除。为了避免副作用影响 Tree-shaking,可以采取以下措施:
- 避免使用全局变量,尽量使用模块作用域变量。
- 将代码封装在函数中,避免在模块加载时执行。
- 避免使用
import语句执行副作用。
// 避免 // side-effect.js console.log('副作用代码'); // main.js import './side-effect.js'; // 推荐 // side-effect.js export function log() { console.log('副作用代码'); } // main.js import { log } from './side-effect.js'; log();- 避免使用
eval或new Function。使用函数封装动态代码
// 使用函数封装动态代码 function dynamicLog() { console.log('动态代码'); } dynamicLog();- 避免在模块中使用定时器。将定时器封装在函数中
- 避免修改全局状态。尽量将状态封装在模块内部或通过 Vue 的响应式系统管理。
- 使用
sideEffects字段显式声明模块是否包含副作用。
{ "name": "my-library", "sideEffects": ["./src/side-effect.js"] }
-
-
动态导入:
- Vue 3 支持动态导入(
import()),这允许开发者在运行时按需加载模块。这种方式不仅减少了初始加载时间,还提升了用户体验。
- Vue 3 支持动态导入(
2. 移除冷门特性
- 移除冷门feature,如filter和inline-template
更易于维护
1. TypeScript 支持
- 采用TS开发,在编码期间做类型检查,也可定义接口类型,利于IDE对变量类型推导;Flow对复杂场景类型检查支持不好,如组件更新props时,没有正确推导出vm.$options.props;
2. 单元化开发
- 更好的代码管理方式monorepo,根据功能将不同模块拆分到packages目录下不同子目录,每个package有各自API、类型定义和测试,拆封更细化,职责和依赖关系更明确
- 一些package,如reactivity响应式库可独立于Vue使用,减少引用包体积,而Vue2是做不到这一点的
3. 组合式 API
- 组合式API在复用、类型推导更友好;
import { ref, computed } from 'vue';
export default {
setup() {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubleCount, increment };
}
};
- 编译器重构-插件化设计、带位置信息的parser(source maps)。Vue 3 的编译器通过
@vue/compiler-core和@vue/compiler-dom等模块实现了插件化设计。开发者可以通过自定义插件来扩展编译器的功能,例如添加新的指令、优化代码生成等。Vue 3 的编译器在解析模板时,会为每个节点生成位置信息(如行号、列号等)。这些信息被存储在抽象语法树(AST)中,可以在后续的代码生成和调试中使用。
如何通过插件扩展编译器的功能
import { CompilerOptions, extendCompilerOptions } from '@vue/compiler-core';
// 自定义插件
function myCustomPlugin(options: CompilerOptions) {
// 扩展编译器选项
extendCompilerOptions(options, {
onNodeTransform: (node) => {
// 自定义节点转换逻辑
if (node.type === 'Element' && node.tag === 'div') {
node.tag = 'span'; // 将 <div> 转换为 <span>
}
}
});
}
// 使用插件
import { compile, parse, generate } from '@vue/compiler-core';
const template = `
<div>
<h1>Hello World</h1>
<p>{{ message }}</p>
</div>
`;
//编译模板:
const code = compile(template, { plugins: [myCustomPlugin] });
// 解析模板:将模板字符串解析为抽象语法树(AST)。
const ast = parse(template, {
outputSourceRange: true // 启用位置信息
});
// 转换节点:对 AST 中的节点进行转换,例如处理指令、动态绑定等。
transform(ast, {
nodeTransforms: [
(node) => {
if (node.type === 'Element' && node.tag === 'h1') {
node.tag = 'span'; // 将 <h1> 转换为 <span>
}
}
]
});
// 生成代码:根据转换后的 AST 生成渲染函数代码。
const code = generate(ast);
console.log(code); // 输出编译后的代码
console.log(ast); // 输出 AST,包含位置信息
更好的多端渲染支持
- 引入一个Custom Render API,更好的多端渲染支持;
1. Custom Renderer API 的使用
允许开发者定义自己的渲染逻辑。通过实现特定的接口方法,开发者可以控制如何创建元素、设置属性、挂载子节点等。
import { createApp, h } from 'vue';
// 定义一个简单的自定义渲染器
function customRenderer(vNode) {
// 将 vNode 转化为文本描述
const description = `Component: ${vNode.type.name}, Props: ${JSON.stringify(vNode.props)}`;
console.log(description); // 打印文本描述
}
// 创建一个简单的 Vue 组件
const MyComponent = {
name: 'MyComponent',
props: {
message: String
},
render() {
return h('div', null, this.message);
}
};
// 使用自定义渲染器创建 Vue 应用
const app = createApp({
render() {
return h(MyComponent, { message: 'Hello, Custom Renderer!' });
}
});
// 运行应用,传入自定义渲染器
app.mount({ render: customRenderer });
多端渲染支持: Vue 组件渲染到 Canvas
使用了 Pixi.js 来创建一个 Canvas 渲染器。我们定义了 createElement、patchProp 和 insert 方法,这些方法控制了如何创建元素、设置属性和挂载子节点。
import { createRenderer } from 'vue';
import { Application, Graphics } from 'pixi.js';
// 创建一个自定义渲染器
const renderer = createRenderer({
createElement(type) {
let element;
switch (type) {
case 'rect':
element = new Graphics();
element.beginFill(0xff0000);
element.drawRect(0, 0, 500, 500);
element.endFill();
break;
case 'circle':
element = new Graphics();
element.beginFill(0xffff00);
element.drawCircle(0, 0, 50);
element.endFill();
break;
}
return element;
},
patchProp(el, key, prevValue, nextValue) {
switch (key) {
case 'x':
el.x = nextValue;
break;
case 'y':
el.y = nextValue;
break;
}
},
insert(el, parent) {
parent.addChild(el);
}
});
// 创建一个 Vue 应用
export function createApp(rootComponent) {
return renderer.createApp(rootComponent);
}
// 初始化 Canvas 容器
const game = new Application({ width: 750, height: 750 });
document.body.append(game.view);
// 使用自定义渲染器渲染 Vue 组件
import App from './App.vue';
createApp(App).mount(game.stage);
新功能
1. renderTriggered API
- Vue 3 提供了
renderTriggeredAPI,可以在浏览器中直接查看触发更新的具体代码位置。这使得开发者能够更直观地调试和优化性能问题。
<template>
<div>
<p>{{ message }}</p>
<button @click="updateMessage">更新消息</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue 3!'
};
},
methods: {
updateMessage() {
// 组件中,数据的变化会通过响应式系统通知渲染器进行更新。渲染器会生成一个新的虚拟 DOM,并通过 `diff` 算法比较新旧虚拟 DOM 的差异,然后更新真实 DOM。
this.message = 'Message updated!';
}
},
// 生命周期钩子,它在组件的渲染函数被触发时被调用。- 在开发模式下,Vue 会在触发渲染时收集相关信息(如触发原因、虚拟节点等),并通过 `renderTriggered` 钩子暴露给开发者。
// 在生产模式下,Vue 3 的构建工具会自动移除这些调试代码,确保不会影响性能。因此,你不需要手动关闭 `renderTriggered` 钩子。
renderTriggered(event) {
console.log('renderTriggered:', event);
console.log('更新触发的组件:', event.vnode);
console.log('触发更新的原因:', event.reason);
}
};
</script>
2. 跨组件状态共享
- Vue 3 通过
observable和effect实现了跨组件的状态共享。这种机制使得状态管理更加灵活,适用于复杂的应用场景。
// 祖先组件
import { ref, provide } from 'vue';
export default {
setup() {
const sharedState = ref('Hello from App Component');
provide('sharedState', sharedState);
return {};
}
};
// 后代组件
import { inject } from 'vue';
export default {
setup() {
const sharedState = inject('sharedState');
return { sharedState };
}
};
问题
尽管 Vue 3 带来了诸多改进,但它也面临一些挑战。例如,Vue 3 不再支持 IE9 等较旧的浏览器。