React1 分钟阅读

React Ref

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素 为 DOM/类组件 添加 可以使用 ref 去存储 DO

李李顺2022 · 08 · 16

  • Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素

为 DOM/类组件 添加

  • 可以使用 ref 去存储 DOM 节点的引用

  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层DOM元素作为其current属性

  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性

使用

src/index.js

js
import React from "./lib/react";
import ReactDOM from "./lib/react-dom";

class Form extends React.Component {
  constructor(props) {
    super(props);
    this.userNameRef = React.createRef();
  }
  getFocus = () => {
    this.userNameRef.current.getFocus();
  };
  render() {
    return (
      <div>
        <ForwardUserName ref={this.userNameRef} />
        <button onClick={this.getFocus}>focus</button>
      </div>
    );
  }
}
// 为类组件添加ref
/* class UserName extends React.Component {
  constructor(props) {
    super(props)
    this.inputRef = React.createRef()
  }
  getFocus = () => {
    this.inputRef.current.focus()
  }
  render() {
    return (
      <input ref={this.inputRef} />
    )
  }
} */
class Sum extends React.Component {
  constructor(props) {
    super(props);
    this.a = React.createRef();
    this.b = React.createRef();
    this.result = React.createRef();
  }
  handleAdd = () => {
    let a = this.a.current.value;
    let b = this.b.current.value;
    this.result.current.value = Number(a) + Number(b);
  };
  render() {
    return (
      <div>
        <input ref={this.a} /> + <input ref={this.b} />
        <button onClick={this.handleAdd}>=</button>
        <input ref={this.result} />
      </div>
    );
  }
}

ReactDOM.render(<Form />, document.getElementById("root"))``;

实现

lib/react.js

js{31-35,40}
import { REACT_ELEMENT } from "./element-types";
import { wrapToVdom } from "./utils";
import { Component } from "./Component";
// 把babel转换后的结果转成ReactDOM
function createElement(type, config, children) {
  // children:这里已经是一个虚拟DOM
  let key = config.key;
  let ref = config.ref;
  if (config) {
    delete config.__source;
    delete config._owner;
    delete config.__self;
    delete config.key;
    delete config.ref;
  }
  let props = { ...config };
  if (arguments.length > 3) {
    // 如果参数大于3,说明子节点是数组
    props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
  } else {
    props.children = wrapToVdom(children);
  }
  return {
    $$typeof: REACT_ELEMENT, // 表示是一个虚拟DOM,或者说是React元素
    key,
    ref,
    type, // 虚拟DOM的类型,如果是函数/类组件,type就是组件本身
    props, // 像事件,id,属性,class,style等都在这里面
  };
}
function createRef() {
  return {
    current: null,
  };
}

const React = {
  createElement,
  Component,
  createRef,
};
export default React;

lib/react-dom.js

js{37,42,57}
import { addEvent } from "./event";
import { REACT_TEXT } from "./element-types";

/**
 * 要把虚拟DOM转换成真实DOM并且挂载到容器中
 * @param {*} vdom 虚拟DOM
 * @param {*} container 容器
 */
function render(vdom, container) {
  mount(vdom, container);
}
function mount(vdom, container) {
  let newDOM = createDOM(vdom);
  container.appendChild(newDOM);
}
function createDOM(vdom) {
  const { props, type, ref } = vdom;
  let dom; // 真实的DOM节点
  if (type === REACT_TEXT) {
    dom = document.createTextNode(props); // props是一个字符串
  } else if (typeof type === "function") {
    if (type.isReactComponent) {
      // 类组件
      return mountClassComponent(vdom);
    }
    return mountFunctionComponent(vdom); // 函数组件
  } else {
    dom = document.createElement(type);
  }
  if (props) {
    // 如果props存在,更新属性
    updateProps(dom, {}, props);
    if (typeof props.children === "object" && props.children.type) {
      // 说明只有一个子节点
      mount(props.children, dom); // 递归渲染子节点
    } else if (Array.isArray(props.children)) {
      reconcileChildren(props.children, dom);
    }
  }
  // 创建真实DOM的时候和虚拟DOM进行关联
  vdom.dom = dom;
  if (ref) ref.current = dom;
  return dom;
}

function mountFunctionComponent(vdom) {
  const { props, type: FunctionComponent } = vdom;
  let renderVdom = FunctionComponent(props); // renderVdom才是要渲染的虚拟DOM
  // 保存老的虚拟DOM
  vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}
function mountClassComponent(vdom) {
  const { props, type: ClassComponent, ref } = vdom;
  // 先创建类组件实例
  let classInstance = new ClassComponent(props);
  if(ref) ref.current = classInstance; // 为类组件添加ref
  // 调用render方法返回renderVdom
  let renderVdom = classInstance.render();
  vdom.classInstance = classInstance; // vdom和实例进行关联
  // 类的实例上保存老的虚拟DOM,同时给vdom上也添加oldRenderVdom指向老的vdom
  classInstance.oldRenderVdom = vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}
function reconcileChildren(children, dom) {
  children.forEach((child) => {
    mount(child, dom);
  });
}

function updateProps(dom, oldProps = {}, newProps) {
  for (let key in newProps) {
    // 先遍历新的属性
    if (key === "children") continue;
    if (key === "style") {
      let styleObj = newProps[key];
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr];
      }
    } else if (/^on[A-Z]*/.test(key)) {
      // 绑定的事件
      addEvent(dom, key.toLowerCase(), newProps[key]);
      // dom[key.toLowerCase()] = newProps[key]
    } else {
      // dom.className = 'title'
      dom[key] = newProps[key];
    }
  }
  // 再遍历老属性,如果新属性中没有直接删除掉
  for (let key in oldProps) {
    if (!newProps.hasOwnProperty(key)) {
      dom[key] = null;
    }
  }
}
/**
 * 根据虚拟DOM找到真实DOM
 * @param {*} vdom
 * 如果是像div节点,这样是可以通过vdom.dom找到真实DOM
 * 如果是类组件或者函数组件有两种情况
 *  1、创建函数/类组件的时候在虚拟DOM上通过oldRenderVdom绑定了虚拟DOM
 *  2、如果是组件嵌套组件,那就需要进行递归查找了
 */
export function findDOM(vdom) {
  if (!vdom) return null;
  if (vdom.dom) {
    // vdom对应原生的组件
    return vdom.dom;
  } else {
    // 类组件或函数组件
    let renderVdom = vdom.classInstance
      ? vdom.classInstance.oldRenderVdom
      : vdom.oldRenderVdom;
    return findDOM(renderVdom);
  }
}

export function compareTwoVdom(parentDOM, oldVdom, newVdom) {
  let oldDOM = findDOM(oldVdom); // 老的真实DOM
  let newDOM = createDOM(newVdom); // 新的真实DOM
  parentDOM.replaceChild(newDOM, oldDOM); // 新的替换老的
}

const ReactDOM = {
  render,
};
export default ReactDOM;

为函数组件添加

使用

  • 不能在函数组件上使用ref属性,因为他们没有实例
  • Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧
  • Ref 转发允许某些组件接收 ref,并将其向下传递给子组件

src/index.js

js
import React from "./lib/react";
import ReactDOM from "./lib/react-dom";

// 函数组件绑定ref
/* function UserName(props, ref) {
  return <input ref={ref} />
} */
// 限制行为
function UserName(props, ref) {
  let inputRef = React.createRef();
  ref.current = {
    focus: () => {
      inputRef.current.focus();
    },
  };
  return <input ref={inputRef} />;
}

const ForwardUserName = React.forwardRef(UserName);
class Form extends React.Component {
  constructor(props) {
    super(props);
    this.userNameRef = React.createRef();
  }
  getFocus = () => {
    this.userNameRef.current.focus();
  };
  render() {
    return (
      <div>
        <ForwardUserName ref={this.userNameRef} />
        <button onClick={this.getFocus}>focus</button>
      </div>
    );
  }
}
ReactDOM.render(<Form />, document.getElementById("root"));

实现

lib/react.js

js{37-42,47}
import { REACT_ELEMENT, REACT_FORWARD_REF } from "./element-types";
import { wrapToVdom } from "./utils";
import { Component } from "./Component";
// 把babel转换后的结果转成ReactDOM
function createElement(type, config, children) {
  // children:这里已经是一个虚拟DOM
  let key = config.key;
  let ref = config.ref;
  if (config) {
    delete config.__source;
    delete config._owner;
    delete config.__self;
    delete config.key;
    delete config.ref;
  }
  let props = { ...config };
  if (arguments.length > 3) {
    // 如果参数大于3,说明子节点是数组
    props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
  } else {
    props.children = wrapToVdom(children);
  }
  return {
    $$typeof: REACT_ELEMENT, // 表示是一个虚拟DOM,或者说是React元素
    key,
    ref,
    type, // 虚拟DOM的类型,如果是函数/类组件,type就是组件本身
    props, // 像事件,id,属性,class,style等都在这里面
  };
}
function createRef() {
  return {
    current: null,
  };
}

function forwardRef(render) {
  return {
    $$typeof: REACT_FORWARD_REF,
    render,
  };
}
const React = {
  createElement,
  Component,
  createRef,
  forwardRef,
};
export default React;

lib/react-dom.js

js{19-21,49-54}
import { addEvent } from "./event";
import { REACT_FORWARD_REF, REACT_TEXT } from "./element-types";

/**
 * 要把虚拟DOM转换成真实DOM并且挂载到容器中
 * @param {*} vdom 虚拟DOM
 * @param {*} container 容器
 */
function render(vdom, container) {
  mount(vdom, container);
}
function mount(vdom, container) {
  let newDOM = createDOM(vdom);
  container.appendChild(newDOM);
}
function createDOM(vdom) {
  const { props, type, ref } = vdom;
  let dom; // 真实的DOM节点
  if (type && type.$$typeof === REACT_FORWARD_REF) {
    // 挂载forward组件
    return mountForwardComponent(vdom);
  } else if (type === REACT_TEXT) {
    dom = document.createTextNode(props); // props是一个字符串
  } else if (typeof type === "function") {
    if (type.isReactComponent) {
      // 类组件
      return mountClassComponent(vdom);
    }
    return mountFunctionComponent(vdom); // 函数组件
  } else {
    dom = document.createElement(type);
  }
  if (props) {
    // 如果props存在,更新属性
    updateProps(dom, {}, props);
    if (typeof props.children === "object" && props.children.type) {
      // 说明只有一个子节点
      mount(props.children, dom); // 递归渲染子节点
    } else if (Array.isArray(props.children)) {
      reconcileChildren(props.children, dom);
    }
  }
  // 创建真实DOM的时候和虚拟DOM进行关联
  vdom.dom = dom;
  if (ref) ref.current = dom;
  return dom;
}

function mountForwardComponent(vdom) {
  const { type, props, ref } = vdom;
  let renderVdom = type.render(props, ref);
  vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}

function mountFunctionComponent(vdom) {
  const { props, type: FunctionComponent } = vdom;
  let renderVdom = FunctionComponent(props); // renderVdom才是要渲染的虚拟DOM
  // 保存老的虚拟DOM
  vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}
function mountClassComponent(vdom) {
  const { props, type: ClassComponent, ref } = vdom;
  // 先创建类组件实例
  let classInstance = new ClassComponent(props);
  if (ref) ref.current = classInstance; // 类组件添加ref
  // 调用render方法返回renderVdom
  let renderVdom = classInstance.render();
  vdom.classInstance = classInstance; // vdom和实例进行关联
  // 类的实例上保存老的虚拟DOM,同时给vdom上也添加oldRenderVdom指向老的vdom
  classInstance.oldRenderVdom = vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}
function reconcileChildren(children, dom) {
  children.forEach((child) => {
    mount(child, dom);
  });
}

function updateProps(dom, oldProps = {}, newProps) {
  for (let key in newProps) {
    // 先遍历新的属性
    if (key === "children") continue;
    if (key === "style") {
      let styleObj = newProps[key];
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr];
      }
    } else if (/^on[A-Z]*/.test(key)) {
      // 绑定的事件
      addEvent(dom, key.toLowerCase(), newProps[key]);
      // dom[key.toLowerCase()] = newProps[key]
    } else {
      // dom.className = 'title'
      dom[key] = newProps[key];
    }
  }
  // 再遍历老属性,如果新属性中没有直接删除掉
  for (let key in oldProps) {
    if (!newProps.hasOwnProperty(key)) {
      dom[key] = null;
    }
  }
}
/**
 * 根据虚拟DOM找到真实DOM
 * @param {*} vdom
 * 如果是像div节点,这样是可以通过vdom.dom找到真实DOM
 * 如果是类组件或者函数组件有两种情况
 *  1、创建函数/类组件的时候在虚拟DOM上通过oldRenderVdom绑定了虚拟DOM
 *  2、如果是组件嵌套组件,那就需要进行递归查找了
 */
export function findDOM(vdom) {
  if (!vdom) return null;
  if (vdom.dom) {
    // vdom对应原生的组件
    return vdom.dom;
  } else {
    // 类组件或函数组件
    let renderVdom = vdom.classInstance
      ? vdom.classInstance.oldRenderVdom
      : vdom.oldRenderVdom;
    return findDOM(renderVdom);
  }
}

export function compareTwoVdom(parentDOM, oldVdom, newVdom) {
  let oldDOM = findDOM(oldVdom); // 老的真实DOM
  let newDOM = createDOM(newVdom); // 新的真实DOM
  parentDOM.replaceChild(newDOM, oldDOM); // 新的替换老的
}

const ReactDOM = {
  render,
};
export default ReactDOM;

标签 · TAGS

评论 · COMMENTS

评论由 Giscus 承载 · 需要 GitHub 账号登录

站点不保存任何读者账号与凭证

未配置:在 lib/site.config.ts 填入 giscus 的 repo / repoId / category / categoryId