Vue讀懂這篇,進階高級

Vue讀懂這篇,進階高級javascript

注:得VueComponent着得Vue,經過此文了解VueComponent的強大。php

超級代理

咱們已經知道了 "props down events up",但平常的業務遠遠不止父子之間的「交互」,例如:子孫之間、曾孫之間、曾曾孫之間……!,我該如何想個人下級傳遞命令?下級作好了一件事,如何向上級報告?狀況就越演越烈。vue

一般的解決辦法以下:java

嚴格遵照單向數據流props一層一層的傳遞,像傳遞奧運火炬同樣。events一層一層向上冒泡。不只在編寫方面冗餘且容易出錯,更加大了組件間的「交互」成本。node

Bus又過於稱重且不易維護,在平常開發過程當中,每每由於業務功能加大了組件的維護成本,那有沒有一個方法能夠直達?

1、$listeners【代理events】

官方解讀:在 vue2.4 中,Vue 提供了一個$listeners屬性,它是一個對象,裏面包含了做用在這個組件上的全部監聽器。能夠配合 v-on="$listeners" 將全部的事件監聽器指向這個組件的某個特定的子元素。react

// these are also reactive so they may trigger child update if the child
vm.$listeners = listeners || emptyObject
複製代碼

官方文檔解釋甚微,如下我以km-grid裏的一段代碼爲例:web

注:km-grid-itemkm-grid的子組件vue-router

<div :class="cls" :style="tableStyles">
    <km-grid-item v-if="fixedLeftCol&&fixedLeftCol.length" fixed="left" v-on="$listeners" :columns="fixedLeftCol" :header-styles="leftFixedHeaderStyles" :body-styles="leftFixedBodyStyles"></km-grid-item>
    <km-grid-item v-on="$listeners" :columns="centerCol" :expandColumn="expandCol" :header-styles="headerStyles" :body-styles="bodyStyles"></km-grid-item>
    <km-grid-item v-if="fixedRightCol&&fixedRightCol.length" fixed="right" v-on="$listeners" :columns="fixedRightCol" :header-styles="rightFixedHeaderStyles" :body-styles="rightFixedBodyStyles"></km-grid-item>
</div>
複製代碼

這裏在km-grid-item加上 v-on="$listeners"意思就是說將全部km-grid的監聽器指向 km-grid-item,即我在km-grid-item裏的全部經過$emit拋出的事件均可以被km-grid$listeners屬性採集到。即km-grid-item代理了km-grid的事件。編程

經過v-on="$listeners",咱們就能夠消除 events地獄,下降組件「交互」間的成本,提升了代碼可維護性,提升了性能。數組

2、$attrs【代理props】

官方解讀:包含了父做用域中不做爲 prop 被識別 (且獲取) 的特性綁定 (class 和 style 除外)。當一個組件沒有聲明任何 prop 時,這裏會包含全部父做用域的綁定 (class 和 style 除外),而且能夠經過 v-bind="$attrs" 傳入內部組件——在建立高級別的組件時很是有用。

// these are also reactive so they may trigger child update if the child
vm.$attrs = parentVnode.data.attrs || emptyObject
複製代碼

即當我在子組件加上 v-bind="$attrs"時,並無在子組件內部用props接收,在Vue v2.4以後,多餘的屬性將會被$attrs接收,便可在子組件內部經過this.$attrs獲取。

經過v-bind="$attrs",咱們不用在子組件上去同步props,代理了props,但仍解決不了props地獄的問題。

推薦:使用$parent$parent.$parent獲取父組件的 VueCompont,由於VueCompont是響應式的。在子組件、孫組件引用只是對對象的引用,能解決props地獄的問題,但切記這是對對象的引用,若只想獲取父組件值,請使用deepCopy方法。

export const 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
}
複製代碼

超越父子親情

使用這兩個方法以前要在render樹上存在父子孫關係,可越級。不熟悉render樹的能夠看我以前寫的一篇js執行過程及vue編譯過程

什麼是render樹?即咱們使用的.vue文件最終都會經過vue-Compiler生成render樹。經過slot最終也會正確的變爲render樹,是vnode的原型,也是DOM樹的映射。以下是一個簡單的render樹

with(this){
  return (isShow) ?
    _c('ul', {
        staticClass"list",
        class: bindCls
      },
      _l((data), function(item, index{
        return _c('li', {
          on: {
            "click"function($event{
              clickItem(index)
            }
          }
        },
        [_v(_s(item) + ":" + _s(index))])
      })
    ) : _e()
}
複製代碼

各位看官也可在chorme瀏覽器調試模式下的Sources查看編譯以後的代碼。

1、dispatch

dispatch子廣播,父接收,跟events有點區別。

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 (parentparent.$emit.apply(parent, [eventName].concat(params))
    }
  }
}
複製代碼

如下我以添加form表單label自適應的需求爲例:

<Form ref="entity" class="simpleModal" justify :model="entity">
  <Row>
    <Col span="24">
      <FormItem label="角色名稱" prop="name" :required="true" :maxLen="10">
        <Input v-model="entity.name" placeholder :maxlength="10" />
      </FormItem>
    </Col>
  </Row>
  <Row>
    <Col span="24">
      <FormItem label="職能範圍" :required="true" prop="functionScope">
        <Select
          v-model="entity.functionScope"
          :disabled="!entity.isNewEntity"
          @on-change="changeFunctionScope"
        >

          <Option v-for="(txt,key) in functionScopes" :value="key" :key="key">{{ txt }}</Option>
        </Select>
      </FormItem>
    </Col>
  </Row>
</Form>

複製代碼

首先咱們在父組件Formcreated鉤子裏添加監聽:

created() {
  this.$on('on-form-item-label', (field) => {
    this.labelWidthArr.push(field)
  })
}
computed: {
  labelWidthMax: {
    get () {
      return this.labelWidthArr.sort((a, b) => {return a - b;})[this.labelWidthArr.length-1];
    },
    set (val) {
      this.labelWidthArr.sort((a, b) => {return a - b;})[this.labelWidthArr.length-1] = val
    }
  }
}
複製代碼

而後在子組件FormItemcreated鉤子裏dispatch

created () {
  if (this.form.justify) {
    let span = document.createElement('span')
    let mock = document.createElement('div')
    let FormItemPadding = 12
    span.innerHTML = this.label
    span.style.fontSize = '14px'
    mock.appendChild(span)
    document.body.appendChild(mock)
    let widthContained = span.offsetWidth
    document.body.removeChild(mock)
    if (this.required || this.getRules().some(v => v.required)) {
        widthContained += 10
    }
    widthContained += FormItemPadding
    this.dispatch('iForm''on-form-item-label', widthContained)
  }
}
複製代碼

經過這樣,子組件每次created都像父組件拋出當前計算的label,而後在父組件接收計算出最大值,按最大的那個加載,便可實現自適應。dispatch適用於無限向上發送,只要是存在最終經過vue-Compiler編譯的 render樹上的父子層級關係便可。

顯然Form組件裏面有不少的slot,可是存在render樹的「父子」,是子主動dispatch

2、broadcast

broadcast父主動派發,子孫接收,跟props有點區別。

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: {
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params)
    }
  }
}
複製代碼

如下,我一實現一個在路由激活時,從新加載如下數據爲例:
首先在須要更新的組件B裏監聽:

created () {
  this.loadInventory()
  this.$on('on-load'() => { this.loadInventory() })
}
複製代碼

而後在當前路由組件A裏的activated主動broadcast

activated () {
  const listDataCache = this.$refs.list.data.DataList
  if (listDataCache && listDataCache.length) {
    this.broadcast('checkWatch''on-load')
  }
}
複製代碼

固然這兩個組件之間沒有任何的父子關係,只是存在render樹的「父子」,即A組件在render樹的名義上和B組件存在父子關係,且是父主動broadcast

唾手可得的VueComponent

有的同窗要問,什麼是VueComponentVueComponent有什麼用?

VueComponent即爲組件實例,是響應式的,也是Vue的核心。咱們能夠經過VueComponent訪問組件的datacomputed,調用methods。並且能夠經過掛載的$xx來獲取更多的信息,而且這些信息都是響應式的。能夠說 得VueComponent着得天下

屬性名 描述
$attrs 上面已說明
$listeners 上面已說明
$data Vue 實例觀察的數據對象。Vue 實例代理了對其 data 對象屬性的訪問。
$router vue-router路由對象
$route 當前路由對象
$slots 用來訪問被插槽分發的內容。每一個具名插槽 有其相應的屬性 (例如:v-slot:foo 中的內容將會在 vm.$slots.foo 中被找到)。default 屬性包括了全部沒有被包含在具名插槽中的節點,或 v-slot:default 的內容。
$scopedSlots 用來訪問做用域插槽。對於包括 默認 slot 在內的每個插槽,該對象都包含一個返回相應 VNode 的函數。
$el 返回DOM,HtmlElement對象
$refs 一個對象,持有註冊過 ref 特性 的全部 DOM 元素和組件實例。
$children 當前實例的直接子組件。須要注意 $children 並不保證順序,也不是響應式的。若是你發現本身正在嘗試使用 $children 來進行數據綁定,考慮使用一個數組配合 v-for 來生成子組件,而且使用 Array 做爲真正的來源。
$data Vue 實例觀察的數據對象。Vue 實例代理了對其 data 對象屬性的訪問。
$props 當前組件接收到的 props 對象。Vue 實例代理了對其 props 對象屬性的訪問。
$root 當前組件樹的根 Vue 實例。若是當前實例沒有父實例,此實例將會是其本身。
$parent 父實例,若是當前實例有的話。
$options 用於當前 Vue 實例的初始化選項。須要在選項中包含自定義屬性時會有用。

1、春暉寸草 findComponentUpward & findComponentUpward

一、findComponentUpward
export const findComponentUpward = (context, componentName, componentNames) => {
  if (typeof componentName === 'string') {
      componentNames = [componentName]
  } else {
      componentNames = componentName
  }
  let parent = context.$parent
  let name = parent.$options.name
  while (parent && (!name || componentNames.indexOf(name) < 0)) {
      parent = parent.$parent
      if (parent) name = parent.$options.name
  }
  return parent
}
複製代碼

findComponentUpward 向上匹配最近的componentNameVueComponentcomponentName可傳StringArray,切記componentNameVue.use()的那個name,若使用Vue.component()註冊,則是註冊的key

//match one
const Tree = findComponentUpward(this'Tree');

//match someone
this.$Modal.confirm({
  el: findComponentUpward(this, ['SheetPage''InfoPage']).$el,
  content: `肯定要刪除${this.title}嗎?`,
  onOk: async () => {
    let { data } = await this.doDeleteEntity(this.id, this.Action)
    if (data.code === 0) {
      this.$Message.success('刪除成功。')
      this.$emit('on-sheet-delete'this.id)
    }
  },
  onCancel: () => { }
})
複製代碼
二、findComponentsUpward
export const 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 []
  }
}
複製代碼

findComponentsUpward 向上匹配全部的componentNameVueComponentcomponentNameString,切記componentNameVue.use()的那個name,若使用Vue.component()註冊,則是註冊的key

2、老牛舐犢 findComponentDownward & findComponentsDownward

一、findComponentDownward
export const 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
}
複製代碼

findComponentDownward 向下匹配最近的componentNameVueComponentcomponentNameString,切記componentNameVue.use()的那個name,若使用Vue.component()註冊,則是註冊的key

this.infoInstence = findComponentDownward(this.$parent, 'SheetPage')
複製代碼
二、findComponentsDownward
export const 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)
  }, [])
}
複製代碼

findComponentsDownward 向下匹配全部的componentNameVueComponentcomponentNameString,切記componentNameVue.use()的那個name,若使用Vue.component()註冊,則是註冊的key

let SlideInfoVnode = this.$refs.SlideInfo.$children[0]
let FormItems = findComponentsDownward(SlideInfoVnode, 'FormItem')
複製代碼

4、情同手足 findBrothersComponents

export const 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
}
複製代碼

findBrothersComponents 向兄弟匹配全部的componentNameVueComponentcomponentNameString,切記componentNameVue.use()的那個name,若使用Vue.component()註冊,則是註冊的keyexceptMe默認爲false,排除本身,不然不排除本身。

進階render

1、函數式組件 functional

官方解讀:使組件無狀態 (沒有 data) 和無實例 (沒有 this 上下文)。他們用一個簡單的 render 函數返回虛擬節點使它們渲染的代價更小。

如下我以km-grid業務自定義下拉render爲例,首先我在columns定義

this.columns = [
  {
    type: 'expand',
    width: 50,
    render: (h, params) => {
      if (params.row.status != '4'return ''
      return h(checkWatch, {
        props: {
          row: params.row
        }
      })
    }
  }
]
複製代碼

km-grid-item render,直接調用columns配置的render函數,這個render函數會傳至td-render這個組件裏面:

<div v-if="!fixed&&expandColumn.render" :class="cls+'-tr-expand'">
  <div style="width:100%" v-if="row._clicked">
    <td-render :row="row" :render="expandColumn.render"></td-render>
  </div>
</div>

複製代碼

td-render是如何調用配置的render來實現渲染的呢?

export default {
  name'TdRender',
  functionaltrue,
  props: {
    rowObject,
    renderFunction,
    indexNumber,
    column: {
      typeObject,
      defaultnull
    }
  },
  render(h, ctx) => {
    const params = {
      row: ctx.props.row,
      index: ctx.props.index
    };
    if (ctx.props.column) params.column = ctx.props.column;
    return ctx.props.render(h, params);
  }
}
複製代碼

原來建立組件有兩種方法,一種是一般的template模板字符串形式,另外一種是字符串模板的代替方案,容許你發揮 JavaScript最大的編程能力。該渲染函數接收一個 createElement 方法做爲第一個參數用來建立 VNode。若組件標記爲 functional,這意味它無狀態 (沒有響應式數據),也沒有實例 (沒有 this 上下文),因而咱們能夠利用render來提供上下文。

td-render經過render提供第二個參數context做爲上下文來渲染,而且開銷比template要小。

相關文章
相關標籤/搜索