距离发布 vue3.0 正式版本已经有一段时间了,作为技术人员,随时保持技术同步是很重要的事情。本文带领大家看一看3.0对比2.x到底有哪些改变。
vue3.0 有两种建立脚手架的方式
脚手架 Vite
npm init vite-app hello-vue3 # OR yarn create vite-app hello-vue3
脚手架 vue-cli
npm install -g @vue/cli # OR yarn global add @vue/cli
vue create hello-vue3
# select vue 3 preset
使用 yarn create vite-app hello-vue3 建立脚手架。
使用 yarn 命令安装依赖后,输入 yarn dev 就可以运行起项目。
项目显示如下图所示
在 2.x 中,在组件上使用 v-model 相当于绑定 value prop 和 input 事件
<ChildComponent v-model="pageTitle" />
<!-- 简写: -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
在 3.x 中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件
<ChildComponent v-model="pageTitle" />
<!-- 简写: -->
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
允许我们在自定义组件上使用多个 v-model
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />
<!-- 简写: -->
<ChildComponent
:title="pageTitle"
@update:title="pageTitle = $event"
:content="pageContent"
@update:content="pageContent = $event"
/>
Vue 2.x 有许多全局 API 和配置,例如,要创建全局组件,可以使用 Vue.component 这样的 API
Vue.component('button-counter', {
data: () => ({
count: 0
}),
template: ''
})
全局指令使用 Vue.directive 声明
Vue.directive('focus', {
inserted: el => el.focus()
})
但是全局配置很容易意外地污染其他测试用例,需要自己去除一些副作用
Vue 3 中我们引入 createApp,调用 createApp 返回一个应用实例
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App).mount('#app')
以下是当前全局 API 及其相应实例 API 的表:
2.x 全局 API | 3.x 实例 API (app) |
---|---|
Vue.config | app.config |
Vue.config.ignoredElements | app.config.isCustomElement |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use |
在 Vue 3 中,重构了全局和内部 API ,并考虑了 tree-shaking 的支持。因此,全局 API 现在只能作为 ES 模块构建的命名导出进行访问。没有用到的代码最后不会被打到最终的包中。这可以优化项目体积。当然用法也需要进行相应的改变:
import { nextTick } from 'vue'
nextTick(() => {
// 一些和DOM有关的东西
})
不能再使用 Vue.nextTick/this.$nextTick,调用将会导致 undefined is not a function 错误。
VUE3.0还有一些其他的改变,例如
<!-- template -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="blue"></div>
<!-- template -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- result -->
<div id="red"></div>
Composition API 属于 API 的增强,它并不是 Vue.js 3.0 组件开发的范式,如果你的组件足够简单,你还是可以使用 Options API。
通过以下代码了解一下 Composition API
<template>
<button @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</button>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
return {
state,
increment
}
}
}
</script>
通过这段代码可以看到和 Vue.js 2.x 组件的写法相比,多了一个 setup 启动函数,另外组件中也没有定义 props、data、computed 这些 options。
在setup 函数中,通过 reactive API 创建的一个响应式对象 state 。state 对象有 count 和 double 两个属性,其中 count 对应了一个数字属性的值;而double 则通过 computed API 创建一个计算属性的值。另外也定义了 increment 方法.最后将state对象和increment方法对外暴露,在 template 就可以使用到暴露的内容.
下图是 VUE3 源码中执行到 setupComponent 方法的执行链路
setup 启动函数的主要逻辑是在渲染 vnode 的过程中
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 创建组件实例
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 设置组件实例
setupComponent(instance)
// 设置并运行渲染函数
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
创建组件实例的流程,我们要要关注 createComponentInstance 方法的实现
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null
) {
const type = vnode.type as ConcreteComponent
// 继承父组件实例上的 appContext,如果是根组件,则直接从根 vnode 中取。
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance: ComponentInternalInstance = {
// 组件唯一 id
uid: uid++,
// 组件 vnode
vnode,
// 父组件实例
parent,
// app 上下文
appContext,
// vnode 节点类型
type: vnode.type,
// 根组件实例
root: null,
// 新的组件 vnode
next: null,
// 子节点 vnode
subTree: null,
// 带副作用更新函数
update: null,
// 渲染函数
render: null,
// 渲染上下文代理
proxy: null,
// 带有 with 区块的渲染上下文代理
withProxy: null,
// 响应式相关对象
effects: null,
// 依赖注入相关
provides: parent ? parent.provides : Object.create(appContext.provides),
// 渲染代理的属性访问缓存
accessCache: null,
// 渲染缓存
renderCache: [],
// 渲染上下文
ctx: EMPTY_OBJ,
// data 数据
data: EMPTY_OBJ,
// props 数据
props: EMPTY_OBJ,
// 普通属性
attrs: EMPTY_OBJ,
// 插槽相关
slots: EMPTY_OBJ,
// 组件或者 DOM 的 ref 引用
refs: EMPTY_OBJ,
// setup 函数返回的响应式结果
setupState: EMPTY_OBJ,
// setup 函数上下文数据
setupContext: null,
// 注册的组件
components: Object.create(appContext.components),
// 注册的指令
directives: Object.create(appContext.directives),
// suspense 相关
suspense,
// suspense 异步依赖
asyncDep: null,
// suspense 异步依赖是否都已处理
asyncResolved: false,
// 是否挂载
isMounted: false,
// 是否卸载
isUnmounted: false,
// 是否激活
isDeactivated: false,
// 生命周期,before create
bc: null,
// 生命周期,created
c: null,
// 生命周期,before mount
bm: null,
// 生命周期,mounted
m: null,
// 生命周期,before update
bu: null,
// 生命周期,updated
u: null,
// 生命周期,unmounted
um: null,
// 生命周期,before unmount
bum: null,
// 生命周期, deactivated
da: null,
// 生命周期 activated
a: null,
// 生命周期 render triggered
rtg: null,
// 生命周期 render tracked
rtc: null,
// 生命周期 error captured
ec: null,
// 派发事件方法
emit: null
}
// 初始化渲染上下文
instance.ctx = { _: instance }
// 初始化根组件指针
instance.root = parent ? parent.root : instance
// 初始化派发事件方法
instance.emit = emit.bind(null, instance)
return instance
}
组件实例的设置流程
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
const { props, children, shapeFlag } = instance.vnode
// 判断是否是一个有状态的组件
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
// 初始化 props
initProps(instance, props, isStateful, isSSR)
// 初始化 插槽
initSlots(instance, children)
// 设置有状态的组件实例
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
接下来是 setup 函数判断处理和完成组件实例设置
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
// 0. 创建渲染代理的属性访问缓存
instance.accessCache = Object.create(null)
// 1. 创建渲染上下文代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// 2. 判断处理 setup 函数
const { setup } = Component
if (setup) {
//如果 setup 函数带参数,则创建一个 setupContext
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
currentInstance = instance
pauseTracking()
// 执行 setup 函数,获取结果
const setupResult = callWithErrorHandling(
setup,
instance
)
// 处理 setup 执行结果
if (isPromise(setupResult)) {
instance.asyncDep = setupResult
} else {
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
}
接下来我们需要了解创建渲染上下文代理函数 PublicInstanceProxyHandlers,我们访问 instance.ctx 渲染上下文中的属性时,就会进入 get 函数,当我们修改 instance.ctx 渲染上下文中的属性的时候,就会进入 set 函数。
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) {
const {
ctx,
setupState,
data,
props,
accessCache,
type,
appContext
} = instance
let normalizedProps
if (key[0] !== '$') {
// setupState / data / props / ctx
// 渲染代理的属性访问缓存中
const n = accessCache![key]
if (n !== undefined) {
// 如果缓存有内容,则从缓存里取数据
switch (n) {
case AccessTypes.SETUP:
return setupState[key]
case AccessTypes.DATA:
return data[key]
case AccessTypes.CONTEXT:
return ctx[key]
case AccessTypes.PROPS:
return props![key]
}
} else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
// 从 setupState 中取数据
accessCache![key] = AccessTypes.SETUP
return setupState[key]
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
// 从 data 中取数据
accessCache![key] = AccessTypes.DATA
return data[key]
} else if (
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)
) {
// 从 props 中取数据
accessCache![key] = AccessTypes.PROPS
return props![key]
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
// 从 ctx 中取数据
accessCache![key] = AccessTypes.CONTEXT
return ctx[key]
} else if (!__FEATURE_OPTIONS_API__ || !isInBeforeCreate) {
accessCache![key] = AccessTypes.OTHER
}
}
const publicGetter = publicPropertiesMap[key]
let cssModule, globalProperties
// 公开的 $xxx 属性或方法
if (publicGetter) {
return publicGetter(instance)
} else if (
// css 模块,通过 vue-loader 编译的时候注入
(cssModule = type.__cssModules) &&
(cssModule = cssModule[key])
) {
return cssModule
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
// 用户自定义的属性,也用 `$` 开头
accessCache![key] = AccessTypes.CONTEXT
return ctx[key]
} else if (
// 全局定义的属性
((globalProperties = appContext.config.globalProperties),
hasOwn(globalProperties, key))
) {
return globalProperties[key]
} else if (
__DEV__ &&
currentRenderingInstance &&
(!isString(key) ||
// #1091 avoid internal isRef/isVNode checks on component instance leading
// to infinite warning loop
key.indexOf('__v') !== 0)
) {
if (
data !== EMPTY_OBJ &&
(key[0] === '$' || key[0] === '_') &&
hasOwn(data, key)
) {
// 如果在 data 中定义的数据以 $ 开头,会报警告,因为 $ 是保留字符,不会做代理
warn(
`Property ${JSON.stringify(
key
)} must be accessed via $data because it starts with a reserved ` +
`character ("$" or "_") and is not proxied on the render context.`
)
} else {
// 在模板中使用的变量如果没有定义,报警告
warn(
`Property ${JSON.stringify(key)} was accessed during render ` +
`but is not defined on instance.`
)
}
}
},
set(
{ _: instance }: ComponentRenderContext,
key: string,
value: any
): boolean {
const { data, setupState, ctx } = instance
if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
// 给 setupState 赋值
setupState[key] = value
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
// 给 data 赋值
data[key] = value
} else if (key in instance.props) {
// 不能直接给 props 赋值
return false
}
if (key[0] === '$' && key.slice(1) in instance) {
// 不能给 Vue 内部以 $ 开头的保留属性赋值
return false
} else {
// 用户自定义数据赋值
ctx[key] = value
}
return true
}
}
这里要注意顺序问题,优先判断 setupState,然后是 data,接着是 props。
然后回到 setupStatefulComponent, 判断 setup 函数的参数个数,如果大于1,则使用 createSetupContext 创建 setupContext:
function createSetupContext(
instance: ComponentInternalInstance
): SetupContext {
const expose: SetupContext['expose'] = exposed => {
instance.exposed = proxyRefs(exposed)
}
return {
// 属性
attrs: instance.attrs,
// 插槽
slots: instance.slots,
// 派发事件
emit: instance.emit,
//
expose
}
}
执行 setup 函数,获取结果
function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
) {
let res
try {
// 执行setup,带参数的时候传入参数
res = args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type)
}
return res
}
执行 handleSetupResult 处理 setup 函数执行的结果
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown
) {
if (isFunction(setupResult)) {
instance.render = setupResult as InternalRenderFunction
} else if (isObject(setupResult)) {
instance.setupState = proxyRefs(setupResult)
}
finishComponentSetup(instance)
}
接下来是 finishComponentSetup 函数,主要做了标准化模板或者渲染函数和兼容 Options API
function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
// 对模板或者渲染函数的标准化
if (!instance.render) {
if (compile && Component.template && !Component.render) {
// 运行时编译
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement,
delimiters: Component.delimiters
})
}
// 组件对象的 render 函数赋值给 instance
instance.render = (Component.render || NOOP) as InternalRenderFunction
if (instance.render._rc) {
// 对于使用 with 块的运行时编译的渲染函数,使用新的渲染上下文的代理
instance.withProxy = new Proxy(
instance.ctx,
RuntimeCompiledPublicInstanceProxyHandlers
)
}
}
// 兼容 Vue.js 2.x Options API
if (__FEATURE_OPTIONS_API__) {
currentInstance = instance
pauseTracking()
applyOptions(instance, Component)
resetTracking()
currentInstance = null
}
}
通过分析 VUE3 源码我们了解了组件的初始化流程、创建组件实例、设置组件实例。通过进一步的深入,我们对渲染上下文的代理过程也进行了介绍。了解了 Composition API 中的 setup 启动函数执行的时机,以及如何建立 setup 返回结果和模板渲染之间的联系。
本文从搭建VUE3项目开始入手,列出了 VUE3 和 VUE2.X 的非兼容的变更,演示了VUE3的新特性Composition API,以及相对于Options API的一些优点,最后我们对 Composition API 怎么实现,通过源码给大家解析实现原理。希望大家通过本文可以对VUE3有一定初步的了解并有所收获。