什么是Compsition API
-
Vue.js 3.0
允许我们在编写组件的时候添加一个setup
启动函数 -
setup
是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>
-
setup
中创建响应式数据,并返回 - 模板可以访问到
setup
的返回值
如何建立的这个关系
创建和设置组件实例
- 组件的渲染流程:创建
vnode
、渲染vnode
和生成DOM
挂载组件
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 创建组件实例
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
// 设置组件实例
setupComponent(instance)
// 设置并运行带副作用的渲染函数
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}
创建组件实例的流程
createComponentInstance
function createComponentInstance (vnode, parent, suspense) {
// 继承父组件实例上的 appContext,如果是根组件,则直接从根 vnode 中取。
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
const instance = {
// 组件唯一 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
}
-
Vue.js 2.x
使用new Vue
来初始化一个组件的实例 -
Vue.js 3.0
,直接通过创建对象去创建组件的实例
组件实例的设置流程
- 对
setup
函数的处理就在这里完成,setupComponent
方法的实现
function setupComponent (instance, isSSR = false) {
const { props, children, shapeFlag } = instance.vnode
// 判断是否是一个有状态的组件
const isStateful = shapeFlag & 4
// 初始化 props
initProps(instance, props, isStateful, isSSR)
// 初始化 插槽
initSlots(instance, children)
// 设置有状态的组件实例
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
return setupResult
}
- 从组件
vnode
中获取了props
、children
、shapeFlag
等属性 - 分别对
props
和插槽进行初始化 - 根据
shapeFlag
的值,我们可以判断这是不是一个有状态组件,如果是则要进一步去设置有状态组件的实例
setupStatefulComponent
方法
function setupStatefulComponent (instance, isSSR) {
const Component = instance.type
// 创建渲染代理的属性访问缓存
instance.accessCache = {}
// 创建渲染上下文代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// 判断处理 setup 函数
const { setup } = Component
if (setup) {
// 如果 setup 函数带参数,则创建一个 setupContext
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 执行 setup 函数,获取结果
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])
// 处理 setup 执行结果
handleSetupResult(instance, setupResult)
}
else {
// 完成组件实例设置
finishComponentSetup(instance)
}
}
- 创建渲染上下文代理
- 判断处理
setup
函数 - 完成组件实例设置
创建渲染上下文代理
- 首先是创建渲染上下文代理的流程,它主要对
instance.ctx
做了代理 -
Vue.js 3.0
,为了方便维护,把组件中不同状态的数据存储到不同的属性中- 存储到
setupState
、ctx
、data
、props
中 - 为了方便用户使用,会直接访问渲染上下文
instance.ctx
中的属性 - 需要做一层
proxy
,对渲染上下文instance.ctx
属性的访问和修改,代理到对setupState
、ctx
、data
、props
中的数据的访问和修改
- 存储到
- 1、 访问
instance.ctx
渲染上下文中的属性时,就会进入get
函数
const PublicInstanceProxyHandlers = {
get ({ _: instance }, key) {
const { ctx, setupState, data, props, accessCache, type, appContext } = instance
if (key[0] !== '$') {
// setupState / data / props / ctx
// 渲染代理的属性访问缓存中
const n = accessCache[key]
if (n !== undefined) {
// 从缓存中取
switch (n) {
case 0: /* SETUP */
return setupState[key]
case 1 :/* DATA */
return data[key]
case 3 :/* CONTEXT */
return ctx[key]
case 2: /* PROPS */
return props[key]
}
}
else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
accessCache[key] = 0
// 从 setupState 中取数据
return setupState[key]
}
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
accessCache[key] = 1
// 从 data 中取数据
return data[key]
}
else if (
type.props &&
hasOwn(normalizePropsOptions(type.props)[0], key)) {
accessCache[key] = 2
// 从 props 中取数据
return props[key]
}
else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache[key] = 3
// 从 ctx 中取数据
return ctx[key]
}
else {
// 都取不到
accessCache[key] = 4
}
}
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] = 3
return ctx[key]
}
else if (
// 全局定义的属性
((globalProperties = appContext.config.globalProperties),
hasOwn(globalProperties, key))) {
return globalProperties[key]
}
else if ((process.env.NODE_ENV !== 'production') &&
currentRenderingInstance && key.indexOf('__v') !== 0) {
if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) {
// 如果在 data 中定义的数据以 $ 开头,会报警告,因为 $ 是保留字符,不会做代理
warn(`Property ${JSON.stringify(key)} must be accessed via $data because it starts with a reserved ` +
`character and is not proxied on the render context.`)
}
else {
// 在模板中使用的变量如果没有定义,报警告
warn(`Property ${JSON.stringify(key)} was accessed during render ` +
`but is not defined on instance.`)
}
}
}
}
- 函数首先判断
key
不以 $ 开头的情况,这部分数据可能是setupState
、data
、props
、ctx
中的一种 -
setupState
就是setup
函数返回的数据 -
ctx
包括了计算属性、组件方法和用户自定义的一些数据 - 依次判断是那种类型,在
key
相同时它会决定数据获取的优先级
setupState
>data
>prop
>ctx
-
accessCache
用来防止多次调用hasOwn
- 如果
key
以$
开头- 首先判断是不是
Vue.js
内部公开的$xxx
属性($parent
) - 判断是否是
vue-loader
编译注入的css
模块内部的key
- 判断是否是用户自定义以
$
开头的key
- 判断是否是全局属性
- 首先判断是不是
set 代理过程
- 2、 当修改
instance.ctx
渲染上下文中的属性的时候,就会进入set
函数
const PublicInstanceProxyHandlers = {
set ({ _: instance }, key, value) {
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 赋值
(process.env.NODE_ENV !== 'production') &&
warn(`Attempting to mutate prop "${key}". Props are readonly.`, instance)
return false
}
if (key[0] === '$' && key.slice(1) in instance) {
// 不能给 Vue 内部以 $ 开头的保留属性赋值
(process.env.NODE_ENV !== 'production') &&
warn(`Attempting to mutate public property "${key}". ` +
`Properties starting with $ are reserved and readonly.`, instance)
return false
}
else {
// 用户自定义数据赋值
ctx[key] = value
}
return true
}
}
- 赋值时,也是优先判断
setupState
,然后是data
,接着是props
- 如果是用户自定义的数据,它仅用于组件上下文的共享,如下所示:
export default {
created() {
this.userMsg = 'msg from user'
}
}
- 当执行
this.userMsg
赋值的时候,会触发set
函数,最终userMsg
会被保留到ctx
中 - 3、当我们判断属性是否存在于
instance.ctx
渲染上下文中时,就会进入has
函数
const PublicInstanceProxyHandlers = {
has
({ _: { data, setupState, accessCache, ctx, type, appContext } }, key) {
// 依次判断
return (accessCache[key] !== undefined ||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
(setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||
(type.props && hasOwn(normalizePropsOptions(type.props)[0], key)) ||
hasOwn(ctx, key) ||
hasOwn(publicPropertiesMap, key) ||
hasOwn(appContext.config.globalProperties, key))
}
}
- 依次判断
key
是否存在于 accessCache、
data、
setupState、
props` 、用户数据、公开属性以及全局属性中,然后返回结果。
判断处理 setup 函数
// 判断处理 setup 函数
const { setup } = Component
if (setup) {
// 如果 setup 函数带参数,则创建一个 setupContext
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 执行 setup 函数获取结果
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])
// 处理 setup 执行结果
handleSetupResult(instance, setupResult)
}
- 1、创建
setup
函数上下文- 判断
setup
函数的参数长度,如果大于1
,则创建setupContext
上下文
- 判断
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
- 例子:
<template>
<p>{{ msg }}</p>
<button @click="onClick">Toggle</button>
</template>
<script>
export default {
props: {
msg: String
},
setup (props, { emit }) {
function onClick () {
emit('toggle')
}
return {
onClick
}
}
}
</script>
-
HelloWorld
子组件的setup
函数接收两个参数- 第一个参数
props
对应父组件传入的props
数据 - 第二个参数
emit
是一个对象,实际上就是setupContext
- 第一个参数
- 使用
createSetupContext
函数来创建setupContext
function createSetupContext (instance) {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit
}
}
- 1)返回了一个对象,包括
attrs
、slots
和emit
三个属性。 - 2)
setupContext
让我们在setup
函数内部可以获取到组件的属性、插槽以及派发事件的方法emit
- 3)
setupContext
对应的就是setup
函数第二个参数 - 2、执行
setup
函数并获取结果
const setupResult = callWithErrorHandling(setup,
instance,
0 /* SETUP_FUNCTION */,
[instance.props, setupContext])
- 1)
callWithErrorHandling
函数的实现
function callWithErrorHandling (fn, instance, type, args) {
let res
try {
res = args ? fn(...args) : fn()
}
catch (err) {
handleError(err, instance, type)
}
return res
}
- 1)实就是对
fn
做的一层包装,内部还是执行了fn
- 2)在有参数的时候传入参数,所以
setup
的第一个参数是instance.props
,第二个参数是setupContext
- 3)使用
handleError
处理异常 - 3、处理
setup
函数的执行结果,用handleSetupResult
函数来处理结果
handleSetupResult(instance, setupResult)
function handleSetupResult(instance, setupResult) {
if (isFunction(setupResult)) {
// setup 返回渲染函数
instance.render = setupResult
}
else if (isObject(setupResult)) {
// 把 setup 返回结果变成响应式
instance.setupState = reactive(setupResult)
}
finishComponentSetup(instance)
}
- 1)当
setupResult
是一个对象的时候,我们把它变成了响应式并赋值给instance.setupState
- 2)如果返回一个函数,则作为组件的渲染函数
- 例
<script>
import { h } from 'vue'
export default {
props: {
msg: String
},
setup (props, { emit }) {
function onClick () {
emit('toggle')
}
return (ctx) => {
return [
h('p', null, ctx.msg),
h('button', { onClick: onClick }, 'Toggle')
]
}
}
}
</script>
完成组件实例设置
- 有
setup
函数,执行handleSetupResult
后执行finishComponentSetup
- 没有
setup
函数,直接执行finishComponentSetup
function finishComponentSetup (instance) {
const Component = instance.type
// 对模板或者渲染函数的标准化
if (!instance.render) {
if (compile && Component.template && !Component.render) {
// 运行时编译
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement || NO
})
Component.render._rc = true
}
if ((process.env.NODE_ENV !== 'production') && !Component.render) {
if (!compile && Component.template) {
// 只编写了 template 但使用了 runtime-only 的版本
warn(`Component provided template option but ` +
`runtime compilation is not supported in this build of Vue.` +
(` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
) /* should not happen */)
}
else {
// 既没有写 render 函数,也没有写 template 模板
warn(`Component is missing template or render function.`)
}
}
// 组件对象的 render 函数赋值给 instance
instance.render = (Component.render || NOOP)
if (instance.render._rc) {
// 对于使用 with 块的运行时编译的渲染函数,使用新的渲染上下文的代理
instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
}
}
// 兼容 Vue.js 2.x Options API
{
currentInstance = instance
applyOptions(instance, Component)
currentInstance = null
}
}
- 1、标准化模板或者渲染函数
- 1)组件最终通过运行
render
函数生成子树vnode
,但是我们很少直接去编写render
函数,通常会使用两种方式开发组件 - 1>
SFC(Single File Components)
单文件的开发方式来开发组件
此在
webpack
的编译阶段,它会通过vue-loader
编译生成组件相关的JavaScript
和CSS
,并把template
部分转换成render
函数添加到组件对象的属性中
- 2> 另外一种开发方式是不借助
webpack
编译,直接引入Vue.js
直接在组件对象
template
属性中编写组件的模板,然后在运行阶段编译生成render
函数
// 在 Vue.js 3.0 中,compile 方法是通过外部注册的
let compile;
function registerRuntimeCompiler(_compile) {
compile = _compile;
}
runtime-only
和 runtime-compiled
的主要区别在于是否注册了这个 compile
方法
- 2)主要需要处理以下三种情况
- 1>
compile
和组件template
属性存在,render
方法不存在的情况:runtime-compiled
版本会在JavaScript
运行时进行模板编译,生成render
函数。 - 2>
compile
和render
方法不存在,组件template
属性存在的情况:由于没有compile
,这里用的是runtime-only
的版本,因此要报一个警告来告诉用户,想要运行时编译得使用runtime-compiled
版本的Vue.js
- 3> 组件既没有写
render
函数,也没有写template
模板,此时要报一个警告,告诉用户组件缺少了render
函数或者template
模板。 - 3)处理完后把组件的
render
函数赋值给instance.render
。到了组件渲染的时候,就可以运行instance.render
函数生成组件的子树vnode
了 - 2、兼容
Options API