手写React核心原理,再也不怕面试官问我React原理

开发 前端
React 是一个为数据提供渲染为 HTML 视图的开源 JavaScript 库。React 为程序员提供了一种子组件不能直接影响外层组件的模型,数据改变时对 HTML 文档的有效更新,和现代单页应用中组件之间干净的分离。

[[353784]]

 1. 项目基本准备工作

1.1 创建项目

利用npx create-react-app my_react命令创建项目

1.2 项目结构

将一些用不到的文件删除后,目录变成这样


此时的index.js

  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3.  
  4. ReactDOM.render( 
  5.   "sunny"
  6.   document.getElementById('root'
  7. ); 

2.创建react.js和react-dom.js文件


我们就可以把需要引入react和react-dom的改成自己创建的文件啦

  1. import React from './react'
  2. import ReactDOM from './react-dom'
  3.  
  4. ReactDOM.render( 
  5.   "sunny"
  6.   document.getElementById('root'
  7. ); 

3.完成react-dom

我们在index.js文件中

  1. ReactDOM.render( 
  2.   "sunny"
  3.   document.getElementById('root'
  4. ); 

以这样的方式使用ReactDOM,说明他有render这个方法。

所以我们可以这样实现react-dom

  1. // react-dom.js 
  2. let ReactDOM = { 
  3.     render 
  4. function render(element,container){ 
  5.     container.innerHTML = `<span>${element}</span>` 
  6.      
  7.  
  8. export default ReactDOM 

我们看下运行结果

 

可喜可贺!万里长城迈出了第一步

好了,现在我们给每一个 元素打上 一个标记 ,这样的话 就可以通过这个标记 辨别出与其他 元素的关系,也可以直接通过这标记找到该元素了。

就像下面这张图一样,是不是就直接看出0.0和0.1的父节点就是0了呢?


  1. // react-dom.js 
  2. let ReactDOM = { 
  3.     render, 
  4.     rootIndex:0 
  5. function render(element,container){ 
  6.     container.innerHTML = `<span data-reactid=${ReactDOM.rootIndex}>${element}</span>` 
  7.  
  8. export default ReactDOM 

如代码所示,我们给每一个元素添加了一个标记data-reactid

运行,发现确实标记成功了,哈哈哈


4. 重构render方法

我们前面的render方法

  1. function render(element,container){ 
  2.     container.innerHTML = `<span data-reactid=${ReactDOM.rootIndex}>${element}</span>` 

默认传入的element为字符串, 但是实际情况是有可能是 文本节点,也有可能是DOM节点,也有可能是 自定义组件。所以我们实现一个createUnit方法,将element传入,让它来判断element是什么类型的节点,。然后再返回一个被判断为某种类型,并且添加了对应的方法和属性的对象 。例如,我们的element是字符串类型,那么就返回一个字符串类型的对象,而这个对象自身有element 属性和getMarkUp方法,这个getMarkUp方法,将element转化成真实的dom其实你也可以简单地认为 createUnit 方法 就是 为 element 对象添加 一个getMarkUp方法

  1. // react-dom.js 
  2. import $ from "jquery" 
  3. let ReactDOM = { 
  4.     render, 
  5.     rootIndex:0 
  6. function render(element,container){ 
  7.     let unit = createUnit(element) 
  8.     let markUp = unit.getMarkUp();// 用来返回HTML标记 
  9.     $(container).html(markUp) 
  10.  
  11. export default ReactDOM 

如代码所示,将element传入createUnit方法,获得的unit是一个对象

  1.   _currentElement:element, 
  2.   getMarkUp(){ 
  3.     ... 
  4.   } 

再执行 unit的getMarkUp方法,获得到 真实的dom,然后就可以挂载到container上去啦!

注意,如果传入render的element是字符串"sunny", 即

  1. import React from './react'
  2. import ReactDOM from './react-dom'
  3.  
  4. ReactDOM.render( 
  5.   "sunny"
  6.   document.getElementById('root'
  7. ); 

也就是说传入createUnit的element是字符串"sunny",那么返回的unit是

  1.  _currentElement:"sunny"
  2.  getMarkUp(){ 
  3.    
  4.  } 

那怎么写这个createUnit呢?

5. 实现createUnit方法

我们创建一个新的文件叫做unit.js

在这里插入图片描述

  1. // Unit.js 
  2. class Unit{ 
  3.     
  4. class TextUnit extends Unit{ 
  5.      
  6.  
  7. function createUnit(element){ 
  8.     if(typeof element === 'string' || typeof element === "number"){ 
  9.         return new TextUnit(element) 
  10.     } 
  11.  
  12. export { 
  13.     createUnit 

如代码所示,createUnit判断element是字符串时就 new 一个TextUnit的对象,然后返回出去,这个也就是我们上面讲到的unit对象了。

为什么要 TextUnit 继承 于 Unit呢?

这是因为 element除了字符串 ,也有可能是 原生的标签,列如div,span等,也有可能是我们自定义的组件,所以我们先写 了一个 unit类,这个类实现 这几种element 所共有的属性。然后 具体的 类 ,例如 TextUnit 直接继承 Unit ,再实现自有的 属性就好了。

6. 实现Unitnew

Unit 得到的对象应当是这样的

  1.   _currentElement:element, 
  2.   getMarkUp(){ 
  3.     ... 
  4.   } 

也就是说,这是所有的 种类都有的属性,所以我们可以这样实现 Unit

  1. class Unit{ 
  2.     constructor(element){ 
  3.         this._currentElement = element 
  4.     } 
  5.     getMarkUp(){ 
  6.         throw Error("此方法应该被重写,不能直接被使用"
  7.     } 

为什么getMarkUp 要throw Error("此方法应该被重写,不能直接被使用")呢?

学过 java或其他语言的同学应该秒懂,这是因为getMarkUp希望是被子类重写的方法,因为每个子类执行这个方法返回的结果是不一样的。

7. 实现TextUnit

到这一步,我们只要重写getMarkUp方法就好了,不过不要忘记,给每一个元素添加一个 reactid,至于为什么,已经在上面说过了,也放了一张大图了哈。

  1. class TextUnit extends Unit{ 
  2.     getMarkUp(reactid){ 
  3.         this._reactid = reactid 
  4.         return `<span data-reactid=${reactid}>${this._currentElement}</span>` 
  5.     } 

好了,到这里先看下完整的Unit.js长什么样子吧

  1. // Unit.js 
  2. class Unit{ 
  3.     constructor(element){ 
  4.         this._currentElement = element 
  5.     } 
  6.     getMarkUp(){ 
  7.         throw Error("此方法应该被重写,不能直接被使用"
  8.     } 
  9. class TextUnit extends Unit{ 
  10.     getMarkUp(reactid){ 
  11.         this._reactid = reactid 
  12.         return `<span data-reactid=${reactid}>${this._currentElement}</span>` 
  13.     } 
  14.  
  15. function createUnit(element){ 
  16.     if(typeof element === 'string' || typeof element === "number"){ 
  17.         return new TextUnit(element) 
  18.     } 
  19.  
  20. export { 
  21.     createUnit 

我们在index.js引入 unit测试下

  1. // index.js 
  2. import React from './react'
  3. import ReactDOM from './react-dom'
  4.  
  5. ReactDOM.render( 
  6.   "sunny"
  7.   document.getElementById('root'
  8. ); 

  1. // react-dom.js 
  2. import {createUnit} from './unit' 
  3. import $ from "jquery" 
  4. let ReactDOM = { 
  5.     render, 
  6.     rootIndex:0 
  7. function render(element,container){ 
  8.     let unit = createUnit(element) 
  9.     let markUp = unit.getMarkUp(ReactDOM.rootIndex);// 用来返回HTML标记 
  10.     $(container).html(markUp) 
  11.  
  12. export default ReactDOM 

在这里插入图片描述

意料之内的成功!哈哈哈啊

8. 理解React.creacteElement方法

在第一次学习react的时候,我总会带着许多疑问。比如看到下面的代码就会想:为什么我们只是引入了React,但是并没有明显的看到我们在其他地方用,这时我就会想着既然没有用到,那如果删除之后会不会受到影响呢?答案当然是不行的。

  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3.  
  4. let element = ( 
  5.     <h1 id="title" className="bg" style={{color: 'red'}}> 
  6.         hello 
  7.         <span>world</span> 
  8.     </h1> 
  9.  
  10. console.log({type: element.type, props:element.props}) 
  11.  
  12. ReactDOM.render(element,document.getElementById('root')); 

 当我们带着这个问题去研究的时候会发现其实在渲染element的时候调了React.createElement(),所以上面的问题就在这里找到了答案。

如下面代码所示,这就是从jsx语法到React.createElement的转化

  1. <h1 id="title" className="bg" style={{color: 'red'}}> 
  2.         hello 
  3.         <span>world</span> 
  4. </h1> 
  5.  
  6. //上面的这段代码很简单,但是我们都知道react是所谓的虚拟dom,当然不可能就是我们看到的这样。当我们将上面的代码经过babel转译后,我们再看看 
  7.  
  8. React.createElement("h1", { 
  9.   id: "title"
  10.   className: "bg"
  11.   style: { 
  12.     color: 'red' 
  13.   } 
  14. }, "hello", React.createElement("span"null"world")); 

 document有createElement()方法,React也有createElement()方法,下面就来介绍React的createElement()方法。

  1. var reactElement = ReactElement.createElement( 
  2.    ... // 标签名称字符串/ReactClass, 
  3.    ... // [元素的属性值对对象], 
  4.    ... // [元素的子节点] 

1、参数:

1)第一个参数:可以是一个html标签名称字符串,也可以是一个ReactClass(必须);

2)第二个参数:元素的属性值对对象(可选),这些属性可以通过this.props.*来调用;

3)第三个参数开始:元素的子节点(可选)。

2、返回值:

一个给定类型的ReactElement元素

我们可以改下我们的index.js

  1. // index.js 
  2. import React from './react'
  3. import ReactDOM from './react-dom'
  4.  
  5. var li1 = React.createElement('li', {onClick:()=>{alert("click")}}, 'First'); 
  6. var li2 = React.createElement('li', {}, 'Second'); 
  7. var li3 = React.createElement('li', {}, 'Third'); 
  8. var ul = React.createElement('ul', {className: 'list'}, li1, li2, li3); 
  9. console.log(ul); 
  10. ReactDOM.render(ul,document.getElementById('root')) 

可以就看下 ul 最终的打印 期待结果

 

由此 ,我们只知道了,ReactElement.createElement方法将生产一个给定类型的ReactElement元素,然后这个对象被传入 render方法,然后进行了上面讲到的 createUnit和getMarkUp操作。

9. 实现React.createElement方法

经过上面的讲解,我们大概已经知道React.createElement方法的作用了,现在就来看看是怎么实现的

我们创建了一个新的文件element.js 

  1. // element.js 
  2. class Element { 
  3.     constructor(type,props){ 
  4.         this.type = type 
  5.         this.props = props 
  6.     } 
  7.  
  8. function createElement(type,props={},...children){ 
  9.     props.children = children || []; 
  10.     return new Element(type,props) 
  11.  
  12. export { 
  13.     Element, 
  14.     createElement 

我们 定义了一个 Element 类 ,然后在createElement方法里创建了这个类的对象, 并且return出去了

没错,这个对象就是上面所说的给定类型的ReactElement元素,也就是下面这张图所显示的

 

我们应当是这样React.createElement()调用这个方法的,所以我们要把这个方法挂载到react身上。

我们前面还没有实现react.js

其实,很简单,就是返回一个React对象,这个对象有createElement方法

  1. // react.js 
  2. import {createElement} from "./element" 
  3. const React = { 
  4.    createElement 
  5. export default React 

10. 实现NativeUnit

上面实现了 createElement返回 给定类型的ReactElement元素 后,就将改元素传入,render方法,因此 就会经过 createUnit方法, createUnit方法判断是属于什么类型的 元素,如下面代码

  1. // Unit.js 
  2. import {Element} from "./element" // 新增代码 
  3. class Unit{ 
  4.     constructor(element){ 
  5.         this._currentElement = element 
  6.     } 
  7.     getMarkUp(){ 
  8.         throw Error("此方法应该被重写,不能直接被使用"
  9.     } 
  10. class TextUnit extends Unit{ 
  11.     getMarkUp(reactid){ 
  12.         this._reactid = reactid 
  13.         return `<span data-reactid=${reactid}>${this._currentElement}</span>` 
  14.     } 
  15.  
  16. function createUnit(element){ 
  17.     if(typeof element === 'string' || typeof element === "number"){ 
  18.         return new TextUnit(element) 
  19.     } 
  20.     // 新增代码 
  21.     if(element instanceof Element && typeof element.type === "string"){ 
  22.         return new NativeUnit(element) 
  23.     } 
  24.  
  25. export { 
  26.     createUnit 

好了,现在我们来实现NativeUnit类,其实主要就是实现NativeUnit的getMarkUp方法

  1. class NativeUnit extends Unit{ 
  2.     getMarkUp(reactid){ 
  3.         this._reactid = reactid  
  4.         let {type,props} = this._currentElement; 
  5.     } 

要明确的一点是,NativeUnit 的getMarkUp方法,是要把

这样一个element 对象转化为 真实的dom的 

因此,我们可以这样完善getMarkUp方法

  1. class NativeUnit extends Unit{ 
  2.     getMarkUp(reactid){ 
  3.         this._reactid = reactid  
  4.         let {type,props} = this._currentElement; 
  5.         let tagStart = `<${type} ` 
  6.         let childString = '' 
  7.         let tagEnd = `</${type}>` 
  8.         for(let propName in props){ 
  9.             if(/^on[A-Z]/.test(propName)){ // 添加绑定事件 
  10.                  
  11.             }else if(propName === 'style'){ // 如果是一个样式对象 
  12.  
  13.             }else if(propName === 'className'){ // 如果是一个类名 
  14.  
  15.             }else if(propName === 'children'){ // 如果是子元素 
  16.  
  17.             }else { // 其他 自定义的属性 例如 reactid 
  18.                 tagStart += (` ${propName}=${props[propName]} `) 
  19.             } 
  20.         } 
  21.         return tagStart+'>' + childString +tagEnd 
  22.     } 

这只是 大体上的 一个实现 ,其实就是 把标签 和属性 以及 子元素 拼接成 字符串,然后返回出去。

我们测试下,现在有没有 把ul 渲染出来

  1. // index.js 
  2. import React from './react'
  3. import ReactDOM from './react-dom'
  4.  
  5. var li1 = React.createElement('li', {}, 'First'); 
  6. var li2 = React.createElement('li', {}, 'Second'); 
  7. var li3 = React.createElement('li', {}, 'Third'); 
  8. var ul = React.createElement('ul', {className: 'list'}, li1, li2, li3); 
  9. console.log(ul); 
  10. ReactDOM.render(ul,document.getElementById('root')) 

发现确实成功渲染出来了,但是 属性和 子元素还没有,这是因为我们 还没实现 具体 的功能。

现在我们来实现事件绑定 功能

  1. class NativeUnit extends Unit{ 
  2.     getMarkUp(reactid){ 
  3.         this._reactid = reactid  
  4.         let {type,props} = this._currentElement; 
  5.         let tagStart = `<${type} data-reactid="${this._reactid}"
  6.         let childString = '' 
  7.         let tagEnd = `</${type}>` 
  8.         for(let propName in props){ 
  9.          // 新增代码 
  10.             if(/^on[A-Z]/.test(propName)){ // 添加绑定事件 
  11.                 let eventName = propName.slice(2).toLowerCase(); // 获取click 
  12.                 $(document).delegate(`[data-reactid="${this._reactid}"]`,`${eventName}.${this._reactid}`,props[propName]) 
  13.             }else if(propName === 'style'){ // 如果是一个样式对象 
  14.                 
  15.             }else if(propName === 'className'){ // 如果是一个类名 
  16.                  
  17.             }else if(propName === 'children'){ // 如果是子元素 
  18.                 
  19.             }else { // 其他 自定义的属性 例如 reactid 
  20.                  
  21.             } 
  22.         } 
  23.         return tagStart+'>' + childString +tagEnd 
  24.     } 

在这里,我们是用了事件代理的模式,之所以用事件代理,是因为这些标签元素还没被渲染到页面上,但我们又必须提前绑定事件,所以需要用到事件代理

接下来,实现 样式对象的绑定

  1. class NativeUnit extends Unit{ 
  2.     getMarkUp(reactid){ 
  3.         this._reactid = reactid  
  4.         let {type,props} = this._currentElement; 
  5.         let tagStart = `<${type} data-reactid="${this._reactid}"
  6.         let childString = '' 
  7.         let tagEnd = `</${type}>` 
  8.         for(let propName in props){ 
  9.             if(/^on[A-Z]/.test(propName)){ // 添加绑定事件 
  10.                 ... 
  11.             }else if(propName === 'style'){ // 如果是一个样式对象 
  12.                 let styleObj = props[propName] 
  13.                 let styles = Object.entries(styleObj).map(([attr, value]) => { 
  14.                     return `${attr.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)}:${value}`; 
  15.                 }).join(';'
  16.                 tagStart += (` style="${styles}" `) 
  17.             }else if(propName === 'className'){ // 如果是一个类名 
  18.                  
  19.             }else if(propName === 'children'){ // 如果是子元素 
  20.                 
  21.             }else { // 其他 自定义的属性 例如 reactid 
  22.                
  23.             } 
  24.         } 
  25.         return tagStart+'>' + childString +tagEnd 
  26.     } 

这里 其实就是把

  1. {style:{backgroundColor:"red"}} 

对象中的 style这个对象 属性拿出来,

然后把backgroundColor 通过正则 变化成background-color,

然后再拼接到tagStart中。

接下来再实现className,发现这个也太简单了吧

  1. class NativeUnit extends Unit { 
  2.     getMarkUp(reactid) { 
  3.         this._reactid = reactid 
  4.         let { type, props } = this._currentElement; 
  5.         let tagStart = `<${type} data-reactid="${this._reactid}"
  6.         let childString = '' 
  7.         let tagEnd = `</${type}>` 
  8.         for (let propName in props) { 
  9.             if (/^on[A-Z]/.test(propName)) { // 添加绑定事件 
  10.                ... 
  11.             } else if (propName === 'style') { // 如果是一个样式对象 
  12.                 ... 
  13.             } else if (propName === 'className') { // 如果是一个类名 
  14.                 tagStart += (` class="${props[propName]}"`) 
  15.             } else if (propName === 'children') { // 如果是子元素 
  16.                ... 
  17.             } else { // 其他 自定义的属性 例如 reactid 
  18.                 ... 
  19.             } 
  20.         } 
  21.         return tagStart + '>' + childString + tagEnd 
  22.     } 

为什么这么简单呢?因为只需要把

  1. className: 'list' 

中的className变化成 class就可以了。OMG!!

接下来,是时候实现子元素的拼接了哈

  1. class NativeUnit extends Unit { 
  2.     getMarkUp(reactid) { 
  3.         this._reactid = reactid 
  4.         let { type, props } = this._currentElement; 
  5.         let tagStart = `<${type} data-reactid="${this._reactid}"
  6.         let childString = '' 
  7.         let tagEnd = `</${type}>` 
  8.         for (let propName in props) { 
  9.             if (/^on[A-Z]/.test(propName)) { // 添加绑定事件 
  10.                 ... 
  11.             } else if (propName === 'style') { // 如果是一个样式对象 
  12.                 ... 
  13.             } else if (propName === 'className') { // 如果是一个类名 
  14.                 ... 
  15.             } else if (propName === 'children') { // 如果是子元素 
  16.                 let children = props[propName]; 
  17.                 children.forEach((child, index) => { 
  18.                     let childUnit = createUnit(child); // 可能是字符串 ,也可能是原生标签,也可能是自定义属性 
  19.                     let childMarkUp = childUnit.getMarkUp(`${this._reactid}.${index}`) 
  20.                     childString += childMarkUp; 
  21.                 }) 
  22.             } else { // 其他 自定义的属性 例如 reactid 
  23.                  
  24.             } 
  25.         } 
  26.         return tagStart + '>' + childString + tagEnd 
  27.     } 

发现子元素 ,其实只要进行递归操作,也就是将子元素传进createUnit,把返回的childUnit 通过childMarkUp 方法变成 真实动,再拼接到childString 就好了。其实想想也挺简单,就类似深拷贝的操作。

好了,接下来就是 其他属性了

  1. class NativeUnit extends Unit { 
  2.     getMarkUp(reactid) { 
  3.         this._reactid = reactid 
  4.         let { type, props } = this._currentElement; 
  5.         let tagStart = `<${type} data-reactid="${this._reactid}"
  6.         let childString = '' 
  7.         let tagEnd = `</${type}>` 
  8.         for (let propName in props) { 
  9.             if (/^on[A-Z]/.test(propName)) { // 添加绑定事件 
  10.                ... 
  11.             } else if (propName === 'style') { // 如果是一个样式对象 
  12.                ... 
  13.             } else if (propName === 'className') { // 如果是一个类名 
  14.                ... 
  15.             } else if (propName === 'children') { // 如果是子元素 
  16.                 ... 
  17.             } else { // 其他 自定义的属性 例如 reactid 
  18.                 tagStart += (` ${propName}=${props[propName]} `) 
  19.             } 
  20.         } 
  21.         return tagStart + '>' + childString + tagEnd 
  22.     } 

其他属性直接就拼上去就好了哈哈哈

好了。现在我们已经完成了NativeUini的getMarkUp方法。我们来测试一下是否成功了没有吧!

害,不出所料地成功了。

11. 完成React.Component

接下来我们看看自定义组件是怎么被渲染的,例如下面的Counter组件

  1. // index.js 
  2. class Counter extends React.Component{ 
  3.     constructor(props){ 
  4.         super(props) 
  5.         this.state = {number:0}; 
  6.     } 
  7.     render(){ 
  8.         let p = React.createElement('p',{style:{color:'red'}},this.state.number); 
  9.         let button = React.createElement('button',{},"+"
  10.         return React.createElement('div',{id:'counter'},p,button) 
  11.     } 
  12. let element = React.createElement(Counter,{name:"计时器"}) 
  13. ReactDOM.render(element,document.getElementById('root')) 

我们发现自定义组件好像需要继承React.Component。这是为什么呢?

我之前一直误认为所有的生命周期都是从Component继承过来的,也许有很多小伙伴都和我一样有这样的误解,直到我看了Component源码才恍然大悟,原来我们用的setState和forceUpdate方法是来源于这里

知道这个原因后,我们就可以先简单地实现React.Component了

  1. // component.js 
  2. class Component{ 
  3.     constructor(props){ 
  4.         this.props = props 
  5.     } 
  6.  
  7. export { 
  8.     Component 

然后再引入react中即可

  1. // react.js 
  2. import {createElement} from "./element" 
  3. import {Component} from "./component" 
  4. const React = { 
  5.    createElement, 
  6.    Component 
  7. export default React 

跟 处理NativeUnit一样,先通过createUnit判断element是属于什么类型,如果是自定义组件就 return CompositeUnit

  1. // Unit.js 
  2. import { Element } from "./element" // 新增代码 
  3. import $ from "jquery" 
  4. class Unit { 
  5.     constructor(element) { 
  6.         this._currentElement = element 
  7.     } 
  8.     getMarkUp() { 
  9.         throw Error("此方法应该被重写,不能直接被使用"
  10.     } 
  11. class TextUnit extends Unit { 
  12.      
  13.  
  14. class NativeUnit extends Unit { 
  15.     
  16.  
  17. function createUnit(element) { 
  18.     if (typeof element === 'string' || typeof element === "number") { 
  19.         return new TextUnit(element) 
  20.     } 
  21.     if (element instanceof Element && typeof element.type === "string") { 
  22.         return new NativeUnit(element) 
  23.     } 
  24.     // 新增代码 
  25.     if(element instanceof Element && typeof element.type === 'function'){ 
  26.         return new CompositeUnit(element) 
  27.     } 
  28.  
  29.  
  30.  
  31. export { 
  32.     createUnit 

为什么是用 typeof element.type === 'function'来判断 呢?因为Counter是 一个类,而类在js中的本质就是function

好了,接下来实现一下CompositeUnit类

  1. class CompositeUnit extends Unit{ 
  2.     getMarkUp(reactid){ 
  3.       this._reactid = reactid 
  4.       let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter 
  5.       let componentInstance = new Component(props); 
  6.       let renderElement = componentInstance.render(); 
  7.       let renderUnit = createUnit(renderElement); 
  8.       return renderUnit.getMarkUp(this._reactid) 
  9.     } 

咦,好简短 啊,不过 没那么 简单,但是让 我的三寸不烂之舌来讲解一下,包懂

此时的_currentElement 是:

  1.  type:Counter, 
  2.  props:{} 

let {type:Component,props} = this._currentElement// 实际上,在例子中type就是Counternew Component(props);其实就是new Counter。

也就是我们上面例子中写的

  1. class Counter extends React.Component{ 
  2.     constructor(props){ 
  3.         super(props) 
  4.         this.state = {number:0}; 
  5.     } 
  6.     render(){ 
  7.         let p = React.createElement('p',{style:{color:'red'}},this.state.number); 
  8.         let button = React.createElement('button',{},"+"
  9.         return React.createElement('div',{id:'counter'},p,button) 
  10.     } 
  11. let element = React.createElement(Counter,{name:"计时器"}) 
  12. ReactDOM.render(element,document.getElementById('root')) 

可想而知 ,通过new Counter就获得了Counter的实例

也就是componentInstance ,而每一个Counter的实例都会有render方法,所以执行componentInstance.render()

就获得一个给定类型的ReactElement元素(好熟悉的一句话,对,我们在上面讲到过)。

然后就把这个ReactElement元素对象传给createUnit,获得一个具有getMarkUp的renderUnit 对象, 然后就可以执行renderUnit.getMarkUp(this._reactid)获得真实dom,就可以返回了。

其实,仔细想想,就会发现,在

  1. let renderUnit = createUnit(renderElement); 

之前,我们是在处理自定义组件Counter。

而到了

  1. let renderUnit = createUnit(renderElement); 

这一步,其实就是在处理NativeUnit。(细思极恐。。)

好了,测试一下

发现确实成功了。

12. 实现 componentWillMount

我们在之前的例子上添加个componentWillMount 生命周期函数吧

  1. // index.js 
  2. import React from './react'
  3. import ReactDOM from './react-dom'
  4.  
  5. class Counter extends React.Component{ 
  6.     constructor(props){ 
  7.         super(props) 
  8.         this.state = {number:0}; 
  9.     } 
  10.     componentWillMount(){ 
  11.         console.log("阳光你好,我是componentWillMount"); 
  12.     } 
  13.     render(){ 
  14.         let p = React.createElement('p',{style:{color:'red'}},this.state.number); 
  15.         let button = React.createElement('button',{},"+"
  16.         return React.createElement('div',{id:'counter'},p,button) 
  17.     } 
  18. let element = React.createElement(Counter,{name:"计时器"}) 
  19. ReactDOM.render(element,document.getElementById('root')) 

我们知道componentWillMount 实在组件渲染前执行的,所以我们可以在render之前执行这个生命周期函数

  1. class CompositeUnit extends Unit{ 
  2.     getMarkUp(reactid){ 
  3.       this._reactid = reactid 
  4.       let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter 
  5.       let componentInstance = new Component(props); 
  6.       componentInstance.componentWillMount && componentInstance.componentWillMount() // 添加生命周期函数 
  7.       let renderElement = componentInstance.render(); 
  8.       let renderUnit = createUnit(renderElement); 
  9.       return renderUnit.getMarkUp(this._reactid) 
  10.     } 

可能聪明的小伙伴会问,不是说componentWillMount是在组件重新渲染前执行的吗?那组件没挂到页面上应该都是渲染前,所以componentWillMount也可以在return renderUnit.getMarkUp(this._reactid)前执行啊。

其实要回答这个问题,倒不如回答另一个问题:

父组件的componentWillMount和子组件的componentWillMount哪个先执行。

答案是父组件先执行。

这是因为在父组件中会先执行 父组件的componentWillMount ,然后执行componentInstance.render();的时候,会解析子组件,然后又进入子组件的getMarkUp。又执行子组件的componentWillMount 。

若要回答 为什么componentWillMount 要在 render函数执行前执行,只能说,react就是这么设计的哈哈哈

13. 实现componentDidMount

众所周知,componentDidMount是在组件渲染,也就是挂载到页面后才执行的。

所以,我们可以在返回组件的真实dom之前 就监听 一个mounted事件,这个事件执行componentDidMount方法。

  1. lass CompositeUnit extends Unit{ 
  2.     getMarkUp(reactid){ 
  3.       this._reactid = reactid 
  4.       let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter 
  5.       let componentInstance = new Component(props); 
  6.       componentInstance.componentWillMount && componentInstance.componentWillMount() 
  7.       let renderElement = componentInstance.render(); 
  8.       let renderUnit = createUnit(renderElement); 
  9.       $(document).on("mounted",()=>{ 
  10.           componentInstance.componentDidMount &&  componentInstance.componentDidMount() 
  11.       }) 
  12.       return renderUnit.getMarkUp(this._reactid) 
  13.     } 

然后 再在 把组件的dom挂载到 页面上后再触发这个 mounted事件

  1. // react-dom.js 
  2. import {createUnit} from './unit' 
  3. import $ from "jquery" 
  4. let ReactDOM = { 
  5.     render, 
  6.     rootIndex:0 
  7. function render(element,container){ 
  8.     let unit = createUnit(element) 
  9.     let markUp = unit.getMarkUp(ReactDOM.rootIndex);// 用来返回HTML标记 
  10.     $(container).html(markUp) 
  11.     $(document).trigger("mounted"
  12.  
  13. export default ReactDOM 

由此依赖,就实现了,componentDidMount 生命周期函数,哈哈哈。

测试一下,成功了没有哈

啊,一如既往的成功,可能好奇的你问我为什么每次测试都成功,那是因为,不成功也被我调试到成功了。

为了下面 实现 setState 功能,我们 修改一下 CompositeUnit 的getMarkUp方法。

  1. class CompositeUnit extends Unit{ 
  2.     getMarkUp(reactid){ 
  3.       this._reactid = reactid 
  4.       let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter 
  5.       let componentInstance = this._componentInstance = new Component(props); // 把 实例对象 保存到这个 当前的 unit 
  6.       componentInstance._currentUnit = this // 把 unit 挂到 实例componentInstance  
  7.       componentInstance.componentWillMount && componentInstance.componentWillMount() 
  8.       let renderElement = componentInstance.render(); 
  9.       let renderUnit = this._renderUnit = createUnit(renderElement); // 把渲染内容对象也挂载到当前 unit 
  10.       $(document).on("mounted",()=>{ 
  11.           componentInstance.componentDidMount &&  componentInstance.componentDidMount() 
  12.       }) 
  13.       return renderUnit.getMarkUp(this._reactid) 
  14.     } 

我们为这个 CompositeUnit 的实例添加了

  1. _componentInstance :用了表示 当前组件的实例 (我们所写的Counter组件)
  2. _renderUnit:当前组件的render方法返回的react元素对应的unit._currentElement

另外,我们也通过

  1. componentInstance._currentUnit = this // 把 unit 挂到 实例componentInstance  

把当前 的unit 挂载到了 组件实例componentInstance身上。

可见 组件的实例保存了 当前 unit,当前的unit也保存了组件实例

14. 实现setState

我们看下面的例子,每隔一秒钟就number+1

  1. // index.js 
  2. import React from './react'
  3. import ReactDOM from './react-dom'
  4. import $ from 'jquery' 
  5. class Counter extends React.Component{ 
  6.     constructor(props){ 
  7.         super(props) 
  8.         this.state = {number:0}; 
  9.     } 
  10.     componentWillMount(){ 
  11.         console.log("阳光你好,我是componentWillMount"); 
  12.         $(document).on("mounted",()=>{ 
  13.             console.log(456); 
  14.              
  15.         }) 
  16.     } 
  17.     componentDidMount(){ 
  18.         setInterval(()=>{ 
  19.             this.setState({number:this.state.number+1}) 
  20.         },1000) 
  21.     } 
  22.     render(){ 
  23.          
  24.         return this.state.number 
  25.     } 
  26. let element = React.createElement(Counter,{name:"计时器"}) 
  27. ReactDOM.render(element,document.getElementById('root')) 

前面说到,setState方法是从Component组件继承过来的。所以我们给Component组件添加setState方法

  1. // component.js 
  2. class Component{ 
  3.     constructor(props){ 
  4.         this.props = props 
  5.     } 
  6.     setState(partialState){ 
  7.         // 第一个参数是新的元素,第二个参数是新的状态 
  8.         this._currentUnit.update(null,partialState) 
  9.     } 
  10.  
  11. export { 
  12.     Component 

我们发现原来是在setState方法里调用了当前实例的对应的unit的update方法,它传进去了 部分state的值。

看到这里,我们就知道了,我们需要回到 CompositeUnit类添加一个update方法。

  1. class CompositeUnit extends Unit{ 
  2.     update(nextElement,partialState){ 
  3.         // 有传新元素的话就更新currentElement为新的元素 
  4.         this._currentElement = nextElement || this._currentElement;  
  5.         // 获取新的状态,并且更新组件的state 
  6.         let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState); 
  7.         // 新的属性对象 
  8.         let nextProps = this._currentElement.props 
  9.     } 
  10.     getMarkUp(reactid){ 
  11.      ... 
  12.     } 

我们首先 更换了_currentElement的值,这里为什么会有 有或者没有nextElement的情况呢?

(主要就是因为,如果 _currentElement 是 字符串或者数字的话,那么它就需要 传nextElement 来替换掉旧的 _currentElement 。而如果不是字符串或者数字的话,是不需要传的。而CompositeUnit 必定是组件的,所以不用传nextElement )。

接着,我们 通过下面这句代码获取了最新的state,并且更新了组件的state

  1. let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState); 

获取 最新的 props跟获取state的方式不一样,props是跟_currentElement 绑定在一起的,所以获取最新的props是通过

  1. let nextProps = this._currentElement.props 

接下来,我们要先获取新旧的渲染元素,然后拿来比较,怎么获取呢?

  1. class CompositeUnit extends Unit{ 
  2.     update(nextElement,partialState){ 
  3.         // 有传新元素的话就更新currentElement为新的元素 
  4.         this._currentElement = nextElement || this._currentElement;  
  5.         // 获取新的状态,并且更新组件的state 
  6.         let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState); 
  7.         // 新的属性对象 
  8.         let nextProps = this._currentElement.props 
  9.         // 下面要进行比较更新 
  10.         // 先得到上次渲染的unit 
  11.         let preRenderedUnitInstance = this._renderUnit; 
  12.         // 通过上次渲染的unit得到上次渲染的元素 
  13.         let preRenderElement = preRenderedUnitInstance._currentElement 
  14.         // 得到最新的渲染元素 
  15.         let nextRenderElement = this._componentInstance.render() 
  16.  
  17.     } 
  18.     getMarkUp(reactid){ 
  19.        

我们先得到上次渲染的unit,再通过上次渲染的unit得到上次渲染的元素preRenderElement ,

再通过this._componentInstance.render()得到下次渲染的元素nextRenderElement 。

接下来就可以进行比较这两个元素了

我们首先会判断要不要进行深度比较。

如果不是进行深度比较就非常简单

直接获取新的渲染unit,然后通过getMarkUp获得要渲染的dom,接着就把当前的组件里的dom元素替换掉

  1. class CompositeUnit extends Unit{ 
  2.     update(nextElement,partialState){ 
  3.         // 有传新元素的话就更新currentElement为新的元素 
  4.         this._currentElement = nextElement || this._currentElement;  
  5.         // 获取新的状态,并且更新组件的state 
  6.         let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState); 
  7.         // 新的属性对象 
  8.         let nextProps = this._currentElement.props 
  9.         // 下面要进行比较更新 
  10.         // 先得到上次渲染的unit 
  11.         let preRenderedUnitInstance = this._renderUnit; 
  12.         // 通过上次渲染的unit得到上次渲染的元素 
  13.         let preRenderElement = preRenderedUnitInstance._currentElement 
  14.         // 得到最新的渲染元素 
  15.         let nextRenderElement = this._componentInstance.render() 
  16.         // 如果新旧两个元素类型一样,则可以进行深度比较,如果不一样,直接干掉老的元素,新建新的 
  17.         if(shouldDeepCompare(preRenderElement,nextRenderElement)){ 
  18.  
  19.         }else
  20.             this._renderUnit = createUnit(nextRenderElement) 
  21.             let nextMarkUp = this._renderUnit.getMarkUp(this._reactid) 
  22.             $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp) 
  23.         } 
  24.  
  25.     } 
  26.     getMarkUp(reactid){ 
  27.       
  28.     } 

我们先简单地写一下shouldDeepCompare方法,直接return false,来测试一下 非深度比较,是否能够正确执行

  1. function shouldDeepCompare(){ 
  2.     return false 
  3. class CompositeUnit extends Unit{ 
  4.     update(nextElement,partialState){ 
  5.         // 有传新元素的话就更新currentElement为新的元素 
  6.         this._currentElement = nextElement || this._currentElement;  
  7.         // 获取新的状态,并且更新组件的state 
  8.         let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState); 
  9.         // 新的属性对象 
  10.         let nextProps = this._currentElement.props 
  11.         // 下面要进行比较更新 
  12.         // 先得到上次渲染的unit 
  13.         let preRenderedUnitInstance = this._renderUnit; 
  14.         // 通过上次渲染的unit得到上次渲染的元素 
  15.         let preRenderElement = preRenderedUnitInstance._currentElement 
  16.         // 得到最新的渲染元素 
  17.         let nextRenderElement = this._componentInstance.render() 
  18.         // 如果新旧两个元素类型一样,则可以进行深度比较,如果不一样,直接干掉老的元素,新建新的 
  19.         if(shouldDeepCompare(preRenderElement,nextRenderElement)){ 
  20.  
  21.         }else
  22.             this._renderUnit = createUnit(nextRenderElement) 
  23.             let nextMarkUp = this._renderUnit.getMarkUp(this._reactid) 
  24.             $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp) 
  25.         } 
  26.  
  27.     } 
  28.     getMarkUp(reactid){ 
  29.       
  30.     } 

 

在这里插入图片描述

发现确实成功了。

如果可以进行深度比较呢?

  1. class CompositeUnit extends Unit{ 
  2.     update(nextElement,partialState){ 
  3.         // 有传新元素的话就更新currentElement为新的元素 
  4.         this._currentElement = nextElement || this._currentElement;  
  5.         // 获取新的状态,并且更新组件的state 
  6.         let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState); 
  7.         // 新的属性对象 
  8.         let nextProps = this._currentElement.props 
  9.         // 下面要进行比较更新 
  10.         // 先得到上次渲染的unit 
  11.         let preRenderedUnitInstance = this._renderUnit; 
  12.         // 通过上次渲染的unit得到上次渲染的元素 
  13.         let preRenderElement = preRenderedUnitInstance._currentElement 
  14.         // 得到最新的渲染元素 
  15.         let nextRenderElement = this._componentInstance.render() 
  16.         // 如果新旧两个元素类型一样,则可以进行深度比较,如果不一样,直接干掉老的元素,新建新的 
  17.         if(shouldDeepCompare(preRenderElement,nextRenderElement)){ 
  18.             // 如果可以进行深度比较,则把更新的nextRenderElement传进去 
  19.             preRenderedUnitInstance.update(nextRenderElement) 
  20.              
  21.         }else
  22.             this._renderUnit = createUnit(nextRenderElement) 
  23.             let nextMarkUp = this._renderUnit.getMarkUp(this._reactid) 
  24.             $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp) 
  25.         } 
  26.  
  27.     } 
  28.     getMarkUp(reactid){ 
  29.        
  30.     } 

如果可以深度,就执行

  1. preRenderedUnitInstance.update(nextRenderElement) 

这是什么意思?

我们当前是在执行渲染Counter的话,那preRenderedUnitInstance 是什么呢?

没错!它是Counter组件 执行render方法 ,再执行createUnit获得的

在这里插入图片描述

这个字符串的 unit

然后调用了这个 unit的 update方法

注意,这里 的unit是字符串的 unit,也就是说是 TextUnit

所以我们需要实现 TextUnit 的update 方法

  1. class TextUnit extends Unit { 
  2.     getMarkUp(reactid) { 
  3.         this._reactid = reactid 
  4.         return `<span data-reactid=${reactid}>${this._currentElement}</span>` 
  5.     } 
  6.     update(nextElement){ 
  7.         debugger 
  8.         if(this._currentElement !== nextElement){ 
  9.             this._currentElement = nextElement 
  10.              $(`[data-reactid="${this._reactid}"]`).html(nextElement) 
  11.         } 
  12.     } 

TextUnit 的update方法非常简单,先判断 渲染内容有没有变化,有的话就 替换点字符串的内容

并把当前unit 的_currentElement 替换成最新的nextElement

我们简单的把shouldDeepCompare 改成 return true,测试一下深度比较

  1. function shouldDeepCompare(){ 
  2.     return true 

 

一如既往成功

15. 实现shouldComponentUpdate方法

我们知道有个shouldComponentUpdate,用来决定要不要 重渲染 该组件的

  1. shouldComponentUpdate(nextProps, nextState) { 
  2.   return nextState.someData !== this.state.someData 

显然,它要我们传入 两个参数,分别是 组件更新后的nextProps和nextState

而在 还是上面,实现 update的过程中,我们已经得到了nextState 和nextProps

  1. class CompositeUnit extends Unit{ 
  2.     update(nextElement,partialState){ 
  3.         。。。 
  4.         // 获取新的状态,并且更新组件的state 
  5.         let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState); 
  6.         // 新的属性对象 
  7.         let nextProps = this._currentElement.props 
  8.         // 下面要进行比较更新 
  9.         。。。 
  10.  
  11.     } 
  12.     getMarkUp(reactid){ 
  13.       

所以,我们可以在update里执行shouldComponentUpdate方法,来确定要不要重新渲染组件

  1. class CompositeUnit extends Unit{ 
  2.     update(nextElement,partialState){ 
  3.         // 有传新元素的话就更新currentElement为新的元素 
  4.         this._currentElement = nextElement || this._currentElement;  
  5.         // 获取新的状态,并且更新组件的state 
  6.         let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState); 
  7.         // 新的属性对象 
  8.         let nextProps = this._currentElement.props 
  9.         if(this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps,nextState)){ 
  10.             return
  11.         } 
  12.         // 下面要进行比较更新 
  13.         // 先得到上次渲染的unit 
  14.         let preRenderedUnitInstance = this._renderUnit; 
  15.         // 通过上次渲染的unit得到上次渲染的元素 
  16.         let preRenderElement = preRenderedUnitInstance._currentElement 
  17.         // 得到最新的渲染元素 
  18.         let nextRenderElement = this._componentInstance.render() 
  19.         // 如果新旧两个元素类型一样,则可以进行深度比较,如果不一样,直接干掉老的元素,新建新的 
  20.         if(shouldDeepCompare(preRenderElement,nextRenderElement)){ 
  21.             // 如果可以进行深度比较,则把更新的工作交给上次渲染出来的那个Element元素对应的unit来处理 
  22.             preRenderedUnitInstance.update(nextRenderElement) 
  23.  
  24.         }else
  25.             this._renderUnit = createUnit(nextRenderElement) 
  26.             let nextMarkUp = this._renderUnit.getMarkUp(this._reactid) 
  27.             $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp) 
  28.         } 
  29.  
  30.     } 
  31.     getMarkUp(reactid){ 
  32.       
  33.     } 

16. 实现componentDidUpdate生命周期函数

so Easy。

只要在更新后触发这个事件就好了

  1. class CompositeUnit extends Unit{ 
  2.     update(nextElement,partialState){ 
  3.          
  4.         if(this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps,nextState)){ 
  5.             return
  6.         } 
  7.     
  8.         if(shouldDeepCompare(preRenderElement,nextRenderElement)){ 
  9.             // 如果可以进行深度比较,则把更新的工作交给上次渲染出来的那个Element元素对应的unit来处理 
  10.             preRenderedUnitInstance.update(nextRenderElement) 
  11.             this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate() 
  12.         }else
  13.             this._renderUnit = createUnit(nextRenderElement) 
  14.             let nextMarkUp = this._renderUnit.getMarkUp(this._reactid) 
  15.             $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp) 
  16.         } 
  17.  
  18.     } 
  19.     getMarkUp(reactid){ 
  20.       
  21.     } 

17. 实现shouDeepCompare

判断是否需要深比较极其简单,只需要判断 oldElement 和newElement 是否 都是字符串或者数字,这种类型的就走深比较

接着判断 oldElement 和newElement 是否 都是 Element类型,不是的话就return false,是的 再判断 type是否相同(即判断是否是同个组件,是的话 return true)

其他情况都return false

  1. function shouldDeepCompare(oldElement,newElement){ 
  2.     if(oldElement != null && newElement != null){ 
  3.         let oldType = typeof oldElement 
  4.         let newType = typeof newElement 
  5.         if((oldType === 'string' || oldType === "number")&&(newType === "string" || newType === "number")){ 
  6.             return true 
  7.         } 
  8.         if(oldElement instanceof Element && newElement instanceof Element){ 
  9.             return oldElement.type === newElement.type 
  10.         } 
  11.     } 
  12.     return false 

 【编辑推荐】

 

责任编辑:姜华 来源: 前端阳光
相关推荐

2020-10-23 09:26:57

React-Redux

2021-08-10 18:36:02

Express原理面试

2020-10-20 09:12:57

axios核心原理

2022-08-27 13:49:36

ES7promiseresolve

2021-05-08 07:53:33

面试线程池系统

2022-04-01 07:52:42

JavaScript防抖节流

2022-10-31 11:10:49

Javavolatile变量

2023-11-28 17:49:51

watch​computed​性能

2020-10-15 12:52:46

SpringbootJava编程语言

2021-04-22 07:49:51

Vue3Vue2.xVue3.x

2021-08-04 08:33:25

React服务端渲染

2020-12-09 10:29:53

SSH加密数据安全

2024-03-05 10:33:39

AOPSpring编程

2020-12-03 08:14:45

Axios核心Promise

2020-11-02 09:35:04

ReactHook

2021-06-30 07:19:36

React事件机制

2021-12-02 08:19:06

MVCC面试数据库

2021-06-29 09:47:34

ReactSetState机制

2021-07-06 07:27:45

React元素属性

2023-12-27 18:16:39

MVCC隔离级别幻读
点赞
收藏

51CTO技术栈公众号