假设我们有如下模板:

<MyComponent>
  <div></div>
</MyComponent>
@程序员poetry: 代码已经复制到剪贴板

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




 
 
 
 


const compVNode = {
  flags: VNodeFlags.COMPONENT_STATEFUL_NORMAL,
  tag: MyComponent,
  children: {
    flags: VNodeFlags.ELEMENT,
    tag: 'div'
  }
}
@程序员poetry: 代码已经复制到剪贴板

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





 
 
 
 
 
 
 


const compVNode = {
  flags: VNodeFlags.COMPONENT,
  tag: MyComponent,
  children: null,
  slots: {
    // 默认插槽
    default: {
      flags: VNodeFlags.ELEMENT,
      tag: 'div'
    }
  }
}
@程序员poetry: 代码已经复制到剪贴板

可以看到,如上 VNodechildren 属性值为 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)
}
@程序员poetry: 代码已经复制到剪贴板

MyComponent 组件的 render 函数内,我们就可以通过组件实例访问 slots 数据:






 




class MyComponent {
  render() {
    return {
      flags: VNodeFlags.ELEMENT,
      tag: 'h1'
      children: this.$slots.default
    }
  }
}
@程序员poetry: 代码已经复制到剪贴板

实际上,这就是普通插槽的实现原理,至于作用域插槽(scopedSlots),与普通插槽并没有什么本质的区别,我们知道作用域插槽可以访问子组件的数据,在实现上来看其实就是函数传参:






 
 




class MyComponent {
  render() {
    return {
      flags: VNodeFlags.ELEMENT,
      tag: 'h1'
      // 插槽变成了函数,可以传递参数
      children: this.$slots.default(1)
    }
  }
}
@程序员poetry: 代码已经复制到剪贴板

如上代码所示,只要 this.$slots.default 是函数即可实现,所以在模板编译时,我们最终需要得到如下 VNode





 
 
 
 
 
 
 
 
 
 


const compVNode = {
  flags: VNodeFlags.COMPONENT,
  tag: MyComponent,
  children: null,
  slots: {
    // 作用域插槽,可以接受组件传递过来的数据
    default: (arg) => {
      const tag = arg === 1 ? 'div' : 'h1'
      return {
        flags: VNodeFlags.ELEMENT,
        tag
      }
    }
  }
}
@程序员poetry: 代码已经复制到剪贴板

现在你应该明白为什么普通插槽和作用域插槽本质上并没有区别了,因为普通插槽也可以是函数,只是不接收参数罢了。这么看的话其实普通插槽是作用域插槽的子集,那为什么不将它们合并呢?没错从 Vue2.6 起已经将之合并,所有插槽在 VNode 中都是函数,一个返回 VNode 的函数。

TIP

用过 React 的朋友,这让你想起 Render Prop 了吗!

# key 和 ref

key 就像 VNode 的唯一标识,用于 diff 算法的优化,它可以是数字也可以是字符串:




 


{
  flags: VNodeFlags.ELEMENT_HTML,
  tag: 'li',
  key: 'li_0'
}
@程序员poetry: 代码已经复制到剪贴板

ref 的设计是为了提供一种能够拿到真实DOM的方式,当然如果将 ref 应用到组件上,那么拿到的就是组件实例,我们通常会把 ref 设计成一个函数,假设我们有如下模板:

<div :ref="el => elRef = el"></div>
@程序员poetry: 代码已经复制到剪贴板

我们可以把这段模板编译为如下 VNode




 


const elementVNode = {
  flags: VNodeFlags.ELEMENT_HTML,
  tag: 'div',
  ref: el => elRef = el
}
@程序员poetry: 代码已经复制到剪贴板

在使用 mountElement 函数挂载如上 VNode 时,可以轻松的实现 ref 功能:




 


function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)
  container.appendChild(el)
  vnode.ref && vnode.ref(el)
}
@程序员poetry: 代码已经复制到剪贴板

如果挂载的是组件而非普通标签,那么只需要将组件实例传递给 vnode.ref 函数即可:









 


function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()
  // 渲染
  instance.$vnode = instance.render()
  // 挂载
  mountElement(instance.$vnode, container)

  vnode.ref && vnode.ref(instance)
}
@程序员poetry: 代码已经复制到剪贴板

# parentVNode 以及它的作用

VNodeslots 属性相同,parentVNode 属性也是给组件的 VNode 准备的,组件的 VNode 为什么需要这两个属性呢?它俩的作用又是什么呢?想弄清楚这些,我们至少要先弄明白:一个组件所涉及的 VNode 都有哪些。什么意思呢?看如下模板思考一个问题:

<template>
  <div>
    <MyComponent />
  </div>
</template>
@程序员poetry: 代码已经复制到剪贴板

从这个模板来看 MyComponent 组件至少涉及到两个 VNode,第一个 VNode 是标签 <MyComponent /> 的描述,其次 MyComponent 组件本身也有要渲染的内容,这就是第二个 VNode

  • 第一个 VNode 用来描述 <MyComponent /> 标签:
{
  // 省略...
  tag: MyComponent
}
@程序员poetry: 代码已经复制到剪贴板
  • 第二个 VNode 是组件渲染内容的描述,即组件的 render 函数产出的 VNode
class MyComponent {
  render () {
    return {/* .. */} // 产出的 VNode
  }
}
@程序员poetry: 代码已经复制到剪贴板

组件实例的 $vnode 属性值就是组件 render 函数产出的 VNode,这通过如下代码可以一目了然:





 






function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()
  // 渲染,$vnode 的值就是组件 render 函数产出的 VNode
  instance.$vnode = instance.render()
  // 挂载
  mountElement(instance.$vnode, container)

  vnode.ref && vnode.ref(instance)
}
@程序员poetry: 代码已经复制到剪贴板

instance.$vnode.parentVNode 的值就是用来描述组件(如:<MyComponent />)标签的 VNode,我们只需在如上代码中添加一行代码即可实现:






 
 






function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = new vnode.tag()
  // 渲染,$vnode 的值就是组件 render 函数产出的 VNode
  instance.$vnode = instance.render()
  // vnode 就是用来描述组件标签的 VNode
  instance.$vnode.parentVNode = vnode
  // 挂载
  mountElement(instance.$vnode, c
@程序员poetry: 代码已经复制到剪贴板
阅读全文
Last Updated: 3/30/2025, 1:18:33 PM