首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Vue.js技术揭秘-slot

slot

Vue 的组件提供了一个非常有用的特性 —— 插槽,它让组件的实现变的更加灵活。我们平时在开发组件库的时候,为了让组件更加灵活可定制,经常用插槽的方式让用户可以自定义内容。插槽分为普通插槽和作用域插槽,它们可以解决不同的场景,但它是怎么实现的呢,下面我们就从源码的角度来分析插槽的实现原理。

普通插槽

为了更加直观,我们还是通过一个例子来分析插槽的实现:

这里我们定义了 子组件,它内部定义了 3 个插槽,2 个为具名插槽,一个 为 ,一个 为 ,还有一个没有定义 的是默认插槽。 和 之前填写的内容为默认内容。我们的父组件注册和引用了 的组件,并在组件内部定义了一些元素,用来替换插槽,那么它最终生成的 DOM 如下:

编译

还是先从编译说起,我们知道编译是发生在调用 的时候,所以编译的顺序是先编译父组件,再编译子组件。

首先编译父组件,在 阶段,会执行 处理 ,它的定义在 中:

当解析到标签上有 属性的时候,会给对应的 AST元素节点添加 属性,然后在 阶段,在 中会处理 ,相关代码在 中:

会给 添加一个 属性,并指向 ,之后会用到。在我们的例子中,父组件最终生成的代码如下:

接下来编译子组件,同样在 阶段会执行 处理函数,它的定义在 中:

当遇到 标签的时候会给对应的 AST 元素节点添加 属性,然后在 阶段,会判断如果当前 AST 元素节点是 标签,则执行 函数,它的定义在 中:

我们先不考虑 标签上有 以及 的情况,那么它生成的代码实际上就只有:

这里的 从 AST 元素节点对应的属性上取,默认是 ,而 对应的就是 开始和闭合标签包裹的内容。来看一下我们例子的子组件最终生成的代码,如下:

在编译章节我们了解到, 函数对应的就是 方法,它的定义在 中:

的参数 代表插槽名称 , 代表插槽的默认内容生成的 数组。先忽略 ,只看默认插槽逻辑。如果 有值,就返回它对应的 数组,否则返回 。那么这个 是哪里来的呢?我们知道子组件的 时机是在父组件执行 过程的时候,那这个时候父组件已经编译完成了。并且子组件在 过程中会执行 函数, 的时候获取到 ,相关代码在 中:

是通过执行 返回的,它的定义在 中:

方法接收 2 个参数,第一个参数 对应的是父 的 ,在我们的例子中就是 和 包裹的内容。第二个参数 是父 的上下文,也就是父组件的 实例。 函数的逻辑就是遍历 ,拿到每一个 的 ,然后通过 获取到插槽名称,这个 就是我们之前编译父组件在 阶段设置的 。接着以插槽名称为 把 添加到 中,如果 不存在,则是默认插槽的内容,则把对应的 添加到 中。这样就获取到整个 ,它是一个对象, 是插槽名称, 是一个 类型的数组,因为它可以有多个同名插槽。这样我们就拿到了 了,回到 函数,,我们也就能根据插槽名称获取到对应的 数组了,这个数组里的 都是在父组件创建的,这样就实现了在父组件替换子组件插槽的内容了。对应的 渲染成 ,作为当前组件渲染 的 ,之后的渲染过程之前分析过,不再赘述。我们知道在普通插槽中,父组件应用到子组件插槽里的数据都是绑定到父组件的,因为它渲染成 的时机的上下文是父组件的实例。但是在一些实际开发中,我们想通过子组件的一些数据来决定父组件实现插槽的逻辑,Vue 提供了另一种插槽——作用域插槽,接下来我们就来分析一下它的实现原理。

作用域插槽

为了更加直观,我们也是通过一个例子来分析作用域插槽的实现:

最终生成的 DOM 结构如下:

我们可以看到子组件的 标签多了 属性,以及 属性。父组件实现插槽的部分多了一个 标签,以及 属性,其实在 Vue 2.5+ 版本, 可以作用在普通元素上。这些就是作用域插槽和普通插槽在写法上的差别。在编译阶段,仍然是先编译父组件,同样是通过 函数去处理 ,它的定义在在 中:

这块逻辑很简单,读取 属性并赋值给当前 AST 元素节点的 属性,接下来在构造 AST 树的时候,会执行以下逻辑:

可以看到对于拥有 属性的 AST 元素节点而言,是不会作为 添加到当前 AST 树中,而是存到父 AST 元素节点的 属性上,它是一个对象,以插槽名称 为 。然后在 的过程,会对 做处理:

就是对 对象遍历,执行 ,并把结果用逗号拼接,而 是先生成一段函数代码,并且函数的参数就是我们的 ,也就是写在标签属性上的 对应的值,然后再返回一个对象, 为插槽名称, 为生成的函数代码。对于我们这个例子而言,父组件最终生成的代码如下:

可以看到它和普通插槽父组件编译结果的一个很明显的区别就是没有 了, 部分多了一个对象,并且执行了 方法,在编译章节我们了解到, 函数对的就是 方法,它的定义在 中:

其中, 是一个数组,每一个数组元素都有一个 和一个 , 对应的是插槽的名称, 对应一个函数。整个逻辑就是遍历这个 数组,生成一个对象,对象的 就是插槽名称, 就是函数。这个函数的执行时机稍后我们会介绍。接着我们再来看一下子组件的编译,和普通插槽的过程基本相同,唯一一点区别是在 的时候:

它会对 和 做处理,对应到我们的例子,最终生成的代码如下:

方法我们之前介绍过,对应的是 方法:

我们只关注作用域插槽的逻辑,那么这个 又是在什么地方定义的呢,原来在子组件的渲染函数执行前,在 方法内,有这么一段逻辑,定义在 中:

这个 对应的就是我们在父组件通过执行 返回的对象。所以回到 函数,我们就可以通过插槽的名称拿到对应的 ,然后把相关的数据扩展到 上,作为函数的参数传入,原来之前我们提到的函数这个时候执行,然后返回生成的 ,为后续渲染节点用。后续流程之前已介绍过,不再赘述,那么至此,作用域插槽的实现也就分析完毕。

总结

通过这一章的分析,我们了解了普通插槽和作用域插槽的实现。它们有一个很大的差别是数据作用域,普通插槽是在父组件编译和渲染阶段生成 ,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的 。而对于作用域插槽,父组件在编译和渲染阶段并不会直接生成 ,而是在父节点 的 中保留一个 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 ,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例。简单地说,两种插槽的目的都是让子组件 占位符生成的内容由父组件来决定,但数据的作用域会根据它们 渲染时机不同而不同。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20230201A01OBB00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券