vue3源码分析(二)—— 初始化流程

系列文章目录

  1. 目录分析
  2. 初始化流程
  3. 响应式系统
  4. shared工具函数



前言

vue3项目,需要通过createApp()函数创建一个vue实例,本文将从该方法入口分析~


一、createApp在项目中的使用

在vue3中,每个 Vue 应用都是通过用 createApp 函数创建一个新的应用实例开始的:
(取代了Vue2中 new Vue(options) 方式)

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
  • 这里的App是根组件,作为渲染组件的起点
  • mount('#app') 表示要被挂载的DOM节点

二、createApp源码追溯

vue3支持多平台的,不同的平台createApp都有各自的定义,下面以常见的浏览器环境为例讲解~
如下(packages/runtime-dom/src/index.ts):

export const createApp = ((...args) => {
  // 第一步:创建了app实例,ensureRenderer方法意义在于确保有渲染器
  const app = ensureRenderer().createApp(...args)
  
  // 第二步:重写了app.mount方法
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    ...
  }
  //第三步:返回app实例
  return app
})

这个入口API createApp方法,就三个步骤

  • 第一步,创建app实例
  • 第二步,重新app的mount方法
  • 第三步,返回app

1.创建app实例

下面分析:const app = ensureRenderer().createApp(...args) 这行代码是如何实现的?

1.1 ensureRenderer

// packages/runtime-dom/src/index.ts
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
  • 这个方法称之为“惰性 创建 renderer”——这里是为了只有执行createApp时,才给renderer渲染器赋值(renderer = createRenderer(rendererOptions)),也是优化的一点。
  • createRenderer是在runtime-core模块定义的,是【通用】【核心】的【创建渲染器】的方法。
  • 不同平台的具体渲染API是不一样的,根据传入的rendererOptions,就可以体会对vue3多平台的支持

看看rendererOptions具体传入的什么?

const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)
  • patchPropforcePatchProp 传给runtime-core的render,用于更新DOM节点prop
  • nodeOps主要是利用dom相关API封装了render渲染器需要的方法,传给runtime-core的render,用于更新DOM节点创建、插入、移除…等节点操作
// nodeOps
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },

  remove: child => {
    const parent = child.parentNode
    if (parent) {
      parent.removeChild(child)
    }
  },

  createElement: (tag, isSVG, is, props): Element => {
    const el = isSVG
      ? doc.createElementNS(svgNS, tag)
      : doc.createElement(tag, is ? { is } : undefined)
     ...
	return el
  },

  createText: text => doc.createTextNode(text),
  ...  
}
  • nodeOps主要是利用dom API封装了render渲染器需要的方法,例如用DOM insertBefore封装了 insert 方法,用removeChild 封装了 remove方法……

看看createRenderer<Node, Element>(rendererOptions)?
createRenderer方法在runtime-core中实现,各平台通用的创建渲染器render方法

export function createRenderer< HostNode = RendererNode, HostElement = RendererElement>(options: RendererOptions<HostNode, HostElement>) {
  // baseCreateRenderer是为跨平台设计的
  return baseCreateRenderer<HostNode, HostElement>(options)
}

baseCreateRenderer:方法实现基本是三个部分,如下

  • 第一步:各平台传入的options中的API方法 重新统一命名
  • 第二步:定义各种渲染需要的方法(patch,mountComponent,render…)
  • 第三步:返回一个含有 {render, hydrate, createApp} 属性的对象
function baseCreateRenderer(
  options: RendererOptions,  //跨平台设计,不同平台传入不同的options
  createHydrationFns?: typeof createHydrationFunctions
) {
  
  // 第一步:各平台传入的options中的API方法 重新统一命名
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    ...
  } = options

  // 第二步:定义各种渲染需要的方法
  const patch = (n1, n2,container...)=>{...}
  const render = (vnode, container, isSVG) => {...}
  const mountComponent = (initialVNode, container...) => {...}
  ....
  
  // 第三步:返回一个含有 {render, hydrate, createApp} 属性的对象
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

通过以上分析:
ensureRenderer()得到一个带有 {render, hydrate, createApp} 渲染器属性的对象

1.2 ensureRenderer().createApp(…args)

由上可知,ensureRenderer()中返回的对象含有createApp: createAppAPI(render, hydrate)方法,所以 ensureRenderer().createApp(…args)才可以正常调用,那继续分析:

createAppAPI(render, hydrate) 如下:

  • 通过闭包返回一个createApp方法,并把 render 方法保留下来供内部来使用,这个createApp方法就是我们在项目中使用到的Vue.createApp(App)
  • createApp内部定义一个app实例,包含_uid、_component、_props、_container、_context、version、config属性和use、mixin、component、directive、mount、unmount、provide全局方法
export function createAppAPI<HostElement>(
  render: RootRenderFunction,  // 在baseCreateRenderer中传入的render
  hydrate?: RootHydrateFunction
){  // 闭包
  return function createApp(rootComponent, rootProps = null) {
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }

    const context = createAppContext()
    const installedPlugins = new Set()

    let isMounted = false
    // 创建了app实例,并返回
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      version,
      get config() {return context.config},
      set config(v) {},

      use(plugin: Plugin, ...options: any[]) {...},
	  mixin(mixin: ComponentOptions) {...},
      component(name: string, component?: Component): any {...},
      directive(name: string, directive?: Directive) {...},
      mount(rootContainer: HostElement,isHydrate?: boolean,isSVG?: boolean): any {...},
      unmount() {...},
      provide(key, value) {...}
    })
    
    return app
  }
}

在应用项目中,实际打印app可得:

const app = Vue.createApp(MyComponent);
console.log('app', app);

createApp方法代码中定义的app是一致的:

那么通过以上分析,可得知const app = ensureRenderer().createApp(...args)的实现原理。

2. 重写app.mount方法

2.1 createAppAPI 中的mount

在上面createAppAPI创建app实例方法中,定义了mount:

      mount(rootContainer: HostElement,isHydrate?: boolean,isSVG?: boolean): any {
        if (!isMounted) {
          // 第一步:创建 root vnode
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )

          if (isHydrate && hydrate) {  // 服务端渲染相关
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            render(vnode, rootContainer, isSVG)
          }
          isMounted = true

          return vnode.component!.proxy
        }
      },
  • 应用实例app的mount首次挂载isMounted为false
  • 首先通过createVNode(rootComponent,rootProps)来创建vnode节点
  • 然后在非服务端渲染下通过render(cloneVNode(vnode), rootContainer, isSVG)渲染器将vnode节点转为真正的DOM节点,实现挂载
  • isMounted最终置为true

2.2 createApp 中重定义的mount

export const createApp = ((...args) => {
   ...
   
  // 缓存已有的mount方法
  const { mount } = app
  // 重写mount
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 处理containerOrSelector,获取真实的DOM元素
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    
    // 获取定义的 Vue app 对象, 之前的 rootComponent
    const component = app._component
    // 如果不是函数、没有 render 方法、没有 template 使用 DOM 元素内的 innerHTML 作为内容
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    
    // 挂载之前清空内容
    container.innerHTML = ''
    // 真正的挂载,调用上面缓存原定义的mount方法
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }
  return app
 }
  • 首先缓存了之前定义好的mount挂载方法:const { mount } = app
  • 又重新定义了app.mount,其实是对之前的mount做了一层包装
  • 处理 containerOrSelector ,如果传的是字符串选择器,通过 document.querySelector 方法得到与之对应的DOM元素。(通常我们应用中是这样使用的:mount('#app')
  • const proxy = mount(container, false, container instanceof SVGElement) 调用上面缓存的app实例的mount方法,mount其实也是执行了render进行真正的渲染挂载。
  • 去除 v-cloak 属性。v-cloak这个指令保持在元素上直到关联组件实例结束编译

通过以上分析,可知createApp(App).mount('#app')的实现原理~


总结

我们从以上分析可以得出:

  • creatApp API创造了一个app实例,创造过程中根据不同平台创造了渲染器render,并提供给内部使用。
  • vue3支持跨平台渲染,核心创建渲染器baseCreateRenderer方法是抽离在runtime-core中。
  • mount过程实际就是执行了render

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值