Went quickly to the 2020, time has gone by really fast, accompanied by q group of some warm-hearted friend’s feedback and my personal real business ground scene, Concent has entered a very stable operation stage, in this year, open a new series of gossip, will not be updated regularly, used to do some summary or review, content is heart, The theme of this issue is accurate update. The article will comprehensively compare various solutions in the existing industry and see how Concent has a new way to add the indispensable heavy weapon of accurate update to React.
Change detection, routine is many
This theme is accurate update, why want to bring change detection here, because in the final analysis, three presents the framework, the Vue and React to data driven view, nature is the need to first establish a perfect mechanism to perceived data change and change is what data, thus further to do corresponding view updates to the data of work.
Then the difference part is that each family has realized the specific detail of how to perceive the change of data. Below, we briefly summarize their change detection routines.
Ng dirty check &zone
We know that the Angular team upgraded from NG1 to NG2 with an underlying rewrite, resulting in a lot of disruptive changes. Ng1 is called AngularJs, and ng2 and subsequent versions are all called Angular. This is mainly about the improved dirty checking mechanism since NG2 (including NG2).
When we write a piece of code below statement, after such a component in the process of each component instantiation ng changes necessary maintains a detector, so view rendering is generated after the dom tree, actually ng also have a tree change detection at the same time, presents zone was used to optimize the trigger for the whole cycle of change detection, In each change detection cycle, we collect the changed attributes by shallow comparison to further feel which DOM fragments should be updated. At the same time, ChangeDetectorRef is also provided to allow users to rewrite change detection rules and manually intervene in the closing and activation time of a component change detection to further improve performance.
A simple Angular component looks like this
@Component({
template: ` {{firstName}} {{lastName}}
`
})
class MyApp {
firstName:string = 'Jim';
lastName:string = 'Green';
changeName() {
this.firstName = 'JimNew';
this.lastName = 'GreenNew'; }}Copy the code
Note that the change attribute is collected by shallow comparison during the change detection cycle, which is why when a member variable is an object, we need to reassign the object reference rather than change the value of the original reference to avoid detection failure.
@Component(/ * * * /)
class MyApp {
list: string[] = [];
changeList() {
const list = this.list;
list.push('new item');
this.list = list;// bad
this.list = list.slice();// good}}Copy the code
Vue data hijacking & Publish subscription
Vue is known as responsive MVVM. The core principle is that when you instantiate your Vue component, the framework hijacks your component data source and turns it into an Observable, so the various value expressions in the template silently trigger the getter of the Observable during rendering. These dependency dePs maintain a list of Watcher instances (Watcher instances are component instances, and each component instance corresponds to a Watcher instance) of the subscriber. If the user changes the data, the setter is implicitly triggered. The framework senses a data change and the Dep issues a notification that causes the corresponding subscriber to trigger another rendering, thus changing the view (that is, calling the update method of the relevant component instance)
A simple VUE component is as follows (written in a single file) :
<template>
<h1>{{firstName}} {{lastName}}</h1>
<button @click="changeName">change name</button>
</template>
<script>
export default {
data() {
return {
firstName: "Jim".lastName: "Green",}},methods: {
changeName: function () {
this.firstName = 'JimNew';
this.lastname = 'GreenNew'; }}}</script>
Copy the code
Of course, the transformation of observable objects is not completely transformed as we imagined. Vue will compromise to monitor only one layer for the sake of performance. If the object level is too deep, the watch expression requires the user to write the depth monitoring function, and the object assignment needs to call the tool function to deal with it
- For example 1
methods: {
changeName: function () {
this.somObj.name = 'newName';// bad
Vue.set(this.somObj, 'name'.'newName');// good
this.somObj = Object.assign({}, this.somObj, {name: 'newName'});// good}}Copy the code
- For example 2
methods: {
replaceListItem: function () {
this.somList[2] = 'newName';// bad
Vue.set(this.somList, 2.'newName');// good}}Copy the code
Of course, if you don’t want to use utility functions, you can also use $forUpdate to refresh the view
methods: {
replaceListItem: function () {
// not good, but it works
this.somList[2] = 'newName';
this.$forceUpdate(); }}Copy the code
Note, vue2 and VUe3 transform observable objects in different ways. DefineProperty is used in 2 and proxy is used in 3, so vue3 can also actively perceive data changes in the scenario of dynamically adding attributes to objects.
React scheduling update
I remember a long time ago, you Yuxi talked about the differences between React and VUE in an interview, and also mentioned that React is a pull based framework while VUE is a push based framework. There is no better or worse design concept between react and VUE, but it can only be seen which is more suitable for different scenarios. Push Based allows the framework to actively analyze the update granularity of data and split the dependency of different rendering areas, so it is easier for beginners to write some code with better performance without paying attention to details.
React detects data changes through setState. The user triggers the interface, and the framework pulls the latest data to update the view. However, react doesn’t detect data changes, because when you explicitly call setState, it will drive a new rendering.
As shown in the example below, the previous obj and the new obj are the same reference, and clicking the button will still trigger the view rendering.
class Foo extends React.Component{
state = { obj:{} };
handleClick = ()=> this.setState({obj:this.state.obj});
render() {return <button onCLick={this.handleClick}>click me</button>
}
}
Copy the code
So react obviously leaves change detection up to the user. If obj doesn’t change, why would you call setState? If you call it, you’re telling React to update the view, even if the data source is exactly the same as the next one.
More importantly, the React component is rendered from top to bottom by default, so React has shouldComponentUpdate, React. Memo, and PureComponent components to help React identify areas of the view that don’t need to be updated. As a result, some people argue that the React learning curve is larger and imposes more mental burden on people.
Of course, the Context API, which has been stabilized since React16, is also used for change detection. It uses context. Provider to inject objects of interest from a component root node. Context.Comsumer is wrapped around the specific data that each descendant node in the root node needs to consume.
React&Redux publishing subscription
We mentioned above naked writing there is no change detection react, but provides the function of form a complete set to assist the complete detection, in the community, of course, there are many excellent solutions, such as story, provides a global single data source, make different view to monitor data in different data, so when the user to modify data, iterate over all listen to execute the corresponding correction.
Of course, redux itself is a library independent of the framework, and the specific change detection needs to be implemented by the corresponding framework. The implementation mentioned here is React-Redux, which provides a connect decorator to help components through the detection process to determine whether they need to be updated.
Let’s look at a typical component that uses Redux
const mapStateToProps = state= > {
return { loginName: state.login.name, product: state.product };
}
@connect(mapStateToProps)
class Foo extends React.Component {
render() {
const { loginName, product } = this.props;
// Render logic omitted}}Copy the code
MapStateToProps is a state selector operation. Select the desired state and map it to the props of the instance. The subscription callback calculates whether or not the current component should be rendered. The component we instantiated is actually the wrapped component. This component implements shouldComponentUpdate. ShouldComponentUpdate is called during its rerendering to shouldComponentUpdate according to the React lifecycle process to determine if the current component instance needs to be updated.
Note that we mentioned a subscription mechanism, because the story itself the principle, when a single tree any data node is changed, in fact all of the high order components to subscribe to the callback will be implemented, the specific components should be updated, the callback function will be shallow comparison before and after a moment to decide the current instance need not to update, So that’s why Redux insists that if the state changes, always return to the new state in order for the auxiliary shallow comparison to work properly, and of course with the backtracking feature, but most of the time we don’t need it in our applications. Redux-dev-tool relies heavily on snapshots of a single state at different times for playback.
So shouldComponentUpdate should be able to write better applications from a user’s perspective without having to explicitly care about them.
The following example demonstrates that state has changed and must always return the latest
const initState = { list: []};export const oneReudcer = (state = initState, action) = > {
const { type, payload } = action;
switch (type) {
case 'ADD':
const list = state.list;
list.push(payload);
return { list: [...list] };// right
return { list] };// wrong !!!
default:
returnstate; }}Copy the code
Since list is promoted to store, the following format works in a React method, but in redux, it must be executed strictly according to its rules.
const list = this.state.list;
list.push(payload);
this.setState({list})
Copy the code
Act&mobx observable
To some extent, mobx has a vue taste after combining react. Mobx also has its own store, but its data is observalbe’s, so it can actively detect data changes.
Code was organized more oop than functional.
React&Concent scheduling update
Concent doesn’t extend any additional detection policies per se. It’s 100% the same as React. SetState is the update entry point. The only difference is that Concent has added additional update entry functions for the user’s writing experience, as well as expanded the function’s parameters (which are not required).
Let’s start by creating a submodule of Store, foo, to demonstrate the three main entrances
import { run } from 'concent';
run({
foo: {// Declare a module foo
state: { list: [].name:' '}}});Copy the code
- Entry 1
setState
import { register, useConcent } from 'concent';
/ / class
@register('foo')
class CompClazz extends React.Component {
addItem = (a)= > {
const list = this.state.list;
list.push(Math.random());
this.setState({ list });// trigger render
}
render() {
return (
<div>
{this.state.list.length}
<button onCLick={this.addItem}>add item</button>
</div>)}}// function
function CompFn() {
const ctx = useConcent('foo');
addItem = (a)= > {
const list = ctx.state.list;
list.push(Math.random());
ctx.setState({ list });// trigger render
};
return (
<div>
{ctx.state.list.length}
<button onCLick={ctx.addItem}>add item</button>
</div>)}Copy the code
Of course, we can further optimize the above method by removing the setup, which avoids creating new functions repeatedly in function components and can be reused with classes
const setup = (ctx) = > {
return {
addItem = (a)= > {
const list = ctx.state.list;
list.push(Math.random());
ctx.setState({ list });// trigger render
}
}
}
@register({ module: 'foo', setup })
class CompClazz extends React.Component {
render() {
return (
<div>
{this.state.list.length}
<button onCLick={this.ctx.settings.addItem}>add item</button>
</div>)}}// function
function CompFn() {
const ctx = useConcent({ module: 'foo', setup });
return (
<div>
{ctx.state.list.length}
<button onCLick={ctx.settings.addItem}>add item</button>
</div>)}Copy the code
- The entry 2
dispatch
Add the Reducer function in the module definition first
run({
foo: {// Declare a module foo
state: { list: [].name: ' ' },
reducer: {
addItem(payload, moduleState) {// Define the reducer function
const list = moduleState.list;
list.push(Math.random());
return { list };// trigger render
},
async addItemAsync(){/** Also supports async */}}}});Copy the code
Rewrite the setup
const setup = (ctx) = > {
return {
addItem = (a)= > ctx.dispatch('addItem'),
// Call the reducer function directly
addItem = (a)= > ctx.moduleReducer.addItem(),
}
}
@register({ module: 'foo', setup })
class CompClazz extends React.Component {/ * * * /}
function CompFn() {
const ctx = useConcent({ module: 'foo', setup });
/ * * * /
}
Copy the code
- Entry 3
invoke
Invoke directly bypassed the reducer function definition and called the user’s custom function to rewrite the state. We first defined an addItem function, which had no difference in writing from the reducer function but was placed in a different position. It escaped the Reducer area and was directly placed with the setup.
function addItem(payload, moduleState) {
const list = moduleState.list;
list.push(Math.random());
return { list };// trigger render
}
const setup = (ctx) = > {
return {
addItem = (a)= > ctx.invoke(addItem)
}
}
@register({ module: 'foo', setup })
class CompClazz extends React.Component {/ * * * /}
function CompFn() {
const ctx = useConcent({ module: 'foo', setup });
/ * * * /
}
Copy the code
In any case, it’s still the same as the react data-driven core, i.e. entering a new segment state through a portal to trigger view rendering. However, compared with React, it quietly adds an extra layer of metadata management, allowing components to interact with modules at the moment they are instantiated. That’s how it foreshadows Concent’s precise update strategy.
Note that the metadata, the parameters passed in by the register call in the code above, are carried into the CTX property on the instance when the component is instantiated, which allows us to view an instance of the Concent component printed on the console
React implements Fiber by attaching _reactInternalFiber to component instances. The fiber-linked tree structure simulates function call stacks and further implements Hook, suspense and other features. Concent takes the same approach. CTX attributes are attached to all component instances that need to implement state management, and metadata information such as module identification of component definition phase and observation dependency are recorded on this property, so as to establish a better update scheduling mechanism at the logical level, while not destroying react scheduling itself.
See multi-entry update examples
Accurate updates, who wins
The above said change detection to pave the way, then entered the formal theme of accurate update.
Since accurate update is mentioned, we need to first clarify why accurate update is needed. When our data is promoted to store, there are multiple components consuming different parts of data from different modules of store. Note that there is no concept of modules in redux itself, although the reducer block looks a little embryonic. However, the concept of module encapsulation based on REdux, such as DVA and Rematch, is more in line with our programming ideas. The state and modification methods of modules are clustered under one model instead of being written in various files, so that we can cut each module and organize the code according to the function more friendly.
When there are many modules and many components, there may be some complicated relationships. Different components will connect different modules and consume different parts of the data in the module. When the data in these modules changes, only the corresponding stakeholders should be notified to trigger the rendering, not the whole rendering of violence. So we need some additional mechanism to ensure the accuracy of the render area, that is, to minimize the render range and achieve higher runtime performance.
Here are the scenarios and exact update comparisons for the three frameworks inside React, react-Redux, React-Mobx, and Concent. Vue and Angular are not mentioned
A single module consumes different keys
This is a very common scenario where multiple components consume data from the same module, but at different granularity, assuming we have the state of the following module
bookState = {
name:' '.age:' '.list: [],}Copy the code
Component A connects to the Book module and consumes name and age, component B connects to the Book module and consumes list, and component C connects to all data of the book module
- Redux case pseudocode
@connect(state= > ({name: state.book.name, age: state.book.age }))
class A extends React.Component{}
@connect(state= > ({list: state.book.list }))
class B extends React.Component{}
@connect(state= > state.book)
class C extends React.Component{}
Copy the code
- Mobx case pseudocode
@inject('book')
@observer
class A extends React.Component{
render(){
const { name, age } = this.props.book;
// Use name, age
}
}
@inject('book')
@observer
class B extends React.Component{
render(){
const { list } = this.props.book;
/ / use the list
}
}
@inject('book')
@observer
class C extends React.Component{
render(){
const { name, age, list } = this.props.book;
// Use name age list}}Copy the code
- Concent case pseudocode
@register({ module:'book'.watchedKeys: ['name'.'age']})
class A extends React.Component{
render(){
const { name, age } = this.state;
// Use name, age
}
}
@register({ module:'book'.watchedKeys: ['list']})
class B extends React.Component{
render(){
const { list } = this.state;
/ / use the list
}
}
@register('book')// Aware of all key changes in the Book module, there is no need to explicitly specify watchedKeys
class C extends React.Component{
render(){
const { name, age, list } = this.state;
// Use name age list}}Copy the code
The above codes constrain react rendering. From the perspective of writing, mbox automatically completes dependency collection. Due to the principle of dependency marking, Concent needs to display the key that the user needs to detect changes, so there will be more ink. More code will be generated, so the result in terms of code lightness is
mobx
>concent
>redux
In terms of efficiency, both MBox and Concent are making precise notifications, because MBox collects view dependencies associated with data changes through getter, while Concent collects view dependencies associated with data changes through dependency tags and references. When data changes, it directly notifies corresponding views of direct updates. Redux traverses all of them, triggering subscription callbacks for all instances, which in turn calculate whether the current subscription component instance needs updating.
Concent maintains a global context that classifies and indexes all component instances. When a component instance of Concent changes state, it carries module information. When state changes, Concent already knows how to distribute state to other instances.
The relationship between index modules and classes
Index class and class instance relationship
Locking the associated instance triggers an update
So in terms of efficiency it turns out to be
(mobx, concent) > redux
Because different scenarios have different testing criteria mobx and Concent can’t be compared yet.
See an online example of watchedKeys
A single module that consumes an element in a list or map structure with a key type
This scenario is common, such as iterating over all the elements in a list and rendering a component for each element. This component can modify its store data in a uniform way, but since it is modifying its own data, it should theoretically only trigger its own rendering, not the entire list.
- Redux pseudo code
The following code does not implement this scenario because the Redux-based design does not currently do this. For a view traversed through the Store’s list, there is no parameter to indicate that the current component is consuming an element with an index, and after modifying its element data somewhere in the list, Render only the view that corresponds to this element.
/ / BookItem statement
@conect(state= > {
return { list: state.book.list },
}, dispatch=>{
return {modBookName: (idx) = > dispatch('modBookName', idx)}
})
class BookItem extends React.Component(a){
render(){
const { idx, list } = this.props;
const bookData = list[idx];
const modBookName = (a)= > this.props.modBookName(idx);
/ / UI slightly}}/ / BookItemContainer statement
@conect(state= > {
return { list: state.book.list }
})
class BookItemContainer extends React.Component(a){
render(){
const { list } = this.props;
return (
<div>
{list.map((v, idx) => <BookItem key={idx} idx={idx} />)}
</div>)}}Copy the code
The reducer
export const book = (state, action)=>{
switch(action.type){
case 'modBookName': const list = state.list; const idx = action.payload; const bookItem = list[idx]; bookItem.name = Math.random(); // This must cause the entire BookItemContainer and any BookItem contained therein to be rerenderedreturn{list:[...list]}; }}Copy the code
- Concent pseudo code
@register({module:'book'.watchedKeys: ['list']})
class BookItem extends React.Component(a){
render(){
const { list } = this.state;
const bookData = list[this.props.idx];
const renderKey = this.ctx.ccUniqueKey;
//dispatch(type:string, payload? :any, renderKey? :string)
const modBookName = (a)= > this.ctx.dispatch('modBookName', idx, renderKey;
// Can also be written as
const modBookName = (a)= > this.ctx.moduleReducer.modBookName(idx, renderKey); }}/ / BookItemContainer statement
@register({module:'book'.watchedKeys: ['list']})
class BookItemContainer extends React.Component(a){
render(){
const { list } = this.state;
return (
<div>
{list.map((v, idx) => <BookItem key={idx} idx={idx} />)}
</div>)}}Copy the code
When an instance is called with the renderKey, Concent will look for an instance with the same value as the passed renderKey to trigger the render, and every CC instance, if the renderKey is not manually set, The default renderKey value is the ccUniqueKey(that is, the unique index of each CC instance), so when we have a large number of components that consume different data under the same key in a store module such as sourceList (usually map and List), If the renderKey passed by the caller is its own ccUniqueKey, then the renderKey mechanism will allow components to modify their sourceList data and trigger only their own rendering, without triggering other instances of rendering, greatly improving the rendering performance of such list scenarios.
This sample complete code sample see online stackblitz.com/edit/concen here…
- The mbox pseudo-code MOBx can also describe the above scenarios, but it is a non-negligible cost for MOBX itself to convert an array into an Observable, especially when the array is very large and long. The pseudo-code will not be listed here.
Multi-module consumption data
Module consumption data and there is no difference between single module, more detailed, here no longer just a small way, the Concent has two kinds of components and modules, one belongs to, is a kind of connection, belongs to the relationship between the components can only belong to a module, the connection relationship under the component can connect multiple modules, when the component belongs to a module, Therefore, module data can be directly injected into state. If there are multiple module data consumption, there are some differences in writing.
- Belongs to the foo module
// Register the component
@register('foo')
// or
@register({module:'foo'})
// Get data
render(){
const {f1, f2} = this.state;
}
Copy the code
- Connect to the foo module
@register({connect: ['foo']})
// Get data
render(){
const {f1, f2} = this.ctx.connectedState.foo;
}
Copy the code
- Connect to the foo and bar modules
@register({connect: ['foo'.'bar']})
// Get the foo, bar module data
render(){
const {foo, bar} = this.ctx.connectedState;
}
Copy the code
- Connect to the foo and bar modules and care about all key changes in Foo and some key changes in bar modules
@register({connect: {foo:The '*'.bar: ['key1'.'key2']}})
Copy the code
RenderKey works the same way, although dependency tags and fetch data are written differently, so there is no loss of efficiency due to multiple modules.
conclusion
Redux’s update mechanism can no longer meet the requirements in typical list or map situations. Mobx and Concent can meet the requirements. Mobx tends to organize code in oop way, and Concent is completely function-oriented and has the ability to get through with Store due to its setState. React seamlessly integrates with react, allows direct access to intrusions, and its ability to update accurately remains remarkable.
Concent’s flexible API makes it easier to organize code with separation of concerns, clear responsibilities, and robust architecture, as shown in the two calculators below.
Example 1 is based on Hook and comes from an Indian comrade. Click me to see example 1
Example 2 is based on Concent, and the arrows in the figure above will be abstracted into different parts of the Model. Click me to see example 2
The final view rendering is easily done through useConcent and modules.
conclusion
❤ star me if you like concent ^_^, concent can’t grow without your encouragement and support. We look forward to learning more and providing feedback. Let’s build a more fun, robust, and high-performance react app.
It is strongly recommended that you enter the online IDE fork code modification oh (if clicking on the image is invalid, you can click on the text link)
Edit on CodeSandbox(js)
Edit on CodeSandbox(ts), git repository address point I ts-git-repo
Edit on StackBlitz
If you have any questions about Concent, you can scan the code and consult with the group. I will try my best to answer your questions and help you learn more.