Vue.js 源碼分析(十二) 基礎篇 組件詳解

来源:https://www.cnblogs.com/greatdesert/archive/2019/06/26/11088574.html
-Advertisement-
Play Games

組件是可復用的Vue實例,一個組件本質上是一個擁有預定義選項的一個Vue實例,組件和組件之間通過一些屬性進行聯繫。 組件有兩種註冊方式,分別是全局註冊和局部註冊,前者通過Vue.component()註冊,後者是在創建Vue實例的時候在components屬性里指定,例如: 渲染DOM為: 其中He ...


組件是可復用的Vue實例,一個組件本質上是一個擁有預定義選項的一個Vue實例,組件和組件之間通過一些屬性進行聯繫。

組件有兩種註冊方式,分別是全局註冊和局部註冊,前者通過Vue.component()註冊,後者是在創建Vue實例的時候在components屬性里指定,例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
        <child title="Hello Wrold"></child>
        <hello></hello>
        <button @click="test">測試</button>
    </div>
    <script>
        Vue.component('child',{                     //全局註冊
            props:['title'],
            template:"<p>{{title}}</p>"
        })
        var app = new Vue({
            el:'#app',
            components:{
                hello:{template:'<p>Hello Vue</p>'} //局部組件
            },
            methods:{
                test:function(){
                    console.log(this.$children)                           
                    console.log(this.$children[1].$parent ===this)        
                }
            }
        })
    </script>
</body>
</html>

渲染DOM為:

其中Hello World是全局註冊的組件渲染出來的,而Hello Vue是局部組件渲染出來的。

我們在測試按鈕上綁定了一個事件,點擊按鈕後輸出如下:

可以看到Vue實例的$children屬性是個數組,對應的是當前實例引用的所有組件的實例,其中$children[0]是全局組件child的實例,而children[1]是局部組件hello的實例。

而this.$children[1].$parent ===this輸出為true則表示對於組件實例來說,它的$parent指向的父組件實例,也就是例子里的根組件實例。

Vue內部也是通過$children和$parent屬性實現了組件和組件之間的關聯的。

組件是可以無限復用的,比如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
        <child title="Hello Wrold"></child>
        <child title="Hello Vue"></child>
        <child title="Hello Rose"></child>
    </div>
    <script>
        Vue.component('child',{                   
            props:['title'],
            template:"<p>{{title}}</p>"
        })
        var app = new Vue({el:'#app'})
    </script>
</body>
</html>

渲染為:

註:對於組件來說,需要把data屬性設為一個函數,內部返回一個數據對象,因為如果只返回一個對象,當組件復用時,不同的組件引用的data為同一個對象,這點和根Vue實例不同的,可以看官網的例子:點我點我

例1:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
        <child ></child>
    </div>
    <script>
        Vue.component('child',{    
            data:{title:"Hello Vue"},
            template:"<p>{{title}}</p>"
        })
        var app = new Vue({el:'#app'})
    </script>
</body>
</html>

運行時瀏覽器報錯了,如下:

報錯的內部實現:Vue註冊組件時會先執行Vue.extend(),然後執行mergeOptions合併一些屬性,執行到data屬性的合併策略時會做判斷,如下:

strats.data = function (              //data的合併策略          第1196行
  parentVal,
  childVal,
  vm
) {
  if (!vm) {                            //如果vm不存在,對於組件來說是不存在的
    if (childVal && typeof childVal !== 'function') {     //如果值不是一個函數,則報錯
      "development" !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      );

      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
};

 

 源碼分析


以這個例子為例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
        <child title="Hello Wrold"></child> 
    </div>
    <script>
        Vue.component('child',{
            props:['title'],
            template:"<p>{{title}}</p>"
        })
        var app = new Vue({el:'#app',})
    </script>
</body>
</html>

Vue內部會執行initGlobalAPI()函數給大Vue增加一些靜態方法,其中會執行一個initAssetRegisters函數,該函數會給Vue的原型增加一個Vue.component、Vue.directive和Vue.filter函數函數,分別用於註冊組件、指令和過濾器,如下

function initAssetRegisters (Vue) {       //初始化component、directive和filter函數 第4850行
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(function (type) {     //遍歷//ASSET_TYPES數組 ASSET_TYPES是一個數組,定義在339行,等於:['component','directive','filter']
    Vue[type] = function (
      id,
      definition
    ) {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if ("development" !== 'production' && type === 'component') {
          validateComponentName(id);
        }
        if (type === 'component' && isPlainObject(definition)) {      //如果是個組件
          definition.name = definition.name || id;
          definition = this.options._base.extend(definition);           //則執行Vue.extend()函數     ;this.options._base等於大Vue,定義在5050行
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition };
        }
        this.options[type + 's'][id] = definition;           //將definition保存到this.options[type + 's']里,例如組件保存到this.options['component']裡面
        return definition
      }
    };
  });
}

Vue.extend()將使用基礎Vue構造器,創建一個“子類”。參數是一個包含組件選項的對象,也就是註冊組件時傳入的對象,如下:

  Vue.extend = function (extendOptions) {       //初始化Vue.extend函數  第4770行
    extendOptions = extendOptions || {};
    var Super = this;
    var SuperId = Super.cid;
    var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    var name = extendOptions.name || Super.options.name;
    if ("development" !== 'production' && name) {
      validateComponentName(name);
    }

    var Sub = function VueComponent (options) {             //定義組件的構造函數,函數最後會返回該函數
      this._init(options);
    };
    /*中間進行一些數據的合併*/
    // cache constructor
    cachedCtors[SuperId] = Sub;
    return Sub
  };
}

以例子為例,當載入完後,我們在控制台輸入console.log(Vue.options["components"]),輸出如下:

可以看到child組件的構造函數被保存到Vue.options["components"]["child“]裡面了。其他三個KeepAlive、Transition和TransitionGroup是Vue的內部組件

當vue載入時會執行模板生成的render函數,例子里的render函數等於:

執行_c('child',{attrs:{"title":"Hello Wrold"}})函數時會執行vm.$createElement()函數,也就是Vue內部的createElement函數,如下

function createElement (      //創建vNode 第4335行
  context,
  tag,
  data,
  children,
  normalizationType,
  alwaysNormalize
) {
  if (Array.isArray(data) || isPrimitive(data)) {     //如果data是個數組或者是基本類型
    normalizationType = children;
    children = data;                                      //修正data為children
    data = undefined;                                     //修正data為undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE;
  }
  return _createElement(context, tag, data, children, normalizationType)    //再調用_createElement
}

function _createElement (     //創建vNode
  context,                       //context:Vue對象
  tag,                           //tag:標簽名或組件名
  data,
  children,
  normalizationType
) {
  /**/
  if (typeof tag === 'string') {      //如果tag是個字元串
    var Ctor;
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
    if (config.isReservedTag(tag)) {                                                //如果tag是平臺內置的標簽
      // platform built-in elements
      vnode = new VNode(                                                                //調用new VNode()去實例化一個VNode
        config.parsePlatformTagName(tag), data, children,   
        undefined, undefined, context
      );
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {   //如果該節點名對應一個組件,掛載組件時,如果某個節點是個組件,則會執行到這裡
      // component  
      vnode = createComponent(Ctor, data, context, children, tag);                    //創建組件Vnode
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      );
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children);
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) { applyNS(vnode, ns); }
    if (isDef(data)) { registerDeepBindings(data); }
    return vnode                                                                    //最後返回VNode
  } else {
    return createEmptyVNode()
  }
}
resolveAsset()用於獲取資源,也就是獲取組件的構造函數(在上面Vue.extend裡面定義的構造函數),定義如下:
function resolveAsset (       //獲取資源 第1498行
  options,
  type,
  id,
  warnMissing
) {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  var assets = options[type];
  // check local registration variations first
  if (hasOwn(assets, id)) { return assets[id] }                        //先從當前實例上找id
  var camelizedId = camelize(id);
  if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }     //將id轉化為駝峰式後再找
  var PascalCaseId = capitalize(camelizedId);
  if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }   //如果還沒找到則嘗試將首字母大寫查找
  // fallback to prototype chain
  var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];  //最後通過原型來查找
  if ("development" !== 'production' && warnMissing && !res) {
    warn(
      'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
      options
    );
  }
  return res
}

例子里執行到這裡時就可以獲取到在Vue.extend()里定義的Sub函數了,如下:

我們點擊這個函數時會跳轉到Sub函數,如下:

回到_createElement函數,獲取到組件的構造函數後就會執行createComponent()創建組件的Vnode,這一步對於組件來說很重要,它會對組件的data、options、props、自定義事件、鉤子函數、原生事件、非同步組件分別做一步處理,對於組件的實例化來說,最重要的是安裝鉤子吧,如下:

function createComponent (      //創建組件Vnode 第4182行 Ctor:組件的構造函數  data:數組 context:Vue實例  child:組件的子節點
  Ctor,
  data,
  context,
  children,
  tag
) {
  /**/
  // install component management hooks onto the placeholder node
  installComponentHooks(data);                //安裝一些組件的管理鉤子

  /**/  
  var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
  );                                          //創建組件Vnode
  return vnode                                //最後返回vnode
}

installComponentHooks()會給組件安裝一些管理鉤子,如下:

function installComponentHooks (data) {         //安裝組件的鉤子 第4307行
  var hooks = data.hook || (data.hook = {});        //嘗試獲取組件的data.hook屬性,如果沒有則初始化為空對象
  for (var i = 0; i < hooksToMerge.length; i++) {   //遍歷hooksToMerge里的鉤子,保存到hooks對應的key裡面
    var key = hooksToMerge[i];
    hooks[key] = componentVNodeHooks[key];
  }
}

componentVNodeHooks保存了組件的鉤子,總共有四個:init、prepatch、insert和destroy,對應組件的四個不同的時期,以例子為例執行完後data.hook等於如下:

最後將虛擬VNode渲染為真實DOM節點的時候會執行n createelm()函數,該函數會優先執行createComponent()函數去創建組件,如下:

  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {     //創建組件節點 第5590行   ;註:這是patch()函數內的createComponent()函數,而不是全局的createComponent()函數
    var i = vnode.data;                                                               //獲取vnode的data屬性
    if (isDef(i)) {                                                                   //如果存在data屬性(組件vnode肯定存在這個屬性,普通vnode有可能存在)
      var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;              //這是keepAlive邏輯,可以先忽略
      if (isDef(i = i.hook) && isDef(i = i.init)) {                                   //如果data里定義了hook方法,且存在init方法
        i(vnode, false /* hydrating */, parentElm, refElm);
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue);
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
      }
    }
  }

createComponent會去執行組件的init()鉤子函數:

  init: function init (         //組件的安裝 第4110行
    vnode,                        //vnode:組件的占位符VNode
    hydrating,                    //parentElm:真實的父節點引用
    parentElm,                    //refElm:參考節點
    refElm
  ) {
    if (                                                        //這是KeepAlive邏輯
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      var mountedNode = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      var child = vnode.componentInstance = createComponentInstanceForVnode(      //調用該方法返回子組件的Vue實例,並保存到vnode.componentInstance屬性上
        vnode,
        activeInstance,
        parentElm,
        refElm
      );
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },

createComponentInstanceForVnode會創建組件的實例,如下:

function createComponentInstanceForVnode (      //第4285行 創建組件實例 vnode:占位符VNode parent父Vue實例 parentElm:真實的DOM節點  refElm:參考節點
  vnode, // we know it's MountedComponentVNode but flow doesn't
  parent, // activeInstance in lifecycle state
  parentElm,
  refElm
) {
  var options = {
    _isComponent: true,
    parent: parent,
    _parentVnode: vnode,
    _parentElm: parentElm || null,
    _refElm: refElm || null
  };
  // check inline-template render functions
  var inlineTemplate = vnode.data.inlineTemplate;               //嘗試獲取inlineTemplate屬性,定義組件時如果指定了inline-template特性,則組件內的子節點都是該組件的模板
  if (isDef(inlineTemplate)) {                                  //如果inlineTemplate存在,我們這裡是不存在的
    options.render = inlineTemplate.render; 
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  return new vnode.componentOptions.Ctor(options)               //調用組件的構造函數(Vue.extend()裡面定義的)返回子組件的實例,也就是Vue.extend()里定義的Sub函數
}

最後Vue.extend()里的Sub函數會執行_init方法對Vue做初始化,初始化的過程中會定義組件實例的$parent和父組件的$children屬性,從而實現父組件和子組件的互連,組件的大致流程就是這樣子


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

-Advertisement-
Play Games
更多相關文章
  • 父級元素: letter-spacing: -0.5em;font-size: 0; 子級元素: letter-spacing: normal; display: inline-block; vertical-align: top; 即可解決。 問題剖析: 以input元素為例子:因為input元素 ...
  • 不支持IE瀏覽器(需要使用flash插件), 支持移動端, 未經過完全測試 PC端使用的時候, HTML頁面需要預留video標簽, canvas標簽 移動端使用的時候, HTML頁面需要預留file標簽, canvas標簽, img標簽 ...
  • 01. 變數提升與函數提升 1. 變數聲明提升* 通過var定義(聲明)的變數, 在定義語句之前就可以訪問到* 值: undefined2. 函數聲明提升* 通過function聲明的函數, 在之前就可以直接調用* 值: 函數定義(對象)3. 問題: 變數提升和函數提升是如何產生的? 02. 執行上 ...
  • 理解閉包,先瞭解一下Javascript變數的作用域。有兩種,全局變數和局部變數。 例子1: a是全局變數,b是局部變數。函數內部可以直接讀取全局變數,但是在函數外部無法讀取函數內部的局部變數。 如何從外部讀取函數內部的局部變數? 例子2: fun()將局部變數b作為返回結果; 例子3: 簡單分析一 ...
  • 1.$Vue.$refs.addForm.resetFields() 的resetFields()方法重置到預設值並不是 ,你在form綁定對象上寫的預設值 ,而是這個form被渲染出來之後第一次賦到對象上的值 !! ...
  • 開發場景中用到display:inline-block;然後呢,div間就有間隙,但是ajax載入出來的數據沒有間隙,解決辦法如下 display:inline-block表示行內塊元素,後面自帶空格字元。當然有空隙,如果想去掉空隙,在父繫上加font-size:0;然後,在子div上加一個font ...
  • 我們的使命是讓您瞭解最新和最酷的Web開發趨勢。這就是為什麼我們每個月都會發佈一些精選的資源,這些資源是我們偶然發現並認為值得您關註的。 1.Tessaract.js 強大的javascript(節點和瀏覽器)庫,用於從圖像中提取文本。它能自動檢測文本的位置和方向,識別60多種語言,包括中文、阿拉伯 ...
  • 如何使用html+css實現元素的水平與垂直居中效果,這也是我們網頁在編碼製作中會經常用到的問題。 1)單行文本的居中 主要實現css代碼: 水平居中:text-align:center;垂直居中:line-height:XXpx; /*line-height與元素的height的值一致*/ 我們先 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...