Writing in the front

  • ReactIn thejsxRendering principles.
  • ReactIn aboutstateExisting “problems”.
  • A simpleReactIn thestate/setStateMechanism.

The knowledge involved in this article is gradually explained and developed. Of course, if you are not interested in the content (already understood), you can also directly enter the content of this article, because each chapter will not have a strong coupling with the previous one.

For the code addresses involved in the article, go here to 👇.

The content of the article will be divided into three steps:

  1. implementationReactThe nativeDOMElements of theRef– to obtainDOMNode.
  2. implementationReactIn theClass ComponenttheRef– to obtainClass ComponentInstance.
  3. implementationReactIn theFunction ComponenttheRefforwordRef.

nativeDomtheref

basis

React allows you to obtain an instance of a native Dom node by using a Ref.

Let’s take a look at how it works:

class ClassComponent extends React.Component {
	constructor() {
		super(a);this.refInputPrefix = React.createRef();
		this.refInputSuffix = React.createRef();
		this.ref = React.createRef();
	}

	handleClick = () = > {
		const prefix = this.refInputPrefix.current.value;
		const suffix = this.refInputSuffix.current.value;
		const result = parseInt(prefix) + parseInt(suffix);
		this.ref.current.value = result;
	};

	render() {
		return (
			<div>
				<input ref={this.refInputPrefix}></input>
				<input ref={this.refInputSuffix}></input>
				<button onClick={this.handleClick}>Click the calculation result</button>
				<input ref={this.ref}></input>
			</div>); }}const element = <ClassComponent></ClassComponent>;

ReactDOM.render(element, document.getElementById('root'));
Copy the code

React.createref () creates a ref object with the current attribute, and then assigns it to the Dom node on the JSX template with ref={ref}. .current got the corresponding Dom element.

The REF of the Dom node retrieves the actual Dom element nodes rendered on the page.

implementation

Now that we understand the usage, let’s implement the API, which is actually very simple to implement. (Of course, it is recommended to have a little understanding of the pre-knowledge in the article, and of course, if you don’t understand the code before the article, you can refer to the relevant article before.)

First, we know that to use a ref in the class component we need to create a ref object mounted on this via react.createref ().

So let’s start with crateRef. We’ll create a ref.js at the same level as the react.js:

export function createRef() {
	return { current: null };
}
Copy the code

Its implementation is simple enough to return an empty reference object with a current property.

import { createRef } from './ref';
const React = {
	// Convert JSX to vDOM with createElement
	createElement(type, config, children) {
		let ref; // Define the Ref attribute in addition
		if (config) {
			// They don't belong to props
			ref = config.ref;
		}
		constprops = { ... config, };if (arguments.length > 3) {
			props.children = Array.prototype.slice
				.call(arguments.2)
				.map((i) = > wrapTextNode(i));
		} else {
			props.children = wrapTextNode(children);
		}
		return {
			type,
			props,
			ref,
		};
	},
        / / introduction
        createRef
};
Copy the code

We then introduce this method in the peer react.js.

Let’s see what the ref for JSX in Babel will compile to look like:

The createElement method in the React. Js is used to render a vDom element, and the ref passed in is kept on the props of the vDom.

.createElement(type, config, children) {
		let ref; // Define the Ref attribute in addition
		if (config) {
			// They don't belong to props
			ref = config.ref;
		}
		constprops = { ... config, };if (arguments.length > 3) {
			props.children = Array.prototype.slice
				.call(arguments.2)
				.map((i) = > wrapTextNode(i));
		} else {
			props.children = wrapTextNode(children);
		}
		return {
			type,
			props,
			ref,
		};
	},
Copy the code

The react. createElement method does a separate processing for the ref property, and the object returned has a ref at the same level as type and props.

CreateRef () => {current:null}

The react.createElement () method returns a vDom object and adds a Ref object {current:null} to the vDom object.

Consider what we need to achieve in the end: to point {current:null}’s current property to the actual Dom node rendered by the corresponding vDom.

Remember that when we implemented setState earlier, in the createDom method, we added a DOM attribute to each vDom render pointing to the actual DOM node.

It’s not hard to imagine,

  • invDomRendering becomedomWhen we pass inReact.createElementMethod returnvDomObject.
  • The incomingvDomObject, ownsprops.type.refThese three properties.
  • refIs aobjectIt’s aReference types.
  • So we’re going tovDomRender to realityDomIn the process, just need to{ current:null }In thecurrentProperty points to the corresponding generated realityDomNode.

Let’s get a sense of how the code should be implemented:

  • = >jsxThe incomingrefProperty of the{ current:null }
  • = >jsxElements bybabelTranslated intoReact.createElement(...)
  • => We implementReact.createElement(...)Returns avDomObject,{ ref:... , props:... , type:... }
  • = > when callingcreateDom(vDom)The incomingvDomwillvDomRender to realityDomElement after we modify the passedref.currentThe direction of is trueDomElements.
  • Because of the reference type, this is inside the component instanceReact.creatRefThe return of{ current:null }Has become a{ current: [Dom] }
  • => Finally we can pass in the component instancethis.xxxTo get the realDomElements.
// react-dom.js.// Convert vDom to real Dom
// Convert vDom to real Dom
function createDom(vDom) {
	const { type, props, ref } = vDom;
	let dom;
	if (type == REACT_TEXT) {
		dom = document.createTextNode(props.content);
	} else if (isPlainFunction(type)) {
		if (isClassComponent(type)) {
			return mountClassComponent(vDom);
		} else {
			returnmountFunctionComponent(vDom); }}else {
		dom = document.createElement(type);
	}
	/ / update the props
	if (props) {
		updateProps(dom, {}, props);
		/ / update the children
		if (props.children) {
			// Update the recursive call to children
			if (Array.isArray(props.children)) {
				reconcileChildren(props.children, dom);
			} else{ render(props.children, dom); }}}// The DOM attribute on the virtual DOM points to the real DOM. Only renderVDom will mount the DOM
	vDom.dom = dom;
	// If Ref exists on the Ref attribute, the corresponding real DOM element will be assigned to ref.current after each real DOM creation
	if (ref) {
		ref.current = dom;
	}
	returndom; }...Copy the code

The createDom method determines that if ref is passed, ref. Current = dom.

So the ref attribute for normal Dom elements is already implemented, and it’s actually quite simple. It uses the reference address of the object to modify the attribute value of the object so that the corresponding DOM element can be accessed in the instance.

classThe component’sref

We already implemented Dom refs, so class Component refs are much easier to implement

basis

As always, let’s look at what a ref is in class Component:


// The ref implementation of the class component
class ChildrenComponent extends React.Component {
	constructor() {
		super(a);this.inputRef = React.createRef();
	}
	handleFocus = () = > {
		this.inputRef.current.focus();
	};
	render() {
		return <input ref={this.inputRef}>children</input>; }}class ClassComponent extends React.Component {
	constructor() {
		super(a);this.childrenCmp = React.createRef();
	}

	handleClick = () = > {
		this.childrenCmp.current.handleFocus();
	};

	render() {
		return (
			<div>
				<ChildrenComponent ref={this.childrenCmp}></ChildrenComponent>
				<button onClick={this.handleClick}>Focus on the son node input</button>
			</div>); }}const element = <ClassComponent></ClassComponent>;

ReactDOM.render(element, document.getElementById('root'));
Copy the code

Running this code, the input in ChildComponent will be focused when we click the button.

React uses the ref attribute on the class component to get the instance of the class component.

The instance method on the class component can then be called from the class component instance obtained by this ref.

implementation

CreateDom = ref (vDom); createDom = ref (vDom); createDom = ref (vDom);

If a ref points to an instance of a class component, then we can point the ref.current to the corresponding component instance of the class component.

If that’s what you think, then I’ll tell you. It is easy to implement a ref for a class component.

Let’s take a look at the related react-dom.js:

// Convert vDom to real Dom
function createDom(vDom) {
	const { type, props, ref } = vDom;
	let dom;
	if (type == REACT_TEXT) {
		dom = document.createTextNode(props.content);
	} else if (isPlainFunction(type)) {
		if (isClassComponent(type)) {
			return mountClassComponent(vDom);
		} else {
			returnmountFunctionComponent(vDom); }}else {
		dom = document.createElement(type);
	}
	/ / update the props
	if (props) {
		updateProps(dom, {}, props);
		/ / update the children
		if (props.children) {
			// Update the recursive call to children
			if (Array.isArray(props.children)) {
				reconcileChildren(props.children, dom);
			} else{ render(props.children, dom); }}}// The DOM attribute on the virtual DOM points to the real DOM. Only renderVDom will mount the DOM
	vDom.dom = dom;
	// If Ref exists on the Ref attribute, the corresponding real DOM element will be assigned to ref.current after each real DOM creation
	if (ref) {
		ref.current = dom;
	}
	return dom;
}
Copy the code

In createDom, when we encounter class Component, we go directly to the return mountFunctionComponent(vDom) branch.

Let’s make a few changes to the mountFunctionComponent(vDom) method:

/ / mount ClassComponent
function mountClassComponent(vDom) {
	// Ref is an instance object of the ref class component
	const { type, props, ref } = vDom;
	const instance = new type(props);
	if (ref) {
		// If the ref attribute has an instance of the class assigned to ref.current
		ref.current = instance;
	}
	const renderVDom = instance.render();
	RenderVDom. OldRenderVDom = renderVDom
	instance.oldRenderVDom = vDom.oldRenderVDom = renderVDom; // Mount the current RenderVDom on the class instance object
	return createDom(renderVDom);
}
Copy the code

After we initialize the class component instance, we just need to assign the generated class component instance to the ref.current property.

On the outer component instance, this.[XXX] can be placed into the corresponding ref object, and then this.[XXX].current can be accessed to call the corresponding instance method.

Function Componenttheref

In React, we know there are no refs in Function Component. If you use refs directly on FC components, you get a warning like this:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Function Component is not allowed to use refs.

  • nativeDomThe node’srefProperty can point to the correspondingdomStored in theclassOn an instance of
  • classComponents can also pass throughrefGet the corresponding force object stored on the corresponding parent component instance as a property call

In combination with the above two conclusions, it is not difficult to understand why FC is allowed to have the Ref attribute:

The function component has no instance, which means that the function is destroyed at the end of each run and no instance is returned. Naturally, the root node of the function component is not rendered as a real DOM element so it is not consistent with the native DOM, and we cannot obtain the instance of the function component through ref.

What if we want to use ref for function components at this point? I believe some students have already used the forwardRef API. It means to do a layer of forwarding.

Ref Forwarding is a technique for automatically passing a Ref from a component to its children. This is usually not required for most components in your application. However, it can be useful for certain types of components, especially in reusable component libraries

His specific practical is very simple, is through a layer of forwarding. The ref is passed to the function component, which takes the ref argument internally and passes it on to other elements for use.

basis

// The Ref of the function component
const Child = React.forwardRef(function (props, ref) {
	return <input ref={ref}>{props.name}</input>;
});

class Parent extends React.Component {
	constructor(props) {
		super(props);
		this.ref = React.createRef();
	}

	handleClick = () = > {
		this.ref.current.focus();
	};

	render() {
		return (
			<div>Ref and name */} exist on {/* class components<Child ref={this.ref} name="wang.haoyu">Class components</Child>
				<button onClick={this.handleClick}>Click on the focus</button>
			</div>); }}const element = <Parent></Parent>;

ReactDOM.render(element, document.getElementById('root'));

Copy the code

When we click button, the input inside the function component is focused.

The forwardRef forwards the ref passed to the function component to the corresponding input component.

implementation

As mentioned above, FC has no instance concept, so if we want to implement FC refs, we need to implement the corresponding forwardRef first.

React uses the forwardRef Api to pass in a function component as a class component. Then the forwardRef returns an instance of the class component that can be forwarded through the ref.

Let’s look at the forwardRef implementation code first:

// react.js
import { wrapTextNode } from '.. /utils/index';
import Component from './component';
import { createRef } from './ref';

const React = {
	// Convert JSX to vDOM with createElement
	createElement(type, config, children) {
		let ref; // Define the Ref attribute in addition
		if (config) {
			// They don't belong to props
			ref = config.ref;
		}
		constprops = { ... config, };if (arguments.length > 3) {
			props.children = Array.prototype.slice
				.call(arguments.2)
				.map((i) = > wrapTextNode(i));
		} else {
			props.children = wrapTextNode(children);
		}
		return {
			type,
			props,
			ref,
		};
	},
	// FunctionComponent's ref forwarding
	forwardRef(functionComponent) {
		return class extends Component {
			render() {
				return functionComponent(this.props, this.props.ref); }}; }, createRef,/ / class components
	Component,
};

export default React;
Copy the code

The forwardRef implementation is very simple, like HOC, and takes a function component as an argument and returns a class component.

A call to this function component in the render method of the class component returns the JSX return value of the corresponding function component, passing in the corresponding props and props. Ref objects.

To tease out the process a bit:

When the forwardRef wraps a function component, the outer layer passes a ref to the component returned by the forwardRef:

const Child = function (props, ref) {
	return <input ref={ref}>{props.name}</input>;
};


class Parent extends React.Component {
	constructor(props) {
		super(props);
		this.ref = React.createRef();
	}

	handleClick = () = > {
		this.ref.current.focus();
	};

	render() {
		return (
			<div>Ref and name */} exist on {/* class components<Child ref={this.ref} name="wang.haoyu">Class components</Child>
				<button onClick={this.handleClick}>Click on the focus</button>
			</div>); }}Copy the code

We passed the props name=’wang.haoyu,ref={current:null} to the forwardRef wrapped component Child.

When the forwardRef returns a class component, the props is

{
    name:'wang.haoyu'.ref: { current:null}}Copy the code

In the class component, in the createDom method we create an instance of the class component and pass in the props.

We then return the result of a function call via the render method of the class component, passing two arguments: this.props and this.props. Ref => {current:null}.

Next inside our function component:

const Child = function (props, ref) {
	return <input ref={ref}>{props.name}</input>;
};
Copy the code

We use the ref object that was passed in, and then the input element calls createDom during rendering and redirects the ref. Current so that its current points to the actual Dom node of the input element.

On the Parent layer, we can use this.ref.current to get the input of the corresponding Child component, which is the real DOM element.

At this point, our three types of REFs have been basically implemented, which may be a little confusing when you look at them all at once.

Never mind, the learning path for source code is always steep and gradual, read it a few times and try it out with the demo yourself. I believe you can do it!

All the code in the article is inconsistent with the source code, because the real source code will be more than in the article many branches pulled out at once will have a lot of questions. So the article has been simplified to take out the core ideas and the most simple way to realize the source code of ideas and core principles.

doubt

The forwardRef is essentially using {current:null} to change the direction of the current and forward the ref.

So why not just have all function components support the second argument as ref when you mount the function component, then you don’t need the operation in the source code at all.

In the native code, I tried to modify it directly to look like this, but it can actually implement the function component’s ref forwarding directly without needing the forwardRef API at all.

// react-dom.js
/ / mount FunctionComponent
function mountFunctionComponent(vDom) {
	const { type, props, ref } = vDom;
	// It exists here
	const renderDom = type(props, ref);
	// Consider the FunctionComponent as the root node
	vDom.oldRenderVDom = renderDom;
	const dom = createDom(renderDom);
	return dom;
}
Copy the code

With this modification, each mount of the function component at this point checks if the REF attribute was passed in.

If ref is passed in, we can also change the ref. Current parameter. If ref is passed in, we can change the ref.

Of course, AFTER this, I will continue to dig into the mechanism of React to try to answer this question.