⚡ 30 秒速记

  • 实例保存状态、输入和最近渲染子树
  • 挂载先建立上下文,再执行渲染
  • 插槽是由提供方定义的界面生成能力
  • 外部组件节点与内部子树分别关联
  • 更新与卸载要保持引用、事件和资源一致

有状态组件需要一个跨更新保存信息的上下文,让每次渲染仍属于同一个组件。 挂载时建立状态、输入和插槽,再执行渲染得到内部子树。父级传来的组件节点与内部渲染结果要分别记录,更新时才能知道是输入变了还是子树需要替换。插槽负责让使用方提供界面内容,组件可以传入必要数据,但不会因此接管提供方全部作用域。

版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。

假设我们有如下模板:

<MyComponent>
  <div></div>
</MyComponent>

由这段模板可知,我们为 MyComponent 组件提供了一个空的 div 标签作为默认插槽内容,从DOM结构上看 <MyComponent> 标签有一个 div 标签作为子节点,通常我们可以将其编译为如下 VNode:

const compVNode = {
  flags: VNodeFlags.COMPONENT_STATEFUL_NORMAL,
  tag: MyComponent,
  children: {
    flags: VNodeFlags.ELEMENT,
    tag: 'div'
  }
}

这其实没什么问题,但是我们更倾向于新建一个 slots 属性来存储这些子节点,这在语义上更加贴切,所以我们希望将模板编译为如下 VNode:

const compVNode = {
  flags: VNodeFlags.COMPONENT,
  tag: MyComponent,
  children: null,
  slots: {
    // 默认插槽
    default: {
      flags: VNodeFlags.ELEMENT,
      tag: 'div'
    }
  }
}

可以看到,如上 VNode 的 children 属性值为 null。当我们使用 mountComponent 函数挂载如上 VNode 时,我们可以在组件实例化之后并且在组件的渲染函数执行之前将 compVNode.slots 添加到组件实例对象上,这样当组件的渲染函数执行的时候,就可以访问插槽数据:

function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()

  // 设置 slots
  instance.$slots = vnode.slots

  // 渲染
  instance.$vnode = instance.render()
  // 挂载
  mountElement(instance.$vnode, container)

  vnode.ref && vnode.ref(instance)
}
webapp
公众号
开发者导航
切换夜间模式
点击侧边栏上一篇
点击侧边栏下一篇
折叠侧边栏
收起全部