3-Vue源码之【createApp】

前言

编译篇 简单学习完,上文最后的 runtimeDom,实际是 runtime-dom/index.ts 文件中所导出的内容。我们点进去会发现一个非常眼熟的 API, createAppruntime-dom 导出了该 API,我们先忘记之前的编译篇,从头开始,从一般创建的 main.tscreateApp 开始,看看 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 方法调用 patchpatch 在根据 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 步骤:

  1. 使用 rootComponent 创建了一个 虚拟 DOM
  2. 再用 render 去将虚拟 DOM 转换成真实 DOM 渲染到界面上

创建 VNode,实际上也是返回一个 JS 对象,这个 JS 对象是用来描述真实DOM 的,在生成真实DOM 的 patch 方法内会使用 VNode 的 typeshapeFlag 来判断要创建什么类型的真实DOM


总结

之前背八股文那会,背过一道面试题: 从模板-> 真实 DOM 的过程,现在真正瞅完了下源码,才对这一系列过程有个大概性的了解:

  1. 从模板解析(parse)成 AST 对象,AST 对象在转换(transform, codegen)成带 createVNode 的 render 函数字符串。

  2. 在通过 new Function('Vue', code)(runtimeDom) 创建出了 虚拟DOM

  3. 最终,通过 render 内的 patch 方法去利用 虚拟DOM 生成 真实DOM 最后渲染到界面上。

原文链接:https://juejin.cn/post/7237516248803655736 作者:pnm学编程

(0)
上一篇 2023年5月28日 上午11:06
下一篇 2023年5月29日 上午10:00

相关推荐

发表回复

登录后才能评论