渲染组件

要实现渲染组件,我们先定义组件的类型。并且在渲染器函数里做下组件的渲染判断

function patch(n1, n2, container, anchor) {
  if (n1 && n1.type !== n2.type) {
    unmount(n1)
    n1 = null
  }

  const { type } = n2

  if (typeof type === 'string') {
    // 作为普通元素处理
  } else if (type === Text) {
    // 作为文本节点处理
  } else if (type === Fragment) {
    // 作为片段处理
  } else if (typeof type === 'object') {
    // vnode.type 的值是选项对象,作为组件来处理
    if (!n1) {
      // 挂载组件
      mountComponent(n2, container, anchor)
    } else {
      // 更新组件
      patchComponent(n1, n2, anchor)
    }
  }
}

function mountComponent(vnode, container, anchor) {
  // 通过 vnode 获取组件的选项对象,即 vnode.type
  const componentOptions = vnode.type
  // 获取组件的渲染函数 render
  const { render } = componentOptions
  // 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
  const subTree = render()
  // 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
  patch(null, subTree, container, anchor)
	// 特地说明一下,上面这段是递归渲染子节点
}

组件状态与自更新

假设组件数据结构如下

const MyComponent = {
  name: 'MyComponent',
  // 用 data 函数来定义组件自身的状态
  data() {
    return {
      foo: 'hello world'
    }
  },
  render() {
    return {
      type: 'div',
      children: `foo 的值是: ${this.foo}` // 在渲染函数内使用组件状态
    }
  }
}

我们需要

  1. 将data函数返回值包装成响应式对象
  2. 而render需要用副作用函数包裹调用
  3. 修改render函数中的this指向
  4. 设计调度器,防止同步的调用副作用函数
function mountComponent(vnode, container, anchor) {
  const componentOptions = vnode.type
  const { render, data } = componentOptions

  const state = reactive(data())

  effect(() => {
    const subTree = render.call(state, state)
    patch(null, subTree, container, anchor)
  }, {
    // 指定该副作用函数的调度器为 queueJob 即可
    scheduler: queueJob
  })
}

组件实例与生命周期

上面我们简单实现了一个组件的初始渲染与挂载逻辑。但是有个缺陷就是没有统一集中管理组件状态与数据。

  1. 增加组件实例对象用以储存组件的state\props\events与状态等信息
  2. 在正确的位置触发生命周期回调
function mountComponent(vnode, container, anchor) {
  const componentOptions = vnode.type
  // 从组件选项对象中取得组件的生命周期函数
  const { render, data, beforeCreate, created, beforeMount,
    mounted, beforeUpdate, updated } = componentOptions

  // 在这里调用 beforeCreate 钩子
  beforeCreate && beforeCreate()

  const state = reactive(data())

  const instance = {
    state,
    isMounted: false,
    subTree: null
  }
  vnode.component = instance

  // 在这里调用 created 钩子
  created && created.call(state)

  effect(() => {
    const subTree = render.call(state, state)
    if (!instance.isMounted) {
      // 在这里调用 beforeMount 钩子
      beforeMount && beforeMount.call(state)
      patch(null, subTree, container, anchor)
      instance.isMounted = true
      // 在这里调用 mounted 钩子
      mounted && mounted.call(state)
    } else {
      // 在这里调用 beforeUpdate 钩子
      beforeUpdate && beforeUpdate.call(state)
      patch(instance.subTree, subTree, container, anchor)
      // 在这里调用 updated 钩子
      updated && updated.call(state)
    }
    instance.subTree = subTree
  }, { scheduler: queueJob })
}

props与组件的被动更新

如何区分响应式props还是html的attrs?

在Vue3中,只有在组件内定义了Props的才会被归为响应式的Props,其他的都算是attrs。所以要将所有的Props依据是否有定义在Props中来区分成两部分,Props部分使用浅响应包装成一个响应对象,而attrs则不需要包装。