简单手写实现React类组件的state更新

简介: 简单手写实现React类组件的state更新

简单手写实现React类组件的state更新


继续学习写源码系列,本系列是学习系列。

  1. 简单手写实现 React 的 createElement 和 render,里面有准备工作,不再赘述。
  2. 简单手写实现 react 的函数组件
  3. 简单手写实现 react 的类组件

本文的目标是,手写实现React类组件的 state 更新。

TL;DR

  • setState两件事,更新state,更新DOM
  • 更新DOM,麻烦些,需要将旧DOM挂到组件实例上,新DOM重新生成,然后替换

准备工作

先将index.js加入类组件

/// import React from './source/react';
// import ReactDOM from './source/react-dom';
import React from 'react';
import ReactDOM from 'react-dom';
class Count extends React.Component {
  constructor(props) {
    super(props);
    this.state = { number: 1 };
  }
  handleClick = () => {
    this.setState({ number: this.state.number + 1 });
  };
  render() {
    return (
      <div>
        {this.state.number} <br />
        <button onClick={this.handleClick}>增加</button>
      </div>
    );
  }
}
const reactElement = <Count />;
ReactDOM.render(reactElement, document.getElementById('root'));

加了 state,点击数据变化。

分析 state

  1. this.statethis.setState肯定在Component类上定义了,不然没法直接使用
  2. setState应该做了两件事:更新state和更新DOM

实现 state

1. 更新state

这没啥,在原始类上,直接更改

class Component {
  static isClassComponent = true;
  constructor(props) {
    this.props = props;
    this.state = {};
  }
  setState(partialState) {
    this.state = { ...this.state, ...partialState };
    // this.updateDom()
  }
}

2. 更新DOM

同样,也需要在原始类上,这边,暂不做DOM diff,只是单纯的将组件的新DOM替换旧DOM

于是:

  1. 必须知道组件生成的真实 DOM 的父节点
  2. state 修改之后,需要用新生成的 DOM,替换掉旧 DOM

问题在于,原始类里此时并不知道父节点,也不知道旧 DOM,更不知道新 DOM,=。=

想想怎么办呢????

回归到最开始的步骤,ReactDOM.render(<Count/>,root);

<Count/>实际上就是React.createElement({type: class Count..,props://}),既然是类,那么可以将真实的 DOM 以属性的形式添加到实例上,真实的 DOM 挂载之后,父节点可以通过 parentNode 获取。而新的 DOM,可以通过之前的方法createDOM(vdom)去生成。

2.1 更新 react-dom.js

// {type:class xx,props:{}}
const handleClassType = (vdomClassType) => {
  const { type: classFn, props } = vdomClassType;
  const instance = new classFn(props);
  // 这里添加
  // 这里将本身的vdom绑定到实例上,再在vdom转化成dom的时候,再把dom挂上来
  instance.curRenderVdom = vdomClassType;
  const vdomReturn = instance.render(props);
  return createElementDOM(vdomReturn);
};
export function createDOM(vdom) {
  const type = getType(vdom);
  const getDom = typeMap.get(type);
  let DOM = getDom(vdom);
  // 这里添加
  // 这里DOM挂到vdom上,这样从instance.curRenderVdom.dom就可以拿到现存dom了
  type !== 'text' && (vdom.dom = DOM);
  console.log(vdom);
  return DOM;
}

2.2 更新 react.js

setState(partialState) {
    this.state = { ...this.state, ...partialState };
    this.updateDom();
  }
  updateDom() {
    // 找到组件挂载的元素,也就是父元素
    // 重新生成组件的DOM,然后替换掉 现在的DOM,就更新啦
    const curDom = this.curRenderVdom.dom;
    const parentNode = curDom.parentNode;
    console.log('setState', this.curRenderVdom, curDom);
    // render是类组件必须有的,返回JSX,本质就是React.createElement(),返回值跟参数七七八八,也是有{type:class xx,props:{}}
    const newVdom = this.render();
    const newDom = createDOM(newVdom);
    // 这里注意,需要手动将curRenderVdom替换
    this.curRenderVdom = newVdom;
    // 替换
    parentNode.replaceChild(newDom, curDom);
  }

index.js里面的reactreact-dom重新换成我们自己写的,页面正常显示,✌️~~

补充

这里,React.createElement(参数),参数的类型大约四种,不同的种类,生成 DOM 的方式略有差别,所以分开了,贴上所有react-dom.js的代码。 对了,还有事件属性,需要额外处理,也一并在下面补充了,不再赘述。

function render(vdom, container) {
  // 本质就是挂载,因container本身存在于文档之中,所以挂载操作会触发渲染
  mount(vdom, container);
}
function mount(vdom, container) {
  // 将第一个参数变成真实DOM,插入到第二个参数挂载元素上
  const DOM = createDOM(vdom);
  container.append(DOM);
}
// vdom有四种类型
// 1. "直接就是字符串"
const handleTextType = (vdom) => document.createTextNode(vdom || '');
// 2. {type:'div',props:{...}}  type是字符串,组件根元素是原生标签
const handleElementType = (vdom) => createElementDOM(vdom);
// 3. {type:function X(){ return <h1/>},props:{...}},type是函数,返回值才是vdom
const handleFunctionType = (vdom) => {
  const { type: fn, props } = vdom;
  const vdomReturn = fn(props);
  return createElementDOM(vdomReturn);
};
// 4. {type: class x ...{ render(){return <h1/>} },props:{///}},type是函数,
// 但是静态属性有isClassComponent,实例的render函数返回值才是vdom
const handleClassType = (vdom) => {
  const { type: classFn, props } = vdom;
  const instance = new classFn(props);
  // 这里将本身的vdom 绑定到实例上(不是vdomReturn,不然找不到curRenderVdom)
  instance.curRenderVdom = vdom;
  console.log('执行');
  const vdomReturn = instance.render(props);
  return createElementDOM(vdomReturn);
};
// 根据vdom得到类型,从而根据类型,调用相应的方法生成真实DOM
const getType = (vdom) => {
  const isTextNode =
    typeof vdom === 'string' || typeof vdom === 'number' || vdom == null;
  if (isTextNode) return 'text';
  const isObject = typeof vdom === 'object';
  const isElementNode = isObject && typeof vdom.type === 'string';
  if (isElementNode) return 'element';
  const isFn = isObject && typeof vdom.type === 'function';
  return isFn && vdom.type.isClassComponent ? 'class' : 'function';
};
const typeMap = new Map([
  ['text', handleTextType],
  ['element', handleElementType],
  ['function', handleFunctionType],
  ['class', handleClassType],
]);
export function createDOM(vdom) {
  const type = getType(vdom);
  const getDom = typeMap.get(type);
  // @ts-ignore
  let DOM = getDom(vdom);
  // vdom和DOM一一对应,这里的vdom是四种类型
  type !== 'text' && (vdom.dom = DOM);
  console.log(vdom);
  return DOM;
}
function createElementDOM(vdom) {
  const { type, props } = vdom;
  let DOM = document.createElement(type);
  if (props) {
    updateProps(DOM, props);
    const { children } = props;
    children && updateChildren(DOM, children);
  }
  return DOM;
}
function updateProps(DOM, props) {
  // 正常遍历就好,特殊的特殊处理
  for (const key in props) {
    if (key === 'children') continue;
    // 事件处理!!
    if (/on[A-Z]+/.test(key)) {
      DOM[key.toLowerCase()] = props[key];
      continue;
    }
    if (key === 'style') {
      updateStyle(DOM, props[key]);
      continue;
    }
    DOM[key] = props[key];
  }
  function updateStyle(DOM, styleObj) {
    for (const key in styleObj) {
      DOM.style[key] = styleObj[key];
    }
  }
}
function updateChildren(DOM, children) {
  // 单个节点,直接插入(挂载)到DOM上; 多个节点,遍历插入
  const isOneChildren = !Array.isArray(children);
  isOneChildren
    ? mount(children, DOM)
    : children.forEach((child) => mount(child, DOM));
}
const ReactDOM = {
  render,
};
export default ReactDOM;


目录
相关文章
|
17天前
|
前端开发 UED 索引
React 图片灯箱组件 Image Lightbox
图片灯箱组件是一种常见的Web交互模式,用户点击缩略图后弹出全屏窗口展示大图,并提供导航功能。本文介绍了基于React框架的图片灯箱组件开发,涵盖初始化状态管理、图片加载与预加载、键盘和鼠标事件处理等常见问题及解决方案。通过`useState`和`useEffect`钩子管理状态,使用懒加载和预加载优化性能,确保流畅的用户体验。代码案例展示了组件的基本功能实现,包括打开/关闭灯箱、切换图片及键盘操作。
115 80
|
16天前
|
移动开发 前端开发 JavaScript
React 视频播放器组件:Video Player
本文介绍了如何使用 React 和 HTML5 `&lt;video&gt;` 标签构建自定义视频播放器组件。首先,通过创建基础的 React 项目和 VideoPlayer 组件,实现了基本的播放、暂停功能。接着,探讨了常见问题如视频加载失败、控制条样式不一致、性能优化不足及状态管理混乱,并提供了相应的解决方案。最后,总结了构建高效视频播放器的关键要点,帮助开发者应对实际开发中的挑战。
70 27
|
19天前
|
前端开发 JavaScript API
React 图片放大组件 Image Zoom
本文介绍如何使用React创建图片放大组件(Image Zoom),提升用户体验。组件通过鼠标悬停或点击触发放大效果,利用`useState`管理状态,CSS实现视觉效果。常见问题包括图片失真、性能下降和移动端支持,分别可通过高质量图片源、优化事件处理和添加触摸事件解决。易错点涉及状态管理混乱、样式冲突和过多事件绑定,建议使用上下文API、CSS模块及优化事件绑定逻辑。高级功能扩展如多张图片支持和自定义放大区域进一步丰富了组件的实用性。
50 25
|
15天前
|
存储 编解码 前端开发
React 视频上传组件 Video Upload
随着互联网的发展,视频内容在网站和应用中愈发重要。本文探讨如何使用React构建高效、可靠的视频上传组件,涵盖基础概念、常见问题及解决方案。通过React的虚拟DOM和组件化开发模式,实现文件选择、进度显示、格式验证等功能,并解决跨域请求、并发上传等易错点。提供完整代码案例,确保用户能顺畅上传视频。
120 92
|
20天前
|
移动开发 前端开发 JavaScript
React 图片裁剪组件 Image Cropper
本文介绍了在React中实现图片裁剪功能的方法,涵盖基础知识、常见问题及解决方案。首先,通过第三方库如`react-image-crop`或`cropperjs-react`可轻松实现图片裁剪。接着,针对性能和兼容性问题,提供了优化图片加载、处理裁剪区域响应慢、解决浏览器差异等方案。最后,通过代码案例详细解释了如何创建一个基本的图片裁剪组件,并提出了优化建议,如使用`React.memo`、添加样式支持及处理大图片预览,帮助开发者避免常见错误并提升用户体验。
108 67
|
1天前
|
移动开发 前端开发 JavaScript
React 视频播放控制组件 Video Controls
本文介绍了如何使用 React 构建视频播放控制组件(Video Controls),涵盖基本概念、创建步骤和常见问题解决。首先,通过 HTML5 `&lt;video&gt;` 标签和 React 组件化思想,实现播放/暂停按钮和进度条等基础功能。接着,详细讲解了初始化项目、构建 `VideoControls` 组件及与主应用的集成方法。最后,针对视频无法播放、控制器状态不同步、进度条卡顿和音量控制失效等问题提供了具体解决方案,并介绍了全屏播放和自定义样式等进阶功能。希望这些内容能帮助你在实际项目中更好地实现和优化视频播放功能。
71 40
|
25天前
|
前端开发 JavaScript UED
React 拖拽排序组件 Draggable List
在现代Web应用中,拖拽排序功能显著提升用户体验。使用React结合`react-dnd`库,可以轻松创建高效且易于维护的拖拽排序组件。通过简单的拖拽操作,用户能直观调整列表项顺序,适用于任务管理、看板工具等场景。实现步骤包括项目初始化、安装依赖、创建基础组件、添加拖拽功能及管理状态和事件。常见问题如拖拽效果不流畅、顺序未更新等可通过性能优化、正确处理索引交换等方式解决。移动端支持也需考虑,确保跨平台的良好体验。
90 25
|
18天前
|
存储 前端开发 索引
React 图片轮播组件 Image Carousel
本文介绍了如何使用React创建图片轮播组件。首先,解释了图片轮播的基本概念和组件结构,包括图片容器、导航按钮、指示器和自动播放功能。接着,通过代码示例详细说明了创建基本组件、添加自动播放、处理边界情况的步骤。还探讨了常见问题如状态更新不及时、内存泄漏和样式问题,并提供了解决方案。最后,介绍了进阶优化,如添加过渡效果、支持触摸事件和动态加载图片,帮助读者构建更完善的轮播组件。
36 16
|
22天前
|
移动开发 前端开发 数据可视化
React 拖拽布局组件 Drag & Drop Layout
本文介绍了如何在React中构建拖拽布局组件,涵盖基础知识、常见问题及解决方案。首先解释了拖拽操作的三个阶段:开始、过程中和结束。接着推荐了几个常用的拖拽库,如`react-beautiful-dnd`,并详细展示了如何使用该库创建基础拖拽组件,包括安装依赖、初始化容器和处理拖拽结束事件。文章还探讨了常见问题,如拖拽不生效、性能优化、嵌套拖拽和跨浏览器兼容性,并提供了进阶技巧,如自定义样式、多列布局和集成其他UI组件。通过这些内容,读者可以掌握构建高效拖拽布局的方法。
52 16
|
23天前
|
前端开发 安全 UED
React 文件预览组件:File Preview
在现代Web应用中,文件上传和预览功能至关重要。本文基于React库,详细介绍如何构建文件预览组件,涵盖文件选择器、图片预览、文件大小限制及多种文件类型支持。通过实际代码示例,解析常见问题如跨域请求、文件路径处理和状态管理,并提供解决方案。帮助开发者提升用户体验,减少误操作。
38 2