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

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

普通的插槽裡面的數據是在父組件里定義的,而作用域插槽里的數據是在子組件定義的。 有時候作用域插槽很有用,比如使用Element-ui表格自定義模板時就用到了作用域插槽,Element-ui定義了每個單元格數據的顯示格式,我們可以通過作用域插槽自定義數據的顯示格式,對於二次開發來說具有很強的擴展性。 ...


普通的插槽裡面的數據是在父組件里定義的,而作用域插槽里的數據是在子組件定義的。

有時候作用域插槽很有用,比如使用Element-ui表格自定義模板時就用到了作用域插槽,Element-ui定義了每個單元格數據的顯示格式,我們可以通過作用域插槽自定義數據的顯示格式,對於二次開發來說具有很強的擴展性。

作用域插槽使用<template>來定義模板,可以帶兩個參數,分別是:

     slot-scope    ;模板里的變數,舊版使用scope屬性

     slot              ;該作用域插槽的name,指定多個作用域插槽時用到,預設為default,即預設插槽

例如:

<!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">
          <Child>
            <template slot="header" slot-scope="props">                 <!--定義了名為header的作用域插槽的模板-->
              <h1>{{props.info.name}}-{{props.info.age}}</h1>
            </template>
            <template slot-scope="show">                                <!--定義了預設作用域插槽的模板-->
              <p>{{show.today}}</p>
            </template>
          </Child>
      </div>
      <script>
        Vue.config.productionTip=false;
        Vue.config.devtools=false;
        Vue.component('Child',{
          template:`<div class="container">
                        <header><slot name="header" :info="info"></slot></header>     //header插槽
                        <main><slot today="禮拜一">預設內容</slot></main>               //預設插槽
                    </div>`,
              data(){
                return { info:{name:'ge',age:25} }
              }
        })
        debugger
        new Vue({
          el: '#app',
          data:{
            title:'我是標題',
            msg:'我是內容'
          }
        })
      </script>
</body>
</html>

我們在子組件定義了兩個插槽,如下:

     header插槽內通過v-bind綁定了一個名為info的特性,值為一個對象,包含一個name和age屬性

     另一個是普通插槽,傳遞了一個today特性,值為禮拜一

父組件引用子組件時定義了模板,渲染後結果如下:

對應的html代碼如下:

其實Vue內部把父組件template下的子節點編譯成了一個函數,在子組件實例化時調用的,所以作用域才是子組件的作用域

 

 源碼分析


 父組件解析模板將模板轉換成AST對象時會執行processSlot()函數,如下:

function processSlot (el) {       //第9767行   解析slot插槽
  if (el.tag === 'slot') {          //如果是slot
    /*普通插槽的邏輯*/
  } else {
    var slotScope;
    if (el.tag === 'template') {                //如果標簽名為template(作用域插槽的邏輯)
      slotScope = getAndRemoveAttr(el, 'scope');          //嘗試獲取scope
      /* istanbul ignore if */  
      if ("development" !== 'production' && slotScope) {  //在開發環境下報一些信息,因為scope屬性已淘汰,新版本開始用slot-scope屬性了
        warn$2(
          "the \"scope\" attribute for scoped slots have been deprecated and " +
          "replaced by \"slot-scope\" since 2.5. The new \"slot-scope\" attribute " +
          "can also be used on plain elements in addition to <template> to " +
          "denote scoped slots.",
          true
        );
      }
      el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope'); //獲取slot-scope特性,值保存到AST對象的slotScope屬性里
    } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
      /*其它分支*/
    }
    var slotTarget = getBindingAttr(el, 'slot');          //嘗試獲取slot特性
    if (slotTarget) {                                     //如果獲取到了
      el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget;   //則保存到el.slotTarget裡面
      // preserve slot as an attribute for native shadow DOM compat
      // only for non-scoped slots.
      if (el.tag !== 'template' && !el.slotScope) {
        addAttr(el, 'slot', slotTarget);
      }
    }
  }
}

執行到這裡,對於<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對象。 

if (currentParent && !element.forbidden) {    //第9223行  解析模板時的邏輯 如果當前對象不是根對象, 且不是style和text/javascript類型script標簽
  if (element.elseif || element.else) {           //如果有elseif或else指令存在(設置了v-else或v-elseif指令)
    processIfConditions(element, currentParent);
  } else if (element.slotScope) { // scoped slot  //如果存在slotScope屬性,即是作用域插槽
    currentParent.plain = false;
    var name = element.slotTarget || '"default"';(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;   //給父元素增加一個scopedSlots屬性,值為數組,每個鍵名為對應的目標名稱,值為對應的作用域插槽AST對象
  } else { 
    currentParent.children.push(element);
    element.parent = currentParent;
  }
}

這樣父節點就存在一個slotTarget屬性了,值為對應的作用域插槽AST對象,例子里執行到這一步對應slotTarget如下:

default和header分別對應父組件里的兩個template節點

父組件執行generate的時候,如果AST對象的scopedSlots屬性存在,則執行genScopedSlots()函數拼湊data:

  if (el.scopedSlots) {     //如果el.scopedSlots存在,即子節點存在作用域插槽  
    data += (genScopedSlots(el.scopedSlots, state)) + ",";    //調用genScopedSlots()函數,並拼接到data裡面
  } 

genScopedSlots函數會返回scopedSlots:_u([])函數字元串,_u就是全局的resolveScopedSlots函數,genScopedSlots如下:

function genScopedSlots (     //第10390行 
  slots,
  state
) {
  return ("scopedSlots:_u([" + (Object.keys(slots).map(function (key) {     //拼湊一個_u字元串
      return genScopedSlot(key, slots[key], state)                            //遍歷slots,執行genScopedSlot,將返回值保存為一個數組,作為_u的參數
    }).join(',')) + "])")
}

genScopedSlot會拼湊每個slots,如下:

function genScopedSlot ( //第10399行 
  key,
  el,
  state
) {
  if (el.for && !el.forProcessed) {
    return genForScopedSlot(key, el, state)
  }
  var fn = "function(" + (String(el.slotScope)) + "){" +      //拼湊一個函數,el.slotScope就是模板里設置的slot-scope屬性
    "return " + (el.tag === 'template'
      ? el.if
        ? ((el.if) + "?" + (genChildren(el, state) || 'undefined') + ":undefined")
        : genChildren(el, state) || 'undefined'
      : genElement(el, state)) + "}";
  return ("{key:" + key + ",fn:" + fn + "}")
}

解析後生成的render函數如下:

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)}

這樣看著不清楚,我們整理一下,如下:

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)
}

可以看到_u的參數是一個對象,鍵名為插槽名,值是一個函數,最後子組件會執行這個函數的,創建子組件的實例時,會將scopedSlots屬性保存到data.scopedSlots上

對於子組件的編譯過程和普通插槽沒有什麼區別,唯一不同的是會有attr屬性,例子里的組件編譯後生成的render函數如下:

with(this){return _c('div',{staticClass:"container"},[_c('header',[_t("header",null,{info:info})],2),_v(" "),_c('main',[_t("default",[_v("預設內容")],{today:"禮拜一"})],2)])}

這樣看著也不清楚,我們整理一下,如下:

with(this) {
    return _c('div', {staticClass: "container"},
        [
          _c('header', [_t("header", null, {info: info})], 2), 
          _v(" "), 
          _c('main', [_t("default", [_v("預設內容")], {today: "禮拜一"})], 2)
        ]
      )
}

可以看到最後和普通插槽一樣也是執行_t函數的,不過在_t函數內會優先從scopedSlots中獲取模板,如下:

function renderSlot (       //渲染插槽
  name,
  fallback,
  props,
  bindObject
) {
  var scopedSlotFn = this.$scopedSlots[name];           //嘗試從 this.$scopedSlots中獲取名為name的函數,也就是我們在上面父組件渲染生成的render函數里的作用域插槽相關函數
  var nodes;
  if (scopedSlotFn) { // scoped slot                    //如果scopedSlotFn存在
    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;          //最後執行scopedSlotFn這個函數,參數為props,也就是特性數組
  } else {
    /*普通插槽的分支*/
  }

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

最後將nodes返回,也就是在父節點的template內定義的子節點返回,作為最後渲染的節點集合。


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

-Advertisement-
Play Games
更多相關文章
  • 通過使用框架,你可以在同一個瀏覽器視窗中顯示不止一個頁面。 1、框架標簽(Frame) 簡單的三框架頁面: 所有主流瀏覽器都支持 <frame> 標簽。 標簽定義及使用說明 <frame> 標簽定義 <frameset> 中的子視窗(框架)。 <frameset> 中的每個 <frame> 都可以設 ...
  • 今天我在學習jQuery的addClass操作時遇到了一個小問題,想來跟大家分享一下,避免初學者踩坑。 我的需求是製作一個表格,並讓它隔行換色,在此基礎上再加上滑鼠懸浮變色的效果。(主要訓練jQuery選擇器和addClass) 但是在我真正操作後發現我的奇數行都變色了,而偶數行都沒有變。具體效果如 ...
  • css的動畫效果在style標簽中用@keyframes name{。。。}的形式定義,使用animation:name time 。。。的形式進行調用。 示例: <style> div{width:100px;height:100px;background:red;position:relativ ...
  • css3的2d轉化,我所知的有5種。 因為是css屬性,所以必然,2d轉化的設置在style中設置基本格式,transform:XXX;以選擇哪種轉化,還可以設置transition:time;以設置轉化時間(不過不能設置在初始狀態下,否則無效) 下麵按標號進行介紹: 1.translate 有2種 ...
  • 學習中,這篇是試驗品 ...
  • 邊框的陰影效果,可以在css中使用box-shadow來實現 屬性值有X-shadow,Y-shadow,blur,spread,color,inset X-shadow是設置水平方向的陰影值,可為負數 Y-shadow是設置垂直方向的陰影值,可為負數 blur是設置陰影的模糊半徑 spread是設 ...
  • 先說比較普遍的就是滑動的下拉菜單,可以通過CSS來做,也可以用JS做 都挺簡單的,主要是通過父元素觸發,然後在子元素能夠保持住父元素的狀態。 都是用原生寫的。 CSS實現方法: JS實現方法: 第二種比較簡單,就是點擊按鈕觸發二級菜單,只能通過JS觸發。 這個就是做一個判斷,也不難。這個效果是只能來 ...
  • 一、wx:showActionSheet(上拉菜單) 二、wx:showModal(彈窗) 三、showToast / hideToast(載入) Object object 屬性類型預設值必填說明 success function 否 介面調用成功的回調函數 fail function 否 介面調 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...