经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » HTML/CSS » HTML5 » 查看文章
Vue.js 源码分析(二十六) 高级应用 作用域插槽 详解
来源:cnblogs  作者:大沙漠  时间:2019/7/19 8:56:45  对本文有异议

普通的插槽里面的数据是在父组件里定义的,而作用域插槽里的数据是在子组件定义的。

有时候作用域插槽很有用,比如使用Element-ui表格自定义模板时就用到了作用域插槽,Element-ui定义了每个单元格数据的显示格式,我们可以通过作用域插槽自定义数据的显示格式,对于二次开发来说具有很强的扩展性。

作用域插槽使用<template>来定义模板,可以带两个参数,分别是:

     slot-scope    ;模板里的变量,旧版使用scope属性

     slot              ;该作用域插槽的name,指定多个作用域插槽时用到,默认为default,即默认插槽

例如:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  7. </head>
  8. <body>
  9. <div id="app">
  10. <Child>
  11. <template slot="header" slot-scope="props"> <!--定义了名为header的作用域插槽的模板-->
  12. <h1>{{props.info.name}}-{{props.info.age}}</h1>
  13. </template>
  14. <template slot-scope="show"> <!--定义了默认作用域插槽的模板-->
  15. <p>{{show.today}}</p>
  16. </template>
  17. </Child>
  18. </div>
  19. <script>
  20. Vue.config.productionTip=false;
  21. Vue.config.devtools=false;
  22. Vue.component('Child',{
  23. template:`<div class="container">
  24. <header><slot name="header" :info="info"></slot></header> //header插槽
  25. <main><slot today="礼拜一">默认内容</slot></main> //默认插槽
  26. </div>`,
  27. data(){
  28. return { info:{name:'ge',age:25} }
  29. }
  30. })
  31. debugger
  32. new Vue({
  33. el: '#app',
  34. data:{
  35. title:'我是标题',
  36. msg:'我是内容'
  37. }
  38. })
  39. </script>
  40. </body>
  41. </html>

我们在子组件定义了两个插槽,如下:

     header插槽内通过v-bind绑定了一个名为info的特性,值为一个对象,包含一个name和age属性

     另一个是普通插槽,传递了一个today特性,值为礼拜一

父组件引用子组件时定义了模板,渲染后结果如下:

对应的html代码如下:

其实Vue内部把父组件template下的子节点编译成了一个函数,在子组件实例化时调用的,所以作用域才是子组件的作用域

 

 源码分析


 父组件解析模板将模板转换成AST对象时会执行processSlot()函数,如下:

  1. function processSlot (el) { //第9767行 解析slot插槽
  2. if (el.tag === 'slot') { //如果是slot
  3. /*普通插槽的逻辑*/
  4. } else {
  5. var slotScope;
  6. if (el.tag === 'template') { //如果标签名为template(作用域插槽的逻辑)
  7. slotScope = getAndRemoveAttr(el, 'scope'); //尝试获取scope
  8. /* istanbul ignore if */
  9. if ("development" !== 'production' && slotScope) { //在开发环境下报一些信息,因为scope属性已淘汰,新版本开始用slot-scope属性了
  10. warn$2(
  11. "the \"scope\" attribute for scoped slots have been deprecated and " +
  12. "replaced by \"slot-scope\" since 2.5. The new \"slot-scope\" attribute " +
  13. "can also be used on plain elements in addition to <template> to " +
  14. "denote scoped slots.",
  15. true
  16. );
  17. }
  18. el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope'); //获取slot-scope特性,值保存到AST对象的slotScope属性里
  19. } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
  20. /*其它分支*/
  21. }
  22. var slotTarget = getBindingAttr(el, 'slot'); //尝试获取slot特性
  23. if (slotTarget) { //如果获取到了
  24. el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget; //则保存到el.slotTarget里面
  25. // preserve slot as an attribute for native shadow DOM compat
  26. // only for non-scoped slots.
  27. if (el.tag !== 'template' && !el.slotScope) {
  28. addAttr(el, 'slot', slotTarget);
  29. }
  30. }
  31. }
  32. }

执行到这里,对于<template slot="header" slot-scope="props"> 节点来说,添加了一个slotScope和slotTarget属性,如下:

 

对于<template slot-scope="show">节点来说,由于没有定义slot属性,它的AST对象如下:

作用域插槽和普通节点最大的不同点是它不会将当前结点挂在AST对象树上,而是挂在了父节点的scopedSlots属性上。

在解析完节点属性后会执行start()函数内的末尾会判断如果发现AST对象.slotScope存在,则会在currentParent对象(也就是父AST对象)的scopedSlots上新增一个el.slotTarget属性,值为当前template对应的AST对象。 

  1. if (currentParent && !element.forbidden) { //第9223行 解析模板时的逻辑 如果当前对象不是根对象, 且不是style和text/javascript类型script标签
  2. if (element.elseif || element.else) { //如果有elseif或else指令存在(设置了v-else或v-elseif指令)
  3. processIfConditions(element, currentParent);
  4. } else if (element.slotScope) { // scoped slot //如果存在slotScope属性,即是作用域插槽
  5. currentParent.plain = false;
  6. var name = element.slotTarget || '"default"';(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element; //给父元素增加一个scopedSlots属性,值为数组,每个键名为对应的目标名称,值为对应的作用域插槽AST对象
  7. } else {
  8. currentParent.children.push(element);
  9. element.parent = currentParent;
  10. }
  11. }

这样父节点就存在一个slotTarget属性了,值为对应的作用域插槽AST对象,例子里执行到这一步对应slotTarget如下:

default和header分别对应父组件里的两个template节点

父组件执行generate的时候,如果AST对象的scopedSlots属性存在,则执行genScopedSlots()函数拼凑data:

  1. if (el.scopedSlots) { //如果el.scopedSlots存在,即子节点存在作用域插槽
  2. data += (genScopedSlots(el.scopedSlots, state)) + ","; //调用genScopedSlots()函数,并拼接到data里面
  3. }

genScopedSlots函数会返回scopedSlots:_u([])函数字符串,_u就是全局的resolveScopedSlots函数,genScopedSlots如下:

  1. function genScopedSlots ( //第10390行
  2. slots,
  3. state
  4. ) {
  5. return ("scopedSlots:_u([" + (Object.keys(slots).map(function (key) { //拼凑一个_u字符串
  6. return genScopedSlot(key, slots[key], state) //遍历slots,执行genScopedSlot,将返回值保存为一个数组,作为_u的参数
  7. }).join(',')) + "])")
  8. }

genScopedSlot会拼凑每个slots,如下:

  1. function genScopedSlot ( //第10399行
  2. key,
  3. el,
  4. state
  5. ) {
  6. if (el.for && !el.forProcessed) {
  7. return genForScopedSlot(key, el, state)
  8. }
  9. var fn = "function(" + (String(el.slotScope)) + "){" + //拼凑一个函数,el.slotScope就是模板里设置的slot-scope属性
  10. "return " + (el.tag === 'template'
  11. ? el.if
  12. ? ((el.if) + "?" + (genChildren(el, state) || 'undefined') + ":undefined")
  13. : genChildren(el, state) || 'undefined'
  14. : genElement(el, state)) + "}";
  15. return ("{key:" + key + ",fn:" + fn + "}")
  16. }

解析后生成的render函数如下:

  1. with(this){return _c('div',{attrs:{"id":"app"}},[_c('child',{scopedSlots:_u([{key:"header",fn:function(props){return [_c('h1',[_v(_s(props.info.name)+"-"+_s(props.info.age))])]}},{key:"default",fn:function(show){return [_c('p',[_v(_s(show.today))])]}}])})],1)}

这样看着不清楚,我们整理一下,如下:

  1. with(this) {
  2. return _c(
  3. 'div',
  4. {attrs: {"id": "app"}},
  5. [_c('child', {
  6. scopedSlots: _u([
  7. {key: "header",fn: function(props) {return [_c('h1', [_v(_s(props.info.name) + "-" + _s(props.info.age))])]}},
  8. {key: "default",fn: function(show) {return [_c('p', [_v(_s(show.today))])]}}
  9. ])
  10. }
  11. )],
  12. 1)
  13. }

可以看到_u的参数是一个对象,键名为插槽名,值是一个函数,最后子组件会执行这个函数的,创建子组件的实例时,会将scopedSlots属性保存到data.scopedSlots上

对于子组件的编译过程和普通插槽没有什么区别,唯一不同的是会有attr属性,例子里的组件编译后生成的render函数如下:

  1. with(this){return _c('div',{staticClass:"container"},[_c('header',[_t("header",null,{info:info})],2),_v(" "),_c('main',[_t("default",[_v("默认内容")],{today:"礼拜一"})],2)])}

这样看着也不清楚,我们整理一下,如下:

  1. with(this) {
  2. return _c('div', {staticClass: "container"},
  3. [
  4. _c('header', [_t("header", null, {info: info})], 2),
  5. _v(" "),
  6. _c('main', [_t("default", [_v("默认内容")], {today: "礼拜一"})], 2)
  7. ]
  8. )
  9. }

可以看到最后和普通插槽一样也是执行_t函数的,不过在_t函数内会优先从scopedSlots中获取模板,如下:

  1. function renderSlot ( //渲染插槽
  2. name,
  3. fallback,
  4. props,
  5. bindObject
  6. ) {
  7. var scopedSlotFn = this.$scopedSlots[name]; //尝试从 this.$scopedSlots中获取名为name的函数,也就是我们在上面父组件渲染生成的render函数里的作用域插槽相关函数
  8. var nodes;
  9. if (scopedSlotFn) { // scoped slot //如果scopedSlotFn存在
  10. props = props || {};
  11. if (bindObject) {
  12. if ("development" !== 'production' && !isObject(bindObject)) {
  13. warn(
  14. 'slot v-bind without argument expects an Object',
  15. this
  16. );
  17. }
  18. props = extend(extend({}, bindObject), props);
  19. }
  20. nodes = scopedSlotFn(props) || fallback; //最后执行scopedSlotFn这个函数,参数为props,也就是特性数组
  21. } else {
  22. /*普通插槽的分支*/
  23. }
  24. var target = props && props.slot;
  25. if (target) {
  26. return this.$createElement('template', { slot: target }, nodes)
  27. } else {
  28. return nodes
  29. }
  30. }

最后将nodes返回,也就是在父节点的template内定义的子节点返回,作为最后渲染的节点集合。

原文链接:http://www.cnblogs.com/greatdesert/p/11208087.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号