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

相关推荐
无双_Joney15 分钟前
[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)
前端·后端·nestjs
在云端易逍遥17 分钟前
前端必学的 CSS Grid 布局体系
前端·css
ccnocare18 分钟前
选择文件夹路径
前端
艾小码18 分钟前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月19 分钟前
JavaScript作用域与作用域链详解
前端·面试
泉城老铁23 分钟前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅23 分钟前
JavaScript 作用域常见问题及解决方案
前端·javascript
司宸24 分钟前
Prompt结构化输出:从入门到精通的系统指南
前端
我是日安25 分钟前
从零到一打造 Vue3 响应式系统 Day 9 - Effect:调度器实现与应用
前端·vue.js