likes
comments
collection
share

手写React(一)createElement+render

作者站长头像
站长
· 阅读数 20

从零到1,手写实现react!

传送

网上那些八股文,都是别人对某个知识点掌握后整理的描述,背下来没有任何意义!代码是数学题,不是背课文!

自己写一遍,彻底搞清楚它的实现过程原理,这样收获的才是自己的!用自己的理解总结出来的,才是真的掌握了!知其然,知其所以然!

我这个一步步手写实现React的笔记,希望可以帮到你。

一、React.createElement

前面说过jsx编译后会转换为React.createElement或者jsx/jsxs格式的递归调用函数 其实在此之前还有一个ast转化过程(babel-preset-react-app做的),这里省掉ast,直接从函数调用开始实现

1 前言

我们写的jsx,其实就是转换后的React.createElement,所以我们可以直接写React.createElement,效果也是一样的!

/*
 * @Description  : 手写react-dom 前言
 * @Author       : zhangyuru
 * @FilePath     : react-dom.js
 */
import React from "react";
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(document.getElementById("root"));

// 我们写的jsx 等同于下面的 React.createElement
// let element = (
//   <div className="title" style={{ color: "#fff", background: "#000" }}>
//     <span>hello</span>world
//   </div>
// );

// 等同于上面的jsx,效果是一样的
let element = React.createElement(
  "div",
  {
    className: "title",
    style: {
      color: "#fff",
      background: "#000",
    },
  },
  React.createElement("span", null, "hello"),
  "world"
);

console.log(JSON.stringify(element, null, 2));

root.render(element);

渲染流程图

手写React(一)createElement+render

2 准备工作

1) 功能拆分

为了后面的更多手写实现的扩展,所以需要拆分功能,分别写在不同的文件中

你需要创建以下几个文件

  1. constants.js --- 存放公共常量
  2. utils.js --- 存放工具函数
  3. react.js --- react功能的核心
  4. react-dom.js --- react-dom功能的核心

2) constants.js

/*
 * @Description  : 手写React的常量存放
 * @Author       : zhangyuru
 * @FilePath     : contants.js
 */

/* 表示这是一个文本类型的元素 在源码里没有这样一个类型 */
export const REACT_TEXT = Symbol("REACT_TEXT");

3) utils.js

编写第一个工具函数:wrapToVdom,将传入的属性转为vdom对象

/*
 * @Description  : 手写React的工具函数
 * @Author       : zhangyuru
 * @FilePath     : utils.js
 */

import { REACT_TEXT } from "./contants";

/**
 * @description    : 将传入的数据转为vdom对象
 * @param           { } element
 * @return          { } vdom对象
 */
export function wrapToVdom(element) {
  if (typeof element === "string" || typeof element === "number") {
    // 虚拟DOM.props.content就是此元素的内容
    return { type: REACT_TEXT, props: { content: element } };
  } else {
    return element;
  }
}

3.createElement

react.js中

/*
 * @Description  : 手写react
 * @Author       : zhangyuru
 * @FilePath     : react.js
 */

import { wrapToVdom } from "./utils";

/**
 * @description    : 实现createElement方法
 * @param           { } type 元素类型
 * @param           { } config 元素属性
 * @param           { } children 子元素
 * @return          { } vdom
 */
function createElement(type, config, children) {
  if (config) {
    delete config.__source; // 清理调jsx编译的标记
    delete config.__self; // 清理调jsx编译的标记
  }
  // 将接受的第二个参数转为props
  let props = { ...(config || {}) };
  // 对children的处理
  if (arguments.length > 3) {
    // 如果参数个数大于3个,说明不止一个子节点,截取子节点并用wrapToVdom处理
    props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
    // props.children = Array.prototype.slice.call(arguments, 2);
  } else {
    // 只有一个子节点的情况下,继续用wrapToVdom处理
    props.children = wrapToVdom(children);
  }
  // 返回值后面会继续扩展
  return { type, props };
}

const React = {
  createElement,
};

export default React;

二、ReactDOM.render

1.创建render函数

render函数接收两个参数:vdom,container

react-dom.js中

/*
 * @Description  : 手写react-dom
 * @Author       : zhangyuru
 * @FilePath     : react-dom.js
 */

import { REACT_TEXT } from "./contants";

/**
 * @description    : render方法,创建真实dom,插入到指定容器中
 * @param           { } vdom
 * @param           { } container
 * @return          { } void
 */
function render(vdom, container) {
  let newVdom = createDom(vdom); // 调用此方法创建真实dom
  container.appendChild(newVdom); // 将真实dom插入到容器中
}

2.createDom

接收vdom,返回真实dom

import { REACT_TEXT } from "./contants";

/**
 * @description    : 根据vdom的描述 创建真实dom
 * @param           { } vdom
 * @return          { } 真实dom元素
 */
function createDom(vdom) {
  let { type, props } = vdom;
  let dom;
  if (type === REACT_TEXT) {
    dom = document.createTextNode(props.content);
  } else {
    dom = document.createElement(type);
  }
  if (props) {
    updateProps(dom, {}, props);
    if (typeof props?.children === "object" && !!props?.children?.type) {
      render(props.children, dom); // 儿子是对象 【只有一个儿子】递归调render继续创建
    }
    if (Array.isArray(props?.children)) {
      reconcileChidren(props.children, dom); // 儿子是数组 需要遍历再递归创建
    }
  }
  vdom.dom = vdom; // 后面如果设置了ref 就把这个dom给ref
  return dom;
}

3.updateProps

根据vdom的描述,挂载/更新元素的属性

/**
 * @description    : 根据vdom的描述,挂载/更新元素的属性
 * @param           { } dom
 * @param           { } oldProps 暂时没用,后面会用作diff
 * @param           { } newProps 新的props
 * @return          { } void
 */
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 {
      dom[key] = newProps[key];
    }
  }
}

4.reconcileChidren

循环创建/更新子节点

/**
 * @description    : 循环创建/更新子节点
 * @param           { } children
 * @param           { } parentDom
 * @return          { } void
 */
function reconcileChidren(children, parentDom) {
  for (let i = 0; i < children.length; i++) {
    let childVdom = children[i];
    render(childVdom, parentDom);
  }
}

三、使用自己的API

1.切换引用

/*
 * @Description  : 手写react + react-dom
 * @Author       : zhangyuru
 * @FilePath     : index.js
 */

// import React from "react";
// import ReactDOM from "react-dom/client";

// 用自己封装的
import React from "./my-react/react";
import ReactDOM from "./my-react/react-dom";

// 模拟vdom创建
let element = React.createElement(
  "div",
  {
    className: "title",
    style: {
      color: "#fff",
      background: "#000",
      height: "200px",
    },
  },
  React.createElement("span", null, "hello"),
  "world"
);

// 实现渲染
ReactDOM.render(element, document.getElementById("root"));

2.浏览器显示效果

手写React(一)createElement+render