Open doreenChenD opened 5 years ago
这篇是个善良的搬运工
vue组件的一个非常重要的特性——slot 插槽,相信大家不陌生。slot使组件的使用更加灵活,父子组件之间的通信也更多样化。插槽分为普通插槽和作用域插槽。
特点:作用域为父组件的作用域,而不是子组件的。即可以访问父组件的属性,而不能访问子组件的属性。 使用方法:
特点:为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。
特点: 将不同的slot的放到子组件对应的位置
slot (2.6.0起被废弃) 不带name的slot 将会隐含名字default
v-slot (2.6.0 起使用) v-slot 只能写在template或者component上
特点: 父组件访问子组件的数据
slot-scope (2.6.0起被废弃) 2.5+ 可以用在任何标签 2.5以前只能用在template标签
v-slot (2.6.0后使用) 作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里,这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。
3.动态插槽名
v-slot缩写,该缩写只在其有参数的时候才可用 (#后面必须有插槽名)
注意:
5 .插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。
6.无渲染组件 无渲染组件——大漠
vue中的slot 源于web components 规范草案,是组件的一块HTML模板。那么slot的实现过程是怎么样的呢?
在Vue中,组件是有一个作用域的:组件模板()内的就是组件作用域,而其之外的就不是组件的作用域了。组件的模板是在其作用域内编译的。 父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。 slot 是父组件向子组件分发内容,所以slot里的html 的编译作用域是父组件的编译作用域。
slot 本质是返回Vnodede的函数。 在2.5之前,如果是普通插槽就直接是VNode的形式了,而如果是作用域插槽,由于子组件需要在父组件访问子组件的数据,所以父组件下是一个未执行的函数 (slotScope) => return h('div', slotScope.msg) ,接受子组件的slotProps参数,在子组件渲染实例时会调用该函数传入数据。
源码基础: Vue2.1.7源码学习 ├── build --------------------------------- 构建相关的文件,一般情况下我们不需要动 ├── dist ---------------------------------- 构建后文件的输出目录 ├── examples ------------------------------ 存放一些使用Vue开发的应用案例 ├── flow ---------------------------------- 类型声明,使用开源项目 Flow ├── package.json -------------------------- 不解释 ├── test ---------------------------------- 包含所有测试文件 ├── src ----------------------------------- 这个是我们最应该关注的目录,包含了源码 │ ├── entries --------------------------- 包含了不同的构建或包的入口文件 │ │ ├── web-runtime.js ---------------- 运行时构建的入口,输出 dist/vue.common.js 文件,不包含模板(template)到render函数的编译器,所以不支持 template 选项,我们使用vue默认导出的就是这个运行时的版本。大家使用的时候要注意 │ │ ├── web-runtime-with-compiler.js -- 独立构建版本的入口,输出 dist/vue.js,它包含模板(template)到render函数的编译器 │ │ ├── web-compiler.js --------------- vue-template-compiler 包的入口文件 │ │ ├── web-server-renderer.js -------- vue-server-renderer 包的入口文件 │ ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数 │ │ ├── parser ------------------------ 存放将模板字符串转换成元素抽象语法树的代码 │ │ ├── codegen ----------------------- 存放从抽象语法树(AST)生成render函数的代码 │ │ ├── optimizer.js ------------------ 分析静态树,优化vdom渲染 │ ├── core ------------------------------ 存放通用的,平台无关的代码 │ │ ├── observer ---------------------- 反应系统,包含数据观测的核心代码 │ │ ├── vdom -------------------------- 包含虚拟DOM创建(creation)和打补丁(patching)的代码 │ │ ├── instance ---------------------- 包含Vue构造函数设计相关的代码 │ │ ├── global-api -------------------- 包含给Vue构造函数挂载全局方法(静态方法)或属性的代码 │ │ ├── components -------------------- 包含抽象出来的通用组件 │ ├── server ---------------------------- 包含服务端渲染(server-side rendering)的相关代码 │ ├── platforms ------------------------- 包含平台特有的相关代码 │ ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包 │ ├── shared ---------------------------- 包含整个代码库通用的代码
template
这里我们定义了 AppLayout 子组件,它内部定义了 3 个插槽,2 个为具名插槽,一个name 为 header,一个name 为 footer,还有一个没有定义 name 的是默认插槽。 最终生成
name
header
footer
组件的编译发生在调用 vm.$mount的时候,所以编译顺序是先父组件,再子组件。 编译父组件时,parse 阶段,会执行 processSlot处理 slot,它的定义在 src/compiler/parser/index.js 中:
vm.$mount
parse
processSlot
src/compiler/parser/index.js
当解析到标签上有 slot 属性的时候,会给对应的 AST 元素节点添加 slotTarget 属性,然后在 codegen 阶段,在 genData 中会处理slotTarget,相关代码在src/compiler/codegen/index.js 中:
codegen
genData
slotTarget
src/compiler/codegen/index.js
会给 data添加一个 slot属性,并指向 slotTarget,之后会用到。在我们的例子中,父组件最终生成的代码如下:
data
slot
接下来编译子组件,同样在 parser阶段会执行 processSlot处理函数,它的定义同样在 src/compiler/parser/index.js 中:
parser
当遇到 slot标签的时候会给对应的 AST 元素节点添加 slotName属性,然后在 codegen阶段,会判断如果当前 AST 元素节点是 slot标签,则执行 genSlot函数,它的定义在 src/compiler/codegen/index.js中: 我们先不考虑 slot 标签上有 attrs 以及 v-bind 的情况,那么它生成的代码实际上就只有: 这里的 slotName从 AST 元素节点对应的属性上取,默认是 default,而 children 对应的就是 slot 开始和闭合标签包裹的内容(默认显示的部分)。来看一下我们例子的子组件最终生成的代码,如下:
slotName
genSlot
default
其中: _t 函数对应的就是 renderSlot方法: 它的定义在 src/core/instance/render-heplpers/render-slot.js中: renderSlot 的参数 name代表插槽名称 slotName,fallback 代表插槽的默认内容生成的 vnode数组。 如果 this.$slot[name] 有值,就返回它对应的 vnode 数组,否则返回 fallback。那么这个 this.$slot 是哪里来的呢?我们知道子组件的 init 时机是在父组件执行 patch过程的时候,那这个时候父组件已经编译完成了。并且子组件在 init过程中会执行 initRender 函数,initRender的时候获取到 vm.$slot,相关代码在 src/core/instance/render.js 中:
_t
renderSlot
src/core/instance/render-heplpers/render-slot.js
fallback
vnode
this.$slot[name]
this.$slot
init
patch
initRender
vm.$slot
src/core/instance/render.js
vm.$slots 是通过执行 resolveSlots(options._renderChildren, renderContext) 返回的,它的定义在 src/core/instance/render-helpers/resolve-slots.js 中:
vm.$slots
resolveSlots(options._renderChildren, renderContext)
src/core/instance/render-helpers/resolve-slots.js
resolveSlots 方法接收 2 个参数,第一个参数 chilren对应的是父 vnode的 children,在我们的例子中就是 和 包裹的内容。第二个参数 context是父 vnode的上下文,也就是父组件的 vm 实例。
resolveSlots
chilren
children
context
resolveSlots函数的逻辑就是遍历 chilren,拿到每一个 child 的 data,然后通过 data.slot 获取到插槽名称,这个 slot 就是我们之前编译父组件在 codegen 阶段设置的 data.slot。接着以插槽名称为 key 把 child 添加到 slots 中,如果 data.slot 不存在,则是默认插槽的内容,则把对应的 child 添加到 slots.defaults 中。这样就获取到整个 slots,它是一个对象,key 是插槽名称,value 是一个 vnode 类型的数组,因为它可以有多个同名插槽。
参考文章: Vue 2.0学习笔记:Vue组件内容分发(slot)——大漠 vue.js技术揭秘 Vue.js文档 无渲染组件——大漠
vue之关于slot(内容分发、插槽)(一)
这篇是个善良的搬运工
目录
用法
vue组件的一个非常重要的特性——slot 插槽,相信大家不陌生。slot使组件的使用更加灵活,父子组件之间的通信也更多样化。插槽分为普通插槽和作用域插槽。
普通插槽
特点:作用域为父组件的作用域,而不是子组件的。即可以访问父组件的属性,而不能访问子组件的属性。 使用方法:
父组件
子组件
后备内容
特点:为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。
具名插槽
特点: 将不同的slot的放到子组件对应的位置
slot (2.6.0起被废弃) 不带name的slot 将会隐含名字default
v-slot (2.6.0 起使用) v-slot 只能写在template或者component上
作用域插槽
特点: 父组件访问子组件的数据
slot-scope (2.6.0起被废弃) 2.5+ 可以用在任何标签 2.5以前只能用在template标签
v-slot (2.6.0后使用) 作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里,这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。
3.动态插槽名
v-slot缩写,该缩写只在其有参数的时候才可用 (#后面必须有插槽名)
注意:
5 .插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。
6.无渲染组件 无渲染组件——大漠
实现过程 (以slot 为例)
vue中的slot 源于web components 规范草案,是组件的一块HTML模板。那么slot的实现过程是怎么样的呢?
编译作用域
在Vue中,组件是有一个作用域的:组件模板()内的就是组件作用域,而其之外的就不是组件的作用域了。组件的模板是在其作用域内编译的。 父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。 slot 是父组件向子组件分发内容,所以slot里的html 的编译作用域是父组件的编译作用域。
slot 实现过程
slot 本质是返回Vnodede的函数。 在2.5之前,如果是普通插槽就直接是VNode的形式了,而如果是作用域插槽,由于子组件需要在父组件访问子组件的数据,所以父组件下是一个未执行的函数 (slotScope) => return h('div', slotScope.msg) ,接受子组件的slotProps参数,在子组件渲染实例时会调用该函数传入数据。
开始
源码基础: Vue2.1.7源码学习 ├── build --------------------------------- 构建相关的文件,一般情况下我们不需要动 ├── dist ---------------------------------- 构建后文件的输出目录 ├── examples ------------------------------ 存放一些使用Vue开发的应用案例 ├── flow ---------------------------------- 类型声明,使用开源项目 Flow ├── package.json -------------------------- 不解释 ├── test ---------------------------------- 包含所有测试文件 ├── src ----------------------------------- 这个是我们最应该关注的目录,包含了源码 │ ├── entries --------------------------- 包含了不同的构建或包的入口文件 │ │ ├── web-runtime.js ---------------- 运行时构建的入口,输出 dist/vue.common.js 文件,不包含模板(template)到render函数的编译器,所以不支持
template
选项,我们使用vue默认导出的就是这个运行时的版本。大家使用的时候要注意 │ │ ├── web-runtime-with-compiler.js -- 独立构建版本的入口,输出 dist/vue.js,它包含模板(template)到render函数的编译器 │ │ ├── web-compiler.js --------------- vue-template-compiler 包的入口文件 │ │ ├── web-server-renderer.js -------- vue-server-renderer 包的入口文件 │ ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数 │ │ ├── parser ------------------------ 存放将模板字符串转换成元素抽象语法树的代码 │ │ ├── codegen ----------------------- 存放从抽象语法树(AST)生成render函数的代码 │ │ ├── optimizer.js ------------------ 分析静态树,优化vdom渲染 │ ├── core ------------------------------ 存放通用的,平台无关的代码 │ │ ├── observer ---------------------- 反应系统,包含数据观测的核心代码 │ │ ├── vdom -------------------------- 包含虚拟DOM创建(creation)和打补丁(patching)的代码 │ │ ├── instance ---------------------- 包含Vue构造函数设计相关的代码 │ │ ├── global-api -------------------- 包含给Vue构造函数挂载全局方法(静态方法)或属性的代码 │ │ ├── components -------------------- 包含抽象出来的通用组件 │ ├── server ---------------------------- 包含服务端渲染(server-side rendering)的相关代码 │ ├── platforms ------------------------- 包含平台特有的相关代码 │ ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包 │ ├── shared ---------------------------- 包含整个代码库通用的代码例子
这里我们定义了 AppLayout 子组件,它内部定义了 3 个插槽,2 个为具名插槽,一个
name
为header
,一个name
为footer
,还有一个没有定义 name 的是默认插槽。 最终生成编译
组件的编译发生在调用
vm.$mount
的时候,所以编译顺序是先父组件,再子组件。 编译父组件时,parse
阶段,会执行processSlot
处理 slot,它的定义在src/compiler/parser/index.js
中:当解析到标签上有 slot 属性的时候,会给对应的 AST 元素节点添加 slotTarget 属性,然后在
codegen
阶段,在genData
中会处理slotTarget
,相关代码在src/compiler/codegen/index.js
中:会给
data
添加一个slot
属性,并指向slotTarget
,之后会用到。在我们的例子中,父组件最终生成的代码如下:接下来编译子组件,同样在
parser
阶段会执行processSlot
处理函数,它的定义同样在src/compiler/parser/index.js
中:当遇到
slot
标签的时候会给对应的 AST 元素节点添加slotName
属性,然后在codegen
阶段,会判断如果当前 AST 元素节点是slot
标签,则执行genSlot
函数,它的定义在src/compiler/codegen/index.js
中: 我们先不考虑 slot 标签上有 attrs 以及 v-bind 的情况,那么它生成的代码实际上就只有: 这里的slotName
从 AST 元素节点对应的属性上取,默认是default
,而 children 对应的就是 slot 开始和闭合标签包裹的内容(默认显示的部分)。来看一下我们例子的子组件最终生成的代码,如下:其中:
_t
函数对应的就是renderSlot
方法: 它的定义在src/core/instance/render-heplpers/render-slot.js
中:renderSlot
的参数name
代表插槽名称slotName
,fallback
代表插槽的默认内容生成的vnode
数组。 如果this.$slot[name]
有值,就返回它对应的vnode
数组,否则返回fallback
。那么这个this.$slot
是哪里来的呢?我们知道子组件的init
时机是在父组件执行patch
过程的时候,那这个时候父组件已经编译完成了。并且子组件在init
过程中会执行initRender
函数,initRender
的时候获取到vm.$slot
,相关代码在src/core/instance/render.js
中:vm.$slots
是通过执行resolveSlots(options._renderChildren, renderContext)
返回的,它的定义在src/core/instance/render-helpers/resolve-slots.js
中:resolveSlots
方法接收 2 个参数,第一个参数chilren
对应的是父vnode
的children
,在我们的例子中就是context
是父vnode
的上下文,也就是父组件的 vm 实例。resolveSlots
函数的逻辑就是遍历chilren
,拿到每一个 child 的 data,然后通过 data.slot 获取到插槽名称,这个 slot 就是我们之前编译父组件在 codegen 阶段设置的 data.slot。接着以插槽名称为 key 把 child 添加到 slots 中,如果 data.slot 不存在,则是默认插槽的内容,则把对应的 child 添加到 slots.defaults 中。这样就获取到整个 slots,它是一个对象,key 是插槽名称,value 是一个 vnode 类型的数组,因为它可以有多个同名插槽。2.6+ 的优化
参考文章: Vue 2.0学习笔记:Vue组件内容分发(slot)——大漠 vue.js技术揭秘 Vue.js文档 无渲染组件——大漠