深入浅析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 相关文章推荐
JS 巧妙获取剪贴板数据 Excel数据的粘贴
Jul 09 Javascript
js函数的延迟加载实现代码
Oct 11 Javascript
jquery中对于批量deferred的处理方法
Jan 22 Javascript
jquery五角星评分插件示例分享
Feb 21 Javascript
使用javascript实现简单的选项卡切换
Jan 09 Javascript
javascript实现ecshop搜索框键盘上下键切换控制
Mar 18 Javascript
H5用户注册表单页 注册模态框!
Sep 17 Javascript
JS 事件绑定、事件监听、事件委托详细介绍
Sep 28 Javascript
jquery实现(textarea)placeholder自动换行
Dec 22 Javascript
JS 组件系列之Bootstrap Table 冻结列功能IE浏览器兼容性问题解决方案
Jun 30 Javascript
详解vue中axios的使用与封装
Mar 20 Javascript
angular4+百分比进度显示插件用法示例
May 05 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
神族 PROTOSS 概述
2020/03/14 星际争霸
PHP获取网卡地址的代码
2008/04/09 PHP
Yii学习总结之数据访问对象 (DAO)
2015/02/22 PHP
PHP设计模式之装饰者模式代码实例
2015/05/11 PHP
php数据访问之增删改查操作
2016/05/09 PHP
FireFox JavaScript全局Event对象
2009/06/14 Javascript
js实现在同一窗口浏览图片
2014/09/17 Javascript
JavaScript的History API使搜索引擎抓取AJAX内容
2015/12/07 Javascript
layer弹出层中H5播放器全屏出错的解决方法
2017/02/21 Javascript
详解Angular 4.x Injector
2017/05/04 Javascript
微信小程序与php 实现微信支付的简单实例
2017/06/23 Javascript
使用jQuery实现两个div中按钮互换位置的实例代码
2017/09/21 jQuery
Vue模板语法中数据绑定的实例代码
2019/05/17 Javascript
[51:36]Optic vs Newbee 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/18 DOTA
python实现对excel进行数据剔除操作实例
2017/12/07 Python
分享vim python缩进等一些配置
2018/07/02 Python
Python多叉树的构造及取出节点数据(treelib)的方法
2019/08/09 Python
Python中的相关分析correlation analysis的实现
2019/08/29 Python
python模拟实现斗地主发牌
2020/01/07 Python
pycharm激活码免费分享适用最新pycharm2020.2.3永久激活
2020/11/25 Python
使用Python制作一盏 3D 花灯喜迎元宵佳节
2021/02/26 Python
CSS3 清除浮动的方法示例
2018/06/01 HTML / CSS
关于老式浏览器兼容HTML5和CSS3的问题
2016/06/01 HTML / CSS
HTML5 canvas实现的静态循环滚动播放弹幕
2021/01/05 HTML / CSS
Bally巴利中国官网:经典瑞士鞋履、手袋及配饰奢侈品牌
2018/10/09 全球购物
思想汇报格式
2014/01/05 职场文书
优良学风班总结材料
2014/02/08 职场文书
学生会宣传部部长竞选演讲稿
2014/04/25 职场文书
旅游专业毕业生自荐书
2014/06/30 职场文书
2015年林业工作总结
2015/05/14 职场文书
入党介绍人意见2015
2015/06/01 职场文书
2016元旦主持人开场白
2015/12/03 职场文书
创业计划书之花店
2019/09/20 职场文书
Python 机器学习工具包SKlearn的安装与使用
2021/05/14 Python
「月刊Comic Alive」2022年5月号封面公开
2022/03/21 日漫
Redis分布式锁的7种实现
2022/04/01 Redis