vue3 源码解析(4)— createApp 源码的实现

前言

本文是 vue3 源码分析系列的第四篇文章,在使用 vue3 时,我们需要使用 createApp 来创建一个应用实例,然后使用 mount 方法将应用挂载到某个DOM节点上。那么在调用 createApp 时,vue 再背后做了些什么事情呢?在这篇文章中,我们将深入探讨 createApp 的实现原理,并通过源码分析来理解其工作机制。

createApp 的基本用法

我们先来看一下 createApp 的基本使用方式:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createApptitle>
head>
<body>
<div id="app">div>
<script src="../packages/runtime-dom/dist/runtime-dom.global.js">script>
<script>
  const { createApp, h, reactive } = VueRuntimeDom;
  const App = {
    setup (props, context) {
      const state = reactive({ name: "11" });
      return {
        state
      };
    },
    render(proxy) {
      return h("div", {}, proxy.state.name);
    },
  };
  createApp(App, {}).mount("#app");
script>
body>
html>

在上面的例子中, 我们从 vue 包中导出 createApp 方法,其中一个参数是组件配置对象,返回一个应用实例,然后调用 mount 方法将应用挂载到某个DOM节点上。我们先从入口函数 createApp 出发。

createApp

// 合并传入的参数
const renderOptionDom = extend({ patchProp }, nodeOptions)

const createApp = (rootComponent, rootProps) => {
  // 使用createRenderer函数创建一个渲染器实例,并调用其createApp方法来创建一个应用实例  
  // 这个方法通常用于创建应用实例,可以兼容不同的平台  
  const app = createRenderer(renderOptionDom).createApp(rootComponent, rootProps)
  const { mount } = app // render(vnode, container)
  // 定义一个名为mount的新方法
  app.mount = (selector) => {
    // 使用querySelector方法根据传入的selector选择器查找对应的DOM容器元素 
    const container = nodeOptions.querySelector(selector)
    container.innerHTML = ''
    // 调用mount方法,将容器作为参数传入,用于挂载应用实例到该容器上 
    mount(container)
  }
  return app
}

在源码中,我们直接调用了 createRenderer 方法,这个方法是创建渲染器的方法。
其中 renderOptionDom 是一些渲染器的配置,主要的作用是用来操作DOM的。先简单的来认识一下 renderOptionDom,这个里面的方法后面会用到。

// 定义一个对象包含了一些与DOM节点相关的功能方法  
const nodeOptions = {
  createElement: tag => document.createElement(tag),
  // 将一个子节点插入到父节点中的指定位置(锚点)的方法
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null);
  },
  querySelector: selector => document.querySelector(selector),
  createText: text => document.createTextNode(text),
}

接下来看下 createRenderer 方式的实现。

createRenderer

const createRenderer = (options) => {
  // 这个函数是用于渲染虚拟DOM,其中vnode表示虚拟节点,container表示渲染的目标容器
  // render函数的目的是解耦平台,使其能够兼容不同的平台或框架
  let render = (vnode, container) => {
    // 首次渲染时
    // null 上一次渲染的 vnode
    // vnode 当前需要渲染的 vnode
    // container 挂载的容器
    patch(null, vnode, container)
  }
  return {
    createApp: createAppAPI(render)
  }
}

createRenderer 内部返回 createApp 这个方法。而 createApp 方法的是通过 createAppAPI 方法创建的,所以我们还需要看一下 createAppAPI 方法的实现。

createAppAPI

const createAppAPI = (render) => {
  // 返回一个函数,主要是通过闭包来缓存上面传入的参数
  return function createApp (rootComponent, rootProps) {
    // 创建 app 对象
    const app = {
      // rootComponent 是我们传入的根组件
      _component: rootComponent,
      // rootProps 是我们传入的根组件的 props,这个参数必须是一个对象
      _props: rootProps,
      // 挂载的 DOM 节点
      _container: null,
      // 添加相关的事件
      mount (container) {
        // ...
      },
      use (plugin, ...options) {
        // ...
      },
      mixin(mixin) {
        // ...
      },
      component(name, component) {
        // ...
      },
      directive(name, directive) {
        // ...
      }
    }
    // 返回 app 对象
    return app
  }
}

看到这里,我们可以知道,createApp 方法的实现其实就是 createAppAPI 方法中返回的一个函数。在返回的函数中返回了一堆对象,这里可以看到我们常用的 use、mixin、component、directive、mount 等方法都是在 app 对象上的。这些对象就是我们在使用 createApp 方法时,可以调用的方法。

在入口函数 createApp 中,我们已经了解到我们在调用 createApp 方法时,会返回一个 app 对象,这个对象上有一个 mount 方法,我们需要通过这个方法来挂载我们的根组件。详细看下 mount 方法时如何实现的。

mount

// container 挂载的容器
mount (container) {
  // 创建虚拟dom
  const vnode = createVNode(rootComponent, rootProps)
  // render 函数是在 createRenderer 中定义,传递到 createAppAPI 中,通过闭包缓存下来的
  // 通过传入的自定义渲染函数进行渲染
  render(vnode, container)
  // 设置 app 实例的 _container 属性,指向挂载的容器
  app._container = container
}

这段代码定义了一个 mount 函数,用于将一个应用实例挂载到指定的容器上。首先创建一个虚拟节点,然后使用自定义的渲染函数将应用渲染到容器中,并设置应用实例的_container属性为挂载的容器。

createVNode

虚拟节点其实就是一个 js 对象,包含了 dom 的一些属性,比如 tag、props、children 等等。虚拟节点,大概信息如下:

const createVNode = (type, props, children = null) => {
  const vnode = {
    // 添加一个特殊的属性__v_isVNode,用于标记这个对象是一个虚拟节点
    __v_isVNode: true,
    // 设置虚拟节点的类型
    type,
    // 设置虚拟节点的属性。这些属性通常用于表示元素的属性和样式。  
    props,
    // 设置虚拟节点的子节点。这些子节点可以是另一个虚拟节点,也可以是实际的数据或组件
    children,
    // 为虚拟节点设置一个 key 属性。key 用于优化虚拟DOM的diff算法,帮助识别哪些节点发生了变化
    key: props && props.key, // diff 算法
    // 初始化虚拟节点的 el 属性为 null,表示这个虚拟节点还没有与实际的DOM节点对应起来
    el: null,
    // 初始化虚拟节点的属性为一个空对象,用于存储与该虚拟节点关联的组件实例。
    component: {}, // 组件实例
    // 用于标记虚拟节点的形状或类型
    shapeFlag
  }
  // 返回创建的虚拟节点对象
  return vnode
}

这里就只贴了部分 VNode 的相关定义,这里只是做一个简单的概念介绍。

render

render 函数是在 createRenderer 中定义的。这里可以通过传入的自定义的 render 渲染函数进行不同平台的渲染。具体源码如下:

let render = (vnode, container) => {
  // 将虚拟节点渲染到容器中
  patch(null, vnode, container)
}

patch

patch 函数的主要作用就是将虚拟节点渲染到容器中。由于 patch 函数内部的实现会牵扯到非常多的内容,这里只是大致的了解下原理即可。

/**
 * 
 * @param n1 上一次渲染的 vnode
 * @param n2 当前需要渲染的 vnode
 * @param container 容器
 * @param anchor 锚点, 用来标记插入的位置
 * @returns 
 */
const patch = (n1, n2, container, anchor = null) => {
  // n1 和 n2 是否相同
  if (n1 === n2) {
    return
  }
  // n1 是否存在且与 n2 的类型是否一致
  if (n1 && !isSameVNodeType(n1, n2)) {
    unmount(n1) // 删除元素
    n1 = null // 删除之后重新加载
  }

  const { shapeFlag, type } = n2
  if (type === Text) { // 文本
    console.log('文本')
    // 处理文本节点
    processText(n1, n2, container)
  } else if (shapeFlag & ShapeFlags.ELEMENT) { // 元素
    console.log('元素')
    // 处理元素节点
    processElement(n1, n2, container, anchor)
  } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { // 组件
    console.log('组件')
    // 处理组件节点  
    processComponent(n1, n2, container)
  }
}

这段代码通过判断虚拟节点的类型(文本、元素或组件),来决定如何更新虚拟DOM。通常我们在使用 createApp 的时候,通常会传入一个根组件,这个根组件就会走到 processComponent 函数中。

processComponent

const processComponent = (n1, n2, container) => {
  // n1 为 null 说明这是首次挂载组件首次挂载
  if (n1 === null) {
    // 挂载组件到容器上
    mountComponent(n2, container)
  } else {
    // 更新组件节点 
    updateComponent(n1, n2, container)
  }
}

processComponent 函数做了两件事,一个是挂载组件,一个是更新组件。

const mountComponent = (initialVNode, container, anchor) => {
  // 通过调用组件的 render 方法,获取组件的 vnode
  const subTree = initialVNode.type.render.call(null)
  // 直接调用 patch 函数,将 subTree 渲染到指定的容器和锚点上
  patch(null, subTree, container, anchor);)
}

总结

我们通过阅读源码了解到,createApp 函数是 vue3 的入口函数,通过 createApp 函数我们可以创建一个应用。

createApp 的实现是借助了 createRenderer 函数,createRenderer 的实现内部包装了createAppAPI。

createApp 函数接收一个组件,然后返回一个应用,这个应用中有一个 mount 方法,这个 mount 方法就是用来将应用挂载到容器中的。

在 createApp 中重写了 mount 方法,内部的实现是通过调用渲染器的 mount 方法。

这个 mount 方法是在 createAppAPI 的内部函数 createApp 中实现的,createApp 函数中的 mount 方法会调用 patch 函数。

patch 函数内部会做很多的事情,虽然我们这里只是调用 mountComponent 实现了挂载的逻辑。

以上就是调用 createApp 时 vue 工作过程原理的详细内容。

你可能感兴趣的:(vue.js,javascript,前端,前端框架,源码分析)