本文使用的vue版本为3.0.2
- createApp() & mount()
- setup()
- render() h()
- Virtual Dom
- 生命周期hooks
- Proxy代理
- reactive() ref()
- computed()
- watch()
- provide() inject()
- directives()
- components()
setup()是什么?
从 vue3 的官方文档提炼一下信息:
定义:一个新的组件选项。
产生的背景: 当我们的组件越来越复杂时,不同的逻辑关注点也会越来越多,这会导致组件难以阅读和理解,尤其是对于没有从一开始就参与进来的开发人员而言。
作用: 将同一个逻辑关注点相关的代码配置在一起,避免处理单个逻辑关注点时,必须不断地“跳转”相关代码的选项块。
用法:
- 调用时机:组件被创建之前,所以没有
this
,无法访问data,methods,computed
。 - 参数
(props,context)
:creataApp
的参数props
和 2.x 版本中实例this
上的 3 个属性attrs,slots,emit
。 - 返回值是一个对象:该对象可以被组件的其余部分(computed、methods、声明周期钩子、组件模板等)使用。
- 返回值是一个
render
函数:只能使用在同一作用域中声明的响应式状态(无法使用例如 data、computed 中的响应式状态)。
<script>
import { h,ref } from "vue";
export default {
data() {
return {
dataInData: "dataInData",
};
},
computed: {
dataInComputed() {
return "dataInComputed";
},
},
setup() {
const dataInSetup = ref('dataInSetup')
return () =>
h("div", [
dataInData, // error, dataInData is not defined
dataInComputed, // error, dataInComputed is not defined
dataInSetup.value, // work
]);
},
};
</script>
源码分析 setup()都做了些什么
与本章主要内容关联不大的源码均不再展开,只在注释中做一下简单说明
setup()的调用
setup()
部分代码的源头位于上一篇介绍创建应用或实例的baseCreateRenderer()
中。在挂载应用或实例时,会第一次触发patch()
方法,setup()
在这时会被调用。
// packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
if (n1 && !isSameVNodeType(n1, n2)) {
// 新老VNode类型不同时,解绑老的dom
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
// 处理文本
break
case Comment:
// 处理注释
break
case Static:
// 处理静态节点
break
case Fragment:
// 处理代码段
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理Element
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理Teleport
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理suspense
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
}
}
可以看到,会根据n2
(也就是 vue2.x 版本中的newVNode
)的不同类型做相应的处理。
当n2
是一个组件时,调用processComponent
方法。
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
updateComponent(n1, n2, optimized)
}
}Ï
当n1
不为null
时,会对比新老 VNode 差异,更新 dom 节点。
当n1
为null
时,会根据n2
是否为keep-alive
组件执行不同的挂载逻辑。
下面进入正题,来看mountComponent
方法。
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 创建实例
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
));
// 开发环境注册热更新
if (__DEV__ && (__BROWSER__ || __TEST__) && instance.type.__hmrId) {
registerHMR(instance);
}
// 开发环境添加警告语的上下文
// 开始记录mount过程的性能指标
if (__DEV__) {
pushWarningContext(initialVNode);
startMeasure(instance, `mount`);
}
// 保存keep-alive组件的render方法
if (isKeepAlive(initialVNode)) {
(instance.ctx as KeepAliveContext).renderer = internals;
}
// 开始记录init过程的性能指标
if (__DEV__) {
startMeasure(instance, `init`);
}
// 调用setup()
setupComponent(instance);
// 结束记录init过程的性能指标,与上面的start对应
if (__DEV__) {
endMeasure(instance, `init`);
}
// 为未来支持异步setup()留坑
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect);
// Give it a placeholder if this is not hydration
// TODO handle self-defined fallback
if (!initialVNode.el) {
const placeholder = (instance.subTree = createVNode(Comment));
processCommentNode(null, placeholder, container!, anchor);
}
return;
}
// 定义实例更新视图的方法
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
);
// 结束记录mount的性能指标,与上面的start对应
if (__DEV__) {
popWarningContext();
endMeasure(instance, `mount`);
}
};
// packages/runtime-core/src/component.ts
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR;
const { props, children, shapeFlag } = instance.vnode;
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT;
// 初始化组件的props和slots
initProps(instance, props, isStateful, isSSR);
initSlots(instance, children);
// 安装有状态组件
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
isInSSRComponentSetup = false;
return setupResult;
}
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions;
// 开发环境校验组件、子组件、指令 名称的合法性
if (__DEV__) {
if (Component.name) {
validateComponentName(Component.name, instance.appContext.config);
}
if (Component.components) {
const names = Object.keys(Component.components);
for (let i = 0; i < names.length; i++) {
validateComponentName(names[i], instance.appContext.config);
}
}
if (Component.directives) {
const names = Object.keys(Component.directives);
for (let i = 0; i < names.length; i++) {
validateDirectiveName(names[i]);
}
}
}
// 创建缓存,优化访问速度
instance.accessCache = Object.create(null);
// 创建公用的代理,上一篇文章中app.mount()返回的proxy就是这里
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
if (__DEV__) {
exposePropsOnRenderContext(instance);
}
// 获取setup()方法
const { setup } = Component;
if (setup) {
// 初始化setup()方法的上下文
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null);
currentInstance = instance;
// 暂停依赖收集
pauseTracking();
// 调用setup()方法,保存返回值
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
);
// 恢复依赖收集
resetTracking();
currentInstance = null;
if (isPromise(setupResult)) {
// 处理setup()是异步函数的情况
if (isSSR) {
// 服务端渲染逻辑
return setupResult.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR);
});
} else if (__FEATURE_SUSPENSE__) {
// 给未来支持异步setup()留坑
instance.asyncDep = setupResult;
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +
`does not support it yet.`
);
}
} else {
// 处理setup()返回值
handleSetupResult(instance, setupResult, isSSR);
}
} else {
// 没有setup()直接结束安装
finishComponentSetup(instance, isSSR);
}
}
- 首先校验组件本身、子组件和指令的名称合法性
- 创建访问缓存,创建公用的代理属性供外部访问
- 在执行 setup()期间暂停了依赖收集,执行结束后恢复
- 处理 setup()返回值
这里为什么要在执行时暂停收集依赖?
回到执行setup
时包裹的callWithErrorHandling
方法:
// packages/runtime-core/src/errorHandling.ts
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
) {
let res;
try {
res = args ? fn(...args) : fn();
} catch (err) {
handleError(err, instance, type);
}
return res;
}
callWithErrorHandling
的最后一个参数[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
其实就是setup
的参数,与文档中(props,context)
相对应。而在开发环境中,为了对一些错误操作做提示,对props
包了一层 proxy 代理;由于props
本身就是响应式的,这里再包的一层是无需重复收集依赖的。
处理 setup()返回值
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// 返回值是函数,保存为render属性
instance.render = setupResult as InternalRenderFunction;
} else if (isObject(setupResult)) {
// 返回值是对象
if (__DEV__ && isVNode(setupResult)) {
// 不能直接返回VNode
warn(
`setup() should not return VNodes directly - ` +
`return a render function instead.`
);
}
// setup returned bindings.
// assuming a render function compiled from template is present.
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
instance.devtoolsRawSetupState = setupResult;
}
// 转化为响应式对象,暴露给其他地方使用
instance.setupState = proxyRefs(setupResult);
if (__DEV__) {
exposeSetupStateOnRenderContext(instance);
}
} else if (__DEV__ && setupResult !== undefined) {
warn(
`setup() should return an object. Received: ${
setupResult === null ? "null" : typeof setupResult
}`
);
}
finishComponentSetup(instance, isSSR);
}
这里的逻辑比较简单:
- 判断
setup
不能返回 VNode、undefined、null 等。 proxyRefs
方法判断返回的对象是否是响应式的,若不是,则转化为响应式对象。- 结束
setup
结束安装
function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// 获取实例的options
const Component = instance.type as ComponentOptions;
// 格式化template / render函数
if (__NODE_JS__ && isSSR) {
if (Component.render) {
instance.render = Component.render as InternalRenderFunction;
}
} else if (!instance.render) {
// could be set from setup()
if (compile && Component.template && !Component.render) {
if (__DEV__) {
startMeasure(instance, `compile`);
}
// template转化为render
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement,
delimiters: Component.delimiters,
});
if (__DEV__) {
endMeasure(instance, `compile`);
}
}
instance.render = (Component.render || NOOP) as InternalRenderFunction;
// 支持 with 代码块
if (instance.render._rc) {
instance.withProxy = new Proxy(
instance.ctx,
RuntimeCompiledPublicInstanceProxyHandlers
);
}
}
// 兼容2.x版本
if (__FEATURE_OPTIONS_API__) {
currentInstance = instance;
applyOptions(instance, Component);
currentInstance = null;
}
// 开发环境对缺少 template 或 render 的情况报错
if (__DEV__ && !Component.render && instance.render === NOOP) {
/* istanbul ignore if */
if (!compile && Component.template) {
warn(
`Component provided template option but ` +
`runtime compilation is not supported in this build of Vue.` +
(__ESM_BUNDLER__
? ` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
: __ESM_BROWSER__
? ` Use "vue.esm-browser.js" instead.`
: __GLOBAL__
? ` Use "vue.global.js" instead.`
: ``) /* should not happen */
);
} else {
warn(`Component is missing template or render function.`);
}
}
}
结束安装的过程主要做了 2 件事:
- 统一使用
render
函数 - 调用
applyOptions
转化setup
中的逻辑,支持 2.x 版本
applyOptions
方法很长,对其进行简化:
// packages/runtime-core/src/componentOptions.ts
export function applyOptions(
instance: ComponentInternalInstance,
options: ComponentOptions,
deferredData: DataFn[] = [],
deferredWatch: ComponentWatchOptions[] = [],
deferredProvide: (Data | Function)[] = [],
asMixin: boolean = false
) {
const {
// composition
mixins,
extends: extendsOptions,
// state
data: dataOptions,
computed: computedOptions,
methods,
watch: watchOptions,
provide: provideOptions,
inject: injectOptions,
// assets
components,
directives,
// lifecycle
beforeMount,
mounted,
beforeUpdate,
updated,
activated,
deactivated,
beforeDestroy,
beforeUnmount,
destroyed,
unmounted,
render,
renderTracked,
renderTriggered,
errorCaptured
} = options
// 注册 beforeCreate
callSyncHook(
'beforeCreate',
LifecycleHooks.BEFORE_CREATE,
options,
instance,
globalMixins
)
// 处理全局mixin
applyMixins(
instance,
globalMixins,
deferredData,
deferredWatch,
deferredProvide
)
// setup写法转化为options写法(2.x语法)
if (extendsOptions) {
...
}
// 处理自身mixin
if (mixins) {
...
}
// 配置初始化清单(与2.x保持一致)
// - props (在这个方法执行之前已经完成)
// - inject
// - methods
// - data (延迟执行,因为需要访问'this')
// - computed
// - watch (延迟执行,因为需要访问'this')
if (injectOptions) {
// 处理inject
}
if (methods) {
// 注册methods
}
if (dataOptions) {
// 绑定data
}
if (computedOptions) {
// 注册computed
}
if (watchOptions) {
// 注册watch
}
if (provideOptions) {
// 处理 provide
}
if (components) {
// 注册子组件
}
if (directives) {
// 注册指令
}
// 绑定各个声明周期钩子函数
if (beforeMount) {
onBeforeMount(beforeMount.bind(publicThis))
}
if (mounted) {
onMounted(mounted.bind(publicThis))
}
if (beforeUpdate) {
onBeforeUpdate(beforeUpdate.bind(publicThis))
}
if (updated) {
onUpdated(updated.bind(publicThis))
}
if (activated) {
onActivated(activated.bind(publicThis))
}
if (deactivated) {
onDeactivated(deactivated.bind(publicThis))
}
if (beforeUnmount) {
onBeforeUnmount(beforeUnmount.bind(publicThis))
}
if (unmounted) {
onUnmounted(unmounted.bind(publicThis))
}
可以看到,setup
其实还是被转化为 2.x 的方式去执行的。
一张图简要概括:
至此,setup
相关逻辑已经全部执行完毕。
总结
其实,setup
过程所做的工作很简单:
- 执行函数内的逻辑
- 处理返回值:
- 返回值是函数,保存为实例的
render
函数 - 返回值是对象,转化为响应式对象,暴露出去
- 返回值是函数,保存为实例的
- 将
setup
语法转化为2.x版本的options
语法
实现
基于上一篇的已有成果
在组件被创建之前调用setup()
const createApp = function (...args) {
const render = createRender();
const setupFun = args[0].setup
if (setupFun) {
const setupResult = setupFun()
handleSetupResult(setupResult)
/**
* 转化options语法、注册声明周期钩子等内容在后续章节补充
*/
}
const app = {
version: "0.0.1",
mount(selector) {
const container = document.querySelector(selector);
const vnode = createVNode(container.innerHTML);
container.innerHTML = "";
render(vnode, container);
return this;
},
};
return app;
};
处理setup()
的返回值
function handleSetupResult(res) {
const type = Object.prototype.toString.call(res)
if (type === '[object Object]') {
// 细节在后续章节补充
} else if (type === '[object Function]') {
// 细节在后续章节补充
} else {
console.warn('invaild return value')
}
}
如有错误,欢迎指正!