前言
这是一篇一点都不讲究的文章
记录下时间点吧 9 月 15 号把 Vue3 的 master 分支拉下来了,然后 10 月 15 号开始白嫖,
没看过大佬们的分享,大致扫了一眼,相比 2x 版本最直观的感受是:
- (runtime-) core 代码移至 packages 内,
- reactivity 的抽离,只看名字我猜这个是响应式的核心代码?抽出来做成框架无关的了?
- 在根目录下看到了 rollup 配置文件,虽然 vue2x 较新的版本也是基于 rollup 的打包出来的 esm 文件,因为 rollup-plugin-alias 的加持,没有做到 tree shaking。3x 版本可以通过 esm-bundler 做到 tree shaking。
- 一些 2x 版本印象深刻的入口函数搜索不到了,比如 _init、initState、initData,一些响应式相关的关键字段也没了,比如 new Dep、new Watcher 之类的。
- 进一步发现,几乎搜不到 class 了,感觉对我这种初学者来说,这种函数式的编程可读性要差一些
然后跑一个 demo,我的 vue-cli 版本够新,可以直接创建 vue3 的项目,跑起来以后,入口居然是 main.js!组件还是那个2x 版本的样子!除了这里不太一样。。。
createApp(App).mount('#app')
抱歉,竟然是 cli 没用好,重新搞一下,都勾上回车!变成了 main.ts,然后看到了 ts 的组件:
import { Options, Vue } from 'vue-class-component';
@Options({
props: {
msg: String
}
})
export default class HelloWorld extends Vue {
msg!: string
}
之前公司的项目用的是 ts + mobx + vue2.6.11,类似:import { Vue } from 'vue-property-decorator'
,所以感觉不是很唐突(但是我看网文不都是什么 setup 函数?为啥我这还是类 + 装饰器,先不考究了,看源码和业务代码的风格也没啥关系,慢慢尝试之后你会发现 vue3 对 data 函数依旧是兼容的),入口文件因为 cli 钩的多,所以变长了一点,之前太短都没有注意到链式调用
createApp(App).use(store).use(router).mount('#app')
不禁让我想到了头条的面试。。。你给我写个链式调用吧,你给我写个科里化吧,你给我实现一个 array.reduce 吧
初始化流程的源码瞎看
createApp + mount
跑 demo 就是为了拿个入口。。。直接看源码
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args) // 这个地方大约有 2000 行相关源码
if (__DEV__) {
injectNativeTagCheck(app)
}
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
return proxy
}
return app
}) as CreateAppFunction
相比 2x 版本的 new Vue 然后 $mount 不同(子组件也会执行 new Vue 的操作),这里的 createApp 是最底层的,只执行一次
追一下 app 的相关代码
ensureRenderer => createRenderer => baseCreateRenderer
// packages/runtime-core/src/renderer.ts
function baseCreateRenderer () {
... // render 下面会用到,列一下
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) { // unmount 的逻辑
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else { // mount 逻辑
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
... // 此处省略约 1800 行代码
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
然后我们看下 app 的结构
// packages/runtime-core/src/apiCreateApp.ts
function createAppAPI(render: RootRenderFunction, hydrate?: RootHydrateFunction) {
... //
const context = createAppContext()
... //
return function createApp(rootComponent, rootProps = null): App {
const app: App = { // app 上挂有以下属性,方法内容省略了
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
version,
get config() {},
set config(v) {},
use(plugin: Plugin, ...options: any[]): App {}, // 安装 plugin
mixin(mixin: ComponentOptions): App {}, // 往 context 的 mixins 里面添加 mixin
component(name: string, component?: Component): any {}, // 往 context 的 components 里面添加 component || 获取 component
directive(name: string, directive?: Directive) {}, // 往 context 的 directives 里面添加 directive || 获取 directive
mount(rootContainer: HostElement, isHydrate?: boolean): any {}, // 初始化
unmount() {}, // 销毁:通过 render 传入 null
provide(key, value) {} // context.provides 上添加键值对
}
return app
}
}
再回过头来看,mount 主要干了啥:
- 判断 isMounted,主流程肯定是没 mount
-
const vnode = createVNode(rootComponent as ConcreteComponent, rootProps)
,然后引用 context:vnode.appContext = context
- 判断入参
isHydrate
&&createAppAPI
入参hydrate
a. 为 true 执行hydrate()
,这在 vue2 里是一个和 vdom patch 相关的函数,在这里看上去也是这么个意思
b. 为 false 执行if (isHydrate && hydrate) { hydrate(vnode as VNode
, rootContainer as any) } render(vnode, rootContainer)
,首次加载都是 false,所以会执行这里,render 就是上文在baseCreateRenderer
中提到的render: RootRenderFunction
函数,本质就是首次 vdom 的 patch - 修改
isMounted
状态为 true - 为 app 和 rootContainer 相互添加引用,
app._container = rootContainer
、(rootContainer as any).__vue_app__ = app
return vnode.component!.proxy
createComponentInstance + patch
感觉上述内容都比较明朗,但是 mount 逻辑是 APP 级的,那子组件是怎么初始化的?
其实都发生在上述步骤 3 中
在 vue-next-master/packages/runtime-core/src/renderer.ts
的 baseCreateRenderer下,有大致如下的执行顺序。
其中 patch 函数的源码:
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns: typeof createHydrationFunctions
): HydrationRenderer
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// ...
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
; (type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
; (type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
}
}
}
而子组件的创建最终落实于 mountComponent
中 createComponentInstance
的调用,即创建出对应的组件实例,对应 2x 版本的 VueComponent 的创建
完~ 其实缕一缕感觉套路都差不多,错过了比较多的细节,都追也不太现实,下次准备直接看 reactivity 的代码,了解下核心科技。