Vue.js 源碼分析(二十五) 高級應用 插槽 詳解

来源:https://www.cnblogs.com/greatdesert/archive/2019/07/17/11200220.html
-Advertisement-
Play Games

我們定義一個組件的時候,可以在組件的某個節點內預留一個位置,當父組件調用該組件的時候可以指定該位置具體的內容,這就是插槽的用法,子組件模板可以通過slot標簽(插槽)規定對應的內容放置在哪裡,比如: 渲染結果為: 對應的html節點如下: 引用AppLayout這個組件時,我們指定了header和f ...


我們定義一個組件的時候,可以在組件的某個節點內預留一個位置,當父組件調用該組件的時候可以指定該位置具體的內容,這就是插槽的用法,子組件模板可以通過slot標簽(插槽)規定對應的內容放置在哪裡,比如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/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=false;
        Vue.config.devtools=false;
        Vue.component('AppLayout',{                                     //子組件,通過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標簽(普通插槽,子組件的邏輯))
    /**/
  } else {
    var slotScope;
    if (el.tag === 'template') {                                        //如果標簽名為template(作用域插槽的邏輯)
      /**/
    } else 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
      // preserve 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為false  
    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:"header"},[_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> because slots are abstract outlets " +
        "and can possibly expand into multiple elements. " +
        "Use the key on a wrapping element instead."
      );
    }
  } else {                          
    /**/
  }
}

接下來在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 because 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;
    opts.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 used by render functions compiled from templates
  vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  var parentData = parentVnode && parentVnode.data;

  /* istanbul ignore else */
  {
    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 || []);
      } else {                                                              //如果child.tag不是template
        slot.push(child);                                                       //則push到slot裡面(等於外層的slots[name])
      }
    } else {
      (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;
  } else {
    var slotNodes = this.$slots[name];                   //先嘗試從父組件那裡獲取該插槽的內容,this.$slots就是上面子組件實例化時生成的$slots對象里的信息
    // warn duplicate slot usage
    if (slotNodes) {                                     //如果該插槽VNode存在
      if ("development" !== 'production' && slotNodes._rendered) {  //如果該插槽已存在(避免重覆使用),則報錯
        warn(
          "Duplicate presence of slot \"" + name + "\" found in the same render tree " +
          "- this will likely cause render errors.",
          this
        );
      }
      slotNodes._rendered = true;                               //設置slotNodes._rendered為true,避免插槽重覆使用,初始化執行_render時會將每個插槽內的_rendered設置為false的
    }
    nodes = slotNodes || fallback;                      //如果slotNodes(父組件里的插槽內容)存在,則保存到nodes,否則將fallback保存為nodes
  }

  var target = props && props.slot;
  if (target) {
    return this.$createElement('template', { slot: target }, nodes)
  } else {
    return nodes                                        //最後返回nodes
  }
}

OK,搞定。

註:有段時間沒看Vue源碼了,還好平時有在做筆記,很快就理解了,不管什麼框架,後端也是的,語言其實不難,難的是理解框架的設計思想,從事程式員這一行因為要學的東西很多,我們也不可能每個去記住的,所以筆記很重要。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在我們進入主題前,我先先看下獲取網址URL的方法: window.location.href // 設置或獲取整個URL為字元串 window.location.hash // 設置或獲取href屬性中在井號#後面的部分參數 window.location.search // 設置或獲取href屬性 ...
  • 摘要: mpvue中頁面之間傳值(註意:是頁面之間,不是組件之間) 場景:A頁面跳轉B頁面,在B頁面選擇商品,將商品名帶回A頁面並顯示 使用api: getCurrentPages step1: A頁面js: 先定義一個全局的對象that,然後在mouted中把this賦給that step2: B ...
  • 摘要: 小程式索引選擇器,點擊跳轉相應條目,索引可滑動,滑動也可跳轉 場景:城市選擇列表, 汽車品牌選擇列表 所用組件: scroll-view(小程式原生) https://developers.weixin.qq.com/miniprogram/dev/component/scroll-view ...
  • 微信小程式開發實戰教程 一、微信小程式 它是一種混合開發的方式。 是安裝在微信中的程式(一個程式最多2M空間)。 1.1 註冊 1 2 點擊立即註冊:進入下方頁面 3 4 點擊小程式進入表單填寫頁面 5 6 填寫完畢之後提交,會讓你去郵箱中激活。激活之後就可以進入小程式開發了。 1.2 安裝開發工具 ...
  • 在JavaScript中,函數其實就是對象。使函數不同於其他對象的決定性特點是函數存在一個被稱為[[Call]]的內部屬性。內部屬性無法通過代碼訪問而是定義了代碼執行時的行為。ECMAScript為JavaScript的對象定義了多種內部屬性,這些內部屬性都用雙重中括弧來標註。 ​[[Call]]屬 ...
  • wxss文件樣式 .item-image{ width: 80px; height: 80px; margin-right: 2px; } .item-image{ width: 80px; height: 80px; margin-right: 2px; } wxml 佈局 ...
  • 設置錨點的兩種方式 1.設置一個錨點鏈接<a href="#wang">去找汪星人</a> 在頁面中需要的位置設置錨點<a name="wang">汪星人基地</a> 2.設置一個錨點鏈接<a href="#miao">去找喵星人</a> 在頁面中需要的位置設置錨點<h3 id="miao">喵星人 ...
  • 主要介紹了一些數據保存在本地中的一些處理方法,包括cookie和webSrorage的保存優缺點介紹,以及簡單介紹cookie中的坑 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...