从0实现一个React,个人总结
原文:
https://github.com/hujiulong/blog/issues/4
个人总结::
一、JSX和虚拟DOM
- import React from 'react';
- import ReactDOM from 'react-dom';
- 先说'react',react内部主要实现了创建react元素createElement这个方法,
- 再说'react-dom',react-dom主要实现了将虚拟DOM转换成实体DOM然后挂载的功能
二、组件和生命周期
React定义组件的方式可以分为两种:函数和类,函数定义可以看做是类定义的一种简单形式。
组件基类React.Component
通过类的方式定义组件,我们需要继承React.Component
:
。。。
Componet
React.Component包含了一些预先定义好的变量和方法,我们来一步一步地实现它:
先定义一个Component
类:
- class Component {}
。。。
state & props
通过继承React.Component
定义的组件有自己的私有状态state
,可以通过this.state
获取到。同时也能通过this.props
来获取传入的数据。
所以在构造函数中,我们需要初始化state
和props
。。。
setState
组件内部的state
和渲染结果相关,当state
改变时通常会触发渲染,为了让React知道我们改变了state
,我们只能通过setState
方法去修改数据。我们可以通过Object.assign
来做一个简单的实现。
在每次更新state
后,我们需要调用renderComponent
方法来重新渲染组件,renderComponent
方法的实现后文会讲到。
render
上一篇文章中实现的render方法只支持渲染原生DOM元素,我们需要修改ReactDOM.render
方法,让其支持渲染组件。
。。。
我们需要在其中加一段用来渲染组件的代码:
- function _render( vnode, container ) {
- // ...
- if ( typeof vnode.tag === 'function' ) {
- const component = createComponent( vnode.tag, vnode.attrs );
- setComponentProps( component, vnode.attrs );
- return component.base;
- }
- // ...
- }
组件渲染和生命周期
在上面的方法中用到了createComponent
和setComponentProps
两个方法,组件的生命周期方法也会在这里面实现。
。。。
三、diff算法
对比策略
在前面两篇文章后,我们实现了一个render方法,它能将虚拟DOM渲染成真正的DOM,我们现在就需要改进它,让它不要再傻乎乎地重新渲染整个DOM树,而是找出真正变化的部分。
这部分很多类React框架实现方式都不太一样,有的框架会选择保存上次渲染的虚拟DOM,然后对比虚拟DOM前后的变化,得到一系列更新的数据,然后再将这些更新应用到真正的DOM上。
但也有一些框架会选择直接对比虚拟DOM和真实DOM,这样就不需要额外保存上一次渲染的虚拟DOM,并且能够一边对比一边更新,这也是我们选择的方式。
不管是DOM还是虚拟DOM,它们的结构都是一棵树,完全对比两棵树变化的算法时间复杂度是O(n^3),但是考虑到我们很少会跨层级移动DOM,所以我们只需要对比同一层级的变化。
总而言之,我们的diff算法有两个原则:
- 对比当前真实的DOM和虚拟DOM,在对比过程中直接更新真实DOM
- 只对比同一层级的变化
。。。
实现diff算法可以说性能有了很大的提升,但是在别的地方仍然后很多改进的空间:每次调用setState后会立即调用renderComponent重新渲染组件,但现实情况是,我们可能会在极短的时间内多次调用setState。
假设我们在上文的Counter组件中写出了这种代码
- onClick() {
- for ( let i = 0; i < 100; i++ ) {
- this.setState( { num: this.state.num + 1 } );
- }
- }
那以目前的实现,每次点击都会渲染100次组件,对性能肯定有很大的影响。
下一篇文章我们就要来改进setState方法
React显然也遇到了这样的问题,所以针对setState做了一些特别的优化:React会将多个setState的调用合并成一个来执行,这意味着当调用setState时,state并不会立即更新,举个栗子:
所以,这篇文章的目标也明确了,我们要实现以下两个功能:
- 异步更新state,将短时间内的多个setState合并成一个
- 为了解决异步更新导致的问题,增加另一种形式的setState:接受一个函数作为参数,在函数中可以得到前一个状态并返回下一个状态
setState队列
为了合并setState,我们需要一个队列来保存每次setState的数据,然后在一段时间后,清空这个队列并渲染组件。
队列是一种数据结构,它的特点是“先进先出”,可以通过js数组的push和shift方法模拟
然后需要定义一个”入队“的方法,用来将更新添加进队列。
- const queue = [];
- function enqueueSetState( stateChange, component ) {
- queue.push( {
- stateChange,
- component
- } );
- }
然后修改组件的setState方法,不再直接更新state和渲染组件,而是添加进更新队列。
- setState( stateChange ) {
- enqueueSetState( stateChange, this );
- }
现在队列是有了,怎么清空队列并渲染组件呢?
清空队列
我们定义一个flush方法,它的作用就是清空队列
- function flush() {
- let item;
- // 遍历
- while( item = setStateQueue.shift() ) {
- const { stateChange, component } = item;
- // 如果没有prevState,则将当前的state作为初始的prevState
- if ( !component.prevState ) {
- component.prevState = Object.assign( {}, component.state );
- }
- // 如果stateChange是一个方法,也就是setState的第二种形式
- if ( typeof stateChange === 'function' ) {
- Object.assign( component.state, stateChange( component.prevState, component.props ) );
- } else {
- // 如果stateChange是一个对象,则直接合并到setState中
- Object.assign( component.state, stateChange );
- }
- component.prevState = component.state;
- }
- }
这只是实现了state的更新,我们还没有渲染组件。渲染组件不能在遍历队列时进行,因为同一个组件可能会多次添加到队列中,我们需要另一个队列保存所有组件,不同之处是,这个队列内不会有重复的组件。
我们在enqueueSetState时,就可以做这件事
- const queue = [];
- const renderQueue = [];
- function enqueueSetState( stateChange, component ) {
- queue.push( {
- stateChange,
- component
- } );
- // 如果renderQueue里没有当前组件,则添加到队列中
- if ( !renderQueue.some( item => item === component ) ) {
- renderQueue.push( component );
- }
- }
在flush方法中,我们还需要遍历renderQueue,来渲染每一个组件
- function flush() {
- let item, component;
- while( item = queue.shift() ) {
- // ...
- }
- // 渲染每一个组件
- while( component = renderQueue.shift() ) {
- renderComponent( component );
- }
- }
延迟执行
现在还有一件最重要的事情:什么时候执行flush方法。
我们需要合并一段时间内所有的setState,也就是在一段时间后才执行flush方法来清空队列,关键是这个“一段时间“怎么决定。
一个比较好的做法是利用js的事件队列机制。
先来看这样一段代码:
- setTimeout( () => {
- console.log( 2 );
- }, 0 );
- Promise.resolve().then( () => console.log( 1 ) );
- console.log( 3 );
你可以打开浏览器的调试工具运行一下,它们打印的结果是:
3
1
2
具体的原理可以看阮一峰的这篇文章,这里就不再赘述了。
我们可以利用事件队列,让flush在所有同步任务后执行
- function enqueueSetState( stateChange, component ) {
- // 如果queue的长度是0,也就是在上次flush执行之后第一次往队列里添加
- if ( queue.length === 0 ) {
- defer( flush );
- }
- queue.push( {
- stateChange,
- component
- } );
- if ( !renderQueue.some( item => item === component ) ) {
- renderQueue.push( component );
- }
- }
定义defer方法,利用刚才题目中出现的Promise.resolve
- function defer( fn ) {
- return Promise.resolve().then( fn );
- }
这样在一次“事件循环“中,最多只会执行一次flush了,在这个“事件循环”中,所有的setState都会被合并,并只渲染一次组件。
别的延迟执行方法
除了用Promise.resolve().then( fn )
,我们也可以用上文中提到的setTimeout( fn, 0 )
,setTimeout的时间也可以是别的值,例如16毫秒。
16毫秒的间隔在一秒内大概可以执行60次,也就是60帧,人眼每秒只能捕获60幅画面
另外也可以用requestAnimationFrame
或者requestIdleCallback
- function defer( fn ) {
- return requestAnimationFrame( fn );
- }
试试效果
就试试渲染上文中用React渲染的那两个例子:
- class App extends Component {
- constructor() {
- super();
- this.state = {
- num: 0
- }
- }
- componentDidMount() {
- for ( let i = 0; i < 100; i++ ) {
- this.setState( { num: this.state.num + 1 } );
- console.log( this.state.num );
- }
- }
- render() {
- return (
- <div className="App">
- <h1>{ this.state.num }</h1>
- </div>
- );
- }
- }
效果和React完全一样
同样,用第二种方式调用setState:
- componentDidMount() {
- for ( let i = 0; i < 100; i++ ) {
- this.setState( prevState => {
- console.log( prevState.num );
- return {
- num: prevState.num + 1
- }
- } );
- }
- }
结果也完全一样:
从0实现一个React,个人总结的更多相关文章
- 如何从0创建一个react项目
1. 确保本机电脑安装了yarn和node: 2. 在需要安装的文件夹目录下输入:create-react-app +(项目名称): PS:上图使用的软件为webStorm 3. 此时一个简单的re ...
- React学习笔记-1-什么是react,react环境搭建以及第一个react实例
什么是react?react的官方网站:https://facebook.github.io/react/下图这个就是就是react的标志,非常巧合的是他和我们的github的编辑器Atom非常相似. ...
- 一个 react 小的 demo
一.搭建开发环境: webpack构建工具. 新建一个文件夹(login),进入根目录, 1.输入命令:cnpm init,生成了一个package.json文件,这是一个标准的npm说明文件,里面蕴 ...
- 第一个react
个人觉着react和vue是很相似的,之前还转载过一篇介绍两个异同点的文章,那个时候还完全不懂react,现在才慢慢开始接触,所以只能总结一些个人的心得,首先自然是react的优点了,个人觉着主要有以 ...
- 从零开始搭建一个react项目
Nav logo 120 发现 关注 消息 4 搜索 从零开始搭建一个react项目 96 瘦人假噜噜 2017.04.23 23:29* 字数 6330 阅读 32892评论 31喜欢 36 项目地 ...
- 开源低代码平台开发实践二:从 0 构建一个基于 ER 图的低代码后端
前后端分离了! 第一次知道这个事情的时候,内心是困惑的. 前端都出去搞 SPA,SEO 们同意吗? 后来,SSR 来了. 他说:"SEO 们同意了!" 任何人的反对,都没用了,时代 ...
- C#反序列化XML异常:在 XML文档(0, 0)中有一个错误“缺少根元素”
Q: 在反序列化 Xml 字符串为 Xml 对象时,抛出如下异常. 即在 XML文档(0, 0)中有一个错误:缺少根元素. A: 首先看下代码: StringBuilder sb = new Stri ...
- DECO 一个REACT NAtive 开发IDE工具
DECO 一个REACT NAtive 开发IDE工具. 目前只支持 OS,NO WINDOWS https://www.decosoftware.com/ 一个方便的快速 ERXPRESS 教程:h ...
- Griddle, griddle-react 一个REACT 表格组件
Griddle, griddle-react 一个REACT 表格组件: http://griddlegriddle.github.io/Griddle/index.html
随机推荐
- RESTful API设计方法
1.如果已经开始逐步的接触到了RESTful API设计方法的朋友,首先要对HTTP/HTTPS有一个大致的了解,虽然本身和RESTful API没有什么关系.但是对于增加网站的安全性还是十分重要的, ...
- Git学习笔记(1)
1.安装Git: 在linux下安装:yum install git 其他系统安装在这里略去~~~ 安装完成后,需要设置一下,在命令行输入以下命令: [root@xwq ~]# git config ...
- 【hiho一下 第四周】Trie图
[题目链接]:http://hihocoder.com/problemset/problem/1036?sid=1092555 [题意] [题解] AC自动机的模板题; 在求有没有子串的时候; 注意要 ...
- robot Framework选择单选框
- NYIST 119 士兵杀敌(三)
士兵杀敌(三)时间限制:2000 ms | 内存限制:65535 KB难度:5 描述南将军统率着N个士兵,士兵分别编号为1~N,南将军经常爱拿某一段编号内杀敌数最高的人与杀敌数最低的人进行比较,计算出 ...
- log4j输出多个自定义日志文件,动态配置路径
Log4J的配置文件(Configuration File)就是用来设置记录器的级别.存放器和布局的,它可接key=value格式的设置或xml格式的设置信息.通过配置,可以创建出Log4J的运行环境 ...
- 用户体验之如何优化你的APP
用户体验,速度为王,来几个优化APP“速度”的建议. 1.后台执行 毋庸多言,已是通常做法. 一般在执行下载任务时让其在后台运营,让用户有精力去做别的事情. 后端加载 2.提前显示 客户端与WEB的数 ...
- cocos2dx项目创建
射击类游戏文档 作者:浙江传媒学院 新媒体 张勇 1>编译环境 首先我们先去cocos2dx官网上下载cocos2dx最新版本号 http://www.cocos2d-x.org/ 我下载的 ...
- 分享:Android系统的经常使用权限整理
1.ACCES_NETWORK_STATE 同意应用程序获取网络状态信息的权限 2.ACCESS_WIFI_STATE 同意应用程序获取Wi-Fi网络状态的权限 3.BAT ...
- iOS:编译错误[__NSDictionaryM objectAtIndexedSubscript:]: unrecognized selector sent to instance 0xa79e61
这个意思是,__NSDictionaryM 无法将值传到下标索引对象,言简意赅就是数组越界.可是再看看,这是数组吗?不是,所以.遇到这样的crash,我这里有两种情况: 1.首先看看你 indexP ...