Preact source code analysis, toxic

Time:2021-3-4

Recently, I read the preact source code and took notes. Here, in the form of an example, I take the execution process of the code to the source code and explain some important points by the way. I suggest you look at the preact source code

Vnode and H ()

A virtual node is an extension of a real DOM elementA JS object representation, created by h()

Before the H () method creates a vnode based on the specified node name, attribute, and child node, it processes the child node, including

  1. If the current vnode to be created is not a component but an ordinary label, the text child node is null, undefined, converted to ”, and the text child node is number type, converted to a string
  2. Two consecutive adjacent sub nodes are text nodes, which are merged into one

For example:

h('div',{ id: 'foo', name : 'bar' },[
            h('p',null,'test1'),
            'hello',
            null
            'world', 
            h('p',null,'test2')
        ]
)

Corresponding vnode={

    nodeName:'div',
    attributes:{
        id:'foo',
        name:'bar'
    },
    [
        {
            nodeName:'p',
            children:['test1']
        },
        'hello world',
        {
            nodeName:'p',
            children:['test2']
        }
    ]

}

render()

Render () is in react ReactDOM.render (vnode, parent, merge), convert a vnode to a real Dom and insert it into the parent. There is only one sentence, focusing on diff

return diff(merge, vnode, {}, false, parent, false);

diff

Diff mainly does three things

  1. Call idff() to generate real DOM
  2. Mount DOM
  3. After diff of components and all child nodes is completed, the collected componentdidmount ()

Focus on idiff

Idiff (DOM, vnode) deals with three cases of vnode

  1. Vnode is a JS basic type value, which directly replaces the DOM text or the DOM does not exist. Create a new text based on vnode and return it
  2. vnode.nodeName Is function, that is, the current vnode represents a component
  3. vnode.nodeName Is string, that is, the current vnode represents a JS representation of ordinary HTML elements

Generally, we write react applications, and there is a component similar to < app > in the outermost layer. When renderingReactDOM.render(<App/>>,root)At this time, diff takes the second stepvnode.nodeName==='function'To build components and executebuildComponentFromVNode(), instantiate components, subcomponents, etc

The third situation generally occurs inThe definition of a component is wrapped with a common label. When the internal state of the component changes or is instantiated for the first time, it is necessary to render the component. At this time, the existing dom of the current component should be associated with the execution compoent.render The new vnode obtained by () method is diffed to determine how the current component updates the dom

class Comp1 extends Component{

    render(){
        return <div>
                {
                    list.map(x=>{
                        return <p key={x.id}>{x.txt}</p>
                    })
                }
            <Comp2></Comp2>
        </div>
    }
    //Instead of
    //render(){
    //    return <Comp2></Comp2>
    //}

}

Diff of common label elements and child nodes

Let’s compare the rendering process of a real componentIt represents the diff process between the vnode of the ordinary Dom and its child nodes and the real dom

Suppose there is such a component


class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      change: false,
      data: [1, 2, 3, 4]
    };
  }

 change(){
    this.setState(preState => {
        return {
            change: !preState.change,
            data: [11, 22, 33, 44]
        };
    });
 }

  render(props) {
    const { data, change } = this.state;
    return (
      <div>
        <button onClick={this.change.bind(this)}>change</button>
        {data.map((x, index) => {
          if (index == 2 && this.state.change) {
            return <h2 key={index}>{x}</h2>;
          }
          return <p key={index}>{x}</p>;
        })}
        {!change ? <h1>hello world</h1> : null}
      </div>
    );
  }
}

Initial rendering

The DOM structure of APP components after the initial mount is roughly expressed as

dom = {
       tageName:"DIV",
       childNodes:[
           <button>change</button>
           <p key="0">1</p>,
           <p key="1">2</p>,
           <p key="2">3</p>,
           <p key="3">4</p>,
           <h1>hello world</h1>
       ]
}

to update

Click the button to trigger the set state, and the state changes,App component instances are put into the rendering queue. After a period of time (asynchronously), the components in the rendering queue are rendered and the instance. Render is executed. At this time, the generated vnode structure is roughly

vnode= {
    nodeName:"div"
    children:[
        { nodeName:"button", children:["change"] },
        { nodeName:"p", attributes:{key:"0"}, children:[11]},
        { nodeName:"p", attributes:{key:"1"}, children:[22]},
         { nodeName:"h2", attributes:{key:"2"}, children:[33]},
        { nodeName:"p", attributes:{key:"3"}, children:[44]},
    ]
 }

//Without the last H1 element, the third P element becomes H2

And then in theRendercomponent methodDom and vnode above diffdiff(dom,vnode)At this time, in the idff method called inside diff, the above-mentioned third case is executedvnode.nodeType It’s a normal label, about rendercomponent

First of all, the DOM and vnode tag names are the same, both are div (if not, you need to vnode.nodeName To create a new element, and copy the DOM child node to the new element), and vnode has multiple children, so you can directly enter the innerdiffnode (DOM, vnode.children )Function

Workflow of innerdiffnode (DOM, vchildren)

  1. Traverse the child nodes under the DOM node, and put two arrays keyed and children (those without keys are put in this array) according to whether there are keys
  2. Traversing vchildren,Find a child node under the corresponding DOM for the current vchildchildFor example, the key is the same. If vchild does not have a key, find the tag with the same name from the children array
  3. Child = idiff (child, vchild); recursive diff, get the processed child according to vchild, and apply the child to the current parent element dom

Let’s look at the example above

  1. DOM child node traverses to get two arrays
keyed=[
    <p key="0">1</p>,
       <p key="1">2</p>,
       <p key="2">3</p>,
       <p key="3">4</p>
]
children=[
    <button>change</button>,
    <h1>hello world</h1>
]
  1. Iterate the children array of vnode

There is a key equal

vchild={ nodeName:"p", attributes:{key:"0"}, children:[11]},
child=keyed[0]=<p key="0">1</p>

There is a problem of tag name change

vchild={ nodeName:"h2", attributes:{key:"2"}, children:[33]},
child=keyed[2]=<p key="2">3</p>,

There is a label with the same tag name

vchild={ nodeName:"button", children:["change"] },
child=<button>change</button>,

Then diff the vchild and child

child=idff(child,vchild)

Look at a set of child element updates

Look at the group aboveThere exists the problem that keys are equalThe diff of the child element, vchild.nodeName== ‘p’ is a common label, so it’s the third case in idff.

But here, vchild has only one descendant element, and the child has only one text node. It can be clearly the case of text replacement. It is a bit of optimization to deal with this in the source code instead of entering innerdiffnode

let fc = out.firstChild,
        props = out[ATTR_KEY],
        vchildren = vnode.children;

    if (props == null) {
        props = out[ATTR_KEY] = {};
        for (let a = out.attributes, i = a.length; i--;) props[a[i].name] = a[i].value;
    }

    // Optimization: fast-path for elements containing a single TextNode:
    if (!hydrating && vchildren && vchildren.length === 1 && typeof vchildren[0] === 'string' && fc != null && fc.splitText !== undefined && fc.nextSibling == null) {
        if (fc.nodeValue != vchildren[0]) {
            fc.nodeValue = vchildren[0];
        }
    }

All executionchild=idiff(child,vchild)after

child=<p key="0">11</p>
//Text value updated

Then put the child in the appropriate position under the current DOM, and the update of a child element is completed

If vchild.children The array has multiple elements, and the iterative diff of child elements of vchild will be performed

So far, half of diff has been said, and the other half is the case of vnode representing a component, rendering or updating diff

Component rendering, diff and update

There are three main methods related to component rendering and diff, which call relation in turn

buildComponentFromVNode

  1. Component has not been instantiated before. Instantiate component and apply props, setcomponentprops ()
  2. The component has been instantiated and belongs to the update phase. Setcomponentprops ()

setComponentProps

Doing two things inside setcomponent props (compinst)

  1. Call componentwillmount or componentwillreceiveprops according to whether the current component instance is instantiated for the first time or the property is updated
  2. When judging whether to force rendering, rendercomponent () or put the component into the rendering queue and render asynchronously

renderComponent

Rendercomponent does these things:

  1. Judge whether the component is updated, and execute componentwillupdate(),
  2. Judge the result of shouldcomponentupdate() and decide whether to skip the render method of the executing component
  3. If you need to render, execute the component render() and return a vnode, diffThe real DOM on the page structure represented by the current componentAnd the returned vnode to apply the update

Let’s start with an example. Suppose there is such a component

class Welcom extends Component{

    render(props){
        return <p>{props.text}</p>
    }

}

class App extends Component {

    constructor(props){
        super(props) 
        this.state={
            text:"hello world"
        }
    }

    change(){
        this.setState({
            text:"now changed"
        })
    }

    render(props){

        return <div>
                <button onClick={this.change.bind(this)}>change</button>
                <h1>preact</h1>
                <Welcom text={this.state.text} />
            </div>

    }

}

render(<App></App>,root)

vnode={
    nodeName:App,
}

First render

render(<App/>, root), enter diff(), vnode.nodeName==App , enter buildcomponentfromvnode (null, vnode)

When the program is executed for the first time, the page has no DOM structure, so the first parameter of buildcomponentfromvnode is null,Enter the stage of instantiating app components

c = createComponent(vnode.nodeName, props, context);
if (dom && !c.nextBase) {
    c.nextBase = dom;
    // passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L229:
    oldDom = null;
}
setComponentProps(c, props, SYNC_RENDER, context, mountAll);
dom = c.base;

In setcomponentprops, execute component.componentWillMount (), the component enters the asynchronous rendering queue. After a period of time, the component renders and executes
renderComponent()

rendered = component.render(props, state, context);

According to the above definition, there are

rendered={
    nodeName:"div",
    children:[
        {
            nodeName:"button",
            children:['change']
        },
        {
            nodeName:"h1",
            children:['preact']
        },{
            nodeName:Welcom,
            attributes:{
                text:'hello world'
            }
        }
    ]
}

NodeName is a normal label, so execute

base = diff(null, rendered) 
//It should be noted that there is a component child in rend, so in diff() - > idiff() [* *] - > innerdiffnode(), when idiff() is performed on this component child, it is a component, so it is in the second case and goes to buildcomponentfromvnode, the same process

component.base=base  //Baes here is the real DOM structure generated after vnode diff is completed. There is a base attribute on the component instance, pointing to this dom

Base is roughly expressed as

base={
    tageName:"DIV",
       childNodes:[
        <button>change</button>
           <h1>preact</h1>
        <p>hello world</p>
       ]
}

Then add some component information for the current DOM element

base._component = component;
base._componentConstructor = component.constructor;

So far, the initial component rendering is almost done. Buildcomponentfromvnode returns DOM, that is, the c.base of the instantiated app, and inserts the DOM into the page in diff()

to update

Then click the button now, and the state is updated by setstate (), which is in the source code of setstate

let s = this.state;
if (!this.prevState) this.prevState = extend({}, s);
extend(s, typeof state==='function' ? state(s, this.props) : state);
/**
* _ Rendercallbacks saves the list of callbacks
*/
if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
enqueueRender(this);

The component is in the queue. After delay, execute rendercomponent ()

This time, in rendercomponent,Because the current app instance already has a base attributeSo the instance belongs to the update phaseisUpdate = component.base =true, execute the componentwillupdate() method of the instance. If shouldcomponentupdate() of the instance returns true, the instance enters the render phase.

According to the new props, state

rendered = component.render(props, state, context);

rendered={
    nodeName:"div",
    children:[
        {
            nodeName:"button",
            children:['change']
        },
        {
            nodeName:"h1",
            children:['preact']
        },{
            nodeName:Welcom,
            attributes:{
                Text: 'now changed' // change here
            }
        }
    ]
}

And then, like the first render,base = diff(cbase, rendered)But in this case, Cbase is the DOM generated after the last render, that is, instance. Base, and then the page references the updated new dom.rendered The component child element (welcom) of is also updated once. Enter buildcomponentfromvnode(), and go through buildcomponentfromvnode() — > setcomponentprops() — > rendercomponent() — > render() — > diff(), until the data is updated

summary

There are only 15 JS files in preact SRC, but an article can’t cover all the points. Here we just record some main processes, and finally put a poisonous picture

Preact source code analysis, toxic

github