What should we do when we encounter open source framework bugs? Google? Check the document? Mention issue? Perhaps we can also try to find the cause of the bug in the source code ourselves!

This paper shares the process of how the author derived the source of the problem step by step according to the representation of the problem and finally found a solution when he encountered a bug in iView in his work. Hope to have some inspiration for distinguished readers.

Bug triggered by version upgrade

Recently, I took time to upgrade the iView version used in the project from 2.6.0 to 2.14.2. I thought there would be no big problem, but some errors were reported in the AutoComplete component:

After analyzing the context code of the call, I found that this exception would be raised when setting the AutoComplete component value to NULL. I simplified the context code of the call and made a Demo, as shown below. I have an example on Codepen. I suggest you click on it to get a feel for it. Remember: Please open the console and read the error message.

<template> <div> <AutoComplete v-model="value"> <Option v-for="item in persons" :value="item" :key="item"> <span>{{item}}</span> </Option> </AutoComplete> <Button type="primary" @click="reset">test</Button> </div> </template> <script type="text/ecmascript-6"> const PERSONS = ["tec", "van", "is", "good"]; export default { data() { return { value: "" }; }, computed: { persons() { return PERSONS; }}, methods: {reset() {this.value = null; }}}; </script>Copy the code

From the exception information, the core of the problem is that the I-SELECT component has TypeError: Cannot read property ‘propsData’ of undefined, i.e. some undefined value flows into the i-select logic unexpectedly. I searched around, but didn’t find a solution. There was nothing related to the issue on Github, so I had to solve it by myself.

The Debug from source

First, find where the exception was thrown from the error stack as follows:

var applyProp = function(node, propName, value) {(0, _newArrowCheck3.default)(undefined.undefined);

  return (0, _extends4.default)({}, node, {
    componentOptions: (0, _extends4.default)({}, node.componentOptions, {
      propsData: (0, _extends4.default)(
        {},
        // This line throws undefined exception
        node.componentOptions.propsData,
        (0, _defineProperty3.default)({}, propName, value)
      )
    })
  });
}.bind(undefined);
Copy the code

Tips:

The propsData attribute in the error statement, which is not used in daily development, is a bit sketchy as explained on the Vue website:

Pass props when creating the instance. The main function is to facilitate testing

In simple terms, vue combines the props property passed by the parent component into an object, which is called the propsData property.

This is a period of Babel compiled code, looks really hard, so it is strongly recommended that configuration first iview debugging environment, configuration, can see the error in the SRC/components/select vue file, the source code for:

const applyProp = (node, propName, value) = > {
  return {
    ...node,
    componentOptions: {
      ...node.componentOptions,
      propsData: {
        ...node.componentOptions.propsData,
        [propName]: value
      }
    }
  };
};
Copy the code

Well, it looks so much cleaner. The above code is just a very simple attribute merge function. Indeed, it is a bug triggered by this function, but the main point of the problem is that the parameters passed by the caller do not meet the expectation of the function. Trace back to the source, and then we analyze the problem from several aspects:

  1. When to callapplyPropFunction?
  2. parameternodeWhat is it?
  3. Why does it happen?node.componentOptionsA value ofundefinedThe situation?

1. When to invokeapplyPropFunction?

In the SRC/components/select. Vue file search, found that the functions are called only in the calculator selectOptions, simplify the calling code:

selectOptions() {
    // ...
    const slotOptions = (this.slotOptions || []);
    // ...
    if (this.autoComplete) {
        // ...

        return slotOptions.map(node= > {
            if (node === selectedSlotOption || getNestedProperty(node, 'componentOptions.propsData.value') = = =this.value) {
                return applyProp(node, 'isFocused'.true);
            }
            return copyChildren(node, (child) => {
                if(child ! == selectedSlotOption)return child;
                return applyProp(child, 'isFocused'.true);
            });
        });
    }
    // ...
},
Copy the code

The intent is to add isFocused attributes to the slotOptions that meet the conditions. From SRC/components/select. Vue component code that calculator selectOptions is used to represent component option, the selected item.

But the selectOptions calculator code is too long and has too many dependencies: slotOptions, focusIndex, Values, autoComplete… Based on the nature of the calculated properties, it can be inferred that these dependency changes trigger the selectOptions calculator to recalculate, which in turn implicitly calls the applyProp function. That is, there are too many possibilities for applyProp to be called. The problem becomes very large if you trace these possibilities, so here’s another way to think about it: What parameters does the selectOptions calculator pass to the applyProp function?

Parameters of 2.nodeWhat is it?

At the end of the selectOptions calculator source, we iterate over the slotOptions array, passing the items as node arguments to the applyProp function, To determine the type of node arguments, we just need to determine what type of array slotOptions is. Search the source code and make sure that the slotOptions attribute is assigned in two places:

data () {
    return {
        // ...
        slotOptions: this.$slots.default,
        // ...
    };
},
methods: {// ...
    updateSlotOptions(){
        this.slotOptions = this.$slots.default;
    },
    // ...
}
Copy the code

SlotOptions is just a substitute for the default slots of this.$slots.default.

As you can see, these are vNode instances.

Here’s another thought: why save this.$slot.default via the data attribute?

When the component renders, the corresponding render function is called to generate a Vnode tree, but the vnode tree is not in the vUE’s response system — vnode changes cannot be captured, so iView adds it to the data property to monitor vnode changes. Iview also adds a component called functional-options.vue to check vNode updates.

This is straightforward when we need to monitor changes outside of the response system, but in the case of i-SELECT, there is a more elegant solution:

useslot-scopeInstead of simpleslot

This is a point worth thinking about when it comes to component design, which will be discussed in another article.

3. Whynode.componentOptionsA value ofundefinedThe situation?

Let’s go back to the slotOptions values:

The first item in the array is kind of weird, the tag is undefined, right? Expand this item and the object property values are as follows:

Nodes whose tag is undefined are created using the _t function, i.e. text nodes in vNodes.

Execute the evaluation function in the calculation property selectOptions on this item:

getNestedProperty(node, "componentOptions.propsData.value");
Copy the code

The value obtained is exactly NULL. Ok, the problem has been found here. The steps to trigger the exception are as follows:

  1. The calculatorselectOptionstraversethis.$slots.defaultnode
  2. To find thevnodeWhere, the value is equal tovalueNode of property
  3. whenvalueA value ofnullWhen,$slots.defaultThe first text node is eligible
  4. Pass in the text nodeapplyPropsfunction
  5. applyPropsFunction performsnode.componentOptions.propsDataEvaluate, but text nodecomponentOptionsProperties forundefined
  6. The triggerTypeError: Cannot read property 'propsData' of undefinedabnormal

The solution

So now that we’ve identified the trigger, the scenario, and the problem, it’s time to roll up our sleeves and change the code. For this bug, I have a few sketchy solutions:

1. Avoid using it in codenull

This is probably the most reliable, cheapest, and most frustrating solution the user side can think of: since the framework is unreliable, I’ll limit my own usage. It is possible to avoid using null as a key in most applications. If this is Designed, it is acceptable for most developers to use null as a key. But this behavior is completely unexpected — there is no official documentation on this issue, and there are few issues on Git. As a user, can only silently meet pit, pit, learn from mistakes.

2. Filter out the nodes whose tag is undefined

The i-Select component can be used to check the parameters of the applyProps before calling the selectOptions property.

selectOptions() {
    // ...
    const slotOptions = (this.slotOptions || []);
    // ...
    if (this.autoComplete) {
        // ...

        return slotOptions.map(node= > {
            // Add a statement to simply return the node if the tag is not expected
            if (typeof node.tag === "undefined") return node;
            if (node === selectedSlotOption || getNestedProperty(node, 'componentOptions.propsData.value') = = =this.value) {
                return applyProp(node, 'isFocused'.true);
            }
            return copyChildren(node, (child) => {
                if(child ! == selectedSlotOption)return child;
                return applyProp(child, 'isFocused'.true);
            });
        });
    }
    // ...
},
Copy the code

Modification is cheap and efficient, but this approach presuppositions that values cannot be passed in with a undefined tag, such as:

<AutoComplete v-model="value" placeholder="input">
  <template v-for="item in persons">{{ item }}</template>
</AutoComplete>
Copy the code

The slot value cannot be passed as a string. This is compatible with the way the iView website claims to call, so it is not a problem and is acceptable.

conclusion

Is that the end of the question? No! Such a small bug can lead to a lot of points worth thinking about:

  1. Why do I need monitoring in iViewvnodeStudent: Change in value?
  2. why$slots.defaultUnexpected text nodes appear on the value?
  3. How to use slot-scope approach to achieve more elegant code structure?

The discussion is already in the works, so we’ll see you next time.

Welcome to github, and I will continue to write some original technical articles in the future, not sarcastic, not sarcastic, but focused on the technology itself.