前言
编译篇 简单学习完,上文最后的 runtimeDom
,实际是 runtime-dom/index.ts
文件中所导出的内容。我们点进去会发现一个非常眼熟的 API, createApp
,runtime-dom
导出了该 API,我们先忘记之前的编译篇,从头开始,从一般创建的 main.ts
的 createApp
开始,看看 Vue 都做了什么处理。
// main.ts 例子
const { reactive, createApp } = Vue
createApp(App).mount('#app')
createApp
export const createApp = (...args: any) => {
/**
* 【重要且长】放后面分析
* baseCreateRenderer 接收一个选项对象,该对象主要包含了 nodeOps(真实DOM操作) 和 patchProp(属性的操作)
*/
const app = baseCreateRenderer(extend({ patchProp }, nodeOps)).createApp(...args)
const { mount } = app
// 重写app的mount方法
app.mount = (containerOrSelector: string) => {
const container = document.querySelector(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.template && !component.render) {
// 如果当前 app 实例中不存在 模板,函数,render方法 ,那么则将 container 处的模板赋值给他
component.template = container.innerHTML
}
// 清空 container 的内容(因为 container 之前的内容为未解析的模板内容,浏览器无法识别的)
container.innerHTML = ''
// 调用app的mount方法,并返回 exposeProxy 提供给外部使用
const proxy = mount(container, false, false)
}
return app
}
我们发现,其实 runtimeDom
导出的 createApp
实际上只是拦截重写了 mount
方法的 baseCreateRenderer().createApp()
重写的目的也很明了,这样对用户来说 mount
只需要传递一个 选择器
<div id="app">
<p v-for="item in [1,2]">{{ item }}</p>
</div>
<script>
// 像这种情况,createApp的参数不为 组件,那么久会从 #app 中获取 innerHTML 作为 template 属性使用
createApp({})..mount('#app')
// 忽略 #app 的模板,使用 App 组件渲染
createApp(App)..mount('#app')
// 忽略 #app 的模板,使用 template 渲染, render 同理
createApp({template:`<span>111</span>`})..mount('#app')
createApp({render(){return }})..mount('#app')
</script>
baseCreateRenderer
该方法在源码里有 2000+ 行,重要性不言而喻。但他的 参数 和 返回值 非常简单清晰,他接收一个 options
, 并返回 { render, createApp }
对象
其作用就是根据 VNode
生成 真实DOM
,所以其实内部 2000 多行代码都是对各种类型的 VNode 的处理
function baseCreateRenderer(options: RendererOptions) {
// 首先在上面我们提到了,options 里主要是 nodeOps(真实DOM操作) 和 patchProp(属性的操作)
const {
patchProp: hostPatchProp,
insert: hostInsert, // 插入方法 (child, parent, anchor) => parent.insertBefore(child, anchor || null)
remove: hostRemove, // 移除方法
createElement: hostCreateElement, // 创建元素节点
createText: hostCreateText, // 创建文本节点
createComment: hostCreateComment, // 创建注释节点
setText: hostSetText, // 设置文本
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
insertStaticContent: hostInsertStaticContent,
} = options
/**
* 重点方法(一般称作打补丁)
* 会根据 vnode 的类型,去选择如何创建真实节点
*
* @param n1 旧VNode
* @param n2 新VNode
* @param container 包裹 n2 的 元素容器
* @param anchor // 下一个兄弟节点
* @param parentComponent // 父组件实例, 和 container 感觉上类似,但是区别很大,这是一个组件实例,不是元素
* @param parentSuspense // 暂不考虑,suspense情况
* @param isSVG // 暂不考虑
* @param slotScopeIds
* @returns
*/
const patch = (n1, n2, container, anchor = null, parentComponent = null) => {
// 如果前一次更新 和 当前更新的2个 VNode 相同,那么就不用变化,直接返回
if (n1 == n2) {
return
}
// n1 不为空 且 2个vnode 不同(key 或者 type 不一样)
// 则需要卸载旧的真实DOM
if (n1 && !isSameVNodeType(n1, n2)) {
// 获取 旧节点 的下一个兄弟节点
anchor = getNextHostNode(n1)
// 卸载 n1
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case 'Text':
processText(n1, n2, container, anchor)
break
case 'Fragment':
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, false, null, false)
break
default:
// 【注】 ShapeFlags 是在创建虚拟DOM的时候,保存到Vnode上的
// type 为 object 的那种,或者 function 的都会进入到这里
// 因为在 patch 之前的 createVnode 里已经将该 type 类型的 ShapeFlags 要么设为 STATEFUL_COMPONENT,要么设为 FUNCTIONAL_COMPONENT
if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, false, slotScopeIds, false)
} else if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, false, slotScopeIds, false)
}
}
}
// 处理文本,如果 旧节点为null,那么就用 createTextNode 创建一个新的文本节点
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
// 用 n2.children 是因为,在 createTextVNode 时,是用 text 塞进 children 的
// anchor 为兄弟节点,用来帮助 insertBefore 插入正确的位置
hostInsert((n2.el = hostCreateText(n2.children as string)), container, anchor as any)
} else {
// 如果 旧节点已存在,那么直接改写 文本的 nodeValue 即可
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el as any, n2.children as string)
}
}
}
const render: Function = (vnode: any, container: any, isSVG: boolean) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
container._vnode = vnode
}
return {
render,
createApp: createAppAPI(render),
}
}
重点看返回的 render
,会发现哪怕是 createApp
也是将 render
作为参数传递给了 createAppAPI
,所以这里 2000 多行代码可以看作是以 render
方法作为起始点。 render
方法调用 patch
, patch
在根据 vnode
类型去选择要如何创建真实DOM
。且创建时候,会根据 n1(旧 VNode) 是否存在来判断是 首次加载 还是 更新 ,会有不同的操作,如:处理文本的更新只是简单的替换 nodeValue
有些重要的方法,比如 processComponent
的实现,因为涉及的东西有些多,准备在后续文章中讲解。
createAppAPI
接下来我们在瞅瞅 createApp 是如何创建的,他由 createAppAPI 接收了 render 方法之后返回。
export const createAppAPI = (render: Function) => {
return function createApp(rootComponent: any, rootProps: any = null) {
if (!isFunction(rootComponent)) {
// 浅拷贝rootComponent
rootComponent = { ...rootComponent }
}
if (rootProps != null && !isObject(rootProps)) {
rootProps = null // rootProps 必须为 object
}
// 创建app的上下文,返回的对象保存了该app的全局组件,指令,provides等属性
const context = createAppContext()
// 尚未挂载
let isMounted = false
const app: any = (context.app = {
_uid: uid++, // 标识ID,一直递增
_component: rootComponent, // 如果拿 createApp(App) 举例,那么这个便是我们传递的 App 组件,最爷爷的组件
_props: rootProps,
_container: null,
// context可以通过.app 获取到 app, app 也可以通过 _context 获取到 context
_context: context,
_instance: null,
// 注册组件 or 返回组件
component(name: string, component?: any): any {
if (!component) {
return context.components[name]
}
context.components[name] = component
return app
},
// 挂载到某个节点上
mount(rootContainer: any, isHydrate?: boolean, isSVG?: boolean): any {
if (!isMounted) {
// 按照目前我的例子, rootComponent 为 { setup ,template }
// 最终 vnode 的 type 也会为 { setup ,template }
const vnode = createVNode(rootComponent, rootProps)
vnode.appContext = context
// render 是在 baseCreateRenderer 中创建的
// 用于将 vnode 转换成真实DOM并渲染到页面上
render(vnode, rootContainer, isSVG)
// 设置成已挂载
isMounted = true
app._container = rootContainer
;(rootContainer as any).__vue_app__ = app
return getExposeProxy(vnode.component!) || vnode.component!.proxy
}
},
// 卸载
unmount() {
if (isMounted) {
// 卸载 container 上的真实DOM,传递 null 给 render 即可卸载
render(null, app._container)
delete app._container.__vue_app__
}
},
})
return app
}
}
createAppAPI
返回的 createApp
接收一个 rootComponent
,根据我们的使用经验,rootComponent
可以是一个 组件,setup, render,所以在上面先判断 rootComponent 是否是一个方法,如果不是方法,那么只可能是一个对象,浅拷贝赋值这个对象。
createApp(App)
createApp({ setup() {} })
createApp({ render() {} })
接着便是熟练的老操作 ———— 创建 context, vue 在很多地方都有上下文做关联。这里也不例外,在这个 appContext
中保存了我们成为全局的一些东西。比如 组件,指令, mixin 等。
然后在将 context 和 app 相互关联。这样保证了 app 可以通过 ._context
访问到 context。 context 也可以通过 .app
访问到 app。
接着就是最重点的 mount 方法。我们在最上面说到我们经常使用的 createApp(注意有好几个createApp 别弄混了)
便是重写了这里的 mount 方法 const proxy = mount(container, false, false)
我们可以看到,真正 mount 方法其实是主要是做了这 2 步骤:
- 使用 rootComponent 创建了一个 虚拟 DOM
- 再用 render 去将虚拟 DOM 转换成真实 DOM 渲染到界面上
创建 VNode,实际上也是返回一个 JS 对象,这个 JS 对象是用来描述真实DOM
的,在生成真实DOM
的 patch 方法内会使用 VNode 的 type
和 shapeFlag
来判断要创建什么类型的真实DOM
总结
之前背八股文那会,背过一道面试题: 从模板-> 真实 DOM 的过程,现在真正瞅完了下源码,才对这一系列过程有个大概性的了解:
-
从模板解析
(parse)
成 AST 对象,AST 对象在转换(transform, codegen)
成带 createVNode 的 render 函数字符串。 -
在通过
new Function('Vue', code)(runtimeDom)
创建出了虚拟DOM
。 -
最终,通过 render 内的
patch
方法去利用虚拟DOM
生成真实DOM
最后渲染到界面上。