Vue 源码学习记录
Day 1
今日目标:
- 环境搭建
- 实例创建过程
- 实例挂在过程
环境搭建
- 克隆项目
git clone git@github.com:vuejs/core.git
- 安装依赖
npm i pnpm -g
cd ./core
pnpm install
- 改变脚本
// package.json scripts中
"dev": "node scripts/dev.js --sourcemap"
sourcemap是为了解决开发代码与实际运行代码不一致时帮助我们debug到原始开发代码的技术。
- 运行
pnpm run dev
实例创建过程
在使用vue3过程中,在入口文件中,我们一般会见到如下代码:
createApp(App).mount("#app");
那么在createApp中,到底发生了什么?
根据浏览器中打断点和单步调试的功能,我们轻易找到了 createApp 是 runtime-dom/src/index.ts 中的方法。
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
...
}
接着,我们看 ensureRenderer 这个方法,这里的 renderer 是一个单例的变量,一开始为undefined,因此调用 createRenderer 来创建一个 renderer,那么这里的 app 实际上是 renderer.createApp(...args)
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
那接着来看 createRenderer,经过断点调试,发现这个方法在 runtime-core/src/renderer.ts 中:
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
而这个 createRenderer 又调用了 baseCreateRenderer,那么我们来看 baseCreateRenderer,由于这个函数的代码量实在过于庞大,我们从简看它的 return。
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
...
return {
render, // 把接收到的vnode转换成dom,追加到宿主元素
hydrate, // 服务端渲染,暂且不看
createApp: createAppAPI(render, hydrate) // 重点
}
}
到这里,我们发现 renderer.createApp(...args) 实际上是 renderer.createAppAPI(render, hydrate)(...args),那我们接着来看createAppAPI。它是在 runtime-core/src/apiCreateApp.ts 中。它 return 的是一个名为 createApp(这个createApp不同于上面那个createApp) 的 function。它的作用返回一个 App 的实例,这个实例上有use,mixin,mount等等的方法。
function createAppAPI(render,hydrate) {
return function createApp(rootComponent, rootProps = null) {
...
const context = createAppContext() // 创建一个context
let isMounted = false
const app: App = (context.app = {
...
mount(rootContainer,isHydrate,isSVG) {
...
} else if (__DEV__) {
...
}
},
...
})
...
return app
}
}
当上面的这个createApp执行完,此时的调用栈应该回到最外层的createApp中。我们接着看最外层的createApp。
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
... // 封装一下根节点的mount
}
return app
})
通过阅读上面的代码,其实createApp干的事情有以下几点:
- 调用createApp这个函数 -> 调用 ensureRenderer() -> 调用 baseCreateRenderer()
- 通过第一步的结果生成一个renderer(渲染器)
- renderer是一个对象
{
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
- 调用renderer上面的 createApp(不是第一步中的createApp)方法,这个方法返回一个App类型的实例
- 封装根节点的mount
- 通过最外层的createApp将这个App实例返回
实例挂载过程
搞清楚了createApp干的事情之后,自然就要搞清楚 mount 干的事情。实际上mount调用的就是上面App实例上的mount。而这个mount就是上面createApp中的 app.mount。
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector)
// 标准化容器, containerOrSelector 是 "#app"
// 这里传进一个选择器,normalizeContainer返回一个dom
if (!container) return
const component = app._component
// 这里 component实际上是 createApp中传进来的对象,有setup 和 directive
if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's
// rendered by the server, the template should not contain any user data.
// 这里如果没有template,将innerHTML赋值给template
component.template = container.innerHTML
// 2.x compat check
if (__COMPAT__ && __DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i]
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
compatUtils.warnDeprecation(
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
null
)
break
}
}
}
}
// clear content before mounting
container.innerHTML = ''
// 重点是这一行,这里调用了mount函数
// 而这个mount函数是之前createApp返回的app实例里的那个mount函数
// container是#app的dom
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
在来看mount的实现:
mount(
rootContainer: HostElement, // dom
isHydrate?: boolean, // false
isSVG?: boolean // false
): any {
// 这个isMounted初始化是false
if (!isMounted) {
const vnode = createVNode( // 创建vnode
rootComponent as ConcreteComponent,
// rootComponent:组件对象,有template、setup、directive
rootProps
)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
// 这个context是通过createAppApi中的CreateAppContext调用来生成的一个初始化对象
vnode.appContext = context
if (isHydrate && hydrate) {
// 服务端渲染,不看
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 其实重点是render函数,这个render函数是通过renderer渲染器的一个属性(方法)
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)
}
// 返回一个proxy
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} else if (__DEV__) {
...
}
},
重点我们来看render函数,在runtime-core/src/renderer.ts中:
const render: RootRenderFunction = (vnode, container, isSVG) => {
// vnode是虚拟节点
// container是#app
if (vnode == null) {
// 卸载组件
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 重点是这个patch函数,这个是虚拟dom相关的知识点,先不看,
// 暂且可以理解为打补丁,将vnode更新到contaienr上去
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
// 异步更新相关
// 大致看了下,就是生成一个单例activePostFlushCbs,然后对这个Cbs队列进行更新
flushPostFlushCbs()
container._vnode = vnode
}
所以mount干的事情有以下几点:
- normalizeContainer,标准化容器
- 调用mount
- 创建虚拟节点vnode
- vnode作为参数传给render方法
- render调用patch方法,更新页面
- 更新异步任务队列
- 将vnode挂载到container上去
- 设置isMounted为true
- 返回一个proxy