笔者在上一篇文章《debug一下,你就学会高效阅读开源项目代码~》中以 Vue3 源码调试配置为案例,给大家介绍了调试的基本配置以及强调了其在查看源码过程中的重要性。本文将使用调试的技巧,在不需要详细了解完整代码实现的前提下,快速了解 Vue3 的执行流程。
最简单的代码
众所周知,调用 Vue.createApp
方法便创建了一个 Vue3 应用,本文从这个方法入手,一步一步剖析该方法在源码中的实现,从而使得大家能够了解 Vue3 整体的执行流程。
首先,在源码目录创建 packages/vue/examples/hello.html
文件:
<script src="../dist/vue.global.js"></script>
<div id="demo">{{text}}</div>
<script>
debugger
Vue.createApp({
data: () => ({
text: 'hello world'
})
}).mount('#demo')
</script>
代码功能非常简单:在页面中打印 hello world
字符串。这里在执行 Vue.createApp
前插入 debugger
代码,使得代码执行在 debugger
处暂停下来。
调试开始
这里还是贴一下调试的配置,如需更详细的了解调试原理和流程,请访问笔者上一篇文章:《debug一下,你就学会高效阅读开源项目代码~》。
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch hello",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}",
"file": "${workspaceFolder}/packages/vue/examples/hello.html"
}
]
}
点击调试按钮,程序在 debugger
处暂停,然后执行到 Vue.createApp
处单步进入,断点进入到 packages/runtime-dom/src/index.ts
中的 createApp
方法。
createApp
我们直接来看 createApp
方法的源码,这里有部分代码删减,主要是针对 dev 环境的一些方法实现,不影响主体流程,下同。
// 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
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, false, container instanceof SVGElement)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
}) as CreateAppFunction<Element>
暂且不看 const app = ensureRenderer().createApp(..args)
这行代码的内部实现,我们直接单步跳过,在左侧的调试面板可以看到 app 的值如下所示:
那么我们合理猜测,ensureRenderer().createApp(...args)
这行代码使用传进来的参数进行属性和方法初始化,并且挂载在 app 变量中返回。
返回 app
变量后,取出原本的 mount
方法,然后将一个新的方法实现挂载到 app
的 mount
属性中,也就是 html 文件中 .mount('#demo')
代码块的具体实现,最后返回 app 变量。
ensureRenderer().createApp(...args)
单步进入到 const app = ensureRenderer().createApp(...args)
这行代码如下所示:
这里涉及到两个方法:ensureRenderer
和 createApp
,我们逐个来看。
ensureRenderer
// packages/runtime-dom/src/index.ts
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
这个方法的核心是 createRenderer
,这里不展开这个方法的具体源码,我们看这个方法返回值的定义:
// packages/runtime-core/src/renderer.ts
export interface Renderer<HostElement = RendererElement> {
render: RootRenderFunction<HostElement>
createApp: CreateAppFunction<HostElement>
}
createRenderer
返回一个对象,具有 render
和 createApp
两个属性,render
是下面 mount
提到的渲染函数,这里不展开讲。createApp
方法是 createAppAPI
的返回值,源码如下所示:
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
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,
isSVG?: boolean
): any {
},
unmount() {
},
provide(key, value) {
}
})
return app
}
}
源码验证了我们的猜测:createApp
将若干属性和方法挂载在 app
这个变量中,最后并返回 app
。
mount
在 const proxy = mount(container, false, container instanceof SVGElement)
这句代码中打一个断点并且单步进入,可以得到 mount
方法的代码实现:
// packages/runtime-core/src/apiCreateApp.ts
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// 用一个变量来控制 mount 只执行一次
if (!isMounted) {
// 创建一个虚拟node节点
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__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
return vnode.component!.proxy
}
}
可以看到,当我们单步跳过 render(vnode, rootContainer, isSvg)
这行代码时,hello world
字符串显示在浏览器上了,也就是说,mount
方法将 Vue 组件挂载到浏览器上,而 render
则是关键的渲染方法。
这里有一个亮点是 mount
调用 createVNode
方法来创建虚拟节点,render
接收虚拟节点参数进行渲染,本文不再展开讲这一部分,有兴趣可以关注笔者后面的文章。
总结
本文为笔者连载 Vue3 源码阅读的第一篇,不涉及各种原理的解读,主要目的是在不需要阅读详细源码的前提下,快速了解 Vue3 组件的执行流程。
希望读者在通读本文之后,能够通过调试的方式自行高效地阅读 Vue3 源码,如果各位觉得本文能给大家带来帮助,点个赞再走呗~~~
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!