• Modernization of Reactivity
  • This article is written by Kris Zyp
  • The Nuggets translation Project
  • Translator: Liz
  • Proofreader: LLP0574, luoyaqifei

Reactivity with The Times

In the last decade, the rise of responsive programming has brought a storm of evolution to JavaScript, with front-end development benefiting greatly from its simplicity, with the user interface responding in real time to changes in data, eliminating a lot of error-prone code when updating the UI. However, as it becomes more popular, existing tools and technologies do not always keep up with contemporary browser features such as Web apis, language capabilities and performance optimization algorithms, extensibility, simplified syntax, and persistence. In this article, let’s take a look at some of the new technologies, methods, and capabilities that are available, using a new library, Alkali, as a backdrop.

The techniques we’ll cover include render queues, fine-grained responses based on pull, reactive generators and expressions for ES6, reactive native Web components, and two-way data flows. These technologies are more than just programming whims; they are the result of taking existing browser technologies and combining them with deep research and development, resulting in better performance, cleaner code, better coordination with new components, and better encapsulation.



So we’ll look at a few simple and declarative examples (you can also look at this more complete example: Alkali todo-mvc application ). They use a standard native architecture, with one important feature that might be useful: the ability to display quickly with minimal resource consumption. These cutting edge technologies do deliver scalable benefits, high efficiency and considerable benefits. Given the plethora of libraries, the most predictable and stable library structures are those built directly on top of standards-based browser element (or component) apis.

Push-pull Reactivity (reactive push-pull)

The key to extended responsive programming is the architecture of the data flow. A native reactive approach is to use a simple observer or listener pattern, pushing each update as a judgment stream to each corresponding listener. This quick response results in a lot of unnecessary repetitive intermediate judgments in the event of any multi-step state update, leading to overcalculation. A more scalable approach is to use a pull based approach that only evaluates when a downstream observer (Observable) requests or pulls the latest value (lazy loading). After being notified of dependent data changes, subscribers can request data via DE-bouncing or queuing.

Pull-based methods can also be used in conjunction with caching. Once the data calculation is complete, the results can be cached, and then upstream changes will be notified to invalidate the downstream cached data, thus ensuring the real-time performance of the data. This pull-based reactive cache invalidation follows not only the same design architecture as REST and the extensible design of the web, but also the architecture of modern browser rendering processes.

However, when the scene is gradually updating its current state, it is recommended to use push for certain events. It is very effective when incrementally adding, removing, or updating elements in a collection, and it is better to mix and match with others, such as: Data is pulled primarily from the observer, but incremental updates can be pushed as optimizations through real-time data streams.

Queued Rendering (Rendering queue)

The key to improving application efficiency through reactive Pull in a responsive app is to ensure that rendering execution costs are minimal. Often, multiple parts of the application may be updating state, and if rendering is synchronous, any state changes are performed immediately, which can easily lead to interface jitter and inefficient execution. By queuing rendering we can ensure that rendering is minimized even if multiple states change.

Queuing behavior, or jitter elimination, is a relatively common and well-known technique. However, for optimized queued rendering, browsers actually provide an excellent alternative to the general jitter elimination approach. Because of its name, requestAnimationFrame is often thought of as an animation-related library, but in fact this new API does a pretty good job of rendering queue state changes. It is a macro event Task, so any small tasks, such as those with low resolution, will be allowed to load first. It also allows the browser to determine the exact best time to render new changes, considering the final render, TAB/browser visibility, current load, etc. It can perform callbacks immediately (usually in milliseconds) in visible sleep, and can even be completely delayed when a page/TAB is hidden, if the frame rate is appropriate for sequential rendering. In fact, by changing the state of the requestAnimationFrame rendering queue to render when the view needs to be updated, we are essentially the same optimized rendering flow, precise timing, and sequence/path that modern browsers use. This approach ensures that we and the browser render efficiently and timely in a complementary way, avoiding additional layout and redrawing.

This can be thought of as a two-stage rendering approach. In the first phase, in response to the event handler, we update the canonical data source to invalidate the derived data or components that depend on it. Invalid UI components are queued up for rendering. The second stage is the render stage, retrieving the necessary data and rendering.

Alkali renders the queue through a renderer object, correlating reactive data inputs and corresponding elements (called “variables” in Alkali) in real time, and then rerenders the queue state through the requestAnimationFrame mechanism. This means that any data binding is connected to the render queue. This can also be indicated by instantiating a Variable object and associating it with an element (here we create a greeting). Example code is as follows:

Import {Variable, Div} from 'alkali' var greeting = new Variable('Hello') Body. AppendChild (new Div(greeting)) // Notice, Greeting. put('Hi') // The rendering mechanism here queues up the rendering of greeting.put('Hi again') in the div.Copy the code

The div uses the requestAnimationFrame mechanism, which will automatically update the state of the div at any time, and multiple updates will not result in multiple renders, only the last state will be rendered.

Granular Response (Granular response)

Pure reactive programming allows a single signal or variable to be used and passed through the system. However, some reactive frameworks based on DIFF, such as ReactJS using the virtual DOM, have become popular because they help maintain the familiar state of imperative programming. These frameworks allow you to continue to write programs in the same way you write applications in imperative code. When any state of the application changes, the component simply rerenders, and once that’s done, the output of the component is compared to the previous output to determine the change. Instead of an explicit data stream updating the rendered UI with specific, explicit changes, diff compares the reexecuted output to the previous state.

While this development is convenient and produces the familiar demonstration code, it sacrifices a significant amount of memory and performance. Reactive contrast requires a full copy of the rendered output and complex contrast algorithms to determine differences to mitigate excessive DOM rewriting. The virtual DOM typically requires two or three times more memory usage than the comparison algorithm adds a similar overhead to determine the DOM changes directly.

True responsive programming, on the other hand, explicitly defines variables or values that can be changed and the continuous output of their values as they change. This does not require any additional overhead or comparison algorithms because the output is specified directly by the connections defined in the code.

Program adjustability benefits from fine-grained, functionally active code flow. Debugging imperative programming involves refactoring conditions and the steps of reconstructing code blocks, requiring complex reasoning to evaluate the state worth changing (and how it might be wrong). The functional response flow can perform a static check, and at any time, anywhere, we can see the complete, independent input graph corresponding to the UI output.

Also, using true responsive programming is not an esoteric or pedantic computer science technique. It actually has significant benefits in terms of program extensibility, speed, responsiveness, and ease of debugging application flows.

Canonical and Reversible Data

In fine-grained Reactivity, it is even possible to reverse the direction of explicit data flow, that is, to achieve bidirectional binding, so that consumers downloading streaming data, such as input elements, can request uploaded data changes without additional configuration connections or the necessary logic. This makes it very easy to bind the form to the form’s input controls.

An important principle of responsive programming is “true consistency of source”, where there is a clear specification to distinguish canonical data sources from derived data. Responsive data can be called a directional data. This is critical for data management. Synchronizing multiple data states with no clear source and derived data can confuse data management and lead to multiple claim management problems.

Unidirectional data changes with centralized data changes and is associated with responsive data changes. It is a properly directed graphical data. Unfortunately, one-way data flow basically means that consumers of data may have to manually connect to the data source, which is a classic violation of localization principles and a gradual reduction in encapsulation, leading to more and more entanglements between independent components, making development more onerous.

However, a directed specification of the data does not necessarily only command the data to change by graphically associating this method. By using fine-grained Reactivity, you can support bidirectional data flow. With bidirectional data, the direction of the data can still be preserved by notifying downstream data when it changes or is added, whereas upstream data changes are defined as requests initiated by data changes (which, in future implementations, can be revoked). A change request to derived data can still be implemented as long as it contains a reverse transformation to pass the request raw data (two-way data traversal or transformation is often referred to as a lens functional term). Changes to canonical data still occur at the data source, even if they are initialized or requested by downstream data consumers. With such clear data flow characteristics, directed canonical data and derived data are preserved, maintaining consistency of state while allowing interaction between encapsulated independent data entities, whether they are derived or not. In fact, this simplifies input and form management for the development user, and also facilitates the encapsulation of input components.

DOM Extensions up to date (” Web Components “)

Learning to program with foresight, code maintainability is critical, and it’s challenging to keep your code maintainable in the JavaScript ecosystem with all the new technologies that are emerging. Which new framework will shine in three years? Based on past history, this is hard to predict. How do we develop in this mess? The surest approach is to reduce our reliance on specific API libraries and maximize our reliance on standard browser apis and architectures. It is more feasible to use emerging component apis and capabilities (that is, “Web components”).

The best definition of responsive architecture should not dictate a specific component architecture, but should use native or third-party components flexibly to maximize their survival in future evolution. However, despite all our efforts to reduce coupling, some degree of coupling can be useful. In particular, it is much more convenient to use variables as input to values or attributes than to create a data binding and retrieve the data. Integration with the element or component lifecycle, notification when an element is deleted or detached, easy auto-cleanup dependencies, and listening mechanisms to prevent memory leaks, reduce resource consumption, and simplify component usage.

In addition, today’s browsers make integration of Web components with native elements entirely feasible. It is now possible to define truly DOM-based custom classes from existing HTML prototypes, with responsive surveyable variable constructors, combined with the MutationObserver interface (and future potential Web component callbacks) to allow us to monitor when elements are detached (or attached). ES5’s getters/setters also do a good job of showing style attributes that allow us to extend and recustomize native elements appropriately.

Alkali defines an explicit set of DOM constructors and classes to support these functions. These classes are minimal extensions of native DOM classes, with constructor arguments that support input variable control properties and automatic variable cleanup. Combined with lazy-loaded or Reactivity based on reactive Pull, this means that the element’s data changes are dynamically visible and no longer trigger any decisions through its dependent inputs once the data is detached. This causes the creation and extension of an element to automatically clear listeners themselves. Such as:

Let greetingDiv = new Div(greeting) body.appendChild(greetingDiv) // Changes to greeting will automatically create a binding listener... Body. RemoveChild (greetingDiv) // Greeting binding/listening will be cleaned upCopy the code

Reactive Generators

Not only does the Web API offer significant improvements in responsive programming methods, but the ECMAScript language itself has exciting new features and syntactic optimizations that make it easier to write responsive code. One of the powerful new features is generators, which provide an elegant and intuitive syntax for code flow interactions. Perhaps the biggest inconvenience with reactive data is that JavaScript often requires callback functions to handle state changes. However, ECMAScript’s new generator functions can pause, resume, and restart a function that can apply the input of reactive data through standard sequential syntax, as well as pause and restart any asynchronous input. The generator’s controller can also automatically subscribe to the dependent input and re-execute the function when the input changes. The execution of this control function makes it possible for generators to make leveraged to yield a pun! The function yield, described below, can control complex combinations of variable inputs with intuitive and easy-to-understand syntax.

The Generator was expected to eliminate Callback callbacks as promised and support intuitive sequential syntax. But the Generator does a great job of not only pausing or resuming an asynchronous input, but also restarting it as soon as any input value changes. This is easily done by using the yield operator in front of any input variable, which also allows the corresponding code to listen for changes to its variable and return the expression when the value of the current variable is available.

Let’s take a look at how this is done. In Alkali, generator functions can be converted as input variables, and want to use React to create a reactive function that outputs a new compound variable. The React function acts as a controller for the generator to control the reactive variables. Here’s an example of a step-by-step explanation:

let a = new Variable(2)
let aTimesTwo = react(function*() {
  return 2 * yield a
})
Copy the code

The React controller handles the startup of the provided Generator. A Generator function returns an iterator for interaction. React is responsible for starting the iterator. The Generator does not perform the calculation until the yield operator is present, where the code encounters the yield operator directly and then passes the yield operator’s return value from the iterator to the React function. In this case, the A variable will be returned to the React function, which makes the React function do more than one thing.

First, it can subscribe to or listen to a supplied reactive variable (if it is one), so it can immediately respond to any changes by re-executing them. Second, it can get the value of the current state or response variable, and when resumes, it can return the result of the yield expression. Before finally returning, the React function checks whether the reactive variable is asynchronous and holds a convention value, and if necessary, waits for the convention to return and resumes executing the function. Once the current state is obtained, the generator function restores the value of execution 2, returning it to the yield A expression. If there are more yield expressions, they are executed sequentially and resolved in the same way. In this case, the generator returns a 4, and then terminates the generator sequence (until a changes or repeats).

With the React function, the execution of this code is encapsulated in another compound reactive variable, and any changes to the variable do not trigger reexecution until downstream data is accessed or requested to execute it.

The Alkali generator function can also be used directly to define a rendering function in the element constructor that is automatically reexecuted when any input values change. In both cases, we use the previously mentioned yield in front of all variables.

import { Div, Variable } from 'alkali'
let a = new Variable(2)
let b = new Variable(4)
new Div({
  *render() {
    this.textContent = Math.max(yield a, yield b)
  }
})
Copy the code

This creates a textContent with textContent of 4 (the maximum of two inputs), we can update either variable, and it will be re-executed.

a.put(5)
Copy the code

The current content of A will be updated to 5.

Generators are not universally compatible with all browsers (IE and Safari, for example), but they can be shipped with or emulated by other tools (such as Babel or others).

Properties and Proxies

One important aspect is that reactivities are responsively bound to properties of objects. But it takes more than the value returned by the current standard property access to encapsulate notification of property changes. Therefore, binding properties or variables responsively requires more detailed syntax.

However, another exciting new feature of ECMAScript is proxies, which allow us to define an object to intercept all property access and modify custom functionality. This is a powerful feature that can be used to return a reactive property variable via a normal property access, not to mention the fact that a Reactive object uses a conventional syntax.

Unfortunately, proxies are not as easily emulated by code compilers as Babel. Emulating agents requires not only transpiling agent constructors themselves, but also any code access agents, so emulators are incomplete without native language support. They can execute unreasonably slowly and code is bloated due to the amount of execution required to filter every attribute of the application. However, it is also feasible to use more targeted methods. Let’s take a look.

Reactive expression

As ECMAScript continues to advance, tools such as Babel and its plug-ins continue to evolve, giving us great opportunities to create new compiled language features. When generators can use the Babel plug-in to create a function that performs asynchronous and reactive operations immediately, using ECMAScript syntax to bind properties, the code can be converted into fully responsive data streams. This is more complex than simply re-execution, where operations can be defined between the output and input of an expression, such as reversible operators, reactive properties, and reactive tasks can be generated using simple idiomatic expressions.

Here is a separate project that uses the Alkali-based plug-in Babel to transform reactive expressions. With this we can write an expression that calls the/operator with the react argument:

let aTimes2 = react(a * 2)
Copy the code

Here the value of aTimes2 is bound to the multiplicative value of the input variable A. If we change the value of a (which we can do by using a.put()), aTimes2 will automatically update the value. In fact, this data is reversible because we used the perfectly customized React operator. We can specify a new value for aTimes2, such as 10, and the value of A will be automatically updated to 5.

As mentioned above, it is almost impossible for a proxy to emulate an entire code base, but in our reactive expressions, reactive variables compile the syntax of the property to control the property is a matter of water. Even better, other operators can make the variable transpile reversible. For example, we can write complex combinations of purely reactive code:

let obj, foo react( obj = {foo: 10}, // We can create a new responder object foo = obj.foo, // Get a responsive object property aTimes2 = foo // assign it to aTimes2 (bound to the expression above) obj.foo = 20 // Update the object (which passes the value through foo responsively, ATimes2 passes to a)) a.valueof () // -> 10Copy the code

Technology should keep pace with The Times

Web development is constantly changing and evolving, and every step forward is exciting. Reactivity is one of the most advanced programming concepts in today’s applications, and its language and API are evolving as new technologies and modern browser capabilities evolve. Together, they can move Web development forward. I’m excited about the possibilities for future development and hope that these ideas will come to fruition. I’ll see how new tools will improve Web development in the future.

Alkali is already being used by our team of engineers and we developed it on the Doctor Evidence site. We have been exploring building interactive and responsive tools that query and analyze large datasets of clinical medical research on this site. It was an interesting challenge to maintain a smooth user interface while handling complex and large amounts of data, and many of these approaches worked well for us as we developed our web software using new browser technologies. Nothing more, we just hope that Alkali will serve as an example to inspire Web development further.