Vue讀懂這篇,進階高級javascript
注:得VueComponent着得Vue,經過此文了解VueComponent的強大。php
咱們已經知道了 "props down events up",但平常的業務遠遠不止父子之間的「交互」,例如:子孫之間、曾孫之間、曾曾孫之間……!,我該如何想個人下級傳遞命令?下級作好了一件事,如何向上級報告?狀況就越演越烈。vue
一般的解決辦法以下:java
嚴格遵照單向數據流, props一層一層的傳遞,像傳遞奧運火炬同樣。events一層一層向上冒泡。不只在編寫方面冗餘且容易出錯,更加大了組件間的「交互」成本。node
Bus又過於稱重且不易維護,在平常開發過程當中,每每由於業務功能加大了組件的維護成本,那有沒有一個方法能夠直達?
官方解讀:在 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-item爲km-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地獄,下降組件「交互」間的成本,提升了代碼可維護性,提升了性能。數組
官方解讀:包含了父做用域中不做爲 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查看編譯以後的代碼。
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 (parent) parent.$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>
複製代碼
首先咱們在父組件Form的created鉤子裏添加監聽:
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
}
}
}
複製代碼
而後在子組件FormItem的created鉤子裏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。
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?VueComponent有什麼用?
VueComponent即爲組件實例,是響應式的,也是Vue的核心。咱們能夠經過VueComponent訪問組件的data、computed,調用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 實例的初始化選項。須要在選項中包含自定義屬性時會有用。 |
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 向上匹配最近的componentName的VueComponent。componentName可傳String或Array,切記componentName爲Vue.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: () => { }
})
複製代碼
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 向上匹配全部的componentName的VueComponent。componentName傳String,切記componentName爲Vue.use()的那個name,若使用Vue.component()註冊,則是註冊的key。
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 向下匹配最近的componentName的VueComponent。componentName傳String,切記componentName爲Vue.use()的那個name,若使用Vue.component()註冊,則是註冊的key。
this.infoInstence = findComponentDownward(this.$parent, 'SheetPage')
複製代碼
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 向下匹配全部的componentName的VueComponent。componentName傳String,切記componentName爲Vue.use()的那個name,若使用Vue.component()註冊,則是註冊的key。
let SlideInfoVnode = this.$refs.SlideInfo.$children[0]
let FormItems = findComponentsDownward(SlideInfoVnode, 'FormItem')
複製代碼
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 向兄弟匹配全部的componentName的VueComponent。componentName傳String,切記componentName爲Vue.use()的那個name,若使用Vue.component()註冊,則是註冊的key,exceptMe默認爲false,排除本身,不然不排除本身。
官方解讀:使組件無狀態 (沒有 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',
functional: true,
props: {
row: Object,
render: Function,
index: Number,
column: {
type: Object,
default: null
}
},
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要小。