NutUI component source code revealed

preface

The topic of this article is the design and implementation of the Steps component. The Steps component is a combination of the Steps and Timeline components. Before that, they were two different components. In the latest release of NutUI, they were merged into one.

Speaking of NutUI, for those of you who may not know much about it, let’s give you a brief introduction. NutUI is a set of JINGdong style mobile Vue component library, development and service for mobile Web interface enterprise front, middle and back products. With NutUI, you can quickly build unified pages to improve development efficiency. At present, there are more than 50 components, which are widely used in various mobile terminal businesses of JD.

Previously, they were used separately, but many of the functions were overlapping and did not meet the business scenario of simultaneous steps and time, so they were combined.

Let’s take a look at the final presentation of the Steps component, the data presentation, and some flow logic.

Component functions:

  1. Use different layout methods according to different scenarios
  2. You can specify the current node
  3. They can be arranged horizontally or vertically
  4. Ability to dynamically respond to changes in data

This component is typically used for presentation of logistics information, process information, etc. It can be used as follows.

<nut-steps type="mini">
      <nut-step title="Received" content="Your order has been signed by me. If you have any questions, you can contact the delivery staff. Thank you for shopping on JD.com." time="The 2020-03-03 11:09:9 6" />
      <nut-step title="In transit" content="Your order has reached JINGdong [Beijing Jiugong Business Department]" time="The 2020-03-03 11:09:06" />
      <nut-step content="Your order has reached JINGdong [Beijing Jiugong Business Department]" time="The 2020-03-03 11:09:06" />
      <nut-step content="Your order will be sent by JINGdong [Beijing Shunyi Sorting Center] to [Beijing Jiugong Business Department]" time="The 2020-03-03 11:09:06" />
      <nut-step title="Placed order" content="You have submitted the order, please wait for system confirmation." time="The 2020-03-03 11:09:06"/>
</nut-steps>
Copy the code

The idea of component encapsulation

Most components are a single component that is simple to use, For example, NutUI component library in the

default
,
and so on such a simple way to use the function of the component can be achieved.

This is a great way to design components because it’s really easy for users to use.

This simple and elegant component design approach is suitable for most components with simple functions, but it is not suitable for components with relatively complex logic and layout.

Relatively complex functions of components, components will become very inflexible, fixed templates, use freedom is very low, for developers, component coding will become very bloated.

Therefore, slot slot feature should be properly used in the development process of VUE components to make components more flexible and open. Like this:

<nut-tab @tab-switch="tabSwitch">
  <nut-tab-panel tab-title="TAB one">Here is the content of page 1</nut-tab-panel>
  <nut-tab-panel tab-title="TAB 2">This is TAB 2</nut-tab-panel>
  <nut-tab-panel tab-title="Page 3">Here are the contents of TAB 3</nut-tab-panel>
  <nut-tab-panel tab-title="TAB 4">This is TAB 4</nut-tab-panel>
</nut-tab>

<nut-subsidenavbar title="Human Identification 1" ikey="9">
  <nut-sidenavbaritem ikey="10" title="Human Detection 1"></nut-sidenavbaritem>
  <nut-sidenavbaritem ikey="11" title="Fine grained portrait segmentation 1"></nut-sidenavbaritem>
</nut-subsidenavbar>.Copy the code

There are many relatively complex components that use this approach to ensure the integrity of the component’s functionality and to freely configure the child element content.

Component implementation

Based on the above design ideas, you are ready to implement components.

The Steps component of this article contains two parts: outer

and inner

.

That’s what we do

<-- nut-steps -->
<template>
  <div class="nut-steps" :class="{ horizontal: direction === 'horizontal' }">
    <slot></slot>
  </div>
</template>
Copy the code
<-- nut-step -->
<template>
  <div class="nut-step clearfix" :class="`${currentStatus ? currentStatus : ''}`">.</div>
</template>
Copy the code

The outer components control the layout of the overall components, activation state, etc., while the sub-components render the content, but the correlation between them is a challenge.

Some of the state logic in the child component needs to be controlled by the parent component, and there is communication of properties or states between the parent and child components.

There are two ways to solve this problem: one is to get the information of the child component in the parent component, and then give the information of the child component to the child component; the other is to get the attribute information of the parent component in the child component to render the child component.

The first option:

this.steps = this.$slots.default.filter((vnode) = >!!!!! vnode.componentInstance).map((node) = > node.componentInstance);
this.updateChildProps(true);
Copy the code

First get all the child components from this.$slots.default, then iterate through this.steps in updateChildProps and update the child components based on the properties of the parent component.

Run up to verify, seems to achieve the desired effect!!

Prop Dynamic update

However, in the actual project application, it is found that there is a big problem in dynamic refresh.

Such as:

  1. Changes in the current status require traversal of the used child components, resulting in poor performance
  2. Changes in child component content or a property make it cumbersome to update the component
  3. The parent component maintains and manages the properties of many child components

It even took a clumsy approach at first, passing the list used to render the child to the parent and listening for changes to that property to re-render the child. However, in order to implement this update, a meaningless data listening is added, and deep listening is required, which is not necessary in some scenarios, and re-traversing the render sub-components can also be costly and inefficient.

So this is not appropriate, use the second method.

Access the parent’s properties in the child, using this.$parent to access the parent’s properties.

// Add the component instance to the steps array of the parent component before creating the step component
beforeCreate() {
  this.$parent.steps.push(this);
},
  
data() {
  return {
    index: -1}; },methods: {
  getCurrentStatus() {
    // Access the logical update properties of the parent component
    const { current, type, steps, timeForward } = this.$parent;
    // Logical processing}},mounted() {
  // Listen for index changes and recalculate the related logic
  const unwatch = this.$watch('index'.val= > {
    this.$watch('$parent.current'.this.getCurrentStatus, { immediate: true });
    unwatch();
  });
}
Copy the code

In the parent component, receive the child component instance and set the index property

data() {
  return {
    steps: [],}; },watch: {
  steps(steps) {
    steps.forEach((child, index) = > {
      child.index = index; // Set the index property of the child component, which will be used in the presentation logic of the child component}); }},Copy the code

Let’s look at the graph below.

The change of attributes in the child component only depends on the attributes of the child component, and the change of attributes in the child component does not need to trigger the update of the parent component, while the change of the number of child components will touch the parent component, and re-order the child component to set the index value according to the creation sequence, and then re-render the child component according to the change of index value.

More of the logic is handed over to the child components, while the parent does more of the functional logic of the overall component. The data source that listens to the child component is also not required to update the component.

However, there is one key attribute in the implementation that can be a major bug bug: this.$parent.

Only this.$parent accessed when the child

parent is

is accurate.

If it’s not directly parent-child it’s bound to be buggy.

In practice, not only this component, but other such components may have a child whose immediate parent is not its parent, which can cause bugs. Such as:

<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>
Copy the code

When the

component is the parent of the

component, this.$parent does not refer to

.


Add some hacks to

:

let parent = this.$parent || this.$parent.$parent;
Copy the code

But this can quickly get out of hand, treat the symptoms, add a few more layers of nesting, and the game is over.

Multi-layer delivery artifact – Dependency injection

The main problem now is to give descendant components access to properties or methods on the parent component instance, no matter how many levels in between.

Vue dependency injection comes in handy.

Vue instances have two configuration options:

  1. Provide: Specifies the data/methods we want to provide to future generations of components.
  2. Inject: Receives the specified property that we want to add to this instance.

These two attributes are new to VUE V2.2.0

These two options need to be used together to allow an ancestor component to inject a dependency into all of its descendants, regardless of how deep the component hierarchy is, and remain in effect for as long as its upstream and downstream relationships are established. If you’re familiar with React, this is similar to the context features of React.

The parent component provides properties that can be injected into descendant components using provide.

// The parent component 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); }}},Copy the code

The child component uses Inject to read property provided by the parent component.

// Descendant component step
inject: ['timeForward'.'type'.'current'.'pushStep'.'delStep']
// beforeCreate() {
// this.$parent.steps.push(this);
// // this.pushStep(this);
// },
created() {
  this.pushStep(this);
},
Copy the code

Child components no longer use this.$parent to get data from parent components.

The detail here is that the time when the child component updates the parent’s steps value is changed from beforeCreate to Created. This is because inject initialization is performed after beforeCreate, so the properties in Inject are not accessed until then.

The problem of nesting across hierarchies has been solved, as has another problem of listening for changes in parent component properties. Because:

Provide and Inject binding are not responsive.

For example, the current property can change dynamically, like the injection above, and descendant components will always get the initial injected value, not the latest one.

This is also easy to solve by using functions to get the current value in real time when the parent component injects a dependency.

  provide() {
    return {
      getCurrentIndex: () = > this.current,
    }
  },  
Copy the code

In the child component:

  computed: {
    current() {
      return this.getCurrentIndex(); }},mounted() {
    const unwatch = this.$watch('index'.val= > {
      this.$watch('current'.this.getCurrentStatus, { immediate: true });
      unwatch();
    });
  },
Copy the code

This.$watch() returns an unobserve function to stop triggering the callback. After the component is mounted, listen for index changes, which trigger the listening for current property changes immediately.

In this way, the parent component’s property changes can be obtained in real time, and the data listening refresh component can be realized.

This completes the main challenge of this component.

Of course, this method is only applicable to scenarios where the parent and child level is relatively deep. Sibling components of the same level cannot communicate with each other in this way.

In addition, provide and inject are mainly applicable to the development of high-level components or component libraries, and should not be used in common application code. Because of this, data can be cluttered, business logic mixed, and projects difficult to maintain.

conclusion

In the process of component development, in order to ensure that components of wholeness and flexibility, many components will appear this kind of nesting problem, and deeply nested even lead to the properties of the Shared problems, data monitor, so in this paper, according to the Steps of component development experience to provide a solution, hope to have so a DiuDiuDe help or inspiration.