从使用者角度来分析一下 vue3 原理
社区或论坛上大多都是从最底层原理讲起, 比如 Proxy/Reactive 响应式原理、VNode 虚拟节点 patch过程、 nextTick 如何批量更新等等。大多从深度的层面出发,进行 vue3 的源码剖析。这里我们换一种角度从一个vue 使用者出发。
如何进行 vue3 源码调试
-
首先,下载vue3 源码到本地,然后发现新版本的 vue3 的项目组织形式分成了单独 package (采用lerna框架的 monorepo)。
-
然后,看根目录下的 scripts 脚本命令, 发现有一个 dev 命令(也就是
"dev": "node scripts/dev.js"
),有前端基础的同学一看就知道这是运行调试代码库的命令,我们npm install
安装一下依赖,再运行一下npm run dev
。发现打包生成了vue.global.js
。
[scripts/dev.js
这个命令脚本起了一个 rollup 服务 , 输入是 packages\vue\src\index.ts
, 输出是 packages\vue\dist\vue.global.js
, 具体打包过程不是重点,这里不展开讲解]
-
再然后,可以发现 vue 文件夹下的 src/index.js
就是打包 vue3 库的入口,顺藤摸瓜,我们看到了 examples 示例代码,以examples\composition\todomvc.html
为例,可以发现里面引入了vue.global.js
。到此为止,我们已经完成了调试 vue 源代码的第一步。
从示例代码发现 vue3 的不同之处
首先,观察一下示例代码的书写形式。
const {
createApp, reactive,
computed, watchEffect,
onMounted, onUnmounted
} = Vue
createApp({
setup () {
const state = reactive({
xx: computed(() => xxxx))
// ...
})
watchEffect(() => {})
onMounted(() => {})
// ...
return {
state,
// ...
}
}
}).mount('#app')
只看 js 部分就可以看出很多新的变化和疑惑,比如:
-
vue3 怎么多了一些全新的的 api reactive
watchEffect
, 这是做什么的?原理是什么? -
vue3 怎么是通过 createApp
来创建 Vue 实例的?具体干了什么? -
setup
函数是什么东西?执行过程中做了什么? -
onMounted
生命周期钩子是怎么和对应组件实例进行绑定执行的?
带着这些问题,下面对其进行一一分析。
首先,我们先确定一下代码的底层引用关系,这样可以缩小我们的关注范围,不然 packages 下的文件夹太多,根本不知道从那里下手,从 vue/src/index.js
可以看出,主要引用了以下几个库,以及依赖关系。
vue.js 里包含了三类包:
1.
commom 公共基础包
@vue/shared
2.
tamplate 模板编译 相关包
@vue/compiler-dom -- @vue/compiler-core
3.
runtime 运行时 相关包
@vue/runtime-dom -- @vue/runtime-core -- @vue/reactivity
下面是依赖关系:
+---------------------+ +----------------------+
| | | |
+------------>| @vue/compiler-dom +--->| @vue/compiler-core |
| | | | |
+----+----+ +---------------------+ +----------------------+
| |
| vue |
| |
+----+----+ +---------------------+ +----------------------+ +-------------------+
| | | | | | |
+------------>| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity |
| | | | | | |
| +---------------------+ +----------------------+ +-------------------+
|
|
| +---------------------+
| | |
+------------>| @vue/share |
| |
+---------------------+
现在我们确定了 packages 下的这些包 是我们需要重点关注的对象。
正式开始调试分析
上面已经运行了 npm run dev
命令,rollup 会 watch 检查代码更新实时打包, 接下来运行 npm run serve
可以运行一个 http://localhost:5000
服务,在浏览器打开 http://localhost:5000/packages/vue/examples/composition/todomvc
即可。
createApp 做了什么?
正式开始,从第一行 createApp
开始 debugger,然后跟着代码执行可以跟踪到 packages\runtime-dom\src\index.ts
文件下有一个 export const createApp
,下面我们逐步地对其进行刨根问底。
下面是执行的核心代码:
/*
from '@vue/runtime-dom'
*/
export const createApp = ((...args) => {
/*
示例代码中 args = [{ directives: {…}, setup: ƒ }]
*/
// 调用 ensureRenderer().createApp() 生成 app 实例
const app = ensureRenderer().createApp(...args)
// 从 app 解构出 mount 方法
const { mount } = app;
// 在 app 上挂载一个 mount 私有方法
app.mount = (containerOrSelector: Element | string): any => {
// 获取 container dom
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
// 对 app._component.template 赋值
component.template = container.innerHTML
}
// 挂载之前清空 container.innerHTML
container.innerHTML = ''
// 创建 vnode 并 render
const proxy = mount(container)
// container.removeAttribute('v-cloak')
// container.setAttribute('data-v-app', '')
return proxy
}
// 返回 app 实例,实现可链式调用
return app
}) as CreateAppFunction<Element>
// 序列化 container
function normalizeContainer(container: Element | string): Element | null {
if (isString(container)) {
const res = document.querySelector(container)
return res
}
return container
}
/*
from '@vue/runtime-core'
*/
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
/*
createRenderer 函数接受两个通用参数: HostNode 和 HostElement,它们对应于宿主环境中的 Node和 Element类型。
对于runtime-dom, HostNode 是 DOM 的 'Node' 接口,HostElement 是 DOM 的 'Element' 接口。
*/
function createRenderer(options) {
// 在源码里 baseCreateRenderer 是重载函数,所以单独剥离了出来
return baseCreateRenderer(options)
}
function baseCreateRenderer(options) {
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
forcePatchProp: hostForcePatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent
} = options
// ...上千行代码,这里先不展开
// 里面包括 render 的整个流程
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate) // 返回 createApp 函数
}
}
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// patch 具体过程这里不展开,后续单独进行讲解
patch(container._vnode || null, vnode, container)
}
// 更新任务队列
flushPostFlushCbs()
container._vnode = vnode
}
export function flushPostFlushCbs(seen?: CountMap) {
if (pendingPostFlushCbs.length) {
// 对 pendingPostFlushCbs 任务队列去重
const deduped = [...new Set(pendingPostFlushCbs)]
// 清空 pendingPostFlushCbs
pendingPostFlushCbs.length = 0
if (activePostFlushCbs) {
// 更新 activePostFlushCbs 队列
activePostFlushCbs.push(...deduped)
return
}
activePostFlushCbs = deduped
// flush 之前进行排序
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
for (
postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
// 遍历执行 activePostFlushCbs 队列
activePostFlushCbs[postFlushIndex]()
}
// 清空队列以及重置索引
activePostFlushCbs = null
postFlushIndex = 0
}
}
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
rootProps = null
}
// 初始化 app 上下文 context
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
// 初始化app对象 (具备通用能力)
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
version,
get config() {},
set config(v) {},
use(plugin: Plugin, ...options: any[]) {},
mixin(mixin: ComponentOptions) {},
component(name: string, component?: Component): any {},
directive(name: string, directive?: Directive) {},
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
// 将 rootComponent 初始化成虚拟节点 vnode
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// 在根节点存储 app 上下文
// 在根结点实例初始化挂载时将被设置
vnode.appContext = context
// 执行 render 函数
render(vnode, rootContainer)
isMounted = true
app._container = rootContainer
return vnode.component!.proxy
}
},
unmount() {},
provide(key, value) {},
})
return app;
}
}
export function createAppContext(): AppContext {
return {
app: null as any,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
isCustomElement: NO,
errorHandler: undefined,
warnHandler: undefined
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null)
}
}
到这里,整个 createApp 流程就执行完了,在整个环节中,忽略了很多细节,不过不重要,这里我们主要是摸清楚 createApp 在主要做了什么。
-
首先, ensureRenderer()
执行, 初始化 render 函数和 createApp 函数, -
调用 createApp()
生成 vue 实例对象 app (类似 new Vue), -
把 mount 方法包装, 使其具备从 vnode 渲染到真实 dom 的能力。
另外,好奇的你可能发现,在这里所列的函数中没有发现进行依赖收集和响应式处理相关的过程?这个后续专门进行讲解(会涉及到上面 redner 内部的 patch 处理过程,详细过程相对较复杂)
setup 是个什么东东?
接着在调试源码界面搜索 setup() 函数并打上断点,
跟着一步一步往下看。当然了, 你也可以在对应的源码库中搜索 setup()
,可以发现 setup 在@vue/runtime-core/component.ts
内的setupStatefulComponen
t 函数中被调用,可以看出其是组件内封装的一个函数, 接着向函数调用的方向找,可以发现有一个 setupComponent
函数, 而这个函数在恰好上面 creatApp 内的 render 函数内的 patch 过程中的 mountComponent
时被调用了 。(可能有点绕,但是多看多读几次就好)
下面是执行的核心代码:
//
export function setupComponent(
instance: ComponentInternalInstance
) {
const { props, children, shapeFlag } = instance.vnode
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
//
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
// 0. create render proxy property access cache
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// 2. call setup()
const { setup } = Component
if (setup) {
// setup.length > 1 意味着传 setup(props,ctx)
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// currentInstance 维护全局中 当前处理的组件实例变量
currentInstance = instance
// 停止依赖收集 shouldTrack = false
pauseTracking()
// try catch 包裹的 setup() 返回 setup() 结果
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[setupContext]
)
// 恢复依赖收集 shouldTrack = lastshouldTrack
resetTracking()
// 重置当前处理的组件实例为 null
currentInstance = null
if (isPromise(setupResult)) {
// ...
} else {
// 执行 handleSetupResult 操作
handleSetupResult(instance, setupResult, isSSR)
}
} else {
// 执行 compile
finishComponentSetup(instance, isSSR)
}
}
// 创建 SetupContext 对象
function createSetupContext(instance: ComponentInternalInstance):SetupContext {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit
}
}
// 处理 setup() 执行结果对象 //
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// setup returned an inline render function
instance.render = setupResult as InternalRenderFunction
} else if (isObject(setupResult)) {
// 转换成 proxy 对象, 初始化到 instance.setupState
instance.setupState = proxyRefs(setupResult)
}
// 组件 setup() 函数处理完的后续过程
finishComponentSetup(instance, isSSR)
}
function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// ...
if (!instance.render) {
// could be set from setup()
if (compile && Component.template && !Component.render) {
// 对组件 template 执行 compile (模板编译解析 parse-transform-generate) 过程 生成 h/render 函数,
// function render(_ctx, _cache_xxx${optimizeSources}) { with (_ctx) { //... } }
// 然后 初始化到 Component.render
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement,
delimiters: Component.delimiters
})
}
instance.render = (Component.render || NOOP) as InternalRenderFunction
// for runtime-compiled render functions using `with` blocks, the render
// proxy used needs a different `has` handler which is more performant and
// also only allows a whitelist of globals to fallthrough.
if (instance.render._rc) {
instance.withProxy = new Proxy(
instance.ctx,
RuntimeCompiledPublicInstanceProxyHandlers
)
}
}
}
到这里, setup 相关执行流程已经结束, 流程如下:
-
首先 setupComponent
函数在 creatApp 内 rendder 函数中mountComponent
时被调用, 并且对组件进行参数处理, 然后再对有状态STATEFUL_COMPONENT
进行代理化处理, 然后解构出 setup , -
然后, 对 setup 函数进行 error 包装执行 (期间需要维护全局的当前处理的组件实例, 管理依赖收集的状态), 并对其返回结果进行 proxy 代理. -
最后, setup 函数的处理结束之后, 对组件 template 进行 compile 处理生成最终的 render 函数.
可以发现 setup 扮演了承上启下并维护当前组件实例的
关键角色
.
结语
篇幅关系, 这里主要 对 createApp
setup
这两个函数的整体执行流程做了解读与分析, 其中设计到的整个流程大致也是 vue3 内部的执行原理.
如下图.
初始化 vue 实例对象
createApp()
--> 构造 render 函数 // 内部涉及 patch
|
|--> 执行 setup() (在 patch 的 mountComponent 过程中, 里面还有 setupRenderEffect 依赖处理逻辑)
| |
| |--> data 数据 proxy // 数据对象进行 proxy 代理
| |
| |--> template compile // 对模板进行编译 生成 render function
| |
| |--> ...
|
|--> ...
|
--> 构造 mount 函数 // 使其挂载到真实 dom
--> ...
这样看来, 貌似和整个 vue2 内部的响应式流程也有些吻合了.
资料
[1]vue-next https://github.com/vuejs/vue-next/releases/tag/v3.0.9
你的三连是作者继续创作的动力 😉, 期待下期~
点个关注 点个在看吧