TypeScript implements red dot notifications in wechat

In our lives we often see “red dots” that direct us to a certain place or information.

There are many red dots in wechat. Imagine how our usage would be affected if there were no red dots.

This paper uses a tree to implement a red dot structure that can automatically update the status

Wechat seems to have a lot of red dots, but the process in between is actually triggered by a certain event in the deepest place, such as a message, a friend circle “like”, a friend application. The middle process can be understood as passing the red dot on the edge to the root one by one. Users can click it layer by layer from the root, which is like a guiding path to tell users where the red dot is.

Warehouse address: github.com/oloshe/red

Analysis of the

Take wechat as an example, we simply comb out its red dot structure, as shown in the figure below.

Simple analysis:

  • Each node has a name (orkey) to distinguish it from other sibling nodes
  • Every node has a parent (except the root)
  • Each node has [0, n] children, and the node with the number of 0 is also called leaf node
  • The number of children of some nodes is indeterminate and some nodes need to be added dynamically

So this is a normal tree, not fancy, but useful, let’s call it a red dot tree

Create a simple node class:

class RedNode {
  / * * * / name
  name: string
  Her parents / * * * /
  parent: RedNode | null
  /** Information load */
  _value: number = 0
  /** 后代 */
  children: Record<string, RedNode> = {}
  /** 血统 */
  lineage: string
}
Copy the code

Explain a few fields for the above structure:

  • parent: RedNode | nullparent is RedNode, means it has a parent, and whenparent is nullIs the root node.
  • children: Record<string, RedNode>Why not use child nodesArray? As a father, you should be able to name each child by name, rather than just shoving them into an array, which is a quick way to find them and makes more sense.
  • _value: numberThis right here is the number of red dots that this red dot is subjected to. Start with an underscore because you need to set itgettersetter
  • lineage: stringHere represents the complete path from the root node to this node. In order to facilitate subsequent operations, the complete path of this node can be quickly obtained.
  • For the path, toA/B/CFormat representation, wherein/As a separator, a constant is definedconst splitter = "/";

For red dot, it is usually called Badge, but I think this word is not very good image, let’s break the tradition, we call it RedNode.

The constructor

The constructor assigns values to fields that are not assigned in the above fields:

class RedNode {
  // ...
  constructor(name: string, parent: RedNode | null, lineage? :string) {
    this.name = name, this.parent = parent;
    if(lineage ! = =void 0) {
      this.lineage = lineage
      return
    }
    // Bloodlines
    this.lineage = [...this]
      .map(x= > x.name)
      .reverse()
      .join(splitter);
  }
  /** * self iterator, from self to ancestor (excluding root) */* [Symbol.iterator]() {
    let dynasties: RedNode = this
    while (dynasties && dynasties.parent) {
      yield dynasties
      dynasties = dynasties.parent
    }
  }
}
Copy the code
  • nameUniquely identified by a red dot
  • parentThe parent, isnullIs the root node
  • lineageWhen passed in, you don’t have to iterate over the parent

If lineage is not introduced, it is necessary to ask the lineage generation by generation. From the current node, iterations up to the ancestor node and then a complete path is formed

[…this] normally returns an error because the Symbol. Iterator must be implemented to iterate. So add an iterator to RedNode that makes finding ancestors more elegant.

This iterator will iterate from this all the way to the very first node, not including the root node. (why not including the root node to WeChat as an example, the root node, the number of red dot is assembled four TAB page of the red dot, the final set to applicationIconBadgeNumber notification operation system. In the app, you’re dealing with four tabs. So you can see that a tree is more like a forest, but it depends on how you define the structure.)

Once the constructor is done, we can generate a root node:

class RedNode {
  // ...
  /** red dot tree root */
  static root = new RedNode("@root".null);
}
Copy the code

The name of the root is not important, so it’s arbitrary.

We have the root, but we need a way to add nodes:

/** Add child */
addChild(path: string): RedNode | null {
  if (path === "") return null;
  let keyNames = path.split(splitter);
  let node: RedNode = this;
  let len = keyNames.length, tmpPath = "";
  // start at 0 and end 2
  for (let i = 0; i < len - 1; i++) {
    let k = keyNames[i];
    // If the string is empty, it will be skipped.
    if(! k) {continue }
    tmpPath += k;
    if (node.children[k]) {
      node = node.children[k];
    } else {
      // If there is a non-existent node in the middle, it can be automatically added to it.
      let newNode = new RedNode(k, node, tmpPath + k);
      node.children[k] = newNode;
      node = newNode;
    }
    tmpPath += splitter;
  }
  let leafKey = keyNames[len - 1];
  let newNode = new RedNode(leafKey, node, path);
  node.children[leafKey] = newNode;
  return newNode;
}
Copy the code

This method splits the path by/from the passed path and adds nodes to it. For example, add Chats/ Zhangsan to wechat. Split into [‘Chats’, ‘zhangsan’] and add wechat/Chats/ Zhangsan node. (Intermediate Chats are automatically created if they do not exist, but are not recommended for initialization as it is impossible to distinguish between initial and dynamically added Chats, as described below)

Initialize the

With nodes, roots, constructors, and a way to add children, tree generation comes naturally. Let’s write a simple function that initializes the tree:

class Red {
  static_initial_path_arr? :string[]
  /** Initializes the red dot tree */
  static init(initialPathArr: string[]) {
    red._initial_path_arr = initialPathArr;
    let len = initialPathArr.length;
    for (let i = 0; i < len; i++) {
      letpath = initialPathArr[i] RedNode.root.addChild(path); }}}Copy the code

_initial_path_arr is used to record the red dots generated by initialization, which is convenient to distinguish the initialization node from the dynamic node, so that some unsafe operations can be performed

Call the static method above to initialize the red dot structure of wechat:

red.init([
		/ / message
    'Chats'.// Chats/... '
		/ / the contact
    'Contacts'.'Contacts/newFriends'./ / found
    'Discover'.'Discover/Moments'.'Discover/Channels'.'Discover/TopStories'./ / I
    'Me'.'Me/Pay'.'Me/Cars&Offers',]),Copy the code

Since the nodes of Chats are undefined, such as one day when Josh is deleted, he does not need to be added to the Chats, so it is not generated for the time being. Because it’s different from other structural red dots, like Contacts/newFriends, it never goes away.

The structure after initialization is shown as follows:

Update mechanism

With the structure of the tree in place, the next step is to set its value. Through the previous analysis, we know that by setting the leaf node, to affect the upper node, to achieve automatic update.

Getters and setters for _value fields:

class RedNode {
  
  // ...
  
  get value() {
    return this._value
  }

  set value(newValue: number) {
    if (newValue < 0) { newValue = 0 }

    // Return with the same value
    if (newValue == this._value) return;
    let delta = newValue - this._value;
    this._value += delta;

    console.log(`The ${this.lineage} = ${newValue} (${delta > 0 ? ` +${delta}` : delta}) `);
    
    // Notify all listeners
    red._notifyAll(this.lineage, newValue);

    // pass to the parent
    if (this.parent && this.parent.parent) {
      this.parent.value += delta; }}/ /...
  
}
Copy the code

PS: red. _notifyAll. This method is used to notify all listeners that the value they are listening to has changed and then perform a callback.

We get delta by subtracting the new value from the old value, and then we set our own value, and then finally if we have a parent that’s not the root, we change the parent’s value with the += symbol, and the parent will also trigger its setter method, passing it on.

Lineage came in handy, so you don’t have to iterate over every parent path.

It’s really easy to find the parent from the children, if you’re describing your great-grandfather, it’s easy to describe you, if your great-grandfather wants to describe you, it’s not so easy, because you might be one of his descendants.

The downside of this is that you can’t change the value of a non-leaf node. If you do, it will be a mess. For example:

1, Tom is one of the most popular books in China

If you change the Chats to 0 at this time without simultaneously changing the Chats/ Zhangsan, the Chats will change to 1 if you change the Chats/ Zhangsan to 3 and the Chats are out of sync.

However, the unsafe method is designed later for special situations where non-leaf nodes are required.

Update method

Due to space issues, the following code is simplified:

interface SetOption {
  /** Forces a node to be added (when there is no node) */force? :boolean
}

class red {
	/** * set the red dot status */
  static set(path: string, value: boolean | number, options: SetOption = {}) {
    if (typeof value === "boolean") value = Number(value);
    if (typeofvalue ! = ='number') { console.warn(`red.set('${path}', ${value}Warning! \n type needs to be Boolean or number, but receivedThe ${typeof value}Type. Use the default: 0 '); value = 0 }
    let {
      force,
    } = options;
    let node = red.resolvePath(path, { force, careless: false });
    if(! node) {console.error(`red.set('${path}', ${value}) failed! \n Cause: path does not exist \n To add dynamic nodes set force to true! \noptions:`, options);
      return
    }

    if(! node.isLeftNode) {if(! red._non_leaf_node_change_lock_) {console.log('Modify non-leaf nodes')}else {
        console.error(`red.set('${path}', ${value}) failed! \n Cause: Setting values of non-leaf nodes, which will cause parent and child elements to be out of sync! \n Please try to avoid doing this! \n If you have to change it, use the red.unsafe. Set method. `, node)
        return
      }
    }

    node.value = value
  }
  /** Locks that prevent non-leaf nodes from being modified. True => Not allowed to change. False => Allowed to change */
  static _non_leaf_node_change_lock_: boolean = true
}
Copy the code

The PS: red.resolvePath method is used to find nodes based on the path path. When force is true, it forces additional nodes, namely dynamic nodes. Careless represents a concern about the outcome. If you are concerned but cannot find the path, the prompt message will be written out.

Node. IsLeafNode is the getter under RedNode, indicating whether it is a leaf node. As mentioned above, it is a leaf node when the number of children is 0.

get isLeafNode() :boolean {
  return Object.keys(this.children).length === 0
}
Copy the code

This method is generally well understood, but there are a lot of log places (the log place must be log), the important thing is to call Node. value = value to trigger setter.

Red. _non_leaf_node_change_lock_ is used to prevent the problem of modifying non-leaf nodes.

To modify the value of a non-leaf node, there is an unsafe method, which is defined as follows:

class red {
  static unsafe = {
    set(path: string, value: boolean | number, options: SetOption = {}) {
      red._non_leaf_node_change_lock_ = false
      red.set(path, value, options)
      red._non_leaf_node_change_lock_ = true}}},Copy the code

Just add lock to unlock in fluctuation two lines actually, be very simple! Of course this is not a decoration, it can be very clear to the user that you are performing an unrecommended operation, you better know what you are doing!

Listening to the red dot

The monitoring function is relatively simple. My method is to monitor the path, which is the lineage attribute of the node. As mentioned above, a setter function calls a method, red._notifyAll. It simply iterates through all the listener functions of that path, passing the red dot as it is called.

interface ListenerData {
  callback: (num: number) = > void
}
class red {
  /** Red dot change listener */
  static listeners: Record<string, ListenerData[]> = {}
}
Copy the code

Ignore the red dot

There is another property on the red dot:

class RedNode {
  // ...
  /** Ignore red dot depth-first traversal ignore all descendants */
  ignore() {
    if (this.isLeafNode) {
      this.value = 0;
    } else {
      for (let i in this.children) {
        this.children[i].ignore()
      }
    }
  }
  // ...
}
class red {
  // ...
  /** Clean up the red dot */
  static clear(path: string){ RedNode.find(RedNode.root, path)? .ignore() }// ...
}
Copy the code

Ignore finds the root in depth first and calls the setter to set all the leaves to 0, which will be zero as well.

Red.clear is a method that is exposed to external calls, finds the red dot on the path, and calls ignore.

The release of node

Free the node, remove the associated listener and its own and child memory usage:

class red {
  
  static del(path: string) :boolean {
    if(! path)return false
    // In the initialized red dot, it cannot be deleted by default. Use red.unsafe. Del to delete it
    if(red._initial_path_arr? .indexOf(path) ! = -1) {
      console.error('Delete the red dot${path}Failure! \n Cause: The path cannot be deleted by default during initialization. Use red.unsafe. Del to delete the path. `)
      return false
    }
    return red.unsafe.del(path);
  }
  
  static unsafe = {
    /** * Deleting any red dots frees up the tree and listener memory, and the listener function will not take effect *@param path 
     */
    del(path: string) :boolean {
      let del_node = red.resolvePath(path)
      if(! del_node) {return false }
      // Deleting a node triggers a chained update
      let del_path = del_node.lineage;
      red.unsafe.set(del_path, 0);

      // DFS checks the children
      const check_it_out = (node: RedNode) = > {
        // Whether the listener exists
        let path = node.lineage
        console.log(path)
        let arr = red.listeners[path]
        if (arr && arr.length) {
          warn('Delete red dot:${node.lineage}`);
          delete red.listeners[path]
        }
        // Delete the node
        deletenode.parent? .children[node.name]if(! node.isLeafNode) {// Delete non-leaf nodes by killing all children
          for (let i in node.children) {
            check_it_out(node.children[i]);
          }
        }
      }
      check_it_out(del_node)
      return true}}}Copy the code

Unsafe. del does not delete initialized nodes, but only dynamic ones. And the latter node can be deleted whatever it is. (via red._initial_path_arr)

Deletion methods can be divided into four things:

  1. Trigger updates, that is, callsred.unsafe.setMethod (why do you need to set it?)
  2. Check listeners. If there are listeners, delete all listeners
  3. Delete itself
  4. Iterate through the descendant and continue deleting (there is a point that can be optimized, where do you think it can be optimized?)

Multistate node

In some special cases, the data of a red dot may have multiple sources. In this case, there are two ways to solve the problem:

  1. Use multi-state (this article has deleted the multi-state related code, in fact, the second way can achieve the same effect, interested can go to Github to view the source code)
  2. Create children

Let’s take a second look at how the following two examples are implemented

Example 1: Red dots in moments

Moments of red dots can be divided into two types:

  1. None of my business.
  2. It’s about me (I’m in the news)

In this case, the red dot can be created like this:

red.init([
  // ...
  'Discover/Moments'.'Discover/Moments/aboutMe'.'Discover/Moments/others'.// ...
])
Copy the code

Then, two red dots are monitored in the circle of friends item of the Discover page at the same time. If there is only Discover/Moments/ Others, a small red dot is displayed. If Discover/Moments/aboutMe is not 0, add the corresponding numeric red dot next to it.

Example 2: Chat messages

Chat we assume the following types:

  • Text message
  • Media information (voice, text, video)
  • Link information
  • Transaction Message (Transfer/red envelope)

Red dot structure (dynamic) :

[
  'Chats'.'Chats/zhangsan'.'Chats/zhangsan/text'.'Chats/zhangsan/media'.'Chats/zhangsan/link'.'Chats/zhangsan/transaction',]Copy the code

Clear (‘Chats/ Zhangsan ‘) to clear the red spots of the Chats/ Zhangsan.

If product manager said need to transfer at this time a red envelope is not red dot does not disappear when closed, then you need to set up in addition to Chats/zhangsan/transaction of red dot is 0, the other red dot will let them automatically update all.

Packaging form

The packaging of a component depends on its implementation, and each platform and framework implementation is different. Get method to get the initial value when creating the component, register the listener, update the view when the red dot value changes, and unsubscribe the listener when the component is destroyed.

Red.get: method of getting the value of the red dot, finding the red dot based on the path passed in, and returning its value.

Use React as an example to implement a simple red dot component:

const RedDot = (props: { path: string }) = > {
  let [num, setNum] = useState(0)
  
  const fn = UseCallback(() = > num= > setNum(num), [])
  
  const _listener = UseMemo(() = >{ _listener? .off() red.on(props.path, {callback: fn }
  }), [props.path])
  
  useEffect(() = > {
    setNum(red.get(props.path))
		return () = > {
      _listener.off()
    }
  }, [props.path])
  
	return <div>{num}</div>
}
Copy the code

The above code did not run, temporary handwriting, probably understand the meaning of the line (escape)

Listen for the red dot in the back-end data setting

The value of the red dot is usually determined by the server’s return. Do some global listening for the field in the request method and then call red.set to set the corresponding value.

Dynamic nodes can be created when new data needs a red dot.

The advantages and disadvantages

It’s time to summarize the pros and cons of this plan:

Advantages:

  • The red dots are dependent on each other and automatically update their values
  • Good performance, less computation
  • Set up once, use anywhere.

Disadvantages:

  • Setting values by path, without strong typed language reference tracking, if the requirements are frequently changed between paths, it is possible to change the red dot path but forget to change the use of a certain place will cause errors. But this can be avoided by being careful.
  • Since both parent and child are passed in one direction, it is the default that all values are correct if abusedred.unsafe.setMethods can cause a lot of strange behavior, and since all the red dots are in one object, it can be difficult to find the root of the problem.

Jiao explains bian

The first disadvantage: because the original design is used for JS programs, so the string can better adapt, if the object structure to use the path, every minute to report a ReferenceError. But when it comes to strongly typed languages, there are still specific solutions, so I implemented it again with DART. If I have time, I can write a “Dart Implementation red Dot Tree” article.

The second disadvantage: for not good to find the method of the problem, that is more buried point, the process and problems are log out, give sufficient hints. Dump (red dot, red dot, red dot, red dot); dump (red dot, red dot, red dot);

conclusion

This article only discusses one kind of realization of red dot. It is unknown whether the real realization of wechat is based on tree structure. If you know of other implementations you are welcome to discuss them. If you are interested, you can go to Github to check the code. It will be my pleasure if you can use it. There is not much code and there is no library to rely on.

If you find this article useful, please give it a thumbs up