玩轉Vue組件通信--終極版

引言

簡要總結於 掘金小冊->Vue組件精講
作者:Aresn iView作者

組件通信不管是在業務組件開發還是UI組件開發基本都是不可避免的,實際項目開發中一般會選擇引入vuex,這裏關於vuex不做過多介紹,不瞭解的可直接去 vuex官網 查看。這裏主要使用vue自身的一些東西來達到組件通信的目的。

provide / inject

provide / inject 是 Vue.js 2.2.0 版本後新增的 API,在文檔中這樣介紹 :

這對選項需要一起使用,以允許一個祖先組件向其所有子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。如果你熟悉 React,這與 React 的上下文特性很相似。

並且文檔中有如下提示:

provide 和 inject 主要爲高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。

下面簡要描述使用的Demo

// A.vue
export default {
  provide: {
    name: 'Aresn'
  }
}

// B.vue
export default {
  inject: ['name'],
  mounted () {
    console.log(this.name);  // Aresn
  }
}

需要注意的是:

provide 和 inject 綁定並不是可響應的。這是刻意爲之的。然而,如果你傳入了一個可監聽的對象(傳入一個對象,對象爲引用傳遞),那麼其對象的屬性還是可響應的。

派發與廣播——自行實現 dispatch 和 broadcast 方法

$on 與 $emit

$on 與 $emit常用於父子組件的通信,在vue1.x中跨組件通信 $dispatch 和 $broadcast也是常用的api,在vue2.x中被廢棄了。

$dispatch 和 $broadcast ,前者用於向上級派發事件,只要是它的父級(一級或多級以上),都可以在組件內通過 $on (或 events,2.x 已廢棄)監聽到,後者相反,是由上級向下級廣播事件的。

廢棄原因如下:

因爲基於組件樹結構的事件流方式有時讓人難以理解,並且在組件結構擴展的過程中會變得越來越脆弱。

自行實現 dispatch 和 broadcast 方法

前提需要爲每一個組件添加name屬性的標識,方便遞歸與遍歷查找。

function broadcast(componentName, eventName, params) {
  this.$children.forEach(child => {
    const name = child.$options.name;

    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
export default {
  methods: {
    dispatch(componentName, eventName, params) {
      let parent = this.$parent || this.$root;
      let name = parent.$options.name;

      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.name;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};

上面兩個方法,大家可自己測試!

找到任意組件實例——findComponents 系列方法

provide / inject 和 dispatch / broadcast。它們有各自的使用場景和侷限,比如前者多用於子組件獲取父組件的狀態,後者常用於父子跨級組件間通過自定義事件通信。

findComponents 系列方法最終都是返回組件的實例,進而可以讀取或調用該組件的數據和方法。

它適用於以下場景:

  • 由一個組件,向上找到最近的指定組件;
  • 由一個組件,向上找到所有的指定組件;
  • 由一個組件,向下找到最近的指定組件;
  • 由一個組件,向下找到所有指定的組件;
  • 由一個組件,找到指定組件的兄弟組件。
    5 個不同的場景,對應 5 個不同的函數,實現原理也大同小異。

下面爲具體方法實現:

// 由一個組件,向上找到最近的指定組件
function findComponentUpward (context, componentName) {
    let parent = context.$parent;
    let name = parent.$options.name;

    while (parent && (!name || [componentName].indexOf(name) < 0)) {
        parent = parent.$parent;
        if (parent) name = parent.$options.name;
    }
    return parent;
}
export { findComponentUpward };

// 由一個組件,向上找到所有的指定組件
function findComponentsUpward (context, componentName) {
    let parents = [];
    const parent = context.$parent;

    if (parent) {
        if (parent.$options.name === componentName) parents.push(parent);
        return parents.concat(findComponentsUpward(parent, componentName));
    } else {
        return [];
    }
}
export { findComponentsUpward };

// 由一個組件,向下找到最近的指定組件
function findComponentDownward (context, componentName) {
    const childrens = context.$children;
    let children = null;

    if (childrens.length) {
        for (const child of childrens) {
            const name = child.$options.name;

            if (name === componentName) {
                children = child;
                break;
            } else {
                children = findComponentDownward(child, componentName);
                if (children) break;
            }
        }
    }
    return children;
}
export { findComponentDownward };

// 由一個組件,向下找到所有指定的組件
function findComponentsDownward (context, componentName) {
    return context.$children.reduce((components, child) => {
        if (child.$options.name === componentName) components.push(child);
        const foundChilds = findComponentsDownward(child, componentName);
        return components.concat(foundChilds);
    }, []);
}
export { findComponentsDownward };

// 由一個組件,找到指定組件的兄弟組件
function findBrothersComponents (context, componentName, exceptMe = true) {
    let res = context.$parent.$children.filter(item => {
        return item.$options.name === componentName;
    });
    let index = res.findIndex(item => item._uid === context._uid);
    if (exceptMe) res.splice(index, 1);
    return res;
}
export { findBrothersComponents };

function typeOf(obj) {
    const toString = Object.prototype.toString;
    const map = {
        '[object Boolean]'  : 'boolean',
        '[object Number]'   : 'number',
        '[object String]'   : 'string',
        '[object Function]' : 'function',
        '[object Array]'    : 'array',
        '[object Date]'     : 'date',
        '[object RegExp]'   : 'regExp',
        '[object Undefined]': 'undefined',
        '[object Null]'     : 'null',
        '[object Object]'   : 'object'
    };
    return map[toString.call(obj)];
}
// deepCopy
function deepCopy(data) {
    const t = typeOf(data);
    let o;

    if (t === 'array') {
        o = [];
    } else if ( t === 'object') {
        o = {};
    } else {
        return data;
    }

    if (t === 'array') {
        for (let i = 0; i < data.length; i++) {
            o.push(deepCopy(data[i]));
        }
    } else if ( t === 'object') {
        for (let i in data) {
            o[i] = deepCopy(data[i]);
        }
    }
    return o;
}

export {deepCopy};

具體使用大家可自行測試!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章