Skip to content

Latest commit

 

History

History
292 lines (239 loc) · 7.85 KB

16. 组件的初始化流程.md

File metadata and controls

292 lines (239 loc) · 7.85 KB

组件的初始化流程

在本小节呢,我们将会去探寻一个组件的初始化流程。

我们先放一下初始化的流程图(来源于崔大)

init

1. happy path

我们可以写一个 happy path,来看看一个组件是如何实现 mount 的。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./main.js" type="module"></script>
  </body>
</html>
// main.js
import App from './App.js'
// 和 Vue3 的 API 命名方式一样
createApp(App).mount('#app')
// App.js

export default {
  render() {
    return h('div', 'hi ' + this.title)
  },
  setup() {
    return {
      title: 'mini-vue',
    }
  },
}

通过这个 happy path,我们就可以一窥 Vue3 的 App 初始化流程了,接下来让我们深究一下

2. createApp

首先,我们可以看到,最入口处其实就是 createApp,它接收一个 rootComponent,并内部包含一个 mount 方法,接受一个 rootContainer

下面我们就可以写了

我们目前写的是 runtime-core 的逻辑

// createApp.ts

export function createApp(rootComponent) {
  return {
    mount(rootContainer) {
      // 在 vue3 中,会将 rootComponent 转为一个虚拟节点 VNode
      // 后续所有的操作都会基于虚拟节点
      // 这里就调用了一个 createVNode 的 API 将 rootComponent 转换为一个虚拟节点
      // 【注意了】这个虚拟节点就是程序的入口,所有子节点递归处理
      const vnode = createVNode(rootComponent)
      // 调用 render 来渲染虚拟节点,第二个参数是容器【container】
      render(vnode, document.querySelector(rootContainer))
    },
  }
}

2.1 createVNode

// vnode.ts
export function createVNode(type, props?, children?) {
  // 这里先直接返回一个 VNode 结构,props、children 非必填
  return {
    type,
    props,
    children,
  }
}

2.2 render

// render.ts
export function render(vnode, container) {
  // 这里的 render 调用 patch 方法,方便对于子节点进行递归处理
  patch(vnode, container)
}

2.3 patch

export function patch(vnode, container) {
  // 去处理组件,在脑图中我们可以第一步是先判断 vnode 的类型
  // 这里先只处理 component 类型
  processComponent(vnode, container)
}

3. processComponent

export function processComponent(vnode, container) {
  mountComponent(vnode, container)
}

3.1 mountComponent

function mountComponent(vnode, container) {
  // 通过 vnode 获取组件实例
  const instance = createComponentInstance(vnode)
  // setup component
  setupComponent(instance, container)
  // setupRenderEffect
  setupRenderEffect(instance, container)
}

3.2 createComponentInstance

export function createComponentInstance(vnode) {
  // 这里返回一个 component 结构的数据
  const component = {
    vnode
  }
  return component
}

3.3 setupComponent

export function setupComponent(instance, container) {
  // 初始化分为三个阶段
  // TODO initProps()
  // TODO initSlots()
  // 处理 setup 的返回值
  // 这个函数的意思是初始化一个有状态的 setup,这是因为在 vue3 中还有函数式组件
  // 函数式组件没有状态
  setupStatefulComponent(instance, container)
}

3.4 setupStatefulComponent处理 setup、以及挂载 component 实例

function setupStatefulComponent(instance, container) {
  // 这个函数的处理流程其实非常简单,只需要调用 setup() 获取到返回值就可以了
  // 那么第一步我们就是要获取用户自定义的 setup
  // 通过对初始化的逻辑进行梳理后我们发现,在 createVNode() 函数中将 rootComponent 挂载到了 vNode.type
  // 而 vNode 又通过 instance 挂载到的 instance.vnode 中
  // 所以就可以通过这里传入的 instance.vnode.type 获取到用户定义的 rootComponent
  const component = instance.vnode.type
  // 拿到 component 我们就可以拿到 setup 函数
  const { setup } = component
  // 这里需要判断一下,因为用户是不一定会写 setup 的,所以我们要给其一个默认值
  if (setup) {
    // 获取到 setup() 的返回值,这里有两种情况,如果返回的是 function,那么这个 function 将会作为组件的 render
    // 反之就是 setupState,将其注入到上下文中
    const setupResult = setup()
    handleSetupResult(instance, setupResult)
  }
}

3.5 handleSetupResult

function handleSetupResult(instance, setupResult) {
  // TODO function
  // 这里先处理 Object 的情况
  if (typeof setupResult === 'object') {
    // 如果是 object ,就挂载到实例上
    instance.setupState = setupResult
  }
  // 最后一步,调用初始化结束函数
  finishComponentSetup(instance)
}

3.6 finishComponentSetup

function finishComponentSetup(instance) {
  // 这里为了获取 component 方便,我们可以在 instance 上加一个 type 属性
  // 指向 vnode.type
  const component = instance.type
  // 如果 instance.render 没有的话,我们就让 component.render 赋给 instance.render
  // 而没有 component.render 咋办捏,其实可以通过编译器来自动生成一个 render 函数
  // 这里先不写
  if (!instance.render) {
    instance.render = component.render
  }
}

3.7 setupRenderEffect

mountElement 中,我们还调用了 setupRenderEffect

function setupRenderEffect(instance, container) {
  // 调用 render 和 patch 挂载 component
  const subTree = instance.render()
  // 下面就是 mountElement 了
  patch(subTree, container)
}

4. mountElement

下面我们就可以来写一下 mountElement 的逻辑了,我们知道其实一个组件从初始化到挂载到 DOM 中是有调用了两次 patch 的,第一次是 vnode ,第二次应该是 element 了,所以我们要在 patch 逻辑中加入 processElement 的逻辑

export function patch(vnode, container) {
  // 去处理组件,在脑图中我们可以第一步是先判断 vnode 的类型
  // 如果是 element 就去处理 element 的逻辑
  // 因为两次的 vnode.type 的值不一样,所以我们就可以根据这个来进行判断了
  if (typeof vnode.type === 'string') {
    processElement(vnode, container)
  } else if (isObject(vnode.type)) {
    processComponent(vnode, container)
  }
}

4.1 processElement

function processElement(vnode, container) {
  // 分为 init 和 update 两种,这里先写 init
  mountElement(vnode, container)
}

4.2 mountElement

function mountElement(vnode, container) {
  // 此函数就是用来将 vnode -> domEl 的
  const { type: domElType, props, children } = vnode
  // 创建 dom
  const domEl = document.createElement(domElType)
  // 加入 attribute
  for (const prop in props) {
    domEl.setAttribute(prop, props[prop])
  }
  // 这里需要判断children两种情况,string or array
  if (typeof children === 'string') {
    domEl.textContent = children
  } else if (Array.isArray(children)) {
    // 如果是 array 就递归调用,并将自己作为 container
    mountChildren(vnode, domEl)
  }
  // 最后将 domEl 加入 dom 树中
  container.appendChild(domEl)
}

4.3 mountChildren

function mountChildren(vnode, container) {
  vnode.children.forEach(vnode => {
    // 如果 children 是一个 array,就递归 patch
    patch(vnode, container)
  })
}

5. 初始化结束

这样一个初始化的基本流程就结束了,顺着本篇文章,你就会捋清楚,初始化的主要工作就是初始化 component 实例,处理 setup、props、slots,以及挂载组件(UI)。