假设我们有如下模板:
<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: 代码已经复制到剪贴板
可以看到,如上 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) }
@程序员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 以及它的作用
与 VNode
的 slots
属性相同,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: 代码已经复制到剪贴板