vue3做了哪些优化
1. 项目代码管理层面
采用monorepo
的代码管理方式,代码各个模块间的依赖关系更明确,且各模块可单独被开发者引用。比如,vue3的响应式模块@vue/reactivity
,是可以单独使用的,如果开发者只想用这块的代码,不想用别的代码,可以只引用这个包。
2. 更好的TypeScript
支持
无论是源码层面,还是使用者开发项目层面,都全面拥抱的TypeScript
3. 性能优化
- 源码体积的优化
- tree-shaking
- 响应式优化
vue2的Object.defineProperty(data, key, {})
的缺点
(1)对于嵌套层级很深的对象,需要循环遍历每一层的对象的key,把其都变成响应式的,这是非常大的性能开销
(2)不能监听对象key的新增和删除,要用vue2提供的hack方法去实现响应式更新
vue3的new Proxy(data, {})
,因为监听的整个data
对象,所以能很好的解决监听不到属性新增和删除的问题。但是,需要注意的是,Proxy API
并不能监听到内部深层次的对象变化,因此vue3的处理方式是在getter
中去递归响应式,这样的好处是,真正访问到的内部对象,才会去递归响应式,而不是无脑递归,这在很大程度上提升了性能
4. 编译优化
通过编译,优化虚拟DOM比对过程。如下,是vue3的tempalte compiler编译template的结果。大家可以自己在Vue template Explore这个网站自己体验一下。
- 对所有的节点做了动态节点与静态节点的区分,只对比动态节点。
block tree是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,每个区块只需要一个Array来追踪自身包含的动态节点。
<span :text="ss">static</span>
复制代码
_createElementVNode("span", { text: _ctx.ss }, "static", 8 /* PROPS */, _hoisted_2),
复制代码
- 新增了hoistStatic静态节点提升的功能
- 对事件监听函数做了缓存优化
5. 语法层面优化,提供了composition API
- 有利于组件逻辑的关注点分离,代码的逻辑复用
- 解决mixins存在的可能造成命名冲突,引用来源不清晰等问题
composition API和options API
这两者是共存的,setup
是对options API
的一个更好的补充,如果你想在一个组件中整合一些逻辑。setup
完全有它自己的一方小世界,当他return
一个data出去后,这个data对象可以被整个组件用到。
composition API
适合大型的复杂组件,如果你的组件比较简单,直接用vue2 的options API
即可。
setup中 watch、watchEffect的区别
首先我们来看一下watch的用法,与options API的用法非常相似,当count
的值发生变化后,才会调用。但是,有一种极端的情况是,count
经过一系列操作后,最终值与初始值相同,就不会触发watch。
const state = reactive({
count: 0
})
const count = ref(0)
const plusOne = computed(() => state.count + 1)
watch([() => state.count, count, plusOne], ([newCount, newCount2, newPlusOne], [oldCount, oldCount2, oldPlusOne]) => {})
复制代码
watchEffect的用法如下:
const state = reactive({
count: 0
})
watchEffect(() => {
console.log(state.count)
})
state.count++
复制代码
首先会立即执行一次watchEffect
里面的副作用函数,用于给其中的响应式依赖对象收集副作用函数。然后,每次响应式对象被赋值,都会触发副作用函数的调用。
说到,watch
和watchEffect
的区别,我有一点自己的理解。watchEffect
的触发机制更像computed
的触发机制,但是与computed
的作用不同。computed
更倾向于返回一个实时计算的属性值,而wathcEffect
不会有返回值,只是单纯的针对副作用函数中的响应式对象变更后,会触发该副作用函数。
以前,有很多人在computed
里面写监听多个响应式对象变动的代码,而并不是想真正返回一个computed
对象。我总觉得这有点骚操作的感觉,而watchEffect
的出现正好是对这方面功能需求的一个补充。
下面给一个demo,当props
的id
,或者props
的name
参数发生变化时,都要重新请求接口,更新页面:
vue2的写法是:
props: {
name: {
type: String
},
id: {
type: Number
}
},
methods: {
getNews() {
fetch(url, {id: this.id, name: this.name}).then(() => {
...
})
}
},
watch: {
id(newValue) {
this.getNews()
},
name(newValue) {
this.getNews()
}
},
created() {
this.getNews()
}
复制代码
而vue3,只需要写一个watchEffect
就行了
props: {
name: {
type: String
},
id: {
type: Number
}
},
setup(props) {
watchEffect(() => {
fetch(url, {id: props.id, name: props.name}).then(() => {
...
})
})
}
复制代码
一个组件如何重新渲染的
- watchEffect用于依赖收集
- 获取组件的vnode
- 渲染组件的新的vnode
- 对比vnode更新
watchEffect(() => {
const oldTree = component.vnode
const newTree = component.render.call(renderContext)
patch(oldTree, newTree)
})
复制代码
为什么要重写mount和createApp
为了支持跨多端的渲染器,不单单只是web端,比如webgl的渲染器,甚至小程序的渲染器。因此,mount
和createApp
抽象出与平台无关的部分,针对web端,就要再重新封装一次函数。
@vue/runtime-dom
是与web平台有关的包,即web平台,而@vue/runtime-core
是与平台无关的。所以在@vue/runtime-dom
中,对mount
和createApp
进行了平台特性的封装。
// @vue/runtime-dom
let renderer
function ensureRenderer () {
return (
renderer ||
// 此处的rendererOptions是与DOM操作相关的API:insert,remove,createElement,createText等
(renderer = createRenderer(rendererOptions))
)
}
export const createApp = (...args) => {
// 确定渲染器
const app = ensureRenderer().createApp(...args)
const { mount } = app
// 这个mount函数就包含了浏览器相关的API
app.mount = (containerOrSelector) => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
// 清空container里面的DOM元素
container.innerHTML = ""
// 此处对实例的instance.ctx进行了代理
const proxy = mount(container, false, container instanceof SVGElement)
return proxy
}
return app
}
复制代码
vnode
vnode的类型:
export type VNodeTypes =
| string
| VNode
| Component // 组件标签
| typeof Text // 纯文本
| typeof Static // 静态节点
| typeof Comment // 注释
| typeof Fragment
| typeof TeleportImpl
| typeof SuspenseImpl
复制代码
一个vnode可包含哪些参数
export interface VNode<
HostNode = RendererNode,
HostElement = RendererElement,
ExtraProps = { [key: string]: any }
> {
__v_isVNode: true
[ReactiveFlags.SKIP]: true
type: VNodeTypes
props: (VNodeProps & ExtraProps) | null
key: string | number | symbol | null
ref: VNodeNormalizedRef | null
scopeId: string | null
slotScopeIds: string[] | null
children: VNodeNormalizedChildren
component: ComponentInternalInstance | null
dirs: DirectiveBinding[] | null
transition: TransitionHooks<HostElement> | null
// DOM
el: HostNode | null
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
targetAnchor: HostNode | null // teleport target anchor
/**
* number of elements contained in a static vnode
* @internal
*/
staticCount: number
// suspense
suspense: SuspenseBoundary | null
/**
* @internal
*/
ssContent: VNode | null
/**
* @internal
*/
ssFallback: VNode | null
// optimization only
shapeFlag: number
patchFlag: number
/**
* @internal
*/
dynamicProps: string[] | null
/**
* @internal
*/
dynamicChildren: VNode[] | null
// application root node only
appContext: AppContext | null
/**
* @internal attached by v-memo
*/
memo?: any[]
/**
* @internal __COMPAT__ only
*/
isCompatRoot?: true
/**
* @internal custom element interception hook
*/
ce?: (instance: ComponentInternalInstance) => void
}
复制代码
举例说明,把如下节点改写成vnode
<custom-component msg="test">
<h1></h1>
</custom-component>
复制代码
首先如上template会被编译成渲染函数:
import { createElementVNode as _createElementVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, null, -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_custom_component = _resolveComponent("custom-component")
return (_openBlock(), _createBlock(_component_custom_component, { msg: "test" }, {
default: _withCtx(() => [
_hoisted_1
], undefined, true),
_: 1 /* STABLE */
}))
}
复制代码
运行render函数,生成vnode
{
"__v_isVNode": true,
"__v_skip": true,
"type": "custom-component",
"props": {
"msg": "test"
},
"key": null,
"ref": null,
"scopeId": null,
"slotScopeIds": null,
"children": [{
"__v_isVNode": true,
"__v_skip": true,
"type": "h1",
"props": null,
"key": null,
"ref": null,
"scopeId": null,
"slotScopeIds": null,
"children": null,
"component": null,
"suspense": null,
"ssContent": null,
"ssFallback": null,
"dirs": null,
"transition": null,
"el": null,
"anchor": null,
"target": null,
"targetAnchor": null,
"staticCount": 0,
"shapeFlag": 1,
"patchFlag": -1,
"dynamicProps": null,
"dynamicChildren": null,
"appContext": null
}],
"component": null,
"suspense": null,
"ssContent": null,
"ssFallback": null,
"dirs": null,
"transition": null,
"el": null,
"anchor": null,
"target": null,
"targetAnchor": null,
"staticCount": 0,
"shapeFlag": 17,
"patchFlag": 0,
"dynamicProps": null,
"dynamicChildren": [],
"appContext": null
}
复制代码
setup 写法偏爱
尤雨溪更喜欢用ref而不是reactive的原因:
reactive
生成的data被解构引入后,响应式的功能就消失了。为了解决这个问题,在导出时,必须要使用toRefs
转换reactive
生成的data。此处同理处理其他响应式对象,如props的解构问题。
const state = reactive({
x: 0,
y: 0
})
return {
...toRefs(state)
}
复制代码
- ref能更方便的移动组织代码,因为他是单个单个的,而不是一个整体的reactive对象