React In-depth

    xiaoxiao2022-06-27  142

    We are going to dive deep into react, including how react render DOM element on the page, how things work inside react, especially lifecycle and reconciliation.

    React Component VS React Element

    See React Components, Elements, and Instances. As we get start with React we first come into the concept React Component. We split the view into smaller parts and implement logic in these components. Therefore, React component is the smallest unit and the implementation base of application. From the aspect of code, there are two kinds of components: functional component and class component. The difference between them is that class component has instance while functional component does not. But React manage component instances, and developers can not create or destroy them except accessing. We always use this inside class component, which refers to the component instance. Basically, we write components with JSX, which is convenient and readable for human. Then compiler help to translate JSX lines to code blocks with React.createElement(), which returns React Element. React element is a kind of immutable plain object to describe html DOM node or a component instance. When it meets a html DOM node, it generates corresponding React element as

    { type: 'p', props: { className: 'paragraph', children: 'parapraph' } }

    When it meets a component instance, it generates React element to describe it.

    { type: MyComponent, props: { customProp: 'test', children: { ... } } }

    For a component type, React will invoke its render function to get the React element tree. Therefore a component is an encapsulation with props in and element tree output. Notice that React elements are not the rendered html DOM on the screen, they are just the representation or description of what we want on the screen. Each time render() method is executed, a new React element tree is constructed, and React compare it with the old one to determine whether to update the real html DOM on the screen. Conceptually, React element aims to describe built-in html DOM and user-defined components with a universal interface, similar to the idea of web components. It provide a middle layer between your components and final rendered html DOM, which allows React to optimize rendering, namely, reducing unnecessary html DOM update and repaint. And since React elements are plain objects, it’s fast to iterate, create and destroy compared to html DOM iteration and manipulation. Therefore React actually shift direct html DOM manipulation and updating to the ground of React element. From this aspect, developers are quite far from rendering, as we just write components (prototype), without two much awareness of instantiation, mounting, rendering and destruction. But we find it necessary to fully understand how they work when writing high-performance applications. There are still many issues and tips to be noticed when implement components. Though we just write our component prototype with JSX, It doesn’t mean that we don’t have any chance to work with React element directly. For example, props.children are React elements we frequently used in development. They are not the component instances created, but the generated React element tree we talk above. When console log the props.chilren we could see

    { $$typeof: Symbol(react.element), type: class XXX, props: {}, ... }

    JSX To DOM Element

    Then we are going to get closer to how JSX is translate into React element. As a whole we explain the complete process of how a component in JSX is rendered to a DOM element. From React’s API, JSX is syntactic sugar of React.createElement(component, props, children), where the first parameter component could be a user-defined component (indicated in Capitalize) or html DOM node like div. When it meets a user-defined component, React calls its render method to get the React element tree. For descendant components it just iterates the tree structure and do similar work to translate them.

    render() { return ( <ul> {this.props.items.map((item) => { return <Todo key={item.id} text={item.text} /> })} </ul> ); } // JSX will be tranpiled to render() { return React.createElement('ul', {}, { ...this.props.items.map( item => React.createElement('Todo', { text: item.text }) )}) } // When the render method is executed, React elements are created and a tree is returned { type: ul, props: {}, children: [ { type: Todo, props: { text: 'xxx' }, children: ... } ... ] } // Todo is a user-defined component, React's call its render method to get its React element tree

    When the application is mounting and the render function is executed, React construct the React element tree similar the the above code. As React elements are not the final html DOM painted on the screen, it doesn’t mean that the rendered html DOM will be updated. When render method is executed and React element tree is created again, the html DOM is updated only when the two React element tree different. Continue to read the section of React component’s lifecycle to understand when render method is invoked, and read reconciliation to know how to determined html DOM should be updated based on React element tree.

    Lifecycles

    There are three major common cases in for react lifecycles, which should be deeply understood: mounting, updating and unmounting. See http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/. When mounting, react calls constructor(), static getDerivedStateFromProps(), render() and componentDidMount() in sequence. After invoking constructor(), a react component instance is created, which exists in memory until it’s unmounted(destroyed). static getDerivedStateFromProps() is used for getting new states from props in rare cases. render() function is to construct the react element tree, a tree constructed with plain objects(react element) in memory to describe what and how html DOM should be rendered on page. Each time render() is called, a new react element tree is constructed, which is used for comparison with the old one to optimized the least html DOM updates. Then react update DOM and refs, call componentDidMount(). When updating, react calls static getDerivedStateFromProps(), shouldComponentUpdate(), render(), getSnapshotBeforeUpdate() and componentDidUpdate() in sequence. static getDerivedStateFromProps() is rarely used. shouldComponentUpdate() is to determine whether should invoke render method for updating, namely comparing the react element tree and updating html DOM based on the comparison result. For a react component, any state changes(you could think as calling setState()) will trigger an updating of itself and its descendants, which means the updating process starts from the current component and flows down to every child component. By default shouldComponentUpdate() return true unless you overwrite it to optimize the component updating. After shouldComponentUpdate(), getSnapshotBeforeUpdate() in invoked before render(). It’s useful to capture information from html DOM. Then render() method is invoked to build the react element tree. After html DOM is updated, componentDidUpdate() is called. When unmounting, react calls componentWillUmount() hooks, then relative html DOM is removed from document, then the component instance is destroyed.

    Reconciliation

    See Reconciliation Let’s take an overlook on the whole process of reconciliation. Reconciliation is the process of how React update view (html DOM) based on the React element tree. Since DOM manipulation and updating are high-cost on the page, React aims to update the least DOM possibly to improve the performance. As a whole, reconciliation includes the differing process and html DOM updating after render invocation. It happens when a component’s state changes, and flows down to its every descendants. After invoking the render function and a new React element tree is created, React runs the differing algorithm to compare the constructed tree with the old tree to get their differences, and then it finally update those differences on html DOM. For its descendant components React iterates them and repeat the process. As it just update the compared result on html DOM, updating the smallest part on the page could be achieved. When diffing two trees, React starts from the root elements. If the two root elements have different types, React will destroy the old tree and build the new one. When destroying the old tree, component instance will receive componentWillUnmount(), old html DOM nodes are removed from the document, and then the instance is destroyed either. When building up the new tree, component instance(a new instance) receive componentWillMount, new html DOM nodes are created and inserted into document, and then the instance receive componentDidMount. Any components below the root component will also be destroyed and rebuilt. If the two root elements have the same type, the process is quite different for html DOM node type and component type. For html DOM node type, React keeps the same html DOM node, compares their attributes and only updates the changed ones. For component type, React keeps the component instance, update props, pass them down and call the instance’s componentWillReceiveProps and componentWillUpdate, and then invoke component’s render method and repeat the conciliation inside it. After processing the root element, React repeat the process on its children recursively. The process is almost the same except list with keys. When meeting children with keys, React use keys to match the elements in the original tree so that only changed children are updated and unchanged children are kept with their order might be changed. For example, if the children are with the same key but the order is changed, React will adjust their order in html without removing and re-creating html DOM nodes. The above explanation comes to the core two assumptions:

    Two elements of different types will produce different trees.The developer can hint at which child elements may be stable across different renders with a key prop.

    Common cases to explain

    There are a few common cases help to understand how React works, especially the principle explained above.

    Calling setState()

    At the beginning we spend most of time on which component should own the state instead of how setState() works. When come closer to the reconciliation, we know that setState() will trigger the component and its descendants’ updating and reconciliation. But how setState() trigger the updating and when it actually update the component’s state, is still in mystery. Normally we could not rely on the state’s value after calling setState() since it’s asynchronous. Here asynchronous means that React stores(flush) the state object passed in setState() in queue, and after finishing the current function execution(including descendant component’s execution), React shift all state objects and merge them(batch). Then React create an updater function to update the component state and run render() function for reconciliation. So asynchronous setState() doesn’t mean to apply setTimeout or promise, but just some function will be executed in future. When calling setState() with a function as parameter, React handle with similar actions except that React execute it after shifting it from the queue. This is kind of like thunk function with delay execution feature. Finally, React merge all state result and run render() method. When applying async await and promise to setState(), we find that we could get the correct state we want. This is because the then callback function is executed in next tick, when React has already updated the component and run the reconciliation. Therefore, we don’t recommend to use promise to get new state based on previous state, as it lead to multiple updating and reconciliation. We could achieve same result with passing in function to setState(), which only trigger once reconciliation.

    Same component with different props

    The most common case is that we import a component and used inside render method with props passed in.

    class App extends React.Component { constructor(props) { super(props); this.state = { name: 'test', }; } render() { return <MyComponent name={this.state.name} /> } }

    When App is created, MyComponent runs the mounting process, a new component instance is created. When this.state.name changes, the instance of MyComponent is kept and it runs the updating process in the lifecycle. When App is destroyed, the instance runs the unmounting process and it’s destroyed afterwards. Easy to understand. Then let’s go to a more complicated situation.

    class App extends React.Component { constructor(props) { super(props); this.state = { type: 'student', name: 'test', }; } render() { const { type, name } = this.state; return ( <div> { this.state.type === 'student' ? <MyComponent type={type} name={name} /> : <MyComponent type={type} name={name} /> } </div> ); } }

    The example might look straight, but it does exist for some cases like that we want to render two lists of the same component with different props, but we want to display them conditionally. But here, React will keep the same component instance and update it with different props. When type in state changes, the instance of MyComponent will not be destroy and re-created, instead it runs the updating process. In detail, when type in state changes, the component App invoke render method to get the new React element tree, runs the diffing algorithm and find the root element are the same div, then find React element of type MyComponent are unchanged too, so it call the instance’s render method with updated props to get the new element tree, and finally update the changed part of html DOM.

    Conditional rendering

    If we modify the render method above to the following code, then it become a conditional rendering.

    render() { const { type, name } = this.state; return ( <div> { this.state.type === 'student' ? <MyComponent type={type} name={name} /> : <div><MyComponent type={type} name={name} /></div> } </div> ); }

    Each time type or name in state changes, React will destroy the old instance of MyComponent and re-create a new one, since the root element are different types here. Notice that destroying and creating a component instance need more code executions than updating. Therefore conditional rendering is not suggested for frequently updated content.

    List rendering

    To explain how list rendering is optimized, we give a negative example first.

    class App extends React.Component { constructor(props) { super(props); this.state = { array: [{id: 1, text: '1'}, {id: 2, text: '2'}], }; } render() { return ( <ul> { this.state.array.map(item => <li>{item.text}</li>) } </ul> ); } }

    If we skip the key in list rendering, React will warn us in development mode and by default React use index as keys. When we add this.setState({array: [{id: 3, text: '3'}, ...this.state.array]}), React generates a new tree after invoking render method, and find all element with matched keys are different. Therefore, the whole list are updated. If we add id as key, React could find element with the same key keep unchanged and just need to insert a new node ahead of the list. You might ignore it for small cases but when you meet a long list with thousands items, it really matters. Even you may find the diffing slow for long list.

    Pure component and React.memo

    React provides React.PureComponent and React.memo to reduce the redundant execution of render function. Each time there is an update of the props, it will trigger an updating process of the component, namely call shouldComponentUpdate and render function, then run reconciliation to update the differences of html DOM. React will go through the complete process even the props never change. With React.PureComponent and React.memo, React will run a shallow check with props before invoking render function. Shallow check is just a simple key-value comparison with ===, e.g. { a: 0 } and { a: 1} is equal, let obj1 = obj2 = obj, { b: obj1} and { b: obj2 } is equal. It works well for javascript primitives and some cases with objects, but for complicated cases with objects and array, they can not do more. In these cases, you have to overwrite shouldComponentUpdate by yourself.

    Functional component and class component

    The difference between functional component and class component is that functional component doesn’t have instance and lifecycle hooks. The process of mounting and updating for a functional component is complete the same.

    Component encapsulation and extraction

    Component is the smallest unit in React, so it’s strongly recommended that we should always encapsulate logics with component, no matter they are business logic or framework logic at the bottom. Consider component in React as class in java, then you will commit codes with component possibly. There are some cases you might implement logic with util functions, like actions/reducers in redux. Just fine, they will also be integrated into components. The emphasis point is, that we should break views and logic into smaller components as we can. If you just implement a big component with all logics, any change of state will trigger the updating process of the component, though finally the reconciliation could figure out that only a small part of ui need to be updated. By breaking and encapsulating ui and logic into smaller components, only relatives descendant components will updates when the updating process running from top components to the bottom. With React.PureComponent and React.memo, we could prevent irrelative components (props without changes) from running the updating process.

    Questions

    How React manage component instances?How and when React transfer React element to html DOM element in detail?(vdom/fiber node questions)

    最新回复(0)