首页 > 作文

Vue.js 源码分析(二十五) 高级应用 插槽 详解

更新时间:2023-04-03 03:14:24 阅读: 评论:0

我们定义一个组件的时候,可以在组件的某个节点内预留一个位置,当父组件调用该组件的时候可以指定该位置具体的内容,这就是插槽的用法,子组件模板可以通过slot标签(插槽)规定对应的内容放置在哪里,比如:

<!doctype html><html lang="en"><head>    <meta chart="utf-8">    <title>document</title>    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script></head><body>    <div id="app">        <div>            <app-layout>                <h1 slot="header">{{title}}</h1>                <p>{{msg}}</p>                <p slot="footer"></p>            </app-layout>        </div>    </div>    <script>                vue.config.productiontip=fal;        vue.config.devtools=fal;        vue.component('app夸奖老师layout',{                                     //子组件,通过slot标签预留了三个插槽,分别为header、默认插槽和footer插槽            template:`<div class="container">                        <header><slot name="header"></slot></header>                        <main><slot>默认内容</slot></main>                        <footer><slot name="footer"><h1>默认底部</h1></slot></footer>                      </div>`        })        new vue({          el: '#app',          template:``,          data:{            title:'我是标题',msg:'我是内容'          }        })    </script></body></html>

渲染结果为:

对应的html节点如下:

引用applayout这个组件时,我们指定了header和footer这两个插槽的内容

对于普通插槽来说,插槽里的作用域是父组件的,例如父组件里的<h1 slot=”header”>{{title}}</h1>,里面的{{title}}是在父组件定义的,如果需要使用子组件的插槽,可以使用作用域插槽来实现。

源码分析

vue内部对插槽的实现原理是子组件渲染模板时发现是slot标签则转换为一个_t函数,然后把slot标签里的内容也就是子节点vnode的集合作为一个_t函数的参数,_t等于vue全局的renderslot()函数。

插槽的实现先从父组件实例化开始,如下:

父组件解析模板将模板转换成ast对象时会执行processslot函数,如下:

function processslot (el) {         //第9467行  解析slot插槽  if (el.tag === 'slot') {                          //如果是slot标签(普通插槽,子组件的逻辑))    /*略*/  } el {    var slotscope;    if (el.tag === 'template') {                                        //如果标签名为template(作用域插槽的逻辑)      /*略*/    } el if ((slotscope = getandremoveattr(el, 'slot-scope'))) {      //然后尝试获取slot-scope属性(作用域插槽的逻辑)     /*略*/    }    var slottarget = getbindingattr(el, 'slot');                        //尝试获取slot特性        ;例如例子里的<h1 slot="header">{{title}}</h1>会执行到这里    if (slottarget) {                                                   //如果获取到了      el.slottarget = slottarget === '""' ? '"default"' : slottarget;        //则将值保存到el.slottarget里面,如果不存在,则默认为default      // prerve slot as an attribute for native shadow dom compat      // only for non-scoped slots.      if (el.tag !== 'template' && !el.slotscope) {                         //如果当前不是template标签 且 el.slotscoped非空        addattr(el, 'slot', slottarget);                                        //则给el.slot增加一个ieslottarget属性      }    }  }}

执行到这里后如果父组件某个节点有一个slot的属性则会新增一个slottarget属性,例子里的父组件解析完后对应的ast对象如下:

接下来在generate将ast转换成render函数执行gendata$2获取data属性时会判断如果ast.slottarget存在且el.slotscope不存在(即是普通插槽,而不是作用域插槽),则data上添加一个slot属性,值为对应的值 ,如下:

function gendata$2 (el, state) {    //第10274行  /*略*/   if (el.slottarget && !el.slotscope) {         //如果el有设置了slot属性 且 el.slotscope为fal      data += "slot:" + (el.slottarget) + ",";        //则拼凑到data里面  }  /*略*/}

例子里的父组件执行到这里对应的rendre函数如下:

with(this){return _c('div',{attrs:{"id":"app"}},[_c('div',[_c('app-layout',[_c('h1',{attrs:{"slot":"header"},slot:"headeones和one的区别r"},[_v(_s(title))]),_v(" "),_c('p',[_v(_s(msg))]),_v(" "),_c('p',{attrs:{"slot":"footer"},slot:"footer"})])],1)])}

这样看得不清楚,我们把render函数整理一下,如下:

with(this) {    return _c('div', {attrs: {"id": "app"}},                [_c('div',                     [_c('app-layout',                         [                            _c('h1', {attrs: {"slot": "header"},slot: "header"},[_v(_s(title))]),                             _v(" "),                             _c('p', [_v(_s(msg))]),                             _v(" "),                             _c('p', {attrs: {"slot": "footer"},slot: "footer"})                        ])                    ],                 1)                ]            )}

我们看到引用一个组件时内部的子节点会以一个vnode数组的形式传递给子组件,由于函数是从内到外执行的,因此该render函数渲染时会先执行子节点vnode的生成,然后再调用_c(‘app-layout’, …)去生成子组件vnode

父组件创建子组件的占位符vnode时会把子节点vnode以数组形式保存到占位符vnode.componentoptions.children属性上。

接下来是子组件的实例化过程:

子组件在解析模板将模板转换成ast对象时也会执行processslot()函数,如下:

function processslot (el) {         //第9467行  解析slot插槽  if (el.tag === 'slot') {              //如果是slot标签(普通插槽,子组件的逻辑))    el.slotname = getbindingattr(el, 'name');        //获取name,保存到slotname里面,如果没有设置name属性(默认插槽),则el.slotname=undefined    if ("development" !== 'production' && el.key) {      warn$2(        "`key` does not work on <slot> becau slots are abstract outlets " +        "and can possibly expand into multiple elements. " +        "u the key on a wrapping element instead."      );    }  } el {                              /*略*/  }}

接下来在generate将ast转换成rende函数时,在genelement()函数执行的时候如果判断当前的标签是slot标签则执行genslot()函数,如下:

function genslot (el, state) {      //第10509行  渲染插槽(slot节点)  var slotname = el.slotname || '"default"';            //获取插槽名,如果未指定则修正为default  var children = genchildren(el, state);                //获取插槽内的子节点  var res = "_t(" + slotname + (children ? ("," + children) : '');      //拼凑函数_t  var attrs = el.attrs && ("{" + (el.attrs.map(function (a) { return ((camelize(a.name)) + ":" + (a.value)); }).join(',')) + "}");  //如果该插槽有属性     ;作用域插槽是有属性的  var bind$$1 = el.attrsmap['v-bind'];    if ((attrs || bind$$1) && !children) {    res += ",null";  }  if (attrs) {    res += "," + attrs;  }  if (bind$$1) {    res += (attrs ? '' : ',null') + "," + bind$$1;  }  return res + ')'                                  //最后返回res字符串}

通过genslot()处理后,vue会把slot标签转换为一个_t函数,子组件渲染后生成的render函数如下:

with(this){return _c('div',{staticclass:"container"},[_c('header',[_t("header")],2),_v(" "),_c('main',[_t("default",[_v("默认内容")])],2),_v(" "),_c('footer',[_t("footer",[_c('h1',[_v("默认底部")])])],2)])}

这样看得也不清楚,我们把render函数整理一下,如下:

with(this) {    return _c('div', {staticclass: "container"},            [                _c('header', [_t("header")], 2),                 _v(" "),                 _c('main', [_t("default", [_v("默认内容")])], 2),                 _v(" "),                 _c('footer', [_t("footer", [_c('h1', [_v("默认底部")])])], 2)            ]        )}

可以看到slot标签转换成_t函数了。

接下来是子组件的实例化过程,实例化时首先会执行_init()函数,_init()函数会执行initinternalcomponent()进行初始化组件函数,内部会将占位符vnode.componentoptions.children保存到子组件实例vm.$options._renderchildren上,如下:

function initinternalcomponent (vm, options) {      //第4632行  子组件初始化子组件  var opts = vm.$options = object.create(vm.constructor.options);  // doing this becau it's faster than dynamic enumeration.  var parentvnode = options._parentvnode;  opts.parent = options.parent;  opts._parentvnode = parentvnode;  opts._parentelm = options._parentelm;  opts._refelm = options._refelm;  var vnodecomponentoptions = parentvnode.componentoptions;     //占位符vnode初始化传入的配置信息  opts.propsdata = vnodecomponentoptions.propsdata;  opts._parentlisteners = vnodecomponentoptions.listeners;  opts._renderchildren = vnodecomponentoptions.children;        //调用该组件时的子节点,在插槽、内置组件里中会用到  opts._componenttag = vnodecomponentoptions.tag;  if (options.render) {    opts.render = options.render;    o党务公开工作总结pts.staticrenderfns = options.staticrenderfns;  }}

执行到这里时例子的_renderchildren等于如下:

这就是我们在父组件内定义的子vnode集合,回到_init()函数,随后会调用initrender()函数,该函数会调用resolveslots()解析vm.$options._renderchildren并保存到子组件实例vm.$slots属性上如下:

function initrender (vm) {              //第4471行  初始化渲染  vm._vnode = null; // the root of the child tree  vm._statictrees = null; // v-once cached trees  var options = vm.$options;  var parentvnode = vm.$vnode = options._parentvnode; // the placeholder node in parent tree  var rendercontext = parentvnode && parentvnode.context;  vm.$slots = resolveslots(options._renderchildren, rendercontext);         //执行resolveslots获取占位符vnode下的slots信息,参数为占位符vnode里的子节点, 执行后vm.$slots格式为:{default:[...],footer:[vnode],header:[vnode]}  vm.$scopedslots = emptyobject;  // bind the createelement fn to this instance  // so that we get proper render context inside it.  // args order: tag, data, children, normalizationtype, alwaysnormalize  // internal version is ud by render functions compiled from templates  vm._c = function (a, b, c, d) { return createelement(vm, a, b, c, d, fal); };  // normalization is always applied for the public version, ud in  // ur-written render functions.  vm.$createelement = function (a, b, c, d) { return createelement(vm, a, b, c, d, true); };  // $attrs & $listeners are expod for easier hoc creation.  // they need to be reactive so that hocs using them are always updated  var parentdata = parentvnode && parentvnode.data;  /* istanbul ignore el */  {    definereactive(vm, '$attrs', parentdata && parentdata.attrs || emptyobject, function () {      !isupdatingchildcomponent && warn("$attrs is readonly.", vm);    }, true);    definereactive(vm, '$listeners', options._parentlisteners || emptyobject, function () {      !isupdatingchildcomponent && warn("$listeners is readonly.", vm);    }, true);  }}

resolveslots会解析每个子节点,并将子节点保存到$slots属性上,如下:

function resolveslots (         //第4471行 分解组件内的子组件  children,                         //占位符vnode里的内容  context                           // context:占位符vnode所在的vue实例) {  var slots = {};                       //缓存最后的结果  if (!children) {                      //如果引用当前组件时没有子节点,则返回空对象    return slots  }  for (var i = 0, l = children.length; i < l; i++) {        //遍历每个子节点    var child = children[i];                                        //当前的子节点    var data = child.data;                                          //子节点的data属性    // remove slot attribute if the node is resolved as a vue slot node    if (data && data.attrs && data.attrs.slot) {                    //城乡规划专业排名如果data.attrs.slot存在    ;例如:"slot": "header"         delete data.attrs.slot;                                           //则删除它     }    // named slots should only be respected if the vnode was rendered in the    // same context.    if ((child.context === context || child.fncontext === context) &&   //如果该子节点有data属性且data.slot非空,即设置了slot属性时      data && data.slot != null                                 ) {      var name = data.slot;                                                  //获取slot的名称      var slot = (slots[name] || (slots[name] = []));                        //如果slots[name]不存在,则初始化为一个空数组      if (child.tag === 'template') {                                       //如果tag是一个template        slot.push.apply(slot, child.children || []);      } el {                                                              //如果child.tag不是template        slot.push(child);                                                       //则push到slot里面(等于外层的slots[name])      }    } el {      (slots.default || (slots.default = [])).push(child);    }  }  // ignore slots that contains only whitespace  for (var name$1 in slots) {    if (slots[name$1].every(iswhitespace)) {      delete slots[name$1];    }  }  return slots                              //最后返回slots}

例子里的子组件执行完后$slot等于:

可以看到:slot是一个对象,键名对应着slot标签的name属性,如果没有name属性,则键名默认为default,值是一个vnode数组,对应着插槽的内容

最后执行_t函数,也就是全局的renderslot函数,该函数就比较简单了,如下:

function renderslot (           //第3725行  渲染插槽   name,                                             //插槽名称  fallback,                                         //默认子节点  props,  bindobject) {  var scopedslotfn = this.$scopedslots[name];  var nodes;                                            //定义一个局部变量,用于返回最后的结果,是个vnode数组  if (scopedslotfn) { // scoped slot                        props = props || {};    if (bindobject) {      if ("development" !== 'production' && !isobject(bindobject)) {        warn(          'slot v-bind without argument expects an object',          this        );      }      props = extend(extend({}, bindobject), props);    }    nodes = scopedslotfn(props) 闪烁的意思|| fallback;  } el {    var slotnodes = this.$slots[name];                   //先尝试从父组件那里获取该插槽的内容,this.$slots就是上面子组件实例化时生成的$slots对象里的信息    // warn duplicate slot usage    if (slotnodes) {                                     //如果该插槽vnode存在      if ("development" !== 'production' && slotnodes._rendered) {  //如果该插槽已存在(避免重复使用),则报错        warn(          "duplicate prence of slot \"" + name + "\" found in the same render tree " +          "- this will likely cau render errors.",          this        );      }      slotnodes._rendered = true;                               //设置slotnodes._rendered为true,避免插槽重复使用,初始化执行_render时会将每个插槽内的_rendered设置为fal的    }    nodes = slotnodes || fallback;                      //如果slotnodes(父组件里的插槽内容)存在,则保存到nodes,否则将fallback保存为nodes  }  var target = props && props.slot;  if (target) {    return this.$createelement('template', { slot: target }, nodes)  } el {    return nodes                                        //最后返回nodes  }}

ok,搞定。

注:有段时间没看vue源码了,还好平时有在做笔记,很快就理解了,不管什么框架,后端也是的,语言其实不难,难的是理解框架的设计思想,从事程序员这一行因为要学的东西很多,我们也不可能每个去记住的,所以笔记很重要。

本文发布于:2023-04-03 03:14:22,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/zuowen/0f0578a9d030647ae689680e04cb4b48.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

本文word下载地址:Vue.js 源码分析(二十五) 高级应用 插槽 详解.doc

本文 PDF 下载地址:Vue.js 源码分析(二十五) 高级应用 插槽 详解.pdf

标签:插槽   组件   函数   节点
相关文章
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图