手写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.浏览器显示效果

相关推荐
沙尘暴炒饭几秒前
uniapp 前端解决精度丢失的问题 (后端返回分布式id)
前端·uni-app
昙鱼15 分钟前
springboot创建web项目
java·前端·spring boot·后端·spring·maven
天天进步201520 分钟前
Vue项目重构实践:如何构建可维护的企业级应用
前端·vue.js·重构
小华同学ai23 分钟前
vue-office:Star 4.2k,款支持多种Office文件预览的Vue组件库,一站式Office文件预览方案,真心不错
前端·javascript·vue.js·开源·github·office
APP 肖提莫25 分钟前
MyBatis-Plus分页拦截器,源码的重构(重构total总数的计算逻辑)
java·前端·算法
问道飞鱼36 分钟前
【前端知识】强大的js动画组件anime.js
开发语言·前端·javascript·anime.js
k093338 分钟前
vue中proxy代理配置(测试一)
前端·javascript·vue.js
傻小胖39 分钟前
React 脚手架使用指南
前端·react.js·前端框架
程序员海军1 小时前
2024 Nuxt3 年度生态总结
前端·nuxt.js
m0_748256781 小时前
SpringBoot 依赖之Spring Web
前端·spring boot·spring