五、「深入React源码」--- 手写实现ref

561 阅读5分钟

一、准备

使用: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} />

image.png 打印转发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);
}