13

Vue读懂这篇,进阶高级

 4 years ago
source link: https://juejin.im/post/5e2453e8518825366e13f59a
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Vue读懂这篇,进阶高级

Vue读懂这篇,进阶高级

注:得VueComponent着得Vue,通过此文了解VueComponent的强大。

我们已经知道了 "props down events up",但日常的业务远远不止父子之间的“交互”,例如:子孙之间、曾孙之间、曾曾孙之间……!,我该如何想我的下级传递命令?下级做好了一件事,如何向上级报告?情况就越演越烈。

通常的解决办法如下:

1

严格遵守单向数据流props一层一层的传递,像传递奥运火炬一样。events一层一层向上冒泡。不仅在编写方面冗余且容易出错,更加大了组件间的“交互”成本。

1
Bus又过于称重且不易维护,在日常开发过程中,往往因为业务功能加大了组件的维护成本,那有没有一个方法可以直达?

一、$listeners【代理events】

官方解读:在 vue2.4 中,Vue 提供了一个$listeners属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。

// these are also reactive so they may trigger child update if the child
vm.$listeners = listeners || emptyObject
复制代码

官方文档解释甚微,以下我以km-grid里的一段代码为例:

注:km-grid-itemkm-grid的子组件

<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地狱,降低组件“交互”间的成本,提高了代码可维护性,提高了性能。

二、$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查看编译之后的代码。

一、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 (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>
复制代码

首先我们在父组件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

二、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着得天下

1
属性名 描述
$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 实例的初始化选项。需要在选项中包含自定义属性时会有用。

一、春晖寸草 findComponentUpward & findComponentUpward

1、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: () => { }
})
复制代码
2、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

二、老牛舐犊 findComponentDownward & findComponentsDownward

1、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')
复制代码
2、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')
复制代码

四、情同手足 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

一、函数式组件 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',
  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要小。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK