技术咨询、项目合作、广告投放、简历咨询、技术文档下载 点击这里 联系博主

# React虚拟dom以及如何将虚拟Dom转换成真实dom

# 虚拟 Dom 实现原理

# JSX 和 createElement

我们在实现一个React组件时可以选择两种编码方式,第一种是使用JSX编写:

class Hello extends Component {
  render() {
    return <div>Hello mrgaogang</div>;
  }
}

第二种是直接使用React.createElement编写:

class Hello extends Component {
  render() {
    return React.createElement("div", null, `Hello mrgaogang`);
  }
}

实际上,上面两种写法是等价的,JSX只是为 React.createElement(component, props, ...children)方法提供的语法糖。也就是说所有的JSX代码最后都会转换成React.createElement(...)Babel帮助我们完成了这个转换的过程。

如下面的JSX

function TestComponent() {
  return <div className="aa" style={{ backgroundColor: "white" }} />;
}

class Test2 extends React.Component {
  render() {
    return <div className="test2" onClick={() => {}} />;
  }
}

function App() {
  return (
    <div className="App">
      <TestComponent />
      <Test2 />
      Hello mrgaogang
      <ul>
        <li>苹果</li>
        <li>橘子</li>
      </ul>
    </div>
  );
}

将会被Babel转换为: babel 在线查看 (opens new window)

function TestComponent() {
  return React.createElement("div", {
    className: "aa",
    style: {
      backgroundColor: "white",
    },
  });
}

class Test2 extends React.Component {
  render() {
    return React.createElement("div", {
      className: "test2",
      onClick: () => {},
    });
  }
}

function App() {
  return React.createElement(
    "div",
    {
      className: "App",
    },
    React.createElement(TestComponent, null),
    React.createElement(Test2, null),
    "Hello mrgaogang",
    React.createElement(
      "ul",
      null,
      React.createElement("li", null, "\u82F9\u679C"),
      React.createElement("li", null, "\u6A58\u5B50")
    )
  );
}

注意,babel在编译时会判断JSX中组件的首字母,当首字母为小写时,其被认定为原生DOM标签,createElement的第一个变量被编译为字符串;当首字母为大写时,其被认定为自定义组件,createElement的第一个变量被编译为对象;

另外,由于JSX提前要被Babel编译,所以JSX是不能在运行时动态选择类型的,比如下面的代码:

function Story(props) {
  // Wrong! JSX type can't be an expression.
  return <components[props.storyType] story={props.story} />;
}

需要变成下面的写法:

function Story(props) {
  // Correct! JSX type can be a capitalized variable.
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}

所以,使用JSX你需要安装Babel插件babel-plugin-transform-react-jsx

{
    "plugins": [
        ["transform-react-jsx", {
            "pragma": "React.createElement"
        }]
    ]
}

# 创建虚拟 DOM

下面我们来看看虚拟DOM的真实模样,将下面的JSX代码在控制台打印出来:

function TestComponent() {
  return <div className="aa" style={{ backgroundColor: "white" }} />;
}

class Test2 extends React.Component {
  render() {
    return <div className="test2" onClick={() => {}} />;
  }
}

function App() {
  return (
    <div className="App">
      <TestComponent />
      <Test2 />
      Hello mrgaogang
      <ul>
        <li>苹果</li>
        <li>橘子</li>
      </ul>
    </div>
  );
}

image

那么React是如何将我们的代码转换成这个结构的呢,下面我们来看看createElement函数的具体实现(文中的源码经过精简)。

image

createElement函数内部做的操作很简单,将props和子元素进行处理后返回一个ReactElement对象,下面我们来逐一分析:

(1).处理 props:

image

  • 1.将特殊属性refkeyconfig中取出并赋值
  • 2.将特殊属性selfsourceconfig中取出并赋值
  • 3.将除特殊属性的其他属性取出并赋值给props

后面的文章会详细介绍这些特殊属性的作用。

(2).获取子元素

image

  • 1.获取子元素的个数 —— 第二个参数后面的所有参数
  • 2.若只有一个子元素,赋值给props.children
  • 3.若有多个子元素,将子元素填充为一个数组赋值给props.children

(3).处理默认 props

image

  • 将组件的静态属性defaultProps定义的默认props进行赋值

ReactElement

ReactElement将传入的几个属性进行组合,并返回。

  • type:元素的类型,可以是原生 html 类型(字符串),或者自定义组件(函数或class
  • key:组件的唯一标识,用于Diff算法,下面会详细介绍
  • ref:用于访问原生dom节点
  • props:传入组件的props
  • owner:当前正在构建的Component所属的Component

$$typeof:一个我们不常见到的属性,它被赋值为REACT_ELEMENT_TYPE

var REACT_ELEMENT_TYPE =
  (typeof Symbol === "function" && Symbol.for && Symbol.for("react.element")) ||
  0xeac7;

可见,$$typeof是一个Symbol类型的变量,这个变量可以防止XSS

如果你的服务器有一个漏洞,允许用户存储任意JSON对象, 而客户端代码需要一个字符串,这可能会成为一个问题:

// JSON
let expectedTextButGotJSON = {
  type: "div",
  props: {
    dangerouslySetInnerHTML: {
      __html: "/* put your exploit here */",
    },
  },
};
let message = { text: expectedTextButGotJSON };
<p>{message.text}</p>;

JSON中不能存储Symbol类型的变量。

ReactElement.isValidElement函数用来判断一个React组件是否是有效的,下面是它的具体实现。

ReactElement.isValidElement = function (object) {
  return typeof object === 'object' && object !== null && object.?typeof === REACT_ELEMENT_TYPE;
};

可见React渲染时会把没有?typeof标识,以及规则校验不通过的组件过滤掉。

当你的环境不支持Symbol时,?typeof被赋值为0xeac7,至于为什么,React开发者给出了答案:

0xeac7看起来有点像React

selfsource只有在非生产环境才会被加入对象中。

  • self指定当前位于哪个组件实例。
  • _source指定调试代码来自的文件(fileName)和代码行数(lineNumber)

# 虚拟 DOM 转换为真实 DOM

通常我们启动一个项目大致方法如下:

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

那么 ReactDOM 是如何将 vdom 转换成真实的 dom 呢?这里简单实现了一下ReactDOM.render;真实的情况ReactDOM做了更多的事情。

function render(node, container) {
  // 节点可能为文本节点
  if (typeof node === "string" || typeof node === "number") {
    return container.appendChild(document.createTextNode(node));
  }

  let props = node.props || {};
  let type = node.type;
  // 如果是class类型的组件,需要new实例
  if (type && type.prototype && type.prototype.isReactComponent) {
    const returnElement = new type(props).render();
    type = returnElement.type;
    props = returnElement.props;
  } else if (typeof type === "function") {
    // 如果为函数式组件需要调用
    const returnElement = type(props);
    type = returnElement.type;
    props = returnElement.props;
  }
  const resultDom = document.createElement(type);
  for (const key in props) {
    const value = props[key];
    if (key === "className") {
      // 处理class
      resultDom.className = value;
    } else if (key === "style") {
      // 处理style
      resultDom.style.cssText = value;
    } else if (key === "children") {
      if (typeof value === "string" || typeof value === "number") {
        // children可能为文本或者数组类型
        // 如果内容是文字、数字之类的,把文字、数字放到标签里面
        resultDom.appendChild(document.createTextNode(value));
      } else {
        if (Array.isArray(value)) {
          //如果有多个chilren则循环插入
          value.forEach((ele) => {
            render(ele, resultDom);
          });
        } else {
          //之后一个children的时候直接render
          render(value, resultDom);
        }
      }
    } else {
      // 设置其余的属性
      resultDom.setAttribute(key, value);
    }
  }
  // 添加到父节点上
  container.appendChild(resultDom);
}

export default { render };

文章部分摘录自【React深入】深入分析虚拟DOM的渲染原理和特性 (opens new window)

【未经作者允许禁止转载】 Last Updated: 2/4/2024, 6:06:40 AM