共计 5602 个字符,预计需要花费 15 分钟才能阅读完成。
NutUI 组件源码揭秘
前言
本文的主题是 Steps 组件的设计与实现。Steps 组件是 Steps 步骤和 Timeline 组件联合的组件,在此之前他们是两个不同的组件,在 NutUI 最近一次版本升级的时候将他们合二为一了,来看看在组件的开发过程中是如何一步步实现组件性能的。
说到 NutUI,可能有些人还不太理解,容咱们先简略介绍一下。NutUI 是一套京东格调的挪动端 Vue 组件库,开发和服务于挪动 Web 界面的企业级前中后盾产品。通过 NutUI,能够疾速搭建出格调对立的页面,晋升开发效率。目前已有 50+ 个组件,这些组件被宽泛应用于京东的各个挪动端业务中。
在此之前他们要离开应用,然而又有很多性能是穿插的,而且并不能满足步骤和工夫同时呈现的业务场景,因而将他们进行了合并。
先来看下 Steps 组件的最终出现成果,数据展现,并带有一些流程性的逻辑。
组件的性能:
- 依据不同场景采纳不同的布局形式
- 能够指定以后所在的节点
- 能够横向或者纵向排列
- 可能动静响应数据的变动
一般来说在物流信息、流程信息等内容的展现须要应用到这个组件,能够像上面这样应用它。
<nut-steps type="mini">
<nut-step title="已签收" content="您的订单已由自己签收。如有疑难您能够分割配送员,感谢您在京东购物。" time="2020-03-03 11:09:96" />
<nut-step title="运输中" content="您的订单已达到京东【北京旧宫营业部】" time="2020-03-03 11:09:06" />
<nut-step content="您的订单已达到京东【北京旧宫营业部】" time="2020-03-03 11:09:06" />
<nut-step content="您的订单由京东【北京顺义分拣核心】送往【北京旧宫营业部】" time="2020-03-03 11:09:06" />
<nut-step title="已下单" content="您提交了订单,请期待零碎确认" time="2020-03-03 11:09:06"/>
</nut-steps>
组件封装的思路
大多数的组件是一个独自的组件,应用起来很简略,比方咱们 NutUI 组件库中的 <nut-button block> 默认状态 </nut-button>
、<nut-icon type="top"></nut-icon>
等等这样简略的应用形式就能够实现组件的性能。
这样设计组件是相当优良的,因为使用者用的时候真的十分不便简略。
这样简略而优雅的组件设计形式实用于大多数性能简略的组件,然而对于逻辑绝对简单、布局也比较复杂的组件来说就不适合了。
性能绝对简单的组件,会让组件变得很不灵便,模板固定,应用自由度很低,对于开发者来,组件编码也会变得非常臃肿。
所以在 vue 组件开发过程中正当应用插槽 slot 个性,让组件更加的灵便和凋谢。就像上面这样:
<nut-tab @tab-switch="tabSwitch">
<nut-tab-panel tab-title="页签一"> 这里是页签 1 内容 </nut-tab-panel>
<nut-tab-panel tab-title="页签二"> 这里是页签 2 内容 </nut-tab-panel>
<nut-tab-panel tab-title="页签三"> 这里是页签 3 内容 </nut-tab-panel>
<nut-tab-panel tab-title="页签四"> 这里是页签 4 内容 </nut-tab-panel>
</nut-tab>
<nut-subsidenavbar title="人体辨认 1" ikey="9">
<nut-sidenavbaritem ikey="10" title="人体检测 1"></nut-sidenavbaritem>
<nut-sidenavbaritem ikey="11" title="细粒度人像宰割 1"></nut-sidenavbaritem>
</nut-subsidenavbar>
...
有很多绝对简单的组件采纳这种形式,既能保障组件性能的完整性,也能自在配置子元素内容。
组件的实现
基于下面的设计思路,就能够着手实现组件了。
本文的 Steps 组件,蕴含外层的 <nut-steps>
和内层的 <nut-step>
两个局部。
咱们个别会这样设计
<-- nut-steps -->
<template>
<div class="nut-steps" :class="{horizontal: direction ==='horizontal'}">
<slot></slot>
</div>
</template>
<-- nut-step -->
<template>
<div class="nut-step clearfix" :class="`${currentStatus ? currentStatus :''}`">
...
</div>
</template>
外层组件管制整体组件的布局,激活状态等,子组件次要渲染内容,然而他们之间的关联成了难题。
子组件中的一些状态逻辑须要由父组件来管制,这就存在父子组件之间属性或状态的通信。
解决这个问题有两种思路,一是在父组件中获取子组件信息,再将子组件须要的父组件信息给子组件设置上,二是在子组件中获取父组件的属性信息来渲染子组件。
第一种计划:
this.steps = this.$slots.default.filter((vnode) => !!vnode.componentInstance).map((node) => node.componentInstance);
this.updateChildProps(true);
首先通过 this.$slots.default
获取到所有的子组件,而后在 updateChildProps
中遍历 this.steps
,并依据父组件的属性信息更新子组件。
跑起来验证下,仿佛实现想要的成果!!!
Prop 动静更新
然而,在理论我的项目利用中,发现在动静刷新这块存在很大问题。
例如:
- 以后所处状态产生扭转须要遍历所用子组件,性能低下
- 子组件内容或某个属性变动,想要更新组件会变得异样麻烦
- 父组件中要保护治理很多子组件的属性
在刚开始甚至用了比拟蠢笨的办法,将渲染子组件用到的 list 传递给父组件,并监听该属性的变动状况来从新渲染子组件。然而为了实现这种更新却增加了一个毫无意义的数据监听,还须要深度监听,而局部场景下也并不是必须,从新遍历渲染子组件也会造成性能耗费,效率低下。
所以这种形式并不适合,改用第二种形式。
在子组件中拜访父组件的属性,利用 this.$parent
来拜访父组件的属性。
// step 组件创立之前将组件实例增加到父组件的 steps 数组中
beforeCreate() {this.$parent.steps.push(this);
},
data() {
return {index: -1,};
},
methods: {getCurrentStatus() {
// 拜访父组件的逻辑更新属性
const {current, type, steps, timeForward} = this.$parent;
// 逻辑解决
}
},
mounted() {
// 监听 index 的变动从新计算相干逻辑
const unwatch = this.$watch('index', val => {this.$watch('$parent.current', this.getCurrentStatus, { immediate: true});
unwatch();});
}
在父组件中,接管子组件实例并设置 index 属性
data() {
return {steps: [],
};
},
watch: {steps(steps) {steps.forEach((child, index) => {child.index = index; // 设置子组件的 index 属性,将会用于子组件的展现逻辑});
}
},
通过上面这张图来看下它的数据变动。
子组件中的属性变动只依赖子组件的属性,子组件外部的属性变动并不需要触发父组件的更新,而子组件数量的变动会触达父组件,并依照创立程序给子组件从新排序设定 index 值,子组件再依据 index 值的变动从新渲染。
将更多的逻辑交给了子组件解决,而父组件更多的是做整体组件的性能逻辑。也不必要监听子组件的数据源也能更新组件。
然而,实现过程中有个要害属性可能是造成 bug 的重要隐患,它就是 this.$parent
.
只有子组件 <step>
的父级是 <steps>
时拜访到的 this.$parent
才是精确的。
如果不是间接的父子级就肯定会呈现 bug。
理论应用中,不仅是这个组件,其余这类组件也会呈现子组件的间接父级并不是它对应父级的状况,这就会产生 bug。比方:
<nut-steps :current="active">
<nut-row>
<nut-step v-for="(step, index) in steps" :key="index" :title="step.title" :content="step.content" :time="step.time">
</nut-step>
</nut-row>
</nut-steps>
<nut-row>
组件作为 <nut-step>
组件的父级组件的时候,this.$parent
指向的就不是 <nut-steps>
了。
那么在 <nut-step>
中能够加一些 hack:
let parent = this.$parent || this.$parent.$parent;
但这很快就会失控,治标不治本,再加几层嵌套,立即玩完。
多层传递的神器 – 依赖注入
当初次要要解决的问题是让后辈子组件拜访到父级组件实例上的属性或办法,两头不论跨几级。
vue 依赖注入能够派上用场了。
vue 实例有两个配置选项:
- provide: 指定咱们想要提供给后辈组件的数据 / 办法。
- inject:接管指定的咱们想要增加在这个实例上的 property。
这两个属性是 vue v2.2.0 版本新增
这两选项须要一起应用,以容许一个先人组件向其所有子孙后代注入一个依赖,不管组件档次有多深,并在其上下游关系成立的工夫里始终失效。如果相熟 React,这与 React 的上下文个性很类似。
父组件应用 provide
提供可注入子孙组件的 property。
// 父级组件 steps
provide() {
return {
timeForward: this.timeForward,
type: this.type,
pushStep: this.pushStep,
delStep: this.delStep,
current: this.current,
}
},
methods: {pushStep(step) {this.steps.push(step);
},
delStep(step) {
const steps = this.steps;
const index = steps.indexOf(step);
if (index >= 0) {steps.splice(index, 1);
}
}
},
子组件应用 inject
读取父级组件提供的 property。
// 子孙组件 step
inject: ['timeForward', 'type', 'current', 'pushStep', 'delStep']
// beforeCreate() {// this.$parent.steps.push(this);
// // this.pushStep(this);
// },
created() {this.pushStep(this);
},
子组件不再应用 this.$parent
来获取父级组件的数据了。
这里有个细节,子组件更新父组件的 steps 值的机会从
beforeCreate
变成了created
,这是因为inject
的初始化是在beforeCreate
之后执行的,因而在此之前是拜访不到inject
中的属性的。
解决了跨层级嵌套的问题,还有另一个问题,监听父组件属性的变动。因为:
provide
和inject
绑定并不是可响应的。
比方 current
属性是能够动静扭转的,像下面这个注入,子孙组件拿到的永远是初始化注入的值,并不是最新的。
这个也很容易解决,在父组件注入依赖时应用函数来获取实时的 current 值即可。
provide() {
return {getCurrentIndex: () => this.current,
}
},
在子组件中:
computed: {current() {return this.getCurrentIndex();
}
},
mounted() {
const unwatch = this.$watch('index', val => {this.$watch('current', this.getCurrentStatus, { immediate: true});
unwatch();});
},
this.$watch
和 watch
办法中监听是雷同的成果,能够被动触发监听,this.$watch()
回返回一个勾销察看函数,用来进行触发回调。这里在组件挂载实现后监听 index
的变动,index
变动再立刻触发 current
属性变动的监听。
这样就能实时取得父组件的属性变动了,实现数据监听刷新组件。
至此这个组件的次要难点就攻克了。
当然这种形式只实用于父子层级比拟深的场景,同层级兄弟组件之间是无奈通过这种形式实现通信的。
另外 provide
和 inject
次要实用于开发高阶组件或组件库的时候应用,在一般的利用程序代码中最好不要应用。因为这可能会造成数据凌乱,业务于逻辑混淆,我的项目变得难以保护。
总结
在组件开发过程中,为了保障组件的灵活性、整体性,很多组件都会呈现这种嵌套问题,甚至深层嵌套导致的属性共享问题、数据监听问题,那么本文次要依据 Steps 组件的开发教训提供一种解决方案,心愿对大家有那么一丢丢的帮忙或启发。