手写React(一)createElement+render

网上那些八股文,都是别人对react理解后整理的描述,你去背没有任何意义!

只有自己写一遍,收获的才是自己的!用自己的理解总结出来的,才是你真的掌握了!

本文将带你一步步手写实现React,希望可以帮到你。

一、React.createElement

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

1 前言

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

jsx 复制代码
/*
 * @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);

渲染流程图

2 准备工作

1) 功能拆分

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

你需要创建以下几个文件

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

2) constants.js

jsx 复制代码
/*
 * @Description  : 手写React的常量存放
 * @Author       : zhangyuru
 * @FilePath     : contants.js
 */

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

3) utils.js

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

jsx 复制代码
/*
 * @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中

jsx 复制代码
/*
 * @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) {
  // 将接受的第二个参数转为props
  let props = { ...(config || {}) };
  // 对children的处理
  if (arguments.length > 3) {
    // 如果参数个数大于3个,说明不止一个子节点,截取子节点并用wrapToVdom处理
    props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
  } else {
    // 只有一个子节点的情况下,继续用wrapToVdom处理
    if (typeof children !== "undefined") props.children = wrapToVdom(children);
  }
  // 返回值后面会继续扩展
  return { type, props };
}

const React = {
  createElement,
};

export default React;

二、ReactDOM.render

1.创建render函数

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

react-dom.js中

jsx 复制代码
/*
 * @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

jsx 复制代码
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的描述,挂载/更新元素的属性

jsx 复制代码
/**
 * @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

循环创建/更新子节点

jsx 复制代码
/**
 * @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.切换引用

jsx 复制代码
/*
 * @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.浏览器显示效果

相关推荐
卸任6 小时前
Vibe coding工具使用心得
前端·ai编程
brien97 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance7 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M7 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写7 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李7 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易8 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马8 小时前
前沿视界
前端
kyriewen8 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易8 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员