一、准备
使用:React.createRef( )。
作用:React的ref可以用来引用这个真实DOM元素。通过ref的current属性就可以拿到当前节点的对应的真实DOM。
因此我们想要实现ref需要从两个步骤实现:
- 实现createRef( )
- 给current属性赋上真实dom的地址
二、实现
1. 实现React.createRef()
在react.js中创建createRef
方法并暴露出去,方法返回值为{current: null}
,目的是加上current属性,方便下一步赋值
2. 给current属性赋值
2-1. 原生组件的ref
在createDOM
时,判断如果当前dom有ref属性的话,就把其对应的真实dom赋值给ref.current
2-2. 类组件的ref
类组件的ref属性,ref.current
获取到的是这个类组件的实例。
因此我们在mountClassComponent
方法中,进行判断,如果存在ref
,那么就把类组件的实例赋值给ref.current
2-3. 函数组件的ref
函数组件想要使用ref属性,需要React.forwardRef( )进行一次转发,转发后才可以使用ref。
const FunctionRefComponent = React.forwardRef(FunctionComponent)
<FunctionRefComponent ref={xxx} />
打印转发ref的函数组件发现,此时出现了新的类型:Symbol("react.forward_ref")。
因此我们需要在createDOM时,加一种类型判断如果是
Symbol("react.forward_ref")
的话,调用mountForwardComponent
方法
三、代码实现
1. src/index.js
import React from "./react";
import ReactDOM from "./react-dom";
function TextInput(props, ref) {
// 这里接收的ref其实就是Form上createRef创建的ref
return <input ref={ref} />;
}
const ForwardedTextInput = React.forwardRef(TextInput);
console.log(ForwardedTextInput); // $$typeof: Symbol(react.forward_ref); render: ƒ TextInput(props, ref)
class Form extends React.Component {
constructor(props) {
super(props);
this.input = React.createRef(); // 给TextInput
}
getFocus = () => {
this.input.current.focus();
};
render() {
return (
<div>
{/* 这里的ref其实指向TextInput中返回的input元素对应的真实dom */}
<ForwardedTextInput ref={this.input} />
<button onClick={this.getFocus}>获得焦点</button>
</div>
);
}
}
ReactDOM.render(<Form />, document.getElementById("root"));
2. src/constants.js
//React元素:h1 span div
export const REACT_ELEMENT = Symbol("react.element");
//文本:字符串或数字
export const REACT_TEXT = Symbol("react.text");
> > > // 函数组件转发的ref
> > > export const REACT_FORWARD_REF = Symbol("react.forward_ref");
3. src/react.js
import { wrapToVdom } from "./utils";
import { Component } from "./component";
> > > import { REACT_FORWARD_REF, REACT_ELEMENT } from "./constants";
function createElement(type, config, children) {
//children永远都是数组
let ref, key;
if (config) {
delete config.__source; // source:bable编译时产生的属性
delete config.__self;
ref = config.ref; // ref可以用来引用这个真实DOM元素
key = config.key; // 用来进行DOM-DIFF优化的,是用来唯一标识某个子元素的
delete config.ref;
delete config.key;
}
let props = { ...config };
if (arguments.length > 3) {
// 如果入参多余3个,说明有多个子元素,截取后,以数组形式保存
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else if (arguments.length === 3) {
props.children = wrapToVdom(children); // 可能是React元素对象,也可能是string/number/null/und
}
return {
$$typeof: REACT_ELEMENT,
type,
ref,
key,
props,
};
}
> > > function createRef() {
> > > // current属性的值:是等ref属性所在的原生dom元素变成真实dom后,把真实dom的地址赋给了current
> > > return { current: null };
> > > }
> > > /**
> > > * 接收转发ref的函数组件
> > > * @param {*} render 函数组件
> > > */
> > > function forwardRef(render) {
> > > return {
> > > $$typeof: REACT_FORWARD_REF,
> > > render,
> > > };
> > > }
const React = {
createElement,
Component,
> > > createRef,
> > > forwardRef,
};
export default React;
4. src/react-dom.js
import { createRef } from "react";
> > > import { REACT_TEXT, REACT_FORWARD_REF } from "./constants";
import { addEvent } from "./event";
/**
*把虚拟DOM变成真实DOM插入容器
* @param {*} vdom 虚拟DOM/React元素
* @param {*} container 真实DOM容器
*/
function render(vdom, container) {
mount(vdom, container);
}
/** 页面挂载真实DOM */
function mount(vdom, parentDOM) {
//把虚拟DOM变成真实DOM
let newDOM = createDOM(vdom);
//把真实DOM追加到容器上
parentDOM.appendChild(newDOM);
}
/**
* 把虚拟DOM变成真实DOM
* @param {*} vdom 虚拟DOM
* @return 真实DOM
*/
function createDOM(vdom) {
if (!vdom) return null; // null/und也是合法的dom
> > > let { type, props, ref } = vdom;
> > > let dom; //真实DOM
> > > if (type?.$$typeof === REACT_FORWARD_REF) {
> > > return mountForwardComponent(vdom);
> > > } else if (type === REACT_TEXT) {
// 如果元素为文本,创建文本节点
dom = document.createTextNode(props.content);
} else if (typeof type === "function") {
if (type.isReactComponent) {
// 说明这是一个类组件
return mountClassComponent(vdom);
} else {
// 函数组件
return mountFunctionComponent(vdom);
}
} else if (typeof type === "string") {
//创建DOM节点 span div p
dom = document.createElement(type);
}
// 处理属性
if (props) {
//更新DOM的属性 后面我们会实现组件和页面的更新。
updateProps(dom, {}, props);
let children = props.children;
//如果说children是一个React元素,也就是说也是个虚拟DOM
if (typeof children === "object" && children.type) {
//把这个儿子这个虚拟DOM挂载到父节点DOM上
mount(children, dom);
} else if (Array.isArray(children)) {
reconcileChildren(children, dom);
}
}
vdom.dom = dom; // 给虚拟dom添加dom属性指向这个虚拟DOM对应的真实DOM
if (ref) ref.current = dom;
return dom;
}
/** 挂载类组件 */
function mountClassComponent(vdom) {
let { type: ClassComponent, props, ref } = vdom;
// 把类组件的属性传递给类组件的构造函数,
// 创建类组件的实例,返回组件实例对象
let classInstance = new ClassComponent(props);
> > > // 如果有ref,就把实例赋值给current属性
> > > if (ref) ref.current = classInstance;
//可能是原生组件的虚拟DOM,也可能是类组件的的虚拟DOM,也可能是函数组件的虚拟DOM
let renderVdom = classInstance.render();
//在第一次挂载类组件的时候让类实例上添加一个oldRenderVdom=renderVdom
classInstance.oldRenderVdom = renderVdom;
return createDOM(renderVdom);
}
/** 挂载函数组件 */
function mountFunctionComponent(vdom) {
let { type: functionComponent, props } = vdom;
//获取组件将要渲染的虚拟DOM
let renderVdom = functionComponent(props);
return createDOM(renderVdom);
}
> > > /** 挂载经过转发的ref的函数组件 */
> > > function mountForwardComponent(vdom) {
> > > let { type, props, ref } = vdom;
> > > let renderVdom = type.render(props, ref);
> > > return createDOM(renderVdom);
> > > }
/** 如果子元素为数组,遍历挂载到容器 */
function reconcileChildren(children, parentDOM) {
children.forEach((childVdom) => mount(childVdom, parentDOM));
}
/**
* 把新的属性更新到真实DOM上
* @param {*} dom 真实DOM
* @param {*} oldProps 旧的属性对象
* @param {*} newProps 新的属性对象
*/
function updateProps(dom, oldProps, newProps) {
for (let key in newProps) {
if (key === "children") {
// 子节点另外处理
continue;
} else if (key === "style") {
let styleObj = newProps[key];
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr];
}
} else if (/^on[A-Z].*/.test(key)) {
// 绑定事件 ==> dom.onclick = 事件函数
// dom[key.toLowerCase()] = newProps[key];
// 之后不再把事件函数绑定在对应的DOM上,而是事件委托到文档对象
addEvent(dom, key.toLowerCase(), newProps[key]);
} else {
dom[key] = newProps[key];
}
}
for (let key in oldProps) {
//如果说一个属性老的属性对象里有,新的属性没有,就需要删除
if (!newProps.hasOwnProperty(key)) {
dom[key] = null;
}
}
}
/**
* @param {*} parentDOM 父真实DOM
* @param {*} oldVdom 老的虚拟DOM
* @param {*} newVdom 新的虚拟DOM
*/
export function compareToVdom(parentDOM, oldVdom, newVdom) {
// 获取oldRenderVdom对应的真实DOM
let oldDOM = oldVdom.dom;
// 根据新的虚拟DOM得到新的真实DOM
let newDOM = createDOM(newVdom);
// 把老的真实DOM替换为新的真实DOM
parentDOM.replaceChild(newDOM, oldDOM);
}
const ReactDOM = {
render,
};
export default ReactDOM;
四、优化
书写代码过程中,可能会存在组件嵌套,比如:类组件的返回值是函数组件,函数组件的返回值是类组件,这个类组件的返回值才是原生dom。
那么,如果组件嵌套,我们之前在compareToVdom
方法中,用oldVdom.dom
获取真实元素,就可能获取不到。所以下面我们就把这一步优化一下。把之前的oldVdom.dom抽离出一个函数findDOM
方便复用。
export function compareToVdom(parentDOM, oldVdom, newVdom) {
// 获取oldRenderVdom对应的真实DOM
// let oldDOM = oldVdom.dom;
> > > let oldDOM = findDOM(oldVdom);
// 根据新的虚拟DOM得到新的真实DOM
let newDOM = createDOM(newVdom);
// 把老的真实DOM替换为新的真实DOM
parentDOM.replaceChild(newDOM, oldDOM);
}
/** 虚拟DOM返回的真实DOM */
export function findDOM(vdom) {
if (!vdom) return null;
// 如果有dom属性,说明这个vdom是原生组件的虚拟DOM,会有dom属性指向真实dom
if (vdom.dom) {
return vdom.dom;
} else {
return findDOM(vdom.oldRenderVdom);
}
}
并且,在类组件、函数组件挂载的时候,我们要给组件添加oldRenderVdom属性,指向即将渲染的renderVdom。
/** 挂载类组件 */
function mountClassComponent(vdom) {
let { type: ClassComponent, props, ref } = vdom;
// 把类组件的属性传递给类组件的构造函数,
// 创建类组件的实例,返回组件实例对象
let classInstance = new ClassComponent(props);
// 如果有ref,就把实例赋值给current属性
if (ref) ref.current = classInstance;
//可能是原生组件的虚拟DOM,也可能是类组件的的虚拟DOM,也可能是函数组件的虚拟DOM
let renderVdom = classInstance.render();
//在第一次挂载类组件的时候让类实例上添加一个oldRenderVdom=renderVdom
// 类组件的虚拟dom的oldRenderVdom属性,指向renderVdom
> > > vdom.oldRenderVdom = classInstance.oldRenderVdom = renderVdom;
return createDOM(renderVdom);
}
/** 挂载函数组件 */
function mountFunctionComponent(vdom) {
let { type: functionComponent, props } = vdom;
//获取组件将要渲染的虚拟DOM
let renderVdom = functionComponent(props);
// 函数组件的oldRenderVdom属性,指向渲染的虚拟DOM--renderVdom
> > > vdom.oldRenderVdom = renderVdom;
return createDOM(renderVdom);
}