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

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

  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. <div>
  11. <app-layout>
  12. <h1 slot="header">{{title}}</h1>
  13. <p>{{msg}}</p>
  14. <p slot="footer"></p>
  15. </app-layout>
  16. </div>
  17. </div>
  18. <script>
  19. Vue.config.productionTip=false;
  20. Vue.config.devtools=false;
  21. Vue.component('AppLayout',{ //子组件,通过slot标签预留了三个插槽,分别为header、默认插槽和footer插槽
  22. template:`<div class="container">
  23. <header><slot name="header"></slot></header>
  24. <main><slot>默认内容</slot></main>
  25. <footer><slot name="footer"><h1>默认底部</h1></slot></footer>
  26. </div>`
  27. })
  28. new Vue({
  29. el: '#app',
  30. template:``,
  31. data:{
  32. title:'我是标题',msg:'我是内容'
  33. }
  34. })
  35. </script>
  36. </body>
  37. </html>

 

渲染结果为:

对应的html节点如下:

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

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

 

 源码分析


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

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

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

  1. function processSlot (el) { //第9467行 解析slot插槽
  2. if (el.tag === 'slot') { //如果是slot标签(普通插槽,子组件的逻辑))
  3. /**/
  4. } else {
  5. var slotScope;
  6. if (el.tag === 'template') { //如果标签名为template(作用域插槽的逻辑)
  7. /**/
  8. } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) { //然后尝试获取slot-scope属性(作用域插槽的逻辑)
  9. /**/
  10. }
  11. var slotTarget = getBindingAttr(el, 'slot'); //尝试获取slot特性 ;例如例子里的<h1 slot="header">{{title}}</h1>会执行到这里
  12. if (slotTarget) { //如果获取到了
  13. el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget; //则将值保存到el.slotTarget里面,如果不存在,则默认为default
  14. // preserve slot as an attribute for native shadow DOM compat
  15. // only for non-scoped slots.
  16. if (el.tag !== 'template' && !el.slotScope) { //如果当前不是template标签 且 el.slotScoped非空
  17. addAttr(el, 'slot', slotTarget); //则给el.slot增加一个ieslotTarget属性
  18. }
  19. }
  20. }
  21. }

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

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

  1. function genData$2 (el, state) { //第10274行
  2. /**/
  3. if (el.slotTarget && !el.slotScope) { //如果el有设置了slot属性 且 el.slotScope为false
  4. data += "slot:" + (el.slotTarget) + ","; //则拼凑到data里面
  5. }
  6. /**/
  7. }

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

  1. 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)])}

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

  1. with(this) {
  2. return _c('div', {attrs: {"id": "app"}},
  3. [_c('div',
  4. [_c('app-layout',
  5. [
  6. _c('h1', {attrs: {"slot": "header"},slot: "header"},[_v(_s(title))]),
  7. _v(" "),
  8. _c('p', [_v(_s(msg))]),
  9. _v(" "),
  10. _c('p', {attrs: {"slot": "footer"},slot: "footer"})
  11. ])
  12. ],
  13. 1)
  14. ]
  15. )
  16. }

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

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

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

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

  1. function processSlot (el) { //第9467行 解析slot插槽
  2. if (el.tag === 'slot') { //如果是slot标签(普通插槽,子组件的逻辑))
  3. el.slotName = getBindingAttr(el, 'name'); //获取name,保存到slotName里面,如果没有设置name属性(默认插槽),则el.slotName=undefined
  4. if ("development" !== 'production' && el.key) {
  5. warn$2(
  6. "`key` does not work on <slot> because slots are abstract outlets " +
  7. "and can possibly expand into multiple elements. " +
  8. "Use the key on a wrapping element instead."
  9. );
  10. }
  11. } else {
  12. /**/
  13. }
  14. }

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

  1. function genSlot (el, state) { //第10509行 渲染插槽(slot节点)
  2. var slotName = el.slotName || '"default"'; //获取插槽名,如果未指定则修正为default
  3. var children = genChildren(el, state); //获取插槽内的子节点
  4. var res = "_t(" + slotName + (children ? ("," + children) : ''); //拼凑函数_t
  5. var attrs = el.attrs && ("{" + (el.attrs.map(function (a) { return ((camelize(a.name)) + ":" + (a.value)); }).join(',')) + "}"); //如果该插槽有属性 ;作用域插槽是有属性的
  6. var bind$$1 = el.attrsMap['v-bind'];
  7. if ((attrs || bind$$1) && !children) {
  8. res += ",null";
  9. }
  10. if (attrs) {
  11. res += "," + attrs;
  12. }
  13. if (bind$$1) {
  14. res += (attrs ? '' : ',null') + "," + bind$$1;
  15. }
  16. return res + ')' //最后返回res字符串
  17. }

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

  1. 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函数整理一下,如下:

  1. with(this) {
  2. return _c('div', {staticClass: "container"},
  3. [
  4. _c('header', [_t("header")], 2),
  5. _v(" "),
  6. _c('main', [_t("default", [_v("默认内容")])], 2),
  7. _v(" "),
  8. _c('footer', [_t("footer", [_c('h1', [_v("默认底部")])])], 2)
  9. ]
  10. )
  11. }

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

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

  1. function initInternalComponent (vm, options) { //第4632行 子组件初始化子组件
  2. var opts = vm.$options = Object.create(vm.constructor.options);
  3. // doing this because it's faster than dynamic enumeration.
  4. var parentVnode = options._parentVnode;
  5. opts.parent = options.parent;
  6. opts._parentVnode = parentVnode;
  7. opts._parentElm = options._parentElm;
  8. opts._refElm = options._refElm;
  9. var vnodeComponentOptions = parentVnode.componentOptions; //占位符VNode初始化传入的配置信息
  10. opts.propsData = vnodeComponentOptions.propsData;
  11. opts._parentListeners = vnodeComponentOptions.listeners;
  12. opts._renderChildren = vnodeComponentOptions.children; //调用该组件时的子节点,在插槽、内置组件里中会用到
  13. opts._componentTag = vnodeComponentOptions.tag;
  14. if (options.render) {
  15. opts.render = options.render;
  16. opts.staticRenderFns = options.staticRenderFns;
  17. }
  18. }

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

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

  1. function initRender (vm) { //第4471行 初始化渲染
  2. vm._vnode = null; // the root of the child tree
  3. vm._staticTrees = null; // v-once cached trees
  4. var options = vm.$options;
  5. var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree
  6. var renderContext = parentVnode && parentVnode.context;
  7. vm.$slots = resolveSlots(options._renderChildren, renderContext); //执行resolveSlots获取占位符VNode下的slots信息,参数为占位符VNode里的子节点, 执行后vm.$slots格式为:{default:[...],footer:[VNode],header:[VNode]}
  8. vm.$scopedSlots = emptyObject;
  9. // bind the createElement fn to this instance
  10. // so that we get proper render context inside it.
  11. // args order: tag, data, children, normalizationType, alwaysNormalize
  12. // internal version is used by render functions compiled from templates
  13. vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
  14. // normalization is always applied for the public version, used in
  15. // user-written render functions.
  16. vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
  17. // $attrs & $listeners are exposed for easier HOC creation.
  18. // they need to be reactive so that HOCs using them are always updated
  19. var parentData = parentVnode && parentVnode.data;
  20. /* istanbul ignore else */
  21. {
  22. defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {
  23. !isUpdatingChildComponent && warn("$attrs is readonly.", vm);
  24. }, true);
  25. defineReactive(vm, '$listeners', options._parentListeners || emptyObject, function () {
  26. !isUpdatingChildComponent && warn("$listeners is readonly.", vm);
  27. }, true);
  28. }
  29. }

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

  1. function resolveSlots ( //第4471行 分解组件内的子组件
  2. children, //占位符Vnode里的内容
  3. context // context:占位符Vnode所在的Vue实例
  4. ) {
  5. var slots = {}; //缓存最后的结果
  6. if (!children) { //如果引用当前组件时没有子节点,则返回空对象
  7. return slots
  8. }
  9. for (var i = 0, l = children.length; i < l; i++) { //遍历每个子节点
  10. var child = children[i]; //当前的子节点
  11. var data = child.data; //子节点的data属性
  12. // remove slot attribute if the node is resolved as a Vue slot node
  13. if (data && data.attrs && data.attrs.slot) { //如果data.attrs.slot存在 ;例如:"slot": "header"
  14. delete data.attrs.slot; //则删除它
  15. }
  16. // named slots should only be respected if the vnode was rendered in the
  17. // same context.
  18. if ((child.context === context || child.fnContext === context) && //如果该子节点有data属性且data.slot非空,即设置了slot属性时
  19. data && data.slot != null
  20. ) {
  21. var name = data.slot; //获取slot的名称
  22. var slot = (slots[name] || (slots[name] = [])); //如果slots[name]不存在,则初始化为一个空数组
  23. if (child.tag === 'template') { //如果tag是一个template
  24. slot.push.apply(slot, child.children || []);
  25. } else { //如果child.tag不是template
  26. slot.push(child); //则push到slot里面(等于外层的slots[name])
  27. }
  28. } else {
  29. (slots.default || (slots.default = [])).push(child);
  30. }
  31. }
  32. // ignore slots that contains only whitespace
  33. for (var name$1 in slots) {
  34. if (slots[name$1].every(isWhitespace)) {
  35. delete slots[name$1];
  36. }
  37. }
  38. return slots //最后返回slots
  39. }

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

 

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

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

  1. function renderSlot ( //第3725行 渲染插槽
  2. name, //插槽名称
  3. fallback, //默认子节点
  4. props,
  5. bindObject
  6. ) {
  7. var scopedSlotFn = this.$scopedSlots[name];
  8. var nodes; //定义一个局部变量,用于返回最后的结果,是个VNode数组
  9. if (scopedSlotFn) { // scoped slot
  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;
  21. } else {
  22. var slotNodes = this.$slots[name]; //先尝试从父组件那里获取该插槽的内容,this.$slots就是上面子组件实例化时生成的$slots对象里的信息
  23. // warn duplicate slot usage
  24. if (slotNodes) { //如果该插槽VNode存在
  25. if ("development" !== 'production' && slotNodes._rendered) { //如果该插槽已存在(避免重复使用),则报错
  26. warn(
  27. "Duplicate presence of slot \"" + name + "\" found in the same render tree " +
  28. "- this will likely cause render errors.",
  29. this
  30. );
  31. }
  32. slotNodes._rendered = true; //设置slotNodes._rendered为true,避免插槽重复使用,初始化执行_render时会将每个插槽内的_rendered设置为false的
  33. }
  34. nodes = slotNodes || fallback; //如果slotNodes(父组件里的插槽内容)存在,则保存到nodes,否则将fallback保存为nodes
  35. }
  36. var target = props && props.slot;
  37. if (target) {
  38. return this.$createElement('template', { slot: target }, nodes)
  39. } else {
  40. return nodes //最后返回nodes
  41. }
  42. }

OK,搞定。

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

原文链接:http://www.cnblogs.com/greatdesert/p/11200220.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号