组件挂载

组件挂载

使用方式:

javascript
import { h, ref, createApp } from '../dist/vue.esm.js'
const Comp = {
  render() {
    return h('div', 'hello world')
  }
}
createApp(Comp).mount('#app')

我们需要先处理一下 createVNode 中的组件类型

javascript
// vnode.ts

export function createVNode(type, props?, children = null) {
  let shapeFlag = 0

  if (isString(type)) {
    // div span p h1
    shapeFlag = ShapeFlags.ELEMENT
  } else if (isObject(type)) {
    // type 是 一个对象,表示是一个组件
    // 💡 有状态的组件
    shapeFlag = ShapeFlags.STATEFUL_COMPONENT
  }
  const vnode = {

  }

  return vnode
}

组件和元素、文本一样,都存在两种情况,挂载和更新:

javascript
const patch = (n1, n2, container, anchor = null) => {
  const { shapeFlag, type } = n2

  switch (type) {
    case Text:
      processText(n1, n2, container, anchor)
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        // 处理 dom 元素 div span p h1
        processElement(n1, n2, container, anchor)
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // 💡 处理组件的逻辑
        processComponent(n1, n2, container, anchor)
      }
  }
}

创建一个 processComponent 函数来处理组件的挂载和更新:

  • processComponent
javascript
/**
 * 处理组件的挂载和更新
 */
const processComponent = (n1, n2, container, anchor) => {
  if (n1 == null) {
    // 挂载
    // 💡 在这里,我们创建一个 mountComponent 函数,来完成挂载
    mountComponent(n2, container, anchor)
  } else {
    // 更新
  }
}
  • mountComponent
javascript
const mountComponent = (vnode, container, anchor) => {
  const { type } = vnode
  /**
   * 1. 创建组件实例
   * 2. 初始化组件的状态
   * 3. 将组件挂载到页面中
   */
  // 创建组件实例
  const instance = {
    // 组件类型
    type: vnode.type,
    // 组件的虚拟节点
    vnode,
    // 组件的 props
    props: {},
    // 组件的 attrs
    attrs: {},
    // 组件是否挂载
    isMounted: false,
    // 组件的子树,就是 render 函数的返回值
    subTree: null
  }

  const componentUpdateFn = () => {
    /**
     * 区分挂载和更新
     */
    if (!instance.isMounted) {
      // 调用 render 拿到 subTree
      const subTree = type.render()
      // 将 subTree 挂载到页面
      patch(null, subTree, container, anchor)
      // 保存子树
      instance.subTree = subTree
      // 挂载完了
      instance.isMounted = true
    } else {
      // 响应式数据变化产生的更新逻辑
      const prevSubTree = instance.subTree
      // 调用 render 拿到 subTree,this 指向 setupState
      const subTree = type.render()
      // 将 subTree 挂载到页面
      patch(prevSubTree, subTree, container, anchor)
      // 保存这一次的 subTree
      instance.subTree = subTree
    }
  }

  // 创建 effect
  const effect = new ReactiveEffect(componentUpdateFn)
  effect.run()
}

这样就可以成功挂载了,这里用到了 ReactiveEffect,因为我们需要在数据变化时,重新执行 componentUpdateFn 函数。 这样写下去比较乱,我们拆分一下逻辑,将创建组件实例的逻辑拆分出来,还有初始化状态,我们需要执行 setup 函数,拿到它返回的状态 packages/runtime-core/src/component.ts

javascript
// component.ts
/**
 * 创建组件实例
 */
export function createComponentInstance(vnode) {
  const { type } = vnode
  const instance = {
    type,
    vnode,
    // 渲染函数
    render: null,
    // setup 返回的状态
    setupState: null,
    props: {},
    attrs: {},
    // 子树,就是 render 的返回值
    subTree: null,
    // 是否已经挂载
    isMounted: false
  }

  return instance
}

/**
 * 初始化组件
 */
export function setupComponent(instance) {
  const { type } = instance
  // 使用 proxyRefs 处理 setup 返回值,就不需要 .value 了
  const setupResult = proxyRefs(type.setup())
  // 拿到 setup 返回的状态
  instance.setupState = setupResult
  // 将 render 函数,绑定给 instance
  instance.render = type.render
}

来看一下修改后的 mountComponent 函数:

javascript
const mountComponent = (vnode, container, anchor) => {
  /**
   * 1. 创建组件实例
   * 2. 初始化组件的状态
   * 3. 将组件挂载到页面中
   */
  // 创建组件实例
  const instance = createComponentInstance(vnode)

  // 初始化组件的状态
  setupComponent(instance)

  const componentUpdateFn = () => {
    /**
     * 区分挂载和更新
     */
    if (!instance.isMounted) {
      // 调用 render 拿到 subTree,this 指向 setupState
      const subTree = instance.render.call(instance.setupState)
      // 将 subTree 挂载到页面
      patch(null, subTree, container, anchor)
      // 保存子树
      instance.subTree = subTree
      // 挂载完了
      instance.isMounted = true
    } else {
      // 响应式数据变化产生的更新逻辑
      const prevSubTree = instance.subTree
      // 调用 render 拿到 subTree,this 指向 setupState
      const subTree = instance.render.call(instance.setupState)
      // 将 subTree 挂载到页面
      patch(prevSubTree, subTree, container, anchor)
      // 保存这一次的 subTree
      instance.subTree = subTree
    }
  }

  // 创建 effect
  const effect = new ReactiveEffect(componentUpdateFn)
  effect.run()
}

这样就完成了组件的挂载逻辑,当数据变化的时候,也会触发更新逻辑。注意这个更新和我们 processComponent 中的更新是不同的, 这里是组件的更新,而 processComponent 中的是组件的挂载和父组件传递的属性变化导致的更新。

组件的异步更新
createApp
欢迎来到前端练习生ZM的小站