Vue3源码阅读--创建一个根组件,内部做了哪些事情

Vue3源码阅读(一)

这里笔者以最新的3.4.27版本为例,记录一下Vue3的源码阅读。这篇文章会先梳理一个Vue项目在创建时内部工作的大体脉络。

一般来说,我们通过这样的方式来创建一个Vue3应用。

ts 复制代码
<template>
    <div id="app"></div>
</template>

import { createApp } from 'vue'
const app = createApp({
  /* 根组件选项 */
})
app.mount('#app')

可以看到runtime-dom下的index.ts,注释掉一些与主流程无关的代码。

ts 复制代码
export const createApp = ((...args) => {
  // 创建App对象实例
  const app = ensureRenderer().createApp(...args)

  const { mount } = app
  // 重写mount方法
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // ...
  }
  return app
}) as CreateAppFunction<Element>

这里主要做了两个事情:

  1. 创建了一个实例对象app
  2. 重写mount方法

创建Vue实例对象app

其中,ensureRenderer函数最终返回了一个对象

ts 复制代码
return {
  render,// 渲染函数,核心
  hydrate,
  createApp: createAppAPI(render, hydrate),// 创建app实例方法
}

定位到createAppAPI函数

ts 复制代码
export function createAppAPI (render,hydrate?) {
  return function createApp(rootComponent, rootProps = null) {
    if (!isFunction(rootComponent)) {
      rootComponent = extend({}, rootComponent)
    }
    
    // 创建上下文对象
    const context = createAppContext()
    const installedPlugins = new WeakSet()

    let isMounted = false
    
    // 初始化app对象并挂到context.app下
    const app: App = (context.app = {
      _uid: uid++, // 绑定唯一id
      _component: rootComponent as ConcreteComponent,// 根组件
      _props: rootProps, // 根属性
      _container: null,
      _context: context,
      _instance: null,
      version,
      
      // ...初始化 use、mixin、component、directive、mount、unmount、provide
    })
  
    return app
  }
}

重写mount方法

在初始化的时候,内部还重写了app的mount方法,代码如下

ts 复制代码
 const { mount } = app
 // 重写mount方法
 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, resolveRootNamespace(container))
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
 }
 // 实例内部的mount方法
 mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  namespace?: boolean | ElementNamespace,
): any {
  const vnode = createVNode(rootComponent, rootProps)
  vnode.appContext = context

  if (namespace === true) {
    namespace = 'svg'
  } else if (namespace === false) {
    namespace = undefined
  }

  // 调用Renderer返回的渲染函数
  render(vnode, rootContainer, namespace)

  isMounted = true
  app._container = rootContainer
  // for devtools and telemetry
  ;(rootContainer as any).__vue_app__ = app

  return getExposeProxy(vnode.component!) || vnode.component!.proxy
}

这一步主要做了两个事情

  1. 调用createVNode生成vnode对象;
  2. 调用render函数,将vnode渲染成真实dom挂载到根节点;

生成vnode对象

ts 复制代码
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false,
): VNode {
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
      if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
        currentBlock[currentBlock.indexOf(type)] = cloned
      } else {
        currentBlock.push(cloned)
      }
    }
    cloned.patchFlag |= PatchFlags.BAIL
    return cloned
  }

  if (isClassComponent(type)) {
    type = type.__vccOpts
  }

  // 初始化style和class属性
  if (props) {
    props = guardReactiveProps(props)!
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass)
    }
    if (isObject(style)) {
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }

  // 设置vnode类型,以根节点为例,shapeFlag为4
  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
            
  const vnode = {...} as VNode // 初始化一系列属性
  // 处理子集,若子集存在,创建vnode对象挂载到vnode.children上。并修改到对应的shapeFlag
  normalizeChildren(vnode, children)

  return vnode
}

这一步做了哪些事情呢?

  1. 处理当前已是vnode对象的情况,先拷贝,再生成children的vnode
  2. 初始化style和class属性
  3. 定义shapeFlag
  4. 创建基础VNode对象
  5. 根据子集情况,为子集创建vnode对象,并挂到当前vnode.children上。例如根组件执行时是没有children的

render函数

ts 复制代码
const render: RootRenderFunction = (vnode, container, namespace) => {
  // 若新vnode为null
  if (vnode == null) {
    // 旧vnode存在,卸载组件
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else { // 新vnode存在,执行patch
    patch(
      container._vnode || null,
      vnode,
      container,
      null,
      null,
      null,
      namespace,
    )
  }
}
ts 复制代码
const patch: PatchFn = (
  n1,
  n2,
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  namespace = undefined,
  slotScopeIds = null,
  optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
) => {
  ... // 经过判断,分情况处理
  // 根组件进入了下面这个判断
  else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
        )
  } 

  // set ref
  if (ref != null && parentComponent) {
    setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
  }
}

render函数内部做了什么事情呢?

  1. 调用patch函数
  2. patch 函数通过判断vnode类型作出处理,如shapeFlag为4时调用processComponent函数处理
  3. 初始化时旧节点为null,所以调用mountElement 函数,否则调用patchElement函数
  4. mountElement 函数内部调用了setupComponent 函数,initProps、solts等属性,完成后继续调用setupRenderEffect函数
  5. 内部创建ReactiveEffect 实例对象effect,并绑定上componentUpdateFn 函数,随后调用update函数渲染出html代码,调用hostInsert插入到父节点中
  6. 如监测到数据更新,会继续触发update函数更新dom

至此,完成了根组件项目的初始化。由于这里逻辑非常多,笔者会另外解析。如有理解错误的地方,欢迎大佬指正啊。

总结

在Vue.createApp函数调用后,内部通过渲染器来创建一个app实例对象,随后生成上下文,重写mount方法等完成app的初始化。

在执行app.mount函数后,第一步创建vnode对象,然后调用render函数、patch函数将vnode更新成真实的dom渲染到页面上。

相关推荐
范特西是只猫15 分钟前
echarts 自定义标注样式&自定义tooltip弹窗样式
前端·javascript·echarts
JohnsonXin32 分钟前
【兼容性记录】video标签在 IOS 和 安卓中的问题
android·前端·css·ios·h5·兼容性
建群新人小猿38 分钟前
CRMEB Pro版 DIY功能玩法即将升级,先来一睹为快!
前端·javascript·html
api772 小时前
1688商品详情API返回值中的售后保障与服务信息
java·服务器·前端·javascript·python·spring·pygame
赵广陆2 小时前
SprinBoot+Vue门诊管理系统的设计与实现
前端·javascript·vue.js·spring boot·maven
华山令狐虫2 小时前
el-tabs 样式修改
前端
史努比的大头4 小时前
前端开发深入了解webpack
前端
Dovir多多5 小时前
渗透测试入门学习——php与mysql数据库连接、使用session完成简单的用户注册、登录
前端·数据库·后端·mysql·安全·html·php
B.-5 小时前
Remix 学习 - @remix-run/react 中主要的 hooks
前端·javascript·学习·react.js·web
计算机学姐5 小时前
基于微信小程序的食堂点餐预约管理系统
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis