本系列阅读的是vue3.3版本的源码。在列举源码的时候会省略部分源码,只保留与讲解内容相关的代码(运行环境代码块和兼容以前版本的代码也可能被省略),以此来方便读者理解。
源码阅读方式
我们通常会结合案例和debugger去看源码,这样我们能清楚的沿着代码执行的脉络去屡清楚代码执行的逻辑、以及观察到执行到每一步各个变量的变化。
当然,我们最好是分块、有目的的去看,比如看根组件的挂载过程;怎么加载组件;怎么更新组件;如何实现响应式;diff算法是怎么实现的等等。我们不必想着一下把所有东西都搞清楚,这样势必会被源码复杂的逻辑给搞晕。
代码调试
package/vue/examples中有三类示例,classic还是使用的options API,composition使用的是vue3升级的重点composition API,transition则是transition内置组件的案例,我们应该主要关注composition文件夹里的实例。
我们需要先到根目录打开终端,运行pnpm dev命令,会发现vue文件夹下出现一个dist包,其中就是vue的浏览器可用js文件了。这是我们就可以直接运行example中的实例了。
浏览器打开开发者工具,选中sources,选择对应的运行html文件就可以打上断点去一步步的看源码啦!
vscode插件推荐 bookmarks
bookmarks插件可以帮我们给某行代码打上一个书签(command+option+k),可以帮助我们快速地定位到已经标记过的地方,对于阅读庞大的源码来说非常有用。
根组件渲染
createApp
vue3相较于vue2使用了新的创建app的方法:createApp,我们在createApp代码行打上断点并进入到函数实现,会是先面一段代码
// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
...
}
return app
})
我们可以看到,app的创建并不是由当前文件的createApp函数创建的,而是先执行ensureRenderer函数,再调用此函数返回值上的createApp创建的。
baseCreateRenderer
那我们接下来看看ensureRenderer是干啥的。点进去函数发现套了两层娃:ensureRenderer => createRenderer => baseCreateRenderer,最终是执行baseCreateRenderer函数
我们点进baseCreateRenderer函数一看发现两千行代码!!不要慌,记住阅读代码的宗旨,我们要带着目的性,我们只想看他返回了啥,然后找到createApp就可以了:
// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
...
return {
render, // 把虚拟DOM(第一个参数)转换成原生DOM,追加到宿主元素上(第二个参数)
hydrate, // SSR,将一个vnode转换成html字符串
createApp: createAppAPI(render, hydrate)
}
}
我们发现baseCreateRenderer函数返回了一个对象,里面有一个属性叫createApp,这个属性的值是createAppAPI的返回值。其实baseCreateRenderer函数返回的对象我们称之为渲染器,他可以帮助我们处理浏览器端和服务端的渲染,同时他也是赋予vue框架跨平台能力的关键。
createAppAPI
进入到createAppAPI函数,其返回了一个createApp函数,这才是createApp的最终实现函数
// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
// 浅拷贝 extend = Object.assign
rootComponent = extend({}, rootComponent)
}
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
// 创建app上下文 此上下文将绑定到每个组件实例
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,
_instance: null,
version,
get config() {
return context.config
},
set config(v) {...},
// 注册plugin
use(plugin: Plugin, ...options: any[]) {...},
// 全局混入
mixin(mixin: ComponentOptions) {...},
// 注册全局组件
component(name: string, component?: Component): any {...},
// 注册全局指令
directive(name: string, directive?: Directive) {...},
// 挂载根组件
mount(
rootContainer: HostElement, // 挂载容器
isHydrate?: boolean,
isSVG?: boolean
): any {...},
// 卸载
unmount() {...},
// 分享数据
provide(key, value) {...}
})
// 返回app
return app
}
}
可以看到createApp的内容并不复杂,就是创建并返回了一个app对象,这个对象相信我们应该非常熟悉了,里面有很多我们常用的方法。
挂载(mount)
创建完app我们就要去挂载根组件了,执行mount方法,这里我们要注意,这里执行的mount方法并不是createAppAPI函数返回的app.mount,在最开始的createApp中,对app.mount进行了重新赋值:
// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
...
}
return app
})
可以看到,他先解构得到mount保存了一份createAppAPI创建app的mount,然后才重新赋值,这样做的原因是我们可以看下具体实现:
// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 根组件容器 选择器字符串兼容
const container = normalizeContainer(containerOrSelector)
if (!container) return
// rootComponent
const component = app._component
// 如果不是函数式组件 且 根组件没有render方法和template选项
if (!isFunction(component) && !component.render && !component.template) {
// 根组件template赋值为 容器dom.innerHTML
component.template = container.innerHTML
// 2.x compat check
if (__COMPAT__ && __DEV__) {
...
}
}
// clear content before mounting
// 挂载前清除容器innerHTML
container.innerHTML = ''
// 调用初始app.mount
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
// 组件实例挂载后移除v-cloak属性
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
})
可以看到先调用normalizeContainer兼容传入字符串选择器,调用querySelector获取dom。第12行会判断根组件对象有没有render和template选项,没有的话会将容器的innerHTML赋值给template属性。然后下面就是清除容器内容以及调用事先保存的mount方法去实现根组件的挂载了。
所以这里mount执行的主要目的是将容器内容作为没有定义render和template根组件的内容
真正的挂载mount
// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
...
// app实例
const app: App = (context.app = {
...
// 挂载根组件
mount(
rootContainer: HostElement, // 挂载容器
isHydrate?: boolean,
isSVG?: boolean
): any {
// 判断挂载状态
if (!isMounted) {
if (__DEV__ && (rootContainer as any).__vue_app__) {
...
}
// 创建根组件vnode
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
// HMR root reload
if (__DEV__) {
...
}
// 判断是否是服务器端渲染
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 渲染到根dom
render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component
devtoolsInitApp(app, version)
}
// 返回根组件实例的exposed代理或者是组件实例
// vnode.component是组件内部实例 类型是ComponentInternalInstance
// component.proxy是组件公共实例 类型是ComponentPublicInstance 我们在组件内部使用this访问的就是此实例上的属性或方法
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} else if (__DEV__) {
...
}
},
...
})
// 返回app
return app
}
}
createVNode可以看成是我们熟知的createElement或者h函数,他创建一个vnode对象,然后使用render函数,将vnode挂在到指定dom,这样就实现了根组件的挂载。
这里的render其实就是我们上面说的渲染器中的render方法:
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPreFlushCbs()
flushPostFlushCbs()
container._vnode = vnode
}
可以看到其最终调用了patch函数,他可以实现vnode的挂载和更新。他的实现比较复杂,我们会在后面的章节详细看其具体实现,此章节我们只需要知道他的功能即可。至此我们已经理清楚了根组件的挂载流程。