The Ideas Behind React Easy State: Utilizing ES6 Proxies

前端开发 Medium

Front-end developers often refer to transparent reactivity — at the core of MobX, Vue or React Easy State — as magic, but there is nothing magical about it. It is based on a very simple idea, that I will demonstrate with the following snippet and some common sense.

You can perfectly define when you expect NotesApp and Note to re-render: when a new note is added or removed and when the author or a note’s text is modified. Luckily this conclusion was not driven by complex human intuition, but simple programmable if-else logic.

If a part of a state store — which is used inside a component’s render— mutates, re-render the component to reflect the new state.

You brain is creating the following ternary relations about properties of objects — used inside render methods.

When object.property changes -> re-render component

When a property of an object is modified it looks up all of the components, which belong to that (object, property) pair and re-renders them — imaginarily. Let’s do this in real life!

The rest of the article assumes you have a basic understanding of ES6 Proxies and React Easy State. If you don’t know what I am talking about, a quick look at the MDN Proxy docs and my Introducing React Easy State article is enough to go on.

Making a Reactive Core

In order to construct the (object, property, component) relations, we have to know which objects and properties do NotesApp and Note use during their renders. A developer can tell this by a glance at the code, but a library can not.

We also need to know when a property of an object is mutated, to collect the related components from the saved relations and render them.

Both of these can be solved with ES6 Proxies.

The store Proxy intercepts all property get and set operations and — respectively — builds and queries the relationship table.

There is one big question remaining: what is currentlyRenderingComp in the get trap and how do we know which component is rendering at the moment? This is where view comes into play.

view wraps a component and instruments its render method with a simple logic. It sets the currentlyRenderingComp flag to the component while it is rendering. This way we have all the required information to build the relations in our get traps. object and property are coming from the trap arguments and component is the currentlyRenderingComp — set by view .

Let’s get back to the notes app and see what happens in the reactive code.

  1. NotesApp renders for the first time.
  2. view sets currentlyRenderingComp to the NotesApp component while it is rendering.
  3. NotesApp iterates the notes array and renders a Note for each note.
  4. The Proxy around notes intercepts all get operations and saves the fact that NotesApp uses notes.length to render. It creates a (notes, length, NotesApp) relation.
  5. The user adds a new note, which changes notes.length .
  6. Our reactive core looks up all components in relation with (notes, length) and re-renders them.
  7. In our case: NotesApp is re-rendered.

The Real Challenges

The above section shows you how to make an optimistic reactive core, but the real challenge is in the numerous pitfalls, edge cases and design decisions. In this section I will briefly describe some of them.

Scheduling the Renders

A transparent reactivity library should not do anything other than constructing, saving, querying and cleaning up those (object, property, component) relations on relevant get/set operations. Executing the renders is not part of the job.

Easy State collects stale components on property mutations and passes their renders to a scheduler function. The scheduler can then decide when and how to render them. In our case the scheduler is a dummy setState , which tells React: ‘I want to be rendered, do it when you feel like it’.

A few lines from Easy State’s source code

Some reactivity libraries do not have the flexibility of custom schedulers and call forceUpdate instead of setState , which translates to: ‘Render me now! I don’t care about your priorities’.

This is not yet noticeable — as React still uses a fairly simple render batching logic —but it will become more significant with the introduction of React Fiber ’s scheduler.

Cleaning Up

Saving and querying ternary relations is not so difficult. At least I thought so, until I had to clean up after myself.

If a store object or a component is no longer used, all of their relations have to be cleaned up. This requires some cross references — as the relations have to be queryable by component , by object and by (object, property) pairs. Long story short, I messed up and the reactive core behind Easy State leaked memory for a solid year.

After numerous ‘clever’ ways of solving this, I settled with wiping every relation of a component before all of its renders. The relations would then build up again from the triggered get traps — during the render.

This might seem like an overkill, but it had a surprisingly low performance impact and two huge benefits.

  1. I finally fixed the memory leak.
  2. Easy State became adaptive to render functions. It dynamically un-observes and re-observes conditional branches — based on the current application state.

Car is not — needlessly—re-rendered on speed changes, when car.isMoving is false.

Implementing the Proxy Traps

Easy State aims to augment JavaScript with reactivity without changing it in a breaking way. To implement the reactive augmentation, I had to split basic operations into two groups.

  • Get-like operations retrieve data from an object. These include enumeration, iteration and simple property get/has operations. The (object, property, component) relations are saved inside their interceptors.
  • Set-like operations mutate data. These include property add, set and delete operations and their interceptors query the relationship table for stale components.

After determining the two groups, I had to go through the operations one-by-one and add reactivity to them in a seamless way. This required a deep understanding of basic JavaScript operations and the ECMAScript standard was a huge help here. Check it out if you don’t know the answer to all of the questions below.

  • What is a property descriptor?
  • Do property set operations traverse the prototype chain?
  • Can you delete property accessors with the delete operator?
  • What is the difference between the target and the receiver of a get operation? When can they be different?
  • Is there a way to intercept object enumeration?

Managing a Dynamic Store Tree

So far you have seen that store wraps objects with reactive Proxies, but that only results in one level of reactive properties. Why does the below app re-render when person.name.first is changed?

To support nested properties the ‘get part’ of our reactive core has to be slightly modified.

The most important section is line 15–18 and it needs some explanation.

  • It makes properties reactive lazily — at any depth — by wrapping nested objects in reactive stores at get time.
  • It only wraps objects, if they are used inside a component’s render — thanks to the currentlyRenderingComp check. Other objects could never trigger renders and don’t need reactive instrumentation.
  • Objects with a cached reactive wrapper are certainly used inside component renders, since the currentlyRenderingComp check— at line 15 — passed for them previously. These objects may trigger a reactive render with property mutation, so the get trap has to return their wrapped versions.

These points— and the fact that relations are cleaned up before every render — results in a minimal, adaptive subset of nested reactive store properties.

Monkey Patching Built-in Objects

Some built-in JavaScript objects — like ES6 collections — have special ‘internal slots’. These hidden code pieces can not be altered and they may have expectations towards their this value. If someone calls them with an unexpected this , they fail with an incompatible receiver error .

Unfortunately Proxies are also invalid receivers in these cases and Proxy wrapped objects throw the same error.

To work around this, I had to find a viable alternative to Proxies for built-in objects. Luckily they all have a function based interface, so I could resort to old fashioned monkey patching.

The process is very similar to the Proxy based approach. The built-in’s interface has to be split in two groups: set-like and get-like operations. Then the object’s methods has to be patched with the appropriate reactivity logic — namely constructing and querying the reactive relations.

A Bit of Intuition

I was a bit overgeneralizing when I stated that the reactive core is made with cold logic only. In the end I had to use some intuition too.

Making everything reactive is a nice challenge, but goes against user expectations. I collected some meta operations — that people don’t want to be reactive — and left them out of them fun.

These choices were made by intuition during my usage test rounds. Others might have a different approach to this, but I think I collected a sensible subset of the language. Every single operation in the above table has a good reason not to be reactive.

Conclusion

The reactive core — implemented in this article — is not in the source of React Easy State. In reality the reactive logic is in a more general library — called the Observer Utility — and Easy State is just a thin port for React. I intentionally simplified this to make it more digestible, but the presented ideas are still the same. I hope you learnt something new if you made it so far!

If this article captured your interest please help by sharing it. Also check out the Easy State repo and leave a star before you go.

Thank you!

Medium稿源:Medium (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 前端开发 » The Ideas Behind React Easy State: Utilizing ES6 Proxies

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录