深入浅析Vue中的slots/scoped slots


Posted in Javascript onApril 03, 2018

一直对Vue中的slot插槽比较感兴趣,下面是自己的一些简单理解,希望可以帮助大家更好的理解slot插槽

下面结合一个例子,简单说明slots的工作原理

dx-li子组件的template如下:

<li class="dx-li">
 <slot>
   你好!
 </slot>
</li>
dx-ul父组件的template如下:
<ul>
 <dx-li>
  hello juejin!
 </dx-li>
</ul>
结合上述例子以及vue中相关源码进行分析
dx-ul父组件中template编译后,生成的组件render函数:
module.exports={
 render:function (){
  var _vm=this;
  var _h=_vm.$createElement;
  var _c=_vm._self._c||_h;
  // 其中_vm.v为createTextVNode创建文本VNode的函数
  return _c('ul', 
    [_c('dx-li', [_vm._v("hello juejin!")])],
    1)
 },
 staticRenderFns: []
}

传递的插槽内容'hello juejin!'会被编译成dx-li子组件VNode节点的子节点。

渲染dx-li子组件,其中子组件的render函数:

module.exports={
 render:function (){
  var _vm=this;
  var _h=_vm.$createElement;
  var _c=_vm._self._c||_h;
  // 其中_vm._v 函数为renderSlot函数
  return _c('li', 
    {staticClass: "dx-li" }, 
    [_vm._t("default", [_vm._v("你好 掘金!")])], 
    2
   )
  },
 staticRenderFns: []
}

初始化dx-li子组件vue实例过程中,会调用initRender函数:

function initRender (vm) {
 ...
 // 其中_renderChildren数组,存储为 'hello juejin!'的VNode节点;renderContext一般为父组件Vue实例
 这里为dx-ul组件实例
 vm.$slots = resolveSlots(options._renderChildren, renderContext);
 ...
}

其中resolveSlots函数为:

/**
 * 主要作用是将children VNodes转化成一个slots对象.
 */
export function resolveSlots (
 children: ?Array<VNode>,
 context: ?Component
): { [key: string]: Array<VNode> } {
 const slots = {}
 // 判断是否有children,即是否有插槽VNode
 if (!children) {
 return slots
 }
 // 遍历父组件节点的孩子节点
 for (let i = 0, l = children.length; i < l; i++) {
 const child = children[i]
 // data为VNodeData,保存父组件传递到子组件的props以及attrs等
 const data = child.data
 /* 移除slot属性
 * <span slot="abc"></span> 
 * 编译成span的VNode节点data = {attrs:{slot: "abc"}, slot: "abc"},所以这里删除该节点attrs的slot
 */
 if (data && data.attrs && data.attrs.slot) {
  delete data.attrs.slot
 }
 /* 判断是否为具名插槽,如果为具名插槽,还需要子组件/函数子组件渲染上下文一致。主要作用:
 *当需要向子组件的子组件传递具名插槽时,不会保持插槽的名字。
 * 举个栗子:
 * child组件template: 
 * <div>
 * <div class="default"><slot></slot></div>
 * <div class="named"><slot name="foo"></slot></div>
 * </div>
 * parent组件template:
 * <child><slot name="foo"></slot></child>
 * main组件template:
 * <parent><span slot="foo">foo</span></parent>
 * 此时main渲染的结果:
 * <div>
 * <div class="default"><span slot="foo">foo</span></div>
   <div class="named"></div>
 * </div>
 */
 if ((child.context === context || child.fnContext === context) &&
  data && data.slot != null
 ) {
  const name = data.slot
  const slot = (slots[name] || (slots[name] = []))
  // 这里处理父组件采用template形式的插槽
  if (child.tag === 'template') {
  slot.push.apply(slot, child.children || [])
  } else {
  slot.push(child)
  }
 } else {
  // 返回匿名default插槽VNode数组
  (slots.default || (slots.default = [])).push(child)
 }
 }
 // 忽略仅仅包含whitespace的插槽
 for (const name in slots) {
 if (slots[name].every(isWhitespace)) {
  delete slots[name]
 }
 }
 return slots
}

然后挂载dx-li组件时,会调用dx-li组件render函数,在此过程中会调用renderSlot函数:

export function renderSlot (
  name: string, // 子组件中slot的name,匿名default
  fallback: ?Array<VNode>, // 子组件插槽中默认内容VNode数组,如果没有插槽内容,则显示该内容
  props: ?Object, // 子组件传递到插槽的props
  bindObject: ?Object // 针对<slot v-bind="obj"></slot> obj必须是一个对象
 ): ?Array<VNode> {
 // 判断父组件是否传递作用域插槽
  const scopedSlotFn = this.$scopedSlots[name]
  let nodes
  if (scopedSlotFn) { // scoped slot
  props = props || {}
  if (bindObject) {
   if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
   warn(
    'slot v-bind without argument expects an Object',
    this
   )
   }
   props = extend(extend({}, bindObject), props)
  }
  // 传入props生成相应的VNode
  nodes = scopedSlotFn(props) || fallback
  } else {
  // 如果父组件没有传递作用域插槽
  const slotNodes = this.$slots[name]
  // warn duplicate slot usage
  if (slotNodes) {
   if (process.env.NODE_ENV !== 'production' && slotNodes._rendered) {
   warn(
    `Duplicate presence of slot "${name}" found in the same render tree ` +
    `- this will likely cause render errors.`,
    this
   )
   }
   // 设置父组件传递插槽的VNode._rendered,用于后面判断是否有重名slot
   slotNodes._rendered = true
  }
  // 如果没有传入插槽,则为默认插槽内容VNode
  nodes = slotNodes || fallback
  }
  // 如果还需要向子组件的子组件传递slot
  /*举个栗子:
  * Bar组件: <div class="bar"><slot name="foo"/></div>
  * Foo组件:<div class="foo"><bar><slot slot="foo"/></bar></div>
  * main组件:<div><foo>hello</foo></div>
  * 最终渲染:<div class="foo"><div class="bar">hello</div></div>
  */
  const target = props && props.slot
  if (target) {
  return this.$createElement('template', { slot: target }, nodes)
  } else {
  return nodes
  }
 }

scoped slots理解

dx-li子组件的template如下:

<li class="dx-li"> 
 <slot str="你好 掘金!">
  hello juejin!
 </slot>
</li>
dx-ul父组件的template如下:
<ul>
 <dx-li>
  <span slot-scope="scope">
   {{scope.str}}
  </span>
 </dx-li>
</ul>
结合例子和Vue源码简单作用域插槽
dx-ul父组件中template编译后,产生组件render函数:
module.exports={
 render:function (){
  var _vm=this;
  var _h=_vm.$createElement;
  var _c=_vm._self._c||_h;
   return _c('ul', [_c('dx-li', {
   // 可以编译生成一个对象数组
   scopedSlots: _vm._u([{
    key: "default",
    fn: function(scope) {
    return _c('span', 
     {},
     [_vm._v(_vm._s(scope.str))]
    )
    }
   }])
   })], 1)
  },
 staticRenderFns: []
 }

其中 _vm._u函数:

function resolveScopedSlots (
 fns, // 为一个对象数组,见上文scopedSlots
 res
) {
 res = res || {};
 for (var i = 0; i < fns.length; i++) {
  if (Array.isArray(fns[i])) {
   // 递归调用
   resolveScopedSlots(fns[i], res);
  } else {
   res[fns[i].key] = fns[i].fn;
  }
 }
 return res
}

子组件的后续渲染过程与slots类似。scoped slots原理与slots基本是一致,不同的是编译父组件模板时,会生成一个返回结果为VNode的函数。当子组件匹配到父组件传递作用域插槽函数时,调用该函数生成对应VNode。

总结

其实slots/scoped slots 原理是非常简单的,我们只需明白一点vue在渲染组件时,是根据VNode渲染实际DOM元素的。

slots是将父组件编译生成的插槽VNode,在渲染子组件时,放置到对应子组件渲染VNode树中。

scoped slots是将父组件中插槽内容编译成一个函数,在渲染子组件时,传入子组件props,生成对应的VNode。最后子组件,根据组件render函数返回VNode节点树,update渲染真实DOM元素。同时,可以看出跨组件传递插槽也是可以的,但是必须注意具名插槽传递。

Javascript 相关文章推荐
理解Javascript_08_函数对象
Oct 15 Javascript
JavaScript Promise启示录
Aug 12 Javascript
JavaScript访问字符串中单个字符的两种方法
Jul 03 Javascript
jQuery CSS3相结合实现时钟插件
Jan 08 Javascript
js时间戳格式化成日期格式的多种方法介绍
Feb 16 Javascript
requirejs按需加载angularjs文件实例
Jun 08 Javascript
又拍云 Node.js 实现文件上传、删除功能
Oct 28 Javascript
vue滚动tab跟随切换效果
Jun 29 Javascript
详解nvm管理多版本node踩坑
Jul 26 Javascript
JS实现网站吸顶条
Jan 08 Javascript
Element Dropdown下拉菜单的使用方法
Jul 26 Javascript
html5 录制mp3音频支持采样率和比特率设置
Jul 15 Javascript
使用FileReader API创建Vue文件阅读器组件
Apr 03 #Javascript
vue内置指令详解
Apr 03 #Javascript
详解在React里使用&quot;Vuex&quot;
Apr 02 #Javascript
Angular2进阶之如何避免Dom误区
Apr 02 #Javascript
Vue项目分环境打包的实现步骤
Apr 02 #Javascript
详解webpack-dev-server的简单使用
Apr 02 #Javascript
webpack v4 从dev到prd的方法
Apr 02 #Javascript
You might like
PHP源码之explode使用说明
2011/08/05 PHP
php跨站攻击实例分析
2014/10/28 PHP
php简单实现屏蔽指定ip段用户的访问
2015/04/29 PHP
thinkPHP微信分享接口JSSDK用法实例
2017/07/07 PHP
js为数字添加逗号并格式化数字的代码
2013/08/23 Javascript
Java/JS获取flash高宽的具体方法
2013/12/27 Javascript
网页右侧悬浮滚动在线qq客服代码示例
2014/04/28 Javascript
一个很有趣3D球状标签云兼容IE8
2014/08/22 Javascript
jquery attr()设置和获取属性值实例教程
2016/09/25 Javascript
KnockoutJS 3.X API 第四章之表单textInput、hasFocus、checked绑定
2016/10/11 Javascript
微信小程序实战之自定义模态弹窗(8)
2017/04/18 Javascript
微信小程序页面开发注意事项整理
2017/05/18 Javascript
Vue实现PopupWindow组件详解
2018/04/28 Javascript
解决angular双向绑定无效果,ng-model不能正常显示的问题
2018/10/02 Javascript
axios使用拦截器统一处理所有的http请求的方法
2018/11/02 Javascript
详解Webpack loader 之 file-loader
2018/11/07 Javascript
JavaScript中filter的用法实例分析
2019/02/27 Javascript
layui form表单提交之后重新加载数据表格的方法
2019/09/11 Javascript
原生js实现ajax请求和JSONP跨域请求操作示例
2020/03/14 Javascript
微信小程序文章列表功能完整实例
2020/06/03 Javascript
nuxt 自定义 auth 中间件实现令牌的持久化操作
2020/11/05 Javascript
python 协程 gevent原理与用法分析
2019/11/22 Python
在html5的Canvas上绘制椭圆的几种方法总结
2013/01/07 HTML / CSS
For Art’s Sake官网:手工制作的奢华眼镜
2018/12/15 全球购物
墨尔本最受欢迎的复古风格品牌:Princess Highway
2018/12/21 全球购物
EMPHASIS艾斐诗官网:周生生旗下原创精品珠宝品牌
2020/12/17 全球购物
能否解释一下XSS cookie盗窃是什么意思
2012/06/02 面试题
个人求职简历中英文自我评价
2013/12/16 职场文书
学校联谊活动方案
2014/02/15 职场文书
新年爱情寄语
2014/04/08 职场文书
消防安全宣传口号
2014/06/10 职场文书
公司新人试用期自我评价
2014/09/17 职场文书
2015年物业管理工作总结
2015/04/23 职场文书
《窃读记》教学反思
2016/02/18 职场文书
MySQL系列之三 基础篇
2021/07/02 MySQL
2022新作动画《福星小子》释出宣传影片 加入内田真礼&宫野真守配音演出
2022/04/08 日漫