持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 7 天,点击查看活动详情
背景
Vue.js 3.0 允许我们在编写组件的时候添加一个 setup 启动函数,它是 Composition API 逻辑组织的入口。那么 setup 到底是怎么执行的?我们按照源码捋一遍流程。
执行 setup
组件的渲染流程是:创建 vnode 、渲染 vnode 和生成 DOM。
mountComponent
其中渲染 vnode 的过程主要就是在挂载组件:
function mountComponent(initialVNode, container, parentComponent) {
// 1. 先创建一个 component instance
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent
));
console.log(`创建组件实例:${instance.type.name}`);
// 2. 给 instance 加工加工
setupComponent(instance);
// 3. 设置并运行带副作用的渲染函数
setupRenderEffect(instance, initialVNode, container);
}
挂载组件的代码主要做了三件事情:
- 创建组件实例
- 设置组件实例
- 设置并运行带副作用的渲染函数
setupComponent
组件实例的设置流程,对 setup 函数的处理就在 setupComponent 方法里完成:
export function setupComponent(instance) {
// 1. 处理 props
// 取出存在 vnode 里面的 props
const { props, children } = instance.vnode;
initProps(instance, props);
// 2. 处理 slots
initSlots(instance, children);
// 源码里面有两种类型的 component
// 一种是基于 options 创建的
// 还有一种是 function 的
// 这里处理的是 options 创建的
// 叫做 stateful 类型
setupStatefulComponent(instance);
}
setupStatefulComponent
接下来我们要关注到 setupStatefulComponent 函数:
function setupStatefulComponent(instance) {
// 1. 先创建代理 proxy
// proxy 对象其实是代理了 instance.ctx 对象
// 我们在使用的时候需要使用 instance.proxy 对象
// 因为 instance.ctx 在 prod 和 dev 坏境下是不同的
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
// 用户声明的对象就是 instance.type
// const Component = {setup(),render()} ....
const Component = instance.type;
// 2. 调用 setup
// 调用 setup 的时候传入 props
const { setup } = Component;
if (setup) {
// 设置当前 currentInstance 的值
// 必须要在调用 setup 之前
setCurrentInstance(instance);
const setupContext = createSetupContext(instance);
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[instance.props, setupContext]
)
setCurrentInstance(null);
// 3. 处理 setupResult
handleSetupResult(instance, setupResult);
} else {
// 完成组件实例设置
finishComponentSetup(instance);
}
}
setupStatefulComponent 主要做了三件事:
- 创建渲染上下文代理
- 判断处理 setup 函数
- 完成组件实例设置
创建渲染上下文代理
创建渲染上下文代理主要对 instance.ctx 做了代理。
这里为什么需要代理呢?在执行组件渲染函数的时候,为了方便用户使用,会直接访问渲染上下文 instance.ctx 中的属性,所以要做一层 proxy,对渲染上下文 instance.ctx 属性的访问和修改。
PublicInstanceProxyHandlers
export const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
// 用户访问 proxy[key]
// 这里就匹配一下看看是否有对应的 function
// 有的话就直接调用这个 function
const { setupState, props } = instance;
if (key[0] !== "$") {
// 说明不是访问 public api
// 先检测访问的 key 是否存在于 setupState 中, 是的话直接返回
if (hasOwn(setupState, key)) {
return setupState[key];
} else if (hasOwn(data, key)) {
return data[key];
} else if (hasOwn(props, key)) {
// 看看 key 是不是在 props 中
// 代理是可以访问到 props 中的 key 的
return props[key];
}
}
const publicGetter = publicPropertiesMap[key];
if (publicGetter) {
return publicGetter(instance);
}
},
set({ _: instance }, key, value) {
const { setupState } = instance;
if (setupState !== {} && hasOwn(setupState, key)) {
// 有的话 那么就直接赋值
setupState[key] = value;
}
return true
},
};
可以看到,函数首先判断 key 不以 $ 开头的情况,这部分数据可能是 setupState、data、props、ctx 中的一种;setupState 就是 setup 函数返回的数据。
如果 key 不以 $ 开头,那么就依次判断 setupState、data、props、ctx 中是否包含这个 key,如果包含就返回对应值。注意这个判断顺序很重要,在 key 相同时它会决定数据获取的优先级
判断处理 setup 函数
处理 setup 函数的流程,主要是三个步骤:
- 创建 setup 函数上下文
- 执行 setup 函数并获取结果
- 处理 setup 函数的执行结果
createSetupContext
setup 函数接收两个参数,第一个参数 props 对应父组件传入的 props 数据,第二个参数就是 setupContext。
function createSetupContext(instance) {
console.log("初始化 setup context");
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,
expose: () => {},
};
}
callWithErrorHandling
function callWithErrorHandling (fn, instance, type, args) {
let res
try {
res = args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type)
}
return res
}
可以看到,它其实就是对 fn 做的一层包装,内部还是执行了 fn,并在有参数的时候传入参数,所以 setup 的第一个参数是 instance.props,第二个参数是 setupContext。函数执行过程中如果有 JavaScript 执行错误就会捕获错误,并执行 handleError 函数来处理。
handleSetupResult
function handleSetupResult(instance, setupResult) {
// setup 返回值不一样的话,会有不同的处理
// 1. 看看 setupResult 是个什么
if (typeof setupResult === "function") {
// 如果返回的是 function 的话,那么绑定到 render 上
// 认为是 render 逻辑
// setup(){ return ()=>(h("div")) }
instance.render = setupResult;
} else if (typeof setupResult === "object") {
// 返回的是一个对象的话
// 先存到 setupState 上
// 先使用 @vue/reactivity 里面的 proxyRefs
// 后面我们自己构建
// proxyRefs 的作用就是把 setupResult 对象做一层代理
// 方便用户直接访问 ref 类型的值
// 比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了,而不需要在 count.value
// 这里也就是官网里面说到的自动结构 Ref 类型
instance.setupState = proxyRefs(setupResult);
}
finishComponentSetup(instance);
}
可以看到,当 setupResult 是一个对象的时候,我们把它变成了响应式并赋值给 instance.setupState,这样在模板渲染的时候,依据前面的代理规则,instance.ctx 就可以从 instance.setupState 上获取到对应的数据,这就在 setup 函数与模板渲染间建立了联系。
完成组件实例设置
finishComponentSetup
function finishComponentSetup(instance) {
// 给 instance 设置 render
// 先取到用户设置的 component options
const Component = instance.type;
if (!instance.render) {
// 如果 compile 有值 并且当然组件没有 render 函数,那么就需要把 template 编译成 render 函数
if (compile && !Component.render) {
if (Component.template) {
// 这里就是 runtime 模块和 compile 模块结合点
const template = Component.template;
Component.render = compile(template);
}
}
instance.render = Component.render;
}
// 兼容 Vue.js 2.x Options API
applyOptions()
}
finishComponentSetup 函数主要做了两件事情:
- 标准化模板或者渲染函数
- 兼容 Options API
标准化模板或者渲染函数
通常会使用两种方式开发组件:
1、使用 SFC(Single File Components)单文件的开发方式来开发组件
即通过编写组件的 template 模板去描述一个组件的 DOM 结构。我们知道 .vue 类型的文件无法在 Web 端直接加载,因此在 webpack 的编译阶段,它会通过 vue-loader 编译生成组件相关的 JavaScript 和 CSS,并把 template 部分转换成 render 函数添加到组件对象的属性中。
2、不借助 webpack 编译,直接引入 Vue.js
开箱即用,我们直接在组件对象 template 属性中编写组件的模板,然后在运行阶段编译生成 render 函数。
Options API:兼容 Vue.js 2.x
function applyOptions(instance, options, deferredData = [], deferredWatch = [], asMixin = false) {
const {
// 组合
mixins, extends: extendsOptions,
// 数组状态
props: propsOptions, data: dataOptions, computed: computedOptions, methods, watch: watchOptions, provide: provideOptions, inject: injectOptions,
// 组件和指令
components, directives,
// 生命周期
beforeMount, mounted, beforeUpdate, updated, activated, deactivated, beforeUnmount, unmounted, renderTracked, renderTriggered, errorCaptured
} = options;
// instance.proxy 作为 this
const publicThis = instance.proxy;
const ctx = instance.ctx;
// 处理全局 mixin
// 处理 extend
// 处理本地 mixins
// props 已经在外面处理过了
// 处理 inject
// 处理 方法
// 处理 data
// 处理计算属性
// 处理 watch
// 处理 provide
// 处理组件
// 处理指令
// 处理生命周期 option
}
参考源码
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/errorHandling.ts