vue3源码分析01-初始化渲染流程

  1. 今日目标:
  • 了解 vue3 初始化渲染流程
我们今天的代码
{{ count }}

const {ref , h } = Vue Vue.createApp({ setup(p , c){ const count = ref(0) const add = () => { count.value ++ } return { count, add } }, }).mount('#app')

开始分析 createApp 入口方法 : runtime-dom -> index
从上面代码我们可以看出,vue3 初始化需要经过 createApp 方法, 之后 mount 到选中的容器上. 所以我们看下 createApp 方法的实现
// 入口方法 export const createApp = ((...args) => { // 获取 app 实例 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 } // 清空容器内容 container.innerHTML = '' // 开始挂载 const proxy = mount(container, false, container instanceof SVGElement)return proxy }return app }) as CreateAppFunction

我们可以看出 createApp 方法的核心目的是创建一个 app , 创建之后添加 mount 方法提供给后面链式调用.
我们来看一看 app 的创建过程
ensureRenderer
创建渲染器 : runtime-dom -> index
function ensureRenderer() { return renderer || (renderer = createRenderer(rendererOptions)) }

这个方法试图返回一个 renderer .如果 renderer 不存在,他就去创建一个 renderer.看一下创建的时候传入的 rendererOptions
vue3源码分析01-初始化渲染流程
文章图片

他在这里将 patchProp , forcePatchProp , nodeOps 合并
vue3源码分析01-初始化渲染流程
文章图片

我们可以看到 nodeOps 为真实 DOM 操作.
我们看一下 createRenderer 方法
createRenderer
创建渲染器 : runtime-core -> renderer
vue3源码分析01-初始化渲染流程
文章图片

可以看到, 在这里做了几次重载之后, 最终执行的是 baseCreateRenderer
vue3源码分析01-初始化渲染流程
文章图片

这个方法太长了, 就不全截图了. 总的来说这个方法最终的目的是拿到 render , 和 createAppApi 的执行结果. 我们来看一下 createApp 是做什么的.
createAppApi 生成 app 实例 注册全局 API : runtime-core -> apiCreateApp
vue3源码分析01-初始化渲染流程
文章图片

  1. 这个方法先创建了一个 app 的空的上下文
export function createAppContext(): AppContext { return { app: null as any, config: { isNativeTag: NO, performance: false, globalProperties: {}, optionMergeStrategies: {}, errorHandler: undefined, warnHandler: undefined, compilerOptions: {} }, mixins: [], components: {}, directives: {}, provides: Object.create(null), optionsCache: new WeakMap(), propsCache: new WeakMap(), emitsCache: new WeakMap() } }

这个结构
  1. 同时会创建一个 管理插件的集合
  2. 在 app 中, 注册了一些全局相关的东西. (想看全局指令, 全局组件, 插件相关的可以到这个方法里看)
const app: App = (context.app = { _uid: uid++, _component: rootComponent as ConcreteComponent, _props: rootProps, _container: null, _context: context, _instance: null, version, get config() { return context.config }, set config(v) { if (__DEV__) { warn( `app.config cannot be replaced. Modify individual options instead.` ) } }, use(plugin: Plugin, ...options: any[]) { // more ... return app }, mixin(mixin: ComponentOptions) { // more ... return app }, component(name: string, component?: Component): any { // more ... return app }, directive(name: string, directive?: Directive) { // more ... return app }, mount( rootContainer: HostElement, isHydrate?: boolean, isSVG?: boolean ): any { // more ... }, unmount() { // more ... }, provide(key, value) { // more ... return app } })

【vue3源码分析01-初始化渲染流程】这里面声明了一个 mount 方法 (记住!!!).
之后他就把 app 返回了. 到这里为止, 我们的 app 实例就创建好了. 我们可以看出来, vue3 的设计方式是将平台相关的操作抽离出去, 这样对多平台框架开发者及其友好, 只需要关注对应平台的节点操作, 创建渲染器即可.
我们回到入口方法
暴露挂载方法
vue3源码分析01-初始化渲染流程
文章图片

获取到 app 实例之后, vue 把实例中的 mount 方法缓存了一下, 并且重写实例上的 mount 方法. 当我们调用 mount 时(调用的是重写之后的 mount ),
  1. vue 会先验证你传入的挂载目标是否是合法的目标.
  2. 他会尝试获取实例上的 \_component (当前的 component 为 createApp 时注册的选项)
  3. 如果发现我们注册的选项 不是 function || 没有 render 配置 || 没有 template 配置的话 , 他会给 component 增加 template 属性, (这个属性后续会用到 !!!)
  4. 之后清空容器里的 innerHTML
  5. 执行之前缓存的(实例中的) mount 方法
mount
挂载方法 : runtime-core -> apiCreateApp -> mount
vue3源码分析01-初始化渲染流程
文章图片

  1. 如果当前实例还没被挂载过, 他就会创建一个 vnode . 由于是引用关系,所以我们增加的 template 属性也会在 rootComponent 中
vue3源码分析01-初始化渲染流程
文章图片

看一下 createVNode 方法
/** *runtime-core -> vnode */function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, // rootComponent props: (Data & VNodeProps) | null = null, // null children: unknown = null, // null patchFlag: number = 0, // 0 dynamicProps: string[] | null = null, // null isBlockNode = false // false ): VNode { // 如果传入的参数 已经是个 vnode 了 // 则不需要转换 克隆一个 // 检查子元素是否为合法的子元素 if (isVNode(type)) { const cloned = cloneVNode(type, props, true /* mergeRef: true */) if (children) { normalizeChildren(cloned, children) } return cloned }// 如果是类组件 if (isClassComponent(type)) { // more }// 处理 props 中的 class 和 style if (props) { // more ... }// 打标记 const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : __FEATURE_SUSPENSE__ && isSuspense(type) ? ShapeFlags.SUSPENSE : isTeleport(type) ? ShapeFlags.TELEPORT : isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : isFunction(type) ? ShapeFlags.FUNCTIONAL_COMPONENT : 0// vnode 主体 const vnode: VNode = { __v_isVNode: true, __v_skip: true, type, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, slotScopeIds: null, children: null, component: null, suspense: null, ssContent: null, ssFallback: null, dirs: null, transition: null, el: null, anchor: null, target: null, targetAnchor: null, shapeFlag, patchFlag, dynamicProps, dynamicChildren: null, appContext: null }// 检查子节点是否合法 normalizeChildren(vnode, children)// 检查 suspense 的子节点 if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { // more ... }return vnode }

值得一提的是, vue3 采用 位运算 来给元素类型进行枚举. 个人理解为: 没有使用 number nodeType,而是使用二进制 0000、0010、0100、1000 是为了方便确认节点类型. 只需要比较当前标记的某一位, 即可确定节点类型(欢迎评论区讨论 ~)
  1. 获取到 vnode 后, 将当前实例上下文添加到 vnode
  2. 之后判断是否需要融合(否). 调用 render 方法 (此时的 render 方法是通过创建 app 实例 时的形参传入的, 声明是在渲染器中)
  3. render 结束后将挂载状态置为 true
可以看出 mount 方法只想做两件事
  1. 创建 vnode
  2. render vnode
render
渲染方法入口 : runtime-core -> renderer -> render
const render: RootRenderFunction = (vnode, container, isSVG) => { if (vnode == null) { if (container._vnode) { unmount(container._vnode, null, null, true) } } else { patch(container._vnode || null, vnode, container, null, null, null, isSVG) } flushPostFlushCbs() container._vnode = vnode }

render 方法想要做两件事
  1. patch vnode
  2. flushPostFlushCbs (这部分后续讲)
今天我们先研究一下 patch 打补丁的过程
// 记住调用 patch 时的入参 patch(container._vnode || null, vnode, container, null, null, null, isSVG)

patch
打补丁方法 : runtime-core -> renderer -> patch
const patch: PatchFn = ( n1, // 老 VNODE n2, // 新 VNODE container, // 挂载的节点 anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = false ) => { // 如果 老节点存在并且 n1 n2 的类型不同 // 卸载 老vnode if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null }// 从 新vnode 中获取 type , ref , shapeFlag // 先用 type 去匹配 // 如果 type 没匹配到 就用 shapeFlag 去匹配 const { type, ref, shapeFlag } = n2 // 目前我们的 vnode 中的 type 为 用户传入配置 + 挂载容器的innerHTML // 所以直接匹配到 shapeFlag 的条件 switch (type) { case Text: processText(n1, n2, container, anchor) break case Comment: processCommentNode(n1, n2, container, anchor) break case Static: if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } else if (__DEV__) { patchStaticNode(n1, n2, container, isSVG) } break case Fragment: processFragment( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) break default: if (shapeFlag & ShapeFlags.ELEMENT) { // match element processElement( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.COMPONENT) { // match component // 此时的 shapeFlag 会进入这个条件 processComponent( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.TELEPORT) { // match teleport // more } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { // match suspense // more } }// 处理 ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) } }

可以看到, patch 方法核心目标就是 比较 新老 vnode 节点 并对其进行差异的操作. 在初始化渲染时 , 会先 patch 我们的根组件 . 于是会走到 processComponent 这个逻辑分支, 看一下他具体的实现
processComponent
处理 component : runtime-core -> renderer -> processComponent
const processComponent = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { n2.slotScopeIds = slotScopeIds // 如果 老vnode 是空的 if (n1 == null) { // 如果 新vnode 是 KEPT_ALIVE if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { // more } else { // 挂载节点 mountComponent( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } } // 对比更新 else { updateComponent(n1, n2, optimized) } }

mountComponent
初始化渲染用 : runtime-core -> renderer -> mountComponent
const mountComponent: MountComponentFn = ( initialVNode, // 初始化的 vnode container, // 挂载目标 anchor, parentComponent, parentSuspense, isSVG, optimized ) => { // 创建组件实例 // createComponentInstance 初始化一个组件实例模型 const compatMountInstance = __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component const instance: ComponentInternalInstance = compatMountInstance || (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense ))// !!!!!!!!!!!!!!!!!!!!!!!!!!!! // 处理 prop slot 和 setup // 先分析 if (!(__COMPAT__ && compatMountInstance)) { setupComponent(instance) }// 后分析 setupRenderEffect( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) }

setupComponent
处理注册的 setup : runtime-core -> component -> setupComponent
vue3源码分析01-初始化渲染流程
文章图片

这个方法中初始化了插槽 和 props , 结束之后会先判断这个 component 是否是 stateful 的(根据 component 的 shapeFlag)
我们重点看后面, 他试图获取 setup 的执行结果, 从而通过调用 setupStatefulComponent 方法开始解析 setup
setupStatefulComponent
开始处理注册的 setup : runtime-core -> component -> setupStatefulComponent
function setupStatefulComponent( instance: ComponentInternalInstance, isSSR: boolean ) { // 此时 instance.type === 用户注册的 setup + 容器下面的 innerHTML const Component = instance.type as ComponentOptions// 0. 给实例添加一个缓存对象 instance.accessCache = Object.create(null) // 1. 呈现全局代理 instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers)) // 2.调用 setup 获得return的东西 const { setup } = Component // 解析 setup if (setup) { // 解析 setup 中的参数 // function.length 是获取 function 中参数的个数 // 如果用到了 setup 中的形参数量 > 1 的话 // 创建 setup 上下文 const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null)currentInstance = instance// 在这执行了 setup 拿到 setup 执行完的返回值 // 值得一提的是 // 在 setup 执行时 , 我们注册的 ref reactive 同时会被执行 // 比如在 const count = ref(0) 中 // 我们拿到的 count 已经是被 ref 包装过得数据了 const setupResult = callWithErrorHandling( setup, instance, ErrorCodes.SETUP_FUNCTION, [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext] )currentInstance = null// 如果返回 promise if (isPromise(setupResult)) { // 通过这个函数将 promise 结果保存到外部 const unsetInstance = () => { currentInstance = null } setupResult.then(unsetInstance, unsetInstance) } else { // 走入处理结果方法 handleSetupResult(instance, setupResult, isSSR) } } else { finishComponentSetup(instance, isSSR) } }

handleSetupResult
处理 setup 的执行结果 : runtime-core -> component -> handleSetupResult
export function handleSetupResult( instance: ComponentInternalInstance, setupResult: unknown, isSSR: boolean ) { // setup 中 返回用户自定义 render function if (isFunction(setupResult)) { // 如果当前运行时是 node if (__NODE_JS__ && (instance.type as ComponentOptions).__ssrInlineRender) { instance.ssrRender = setupResult } else { // 在实例上挂载 renderFunction instance.render = setupResult as InternalRenderFunction } } // composition api else if (isObject(setupResult)) { // 把结果添加到 实例的 setupState 中 instance.setupState = proxyRefs(setupResult) } // 如果没返回值 else if (__DEV__ && setupResult !== undefined) { // 警告 ... } // 结束处理 setup finishComponentSetup(instance, isSSR) }

finishComponentSetup
获取 render function && 兼容 vue2.x optionAPI : runtime-core -> component -> finishComponentSetup
export function finishComponentSetup( instance: ComponentInternalInstance, isSSR: boolean, skipOptions?: boolean ) { // Component = 用户注册配置 + 挂载目标下的innerHTML const Component = instance.type as ComponentOptions// 判断 runtime if (__NODE_JS__ && isSSR) { // more } // 如果实例没有 render 函数 // 即 // setup 没有 return function else if (!instance.render) { // 检查 用户如果没在 其他位置注册 render function if (compile && !Component.render) { // 拿到 Component.template // 即 // 容器下的 innerHTML const template = (__COMPAT__ && instance.vnode.props && instance.vnode.props['inline-template']) || Component.templateif (template) { // ...more const finalCompilerOptions: CompilerOptions = extend( // ...more ) // 执行 compile 方法 (compile 方法后续分析 compiler 的时候一起分析) // 获得 render function 挂载在component上 // 目标是通过执行 compile 方法 // 获取 render function Component.render = compile(template, finalCompilerOptions) } } // 拿到 render function 之后 // 给 整个实例挂载 render instance.render = (Component.render || NOOP) as InternalRenderFunction }// 开始兼容 2.x的API // 所以 setup 解析完成才开始兼容2.x的API // support for 2.x options if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) { currentInstance = instance applyOptions(instance) currentInstance = null } }

在这个方法中, 我们可以得到一个很重要的信息. setup 的处理时机在生命周期中是在非常非常靠前的位置. 为什么 vue3 可以兼容 vue2 的 optionAPI 呢, 是因为他先处理的 setup 之后又初始化其他 optionAPI. 因为调用时机早, 所以可以向下兼容(秒啊~)
兼容的过程 和 编译的过程后续会一起整理. 先明白整体运转过程, 再去扣细节.
当我们执行完这个方法后, 我们回到 mountComponent 这个方法. 他执行了 setupRenderEffect
setupRenderEffect
核心方法 , 依赖收集 , 初始化渲染 , 后续的更新 , 都是通过这个方法进行的 . : runtime-core -> renderer -> setupRenderEffect
const setupRenderEffect: SetupRenderEffectFn = ( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) => { // 添加 update 方法 // 值为 effect 的执行结果 // effect 在后续分析 reactive 包(响应式系统)的时候统一分析 // 暂且我们理解成 在初始化渲染时 他会收到一次通知 执行里面传入的 componentEffect 方法 instance.update = effect(function componentEffect() { // 初始化渲染 if (!instance.isMounted) { // ssr 相关 if (el && hydrateNode) { // more ... } else { // !!!!!!!!!!!!!!!! // 构建 subTree // subTree 是 Vnode // 在此时收集依赖 (后续讨论) const subTree = (instance.subTree = renderComponentRoot(instance)) // !!!!!!!!!!!!!!!! // 给 subTree 打补丁 patch( null, subTree, container, anchor, instance, parentSuspense, isSVG ) } // 更改挂载状态 instance.isMounted = true } // 更新 后续分析 else { // more ... } }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) }

我们可以看出, 这个方法想要构建 subTree , 之后给 subTree 打补丁. 我们来分析一下, 构建 subTree 的过程.
renderComponentRoot
构建 subTree : runtime-core -> componentRenderUtils -> renderComponentRoot
export function renderComponentRoot( instance: ComponentInternalInstance ): VNode { const { type: Component, vnode, proxy, withProxy, props, propsOptions: [propsOptions], slots, attrs, emit, render, // 通过 compile 获取的 render function renderCache, data, setupState, ctx, inheritAttrs } = instancelet resulttry { let fallthroughAttrs if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { // 这里调用了 先调用 render // 调用之后我们会拿到 容器下 节点的 vnode tree // 之后校验 vnode tree 是否是合法的 vnode // 如果拿到的是合法的 vnode 就赋值给 result // 在这里进行的依赖收集 result = normalizeVNode( render!.call( proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx ) ) fallthroughAttrs = attrs } else { // 如果是无状态组件 // more ... }let root = result let setRoot: ((root: VNode) => void) | undefined = undefinedif (vnode.dirs) { // 合并指令 root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs } if (vnode.transition) { // 继承 transition root.transition = vnode.transition }if (__DEV__ && setRoot) { setRoot(root) } else { result = root } } catch (err) { blockStack.length = 0 handleError(err, instance, ErrorCodes.RENDER_FUNCTION) result = createVNode(Comment) }return result }

可以看到这个方法把我们容器下的所有节点变成了 vnode .验证完 vnode 的合法性之后, 会合并和继承一些属性. 最终把 vnode 返回.
我们回到 setupRenderEffect 这个方法, subTree 的含义就是挂载容器的 vnodeTree , 我们要给这个 vnodeTree 打补丁
我们又回到了 patch 方法.
const patch: PatchFn = ( n1, // null n2, // subTree container, // 挂载的节点 anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = false ) => { // 如果 老节点存在并且 n1 n2 的类型不同 // 卸载 老vnode if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null } // 此时 // type 为子元素类型 我们这个例子 type 是 div // shapeFlag 依然是组件类型标记 const { type, ref, shapeFlag } = n2 switch (type) { case Text: // more ... break case Comment: // more ... break case Static: // more ... break case Fragment: // more ... break default: if (shapeFlag & ShapeFlags.ELEMENT) { // 此时我们会进入这个条件 processElement( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.COMPONENT) { // more } else if (shapeFlag & ShapeFlags.TELEPORT) { // match teleport // more } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { // match suspense // more } }// 处理 ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) } }

processElement
处理 element : runtime-core -> renderer -> processElement
const processElement = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { isSVG = isSVG || (n2.type as string) === 'svg' // 老节点不存在 直接挂载 if (n1 == null) { mountElement( n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } // 存在 对比更新 else { patchElement( n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) }

mountElement
处理子元素 插入到目标节点 : runtime-core -> renderer -> mountElement
const mountElement = ( vnode: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { let el: RendererElement let vnodeHook: VNodeHook | undefined | null const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode // 走这里 ! // 调用 hostCreateElement 创建真实 DOM el = vnode.el = hostCreateElement( vnode.type as string, isSVG, props && props.is, props ) // 子节点如果是 文本 if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 操作 dom 赋值 text hostSetElementText(el, vnode.children as string) } // 子节点如果是 数组 else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 挂载子节点 mountChildren( vnode.children as VNodeArrayChildren, el, // !!!! null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', slotScopeIds, optimized || !!vnode.dynamicChildren ) }// 如果当前元素有 props if (props) { // 遍历 props for (const key in props) { if (!isReservedProp(key)) { // 给元素打 属性补丁 hostPatchProp( el, key, null, props[key], isSVG, vnode.children as VNode[], parentComponent, parentSuspense, unmountChildren ) } } } // 将当前操作节点 插入到对应容器下 hostInsert(el, container, anchor) }

mountChildren
挂载子节点 : runtime-core -> renderer -> mountChildren
const mountChildren: MountChildrenFn = ( children, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, start = 0 ) => { for (let i = start; i < children.length; i++) { const child = (children[i] = optimized ? cloneIfMounted(children[i] as VNode) : normalizeVNode(children[i])) // 递归 patch 子元素 patch( null, child, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } }

这里我们可以看到一个细节. 他每次在 mount 的时候, 挂载的容器是根据父节点的 type 生成的真实 DOM. 在 mountChildren 结束后, 他会将对应的节点, 插入到父元素下. 由于是递归, 所以先分析的节点后被挂载. 当最外层节点被挂载时. 所有的节点就都被挂载了(秒啊~)
到这里, 初始化渲染的流程大致就已经结束了.
总结一下 初始化渲染流程 创建渲染器 --> 创建 app 实例 --> 调用 mount 方法 --> 创建 vnode (基于初始化时传入的 option) --> 调用 render --> patch --> processComponent --> mountComponent --> 调用 setup 获取他的结果 -> 调用 compile 获取容器下 innerHTML 的 render function -> 兼容 vue2.x 的 API --> setupRenderEffect --> 构建 subTree --> processElement --> mountElement --> 递归 patch 子元素 --> 添加 props --> 插入到对应父节点下
总体来说这个过程还是比较复杂的. 如果过程中哪里存在争议, 望大家在评论区指出. 我会及时更新文章. 同时码字不易, 望多多点赞关注支持 ~
目前需要补充的分析
  1. compile
  2. 向下兼容细节
  3. reactive 相关
  4. 更新相关

    推荐阅读