4ark.me/post/how-ob…

The story background

One day after I went online, the big guy gave feedback to a problem in the group. When the dynamic he just sent generated the share card, the small program code at the bottom of the card was lost. However, other friends said that it was running normally on their mobile phones. In fact, the boss also said that other than this dynamic, the rest is normal.

It shows that this BUG can be reproduced only by using a specific dynamic card + a specific device. Fortunately, my sister’s mobile phone opposite me is the same model as big Guy’s, and the BUG can also be reproduced, avoiding the embarrassment that I, as a social fear, have to borrow a mobile phone from big Guy for testing.

This is a wechat applet project, in which the sharing card function is generated using a library called WXML2Canvas. However, the library now seems to be “out of repair”, and the BUG mentioned above is due to this library. This article shares how to troubleshoot the BUG, how to find the ECMAScript specification for the return order of Object.keys(), and how Object properties are handled in the V8 engine.

We hope you won’t have any more confusing bugs after reading this article because you don’t understand the output order of object.keys ().

TL; DR

This article is long, so if you don’t want to read the whole article, you can read the summary. If you plan to read the whole article, you can skip this paragraph altogether.

If reading the abstract does not help you, skip to the corresponding chapter and read in detail.

Abstract:

  1. How did this BUG come about?
  • wxml2canvasWhen you draw it, it’s based on something calledsortedThe key of this object is the top value of the node, and the value is the node element. The problem is that the library author mistakenly thinks thatObject.keys()Always returned in the order in which the properties were actually created,Whereas when key is a positive integer, the return order does not meet the original expectation, there will be a drawing order disorder, resulting in the generation of this BUG.
  • SRC /index.js#L1146 SRC /index.js#L829
  1. How to fix this BUG
  • Since the key of the object is a number, it can be an integer or a floating-point number. But the anticipatory behavior is hopeObject.keys()Return in the order in which the attributes were actually created, and then simply cast all keys to float.
  1. Object.keys()In what order are the values returned?
  • Object.keys()Called in the same order as when iterating through object properties[[OwnPropertyKeys]]()Internal methods.
  • According to the ECMAScript specification, keys are printed in the order from smallest to largest of array index types (positive integers), and then keys of all string types (negative, floating point) are sorted in the order they were actually created.
  1. How are object attributes handled internally in V8?
  • V8 stores object attributes in order to improve access efficiencyGeneral PropertiesSort attributes (Elements)
    • The sort attribute (elements) is an attribute of the array index type (that is, a positive integer type).
    • Properties, which are strings (negative numbers, floating point numbers).
    • Both of these attributes are stored in linear structures and are called fast attributes.
    • However, this creates a layer of indirection for each query, which can affect efficiency, so V8 introduces in-object-properties.
  • V8 associates each object with a hidden class that records its shape, and objects of the same shape share the same hidden class.
    • When attributes are added to or removed from an object, a new corresponding hidden class is created and re-associated.
  • In-object propertyWill be part of theGeneral propertiesIt is placed directly in the first layer of the object, so it is the most efficient access.
    • When the number of general attributes is less than the number of attributes when the object is initialized, the general attributes are stored directly as in-object attributes.
  • Although fast attributes are fast to access, adding or removing attributes from linear structures can be very inefficient, so if there are too many attributes, or attributes are added and removed, the general attributes will be changed from linear storage to dictionary storage, which is a slow attribute.

Take a look at these two pictures to help you understand:

V8 General and sort properties

V8 in-object properties, fast properties, and slow properties

Credit: Illustrated Google V8 — Geek Time

How to fix this BUG

Since it is a specific dynamic + specific device that can reproduce the problem, the network cause can be easily ruled out, and the nodes associated with the applets can also be seen by printing a list of nodes drawn on the WXML2Canvas.

Since wXML2Canvas has received the applets, but has not drawn them, the problem is naturally within wXML2Canvas, but it is not surprising that I have been getting butterflies in my skin many times since JOINING the project due to various problems with the wXML2Canvas. You should definitely replace this library if you have the opportunity, but since there are already many pages that rely on it, you’re stuck with it.

First of all, it is suspected that the coordinate position of the node of the small program code is not quite right. Through comparison, it is found that the position difference is not big, and this reason is excluded.

Then comparing the drawing order of all nodes, we found an unusual point. On the phone that repeated the BUG, the time to draw the small program code node was relatively early, but because it was at the bottom of the card, it should be relatively late under normal circumstances.

So by looking at the relevant code, sure enough, I found the mystery:

In the process of drawing, I iterated over the sorted Object from top to bottom and left to right, but by comparing the two phones’ object.keys (), I found that their output was different, which made me understand what was going on.

Let’s start with the sorted object, which is an array of elements whose key is the top value of a node and whose value is all the same top values (the same row).

Here’s the code to generate it:

Keys () : 🌰

const sorted = {}

sorted[300] = {}
sorted[200] = {}
sorted[100] = {}

console.log(Object.keys(sorted)) // What is output?
Copy the code

I’m sure most of you know the answer: [‘ 100’, ‘200’, ‘300 ‘].

What if we have floating point numbers?

const sorted = {}

sorted[300] = {}
sorted[100] = {}
sorted[200] = {}
sorted[50.5] = {}

console.log(Object.keys(sorted)) // What is output this time?
Copy the code

Did anyone think the answer was [‘50.5’, ‘100’, ‘200’, ‘300’]?

But the correct answer should be: [‘ 100’, ‘200’, ‘300 ‘, ‘50.5’].

So it’s a reasonable guess that the author of wXML2Canvas made this mistake. He probably assumed that object. keys would be returned in order of magnitude based on keys, thus satisfying the logic of drawing from top to bottom. But he doesn’t take into account floating-point numbers, so when one node has an integer top value, it will be drawn earlier than the other nodes with a floating-point top value, resulting in the later nodes being drawn overwriting the previous nodes.

So, when I change the code to something like this, the shared card mini-code is drawn normally:

  Object
+ .sort((a, b)=> a - b)
  .keys(sorted)
  .forEach((top, topIndex) => {
    //  do something
  }
Copy the code

OK, we’re done.

Test sister: Wait! It’s affecting other places.

I looked and sure enough. So again through comparison, found that in most cases, the top value will be floating point number, and the BUG of the card small program code is only very coincidentally integer, resulting in the drawing order is not correct.

I just noticed that the original logic of WXML2Canvas is to draw sorted according to the order in which it is created, but it doesn’t take into account the fact that the key is an integer.

So, finally fix the problem by modifying it like this:

_sortListByTop (list = []) { let sorted = {}; List. ForEach ((item, index) => {- let top = item.top;
+ let top = item.top.toFixed(6); // Force a decimal point to convert an integer to a floating point numberif (! sorted[top]) { if (sorted[top - 2]) { top = top - 2; }else if (sorted[top - 1]) { top = top - 1; } else if (sorted[top + 1]) { top = top + 1; } else if (sorted[top + 2]) { top = top + 2; } else { sorted[top] = []; } } sorted[top].push(item); }); return sorted; }Copy the code

It is obvious that the wXML2Canvas author is unaware of the mechanism by which object.keys () returns the order that causes this BUG.

Keys () ¶ If you have made the same mistake, it is necessary to introduce the implementation mechanism of object.keys () in depth.

So follow me to find out.

Deeper understanding of Object.keys()

Keys () is not a new API, you can just Google it, so why bother writing an article about it?

It’s true that search engines can quickly figure out the order in which object.keys () is returned, but many of them are superficial, and I’ve even seen partial answers like numbers first and strings last.

So THIS time I want to try to go back to the source and get information from first-hand sources. It is highly likely that information obtained from word of mouth is one-sided or even wrong.

PS: In fact, not only technology, we should maintain the same attitude towards other things we do not understand.

Keys () {object.keys ();

The object.keys () method returns an array of a given Object’s own enumerable properties in the same order as the property names would be returned if the Object were iterated through normally.

emmm… It doesn’t tell us directly what the output order is, but we can look at the Polyfill above:

if (!Object.keys) {
  Object.keys = (function () {
    var hasOwnProperty = Object.prototype.hasOwnProperty, hasDontEnumBug = ! ({toString: null}).propertyIsEnumerable('toString'),
        dontEnums = [
          'toString'.'toLocaleString'.'valueOf'.'hasOwnProperty'.'isPrototypeOf'.'propertyIsEnumerable'.'constructor'
        ],
        dontEnumsLength = dontEnums.length;

    return function (obj) {
      if (typeofobj ! = ='object' && typeofobj ! = ='function' || obj === null) throw new TypeError('Object.keys called on non-object');

      var result = [];

      for (var prop in obj) {
        if (hasOwnProperty.call(obj, prop)) result.push(prop);
      }

      if (hasDontEnumBug) {
        for (var i=0; i < dontEnumsLength; i++) {
          if(hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]); }}returnresult; ()}}});Copy the code

Use for… In to iterate, and then we can look at for… The in document, however, does not tell us what the order is either.

Since it is not available on MDN, we can look directly at the ECMAScript specification. Usually there is a link to the specification for this API on MDN. We can click on the latest Living Standard.

When the keys function is called with argument O, the following steps are taken:

  1. Let obj be ? ToObject(O).
  2. Let nameList be ? EnumerableOwnPropertyNames(obj, key).
  3. Return CreateArrayFromList(nameList).

Object is obtained through EnumerableOwnPropertyNames attribute list, this is its specification defines:

The abstract operation EnumerableOwnPropertyNames takes arguments O (an Object) and kind (key, value, or key+value). It performs the following steps when called:

  1. Let ownKeys be ? O.[OwnPropertyKeys].

  2. Let properties be a new empty List.

  3. For each element key of ownKeys, do a. If Type(key) is String, then

    1. Let desc be ? O.[GetOwnProperty].
    2. If desc is not undefined and desc.[[Enumerable]] is true, then a. If kind is key, append key to properties.

    b. Else, 1. Let value be ? Get(O, key). 2. If kind is value, append value to properties. 3. Else i. Assert: kind is key+value. ii. Let entry be ! CreateArrayFromList(« key, value »).iii. Append Entry to properties.

  4. Return properties.

On the blackboard! Here is a detail, please pay more attention to, later will test.

We then explore the OrdinaryOwnPropertyKeys that finally return:

The [[OwnPropertyKeys]] internal method of an ordinary object O takes no arguments. It performs the following steps when called:

  1. Return ! OrdinaryOwnPropertyKeys(O).

Keys are ordered by OrdinaryOwnPropertyKeys:

The abstract operation OrdinaryOwnPropertyKeys takes argument O (an Object). It performs the following steps when called:

  1. Let keys be a new empty List.
  2. For each own property key P of O such that P is an array index, in ascending numeric index order, do a. Add P as the last element of keys.
  3. For each own property key P of O such that Type(P) is String and P is not an array index, in ascending chronological order of property creation, do a. Add P as the last element of keys.
  4. For each own property key P of O such that Type(P) is Symbol, in ascending chronological order of property creation, do a. Add P as the last element of keys.
  5. Return keys.

Now that we know what we want to know, here’s a summary:

  1. Create an empty list to hold keys
  2. Stores all valid array indexes in ascending order
  3. Store all string type indexes in ascending order by attribute creation time
  4. Store all Symbol type indexes in ascending order by attribute creation time
  5. Returns the keys

This is also to correct a common misconception: the answer to this question is to sort all numeric keys from smallest to largest, but it is not true. Only positive integers ** are allowed, and negative or floating point numbers are treated as strings.

PS: Strictly speaking, object attributes that have no numeric type, whether numeric or string, are treated as strings.

With the above specification in mind, let’s consider what the following code output would be:

const testObj = {}

testObj[-1] = ' '
testObj[1] = ' '
testObj[1.1] = ' '
testObj['2'] = ' '
testObj['c'] = ' '
testObj['b'] = ' '
testObj['a'] = ' '
testObj[Symbol(1)] = ' '
testObj[Symbol('a')] = ' '
testObj[Symbol('b')] = ' '
testObj['d'] = ' '

console.log(Object.keys(testObj))
Copy the code

Please think carefully and check your answer here:

View the result 👈
['1', '2', '- 1', '1.1', 'c', 'b', 'a', 'd']
Copy the code

Is it as you expected? You may wonder why there is no Symbol type.

Remember the front on the blackboard a place to let the students pay attention to, because in the standard stipulated in the return value of EnumerableOwnPropertyNames should contain the string attributes only (it says the number actually is also a string).

So the Symbol attribute is not be returned, we can see on the MDN about Object. GetOwnPropertyNames ().

If you want to return to Symbol attribute can use Object. GetOwnPropertySymbols ().

After reading the ECMAScript specification definition, you won’t mess up the output order of object.keys () anymore. But if you’re curious about how V8 handles object properties, we’ll cover that in the next section.

How does V8 handle object properties

There is a highly recommended article on V8’s official blog, Fast Properties in V8, which explains in great detail how V8 handles JavaScript object properties internally.

I also recommend the geek hour course Illustrated Google V8 (after all, I borrowed some of the images from it).

This section mainly refers to these two places, let’s summarize.

First, V8 divides attributes into two types in order to improve the access efficiency of object attributes:

  • Sort attributes (elements) are attributes that match the array index type (that is, positive integers).

  • Properties, which are strings (negative numbers, floating point numbers).

All sorted attributes are stored in a linear structure, which supports random access through indexes, so the access speed can be accelerated. For attributes stored in a linear structure, they are called fast attributes.

General properties are also stored in another linear structure, as illustrated by the following diagram:

V8 sort attributes and general attributes

But there’s a little extra work to be done with general attributes, and we’ll start by explaining what a hidden class is.

Because JavaScript can modify object properties at runtime, it is slow to query. As you can see from the above diagram, each time an object is accessed, it has to go through one more layer of access. Static languages like C++ need to define the structure (shape) of the object before declaring it. The shape of each object is fixed after compilation, so access is naturally faster because you know the offset of the property.

V8 took the idea of applying this mechanism to JavaScript objects, so it introduced the mechanism of hidden classes. You can easily understand that hidden classes describe the shape of the object, including the location of each attribute, so that queries are much faster.

A few more things to add about hidden classes:

  1. The first field of the object points to its hidden class.
  2. If two objects are exactly the same shape, they share the same hidden class.
  3. When attributes are added to or removed from an object, a new corresponding hidden class is created and redirected to it.
  4. V8 has a transformation tree mechanism to create hidden classes, but this article doesn’t cover that, so you can read it here.

Explained the hidden classes, we again explain conventional properties, through the above picture we can easily find a problem, that is each time you visit a property, all need to pass through a layer of indirection to access, it reduces the access efficiency, in order to solve this problem, the V8 and introduces a called object attribute, as the name implies, It stores some attributes directly in the first layer of the object and is the fastest to access, as shown below:

V8 in-object properties

Note, however, that the properties in the object only store the general properties, the sorting properties remain the same. In addition, the number of general attributes needs to be less than a certain number to directly store the attributes in the object. What is the number?

The answer depends on the size of the object when it is initialized.

PS: Don’t be misled by some articles that say that objects are stored only when there are less than 10 attributes.

In addition to the in-object properties, fast properties, there is also a slow property.

Why does it have the slow property? Fast attribute to visit soon, but if you want to add or delete a large number of attributes, from the object, may produce a lot of time and memory overhead to maintain the hidden class, so much or adding, deleting, repeatedly attribute will be regular properties of storage model from linear structure into a dictionary, is reduced to slow properties, Because the information for a slow attribute is no longer stored in a hidden class, it is slower to access than a fast attribute, but can be added and removed efficiently. To help understand:

V8 slow attribute

At this point, I feel like I have a pretty good knowledge of V8’s fast and slow properties.

But when I look at this code:

function toFastProperties(obj) {
    /*jshint -W027*/
    function f() {}
    f.prototype = obj;
    ASSERT("%HasFastProperties".true, obj);
    return f;
    eval(obj);
}
Copy the code

My mood is as follows:

For details on how this code enables V8 to use object fast attributes, see this article: Enabling “Fast” mode for V8 object attributes.

Alternatively, look at this code: to-fast-properties/index.js.

Write in the last

When encountered in the development of a simple mistake, often use search engine to solve the problem quickly, but if just for programming, Google could have technically difficult to progress, so we not only need to be able to solve the problem, but also understand the what is the cause behind the problem, which is much more to know the why.

It is highly recommended that every JavaScript developer learn something about V8 or any other JavaScript engine, no matter how you do it (no advertising really), so that we can be more handy when we have problems writing JavaScript code.

Finally, due to the limited space in this paper, some details may be omitted. It is highly recommended that students who are interested in further understanding can read the following list.

Thanks for reading.

read

  • Fast properties in V8
    • In the translation
  • Illustrated Google V8 — Geek Time
  • How is data stored in V8 JS engine memory?
  • The fast and slow properties and the fast and slow array in V8
  • Enable the FAST mode for V8 object properties
  • ECMAScript ® 2015 Language Specification
  • Does JavaScript guarantee object property order? – stackoverflow