Vue3.2x源码解析(一):vue初始化过程
Vue3.2x源码解析(一):vue初始化过程
1,目录结构
先回顾下 Vue2 源码目录:
├── src
├── compiler # 编译相关的模块
├── core # 核心语法代码
├── platforms # 平台相关
├── server # 服务端渲染相关
├── sfc # vue单文件组件
├── shared # 公用方法
再看看 Vue3 源码的目录结构:
├──packages
├── compiler-core # 平台无关的编译器核心代码,baseParse生成AST
├── compiler-dom # 基于compiler-core,针对浏览器的附加插件的编译器
├── compiler-sfc # 编译vue单文件组件
├── compiler-ssr # 服务端渲染相关的
├── reactivity # vue3响应式模块,例如:ref/reactive/effect
├── runtime-core # 平台无关的运行时核心代码,组件创建/渲染/更新
├── runtime-dom # 基于runtime-core,针对浏览器的运行时,处理各种原始dom_api
├── vue # 面向公众的完整构建,其中包含编译器和运行时
├── shared # 多个包共享的内部工具,公用方法
├── ...
Vue3采用monorepo是管理项目代码的方式。不同于 Vue2 代码管理,它是在一个 repo中管理多个package项目,每个 package 都有自己的类型声明、单元测试。 package 又可以独立发布,这种部署方式更便于项目的维护和发版。
注意:vue3源码由TS代码编写,阅读源码前请先熟悉TS基本语法。 vue3的强大渲染器可以实现多端跨平台的应用,本文依然以web端为视角进行源码解析。
2,源码入口
vue3每一个项目的开始,都是从vue中引入一个createApp方法,使用这个方法创建一个vue实例,这个实例就是我们的应用实例,一个项目可以按需求创建多个vue实例。
import { createApp } from 'vue'
import App from './App.vue'
// 应用初始化
const app = createApp(App)
app.mount('#app')
-
这里的App是根组件,作为渲染组件的起点。
-
mount('#app') 表示应用挂载的DOM节点容器。
createApp
接下来就从import { createApp } from 'vue'这句代码开始源码的学习。
首先查看createApp的源码:
// packages/runtime-dom/src/index.ts
# 创建vue应用实例
export const createApp = ((...args) => {
// 创建vue实例
const app = ensureRenderer().createApp(...args)
// 取出mount方法
const { mount } = app
# 重写mount方法
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 确定挂载节点容器
const container = normalizeContainer(containerOrSelector)
// 没有容器直接return 无法挂载应用
if (!container) return
// 取出根组件
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
// 如果根组件没有render/template,则把容器的内容赋值给根组件
component.template = container.innerHTML
}
// clear content before mounting
// 加载之前清空dom容器内容
container.innerHTML = ''
# 调用mount开始应用真正的加载
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
})
createApp源码内容看似不多,实则里面执行了非常多的逻辑,后面我们会一直跳转回来查看方法里面的内容。
首先分析第一行代码:
const app = ensureRenderer().createApp(...args)
一进来就执行了一个ensureRenderer方法。
ensureRenderer
我们理解一下这个方法的作用:
// packages/runtime-dom/src/index.ts
# 确定渲染器
function ensureRenderer() {
// 返回一个渲染器对象
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
可以看出这个方法只有一个作用,返回一个确定的渲染器renderer对象。
在讨论createRenderer之前,有一个重点要关注一下rendererOptions:
const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)
rendererOptions由两部分组成:
- patchProp:处理元素的props、Attribute、class、style、event事件等。
- nodeOps:处理dom节点,这个对象里面包含了各种原生dom操作方法。
rendererOptions对象最终会传递到渲染器里面,里面的各种方法最终会在页面渲染的过程中被调用。
(一)renderer渲染器的类型
在知道renderer渲染器如何创建之前,我们先看看renderer对象的类型:
# Renderer web端渲染器 HydrationRenderer服务器端渲染器
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
注意:Hydration开头的相关函数,为SSR服务器端渲染使用,后续会在Vue3源码中多次遇见。
可以看出renderer渲染器可以是web端渲染器,也可以是SSR渲染器,我们在浏览器中使用Vue框架就会确定为web端的渲染器。
(二)rederer渲染器的创建
接下来我们就分析renderer渲染器的创建过程:
// packages/runtime-dom/src/index.ts
renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions)
查看createRenderer源码:
// packages/runtime-core/src/renderer.ts
# 创建渲染器
function createRenderer<HostNode = RendererNode,HostElement = RendererElement>
(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
# 注意;泛型函数 fn<Node, Element>() {}
// vue3源码量非常大,并且其中有非常多的泛型函数,在刚开始阅读源码的时候,可以减少关注具体的类型,重点放在逻辑过程。可以在后面解惑的时候再回头看具体的类型,这可以减轻刚开始阅读源码的压力。
baseCreateRenderer
继续查看baseCreateRenderer源码:
// packages/runtime-core/src/renderer.ts
# Vue3渲染器核心: 非常重要
function baseCreateRenderer(options, createHydrationFns?) {
// 确定全局对象
const target = getGlobalThis()
target.__VUE__ = true
# rendererOptions对象传递过来的原生dom操作方法
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
insertStaticContent: hostInsertStaticContent
} = options
# 渲染器内部有几十个工具函数,这里列出几个常用的
// patch,根据vnode的类型,执行不同的逻辑,
const patch = () => {}
// 加载组件
const mountComponent = () => {}
// 更新组件
const mountComponent = () => {}
// 设置组件渲染renderEffect, 类似于vue2 watcher的renderWatcher
const setupRenderEffect = () => {}
# 渲染应用方法 重点
const render = () => {}
return {
render,
hydrate,
# 重点:初始化createApp方法
createApp: createAppAPI(render, hydrate)
}
}
注意:baseCreateRenderer是基础渲染器,功能非常强大,是Vue3应用渲染的核心,可以创建出不同环境需要的渲染器,自定义渲染器,源码量非常大,这里我们主要介绍渲染器内部的一些重点方法的用途,后面会根据场景再展开每个方法的具体逻辑。
baseCreateRenderer源码量非常多,这里直接看return返回值,返回了一个对象,这就是我们所需要的渲染器对象。
// 渲染器对象有三个方法,主要关注render和createApp
const renderer = {
render,
hydrate, // 忽略
createApp: createAppAPI(render, hydrate)
}
再跳回到ensureRenderer方法,可以看到这个方法的返回值就是渲染器对象renderer。
重点关注渲染器对象的render方法和createApp方法。
再跳回到刚开始的createApp源码的第一行代码:
createApp() {
const app = ensureRenderer().createApp(...args)
}
ensureRenderer生成一个渲染器后就立即调用内部的createApp方法,创建了一个vue应用实例,我们要理解Vue实例的具体创建过程,就得继续查看createApp方法的源码createAppAPI。
createAppAPI
// packages/runtime-core/src/apiCreateApp.ts
# 创建vue应用方法
function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
# 返回值就是createApp方法源码
return function createApp(rootComponent, rootProps = null) { // 参数为根组件:即src/App.vue组件
if (!isFunction(rootComponent)) {
rootComponent = { ...rootComponent }
}
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
# 创建vue应用实例上下文 就是确定应用的一些默认配置
const context = createAppContext()
// 初始化插件集合,用于存储插件列表
const installedPlugins = new Set()
// 初始化应用挂载状态,默认为false
let isMounted = false
# 创建vue实例对象
// 同时将应用实例存储到context上下文的app属性
const app: App = (context.app = {
# 初始化一些应用属性
_uid: uid++, // 项目中可能存在多个vue实例,需使用id标识
_component: // 根组件
_props: rootProps, // 传递给根组件的props
_container: null, // dom容器
_context: context, // app上下文
_instance: null, // 应用实例
version, // 版本
# 定义了一个访问器属性app.config,只能读取,不能直接替换
get config() {
// 读取的是上下文的config
return context.config
},
set config(v) {},
# 下面是vue应用内部定义的几个方法
// 安装插件
use() {}
// 混入
mixin() {}
// 注册全局组件
component() {}
// 注册自定义指令
directive() {}
// 挂载应用
mount() {}
// 卸载应用
unmount() {}
// 全局数据
provide() {}
})
if (__COMPAT__) {
// 若开启兼容,安装vue2相关的API
installAppCompatProperties(app, context, render)
}
# 返回vue实例对象
return app
}
}
以上就是createApp的源码,简单来说就是内部创建了一个vue实例对象,这个对象内部初始化了一些全局的属性和方法,和vue2源码中的initGlobalAPI逻辑比较类似,比如use/mixin/component/directive,这些都是我们比较熟悉的全局方法了。然后根据配置处理vue2兼容的相关API,最后返回了实例对象。
以上的内容就是刚开始createApp源码中第一行代码的逻辑:
createApp() {
const app = ensureRenderer().createApp(...args)
...
}
// 所以我们在项目只使用了一行代码就创建一个vue应用,实际上框架内部已经执行了非常多的逻辑
const app = createApp(App)
我们再跳回到刚开始的createApp源码:
createApp() {
const app = ensureRenderer().createApp(...args)
# 1,取出原来的mount方法
const { mount } = app
// 2,重写app的mount方法
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
...
# 调用mount开始应用真正的加载
const proxy = mount(container, false, container instanceof SVGElement)
return proxy
}
return app
}
在创建vue应用实例后,取出mount方法,然后立即重写了app实例的mount加载方法,最后返回了vue应用实例。
这里有一个重点是两个Mount方法的区别:
- 第一个mount方法为:vue应用的渲染,侧重为调用render方法进行应用渲染。
- 第二个mount方法为:vue应用的挂载,侧重为将应用挂载到container节点容器。
这里的加载执行顺序是:先获取目标DOM节点容器,进行应用挂载,然后调用render方法进行应用渲染。即先执行重写的mount方法,然后在内部执行原来的mount加载方法。
const app = createApp(App)
# 当然,一般在执行mount加载之前,都会注册一些全局组件和插件资源
app.component()
app.directive()
app.use(Element)
app.use(pinia)
# 加载应用
app.mount('#app')
分析到这里,以上的内容就是createApp源码的基本内容。主要作用就是确定渲染器,创建一个Vue应用实例,最后进行加载,下面我们将继续分析vue加载的详细过程。
3,应用加载
# 1,执行重写的mount,应用挂载
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
...
# 2,执行原来的mount,应用渲染
const proxy = mount(container, false, container instanceof SVGElement)
return proxy
}
根据前面的分析,两个加载的执行顺序及内容我们都了解了,下面我们分析应用加载渲染的具体过程:
// packages/runtime-core/src/apiCreateApp.ts
# 应用加载
mount(rootContainer: HostElement,isHydrate?: boolean,isSVG?: boolean): any {
# 首次isMounted为false,执行加载渲染
if (!isMounted) {
# 创建根组件vnode对象,默认编译生成的组件对象只有setup和render方法
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.
// 将app的上下文存储在根虚拟节点
vnode.appContext = context
// 开发环境下的热更新
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
// isHydrate水合函数与ssr有关
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
# 开始应用渲染
render(vnode, rootContainer, isSVG)
}
# 应用渲染完成,设置应用为已挂载状态
isMounted = true
// 设置应用容器节点#app
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)
}
# 加载完成,返回应用的代理对象
return getExposeProxy(vnode.component!) || vnode.component!.proxy
}
}
上面的应用加载过程主要有三个重点:
- 创建根组件vnode对象。
- 执行render方法开始渲染应用【整个应用的渲染过程都在里面】。
- 渲染完成,设置应用为已挂载状态。
其中最重要的就是渲染逻辑,下面我们继续分析render方法:
// packages/runtime-core/src/Renderer.ts
# 渲染方法
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
# 第一次vnode是根组件的内容,开始应用的渲染
// 首次container还没有_vnode属性为underfined ,即旧的_vnode为null
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
# 执行冲刷任务
flushPreFlushCbs()
flushPostFlushCbs()
// 定义_vnode属性:存储当前vnode
container._vnode = vnode
}
到这里,我们就可以对Vue应用的初始化过程做一个总结,简单来说就是:
- 创建Vue应用实例。
- 确定应用挂载容器节点。
- 创建根组件vnode对象。
- 执行render加载渲染应用。
在生产环境下:Vue应用的初始化只会执行一次,除非主动刷新页面。
在开发环境下:由于源码变化热更新的支持,Vue应用可以多次执行初始化渲染。
// 开发环境下的热更新
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
下节我们继续分析patch里面的内容及组件的初始化过程。
转载自:https://juejin.cn/post/7202801134556381245