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

1. 项目基本准备工做

1.1 建立项目

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

文章首发于公众号《前端阳光》,项目已经放到github:github.com/Sunny-lucki… 以为能够的话,给个star鼓励下哈啊哈 有什么不对的或者建议或者疑惑,欢迎指出啊!立志写得通俗易懂html

1.2 项目结构

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

此时的index.jsjava

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  "sunny",
  document.getElementById('root')
);

复制代码

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

咱们就能够把须要引入react和react-dom的改为本身建立的文件啦react

import React from './react';
import ReactDOM from './react-dom';

ReactDOM.render(
  "sunny",
  document.getElementById('root')
);

复制代码

3.完成react-dom

咱们在index.js文件中jquery

ReactDOM.render(
  "sunny",
  document.getElementById('root')
);
复制代码

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

因此咱们能够这样实现react-domgithub

// react-dom.js
let ReactDOM = {
    render
}
function render(element,container){
    container.innerHTML = `<span>${element}</span>`
    
}

export default ReactDOM
复制代码

咱们看下运行结果babel

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

好了,如今咱们给每个 元素打上 一个标记 ,这样的话 就能够经过这个标记 辨别出与其余 元素的关系,也能够直接经过这标记找到该元素了。

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

// react-dom.js
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
    container.innerHTML = `<span data-reactid=${ReactDOM.rootIndex}>${element}</span>`
}

export default ReactDOM
复制代码

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

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

4. 重构render方法

咱们前面的render方法 ​

function render(element,container){
    container.innerHTML = `<span data-reactid=${ReactDOM.rootIndex}>${element}</span>`
}
复制代码

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

// react-dom.js
import $ from "jquery"
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
    let unit = createUnit(element)
    let markUp = unit.getMarkUp();// 用来返回HTML标记
    $(container).html(markUp)
}
​
export default ReactDOM
复制代码

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

{
  _currentElement:element,
  getMarkUp(){
    ...
  }
}
复制代码

​ 再执行 unit的getMarkUp方法,得到到 真实的dom,而后就能够挂载到container上去啦!

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

import React from './react';
import ReactDOM from './react-dom';

ReactDOM.render(
  "sunny",
  document.getElementById('root')
);

复制代码

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

{
	_currentElement:"sunny",
	getMarkUp(){
		
	}
}
复制代码

​ 那怎么写这个createUnit呢? ​

5. 实现createUnit方法

咱们建立一个新的文件叫作unit.js

在这里插入图片描述

// Unit.js
class Unit{
   
}
class TextUnit extends Unit{
    
}

function createUnit(element){
    if(typeof element === 'string' || typeof element === "number"){
        return new TextUnit(element)
    }
}

export {
    createUnit
}
复制代码

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

为何要 TextUnit 继承 于 Unit呢?

这是由于 element除了字符串 ,也有多是 原生的标签,列如div,span等,也有多是咱们自定义的组件,因此咱们先写 了一个 unit类,这个类实现 这几种element 所共有的属性。 而后 具体的 类 ,例如 TextUnit 直接继承 Unit ,再实现自有的 属性就行了。

6. 实现Unit

new Unit 获得的对象应当是这样的

{
  _currentElement:element,
  getMarkUp(){
    ...
  }
}
复制代码

也就是说,这是全部的 种类都有的属性,因此咱们能够这样实现 Unit

class Unit{
    constructor(element){
        this._currentElement = element
    }
    getMarkUp(){
        throw Error("此方法应该被重写,不能直接被使用")
    }
}
复制代码

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

学过 java或其余语言的同窗应该秒懂,这是由于getMarkUp但愿是被子类重写的方法,由于每一个子类执行这个方法返回的结果是不同的。

7. 实现TextUnit

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

class TextUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid
        return `<span data-reactid=${reactid}>${this._currentElement}</span>`
    }
}
复制代码

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

// Unit.js
class Unit{
    constructor(element){
        this._currentElement = element
    }
    getMarkUp(){
        throw Error("此方法应该被重写,不能直接被使用")
    }
}
class TextUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid
        return `<span data-reactid=${reactid}>${this._currentElement}</span>`
    }
}

function createUnit(element){
    if(typeof element === 'string' || typeof element === "number"){
        return new TextUnit(element)
    }
}

export {
    createUnit
}
复制代码

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

// index.js
import React from './react';
import ReactDOM from './react-dom';

ReactDOM.render(
  "sunny",
  document.getElementById('root')
);

复制代码
// react-dom.js
import {createUnit} from './unit'
import $ from "jquery"
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
    let unit = createUnit(element)
    let markUp = unit.getMarkUp(ReactDOM.rootIndex);// 用来返回HTML标记
    $(container).html(markUp)
}

export default ReactDOM
复制代码

在这里插入图片描述

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

8. 理解React.creacteElement方法

在第一次学习react的时候,我总会带着许多疑问。好比看到下面的代码就会想:为何咱们只是引入了React,可是并无明显的看到咱们在其余地方用,这时我就会想着既然没有用到,那若是删除以后会不会受到影响呢?答案固然是不行的。

import React from 'react';
import ReactDOM from 'react-dom';

let element = (
    <h1 id="title" className="bg" style={{color: 'red'}}> hello <span>world</span> </h1>
)

console.log({type: element.type, props:element.props})

ReactDOM.render(element,document.getElementById('root'));
复制代码

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

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

<h1 id="title" className="bg" style={{color: 'red'}}>
        hello
        <span>world</span>
</h1>

//上面的这段代码很简单,可是咱们都知道react是所谓的虚拟dom,固然不可能就是咱们看到的这样。当咱们将上面的代码通过babel转译后,咱们再看看

React.createElement("h1", {
  id: "title",
  className: "bg",
  style: {
    color: 'red'
  }
}, "hello", React.createElement("span", null, "world"));
复制代码

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

var reactElement = ReactElement.createElement(
  	... // 标签名称字符串/ReactClass,
  	... // [元素的属性值对对象],
  	... // [元素的子节点]
)
复制代码

一、参数:

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

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

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

二、返回值:

一个给定类型的ReactElement元素

咱们能够改下咱们的index.js

// index.js
import React from './react';
import ReactDOM from './react-dom';

var li1 = React.createElement('li', {onClick:()=>{alert("click")}}, 'First');
var li2 = React.createElement('li', {}, 'Second');
var li3 = React.createElement('li', {}, 'Third');
var ul = React.createElement('ul', {className: 'list'}, li1, li2, li3);
console.log(ul);
ReactDOM.render(ul,document.getElementById('root'))
复制代码

能够就看下 ul 最终的打印 期待结果 在这里插入图片描述

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

9. 实现React.createElement方法

通过上面的讲解,咱们大概已经知道React.createElement方法的做用了,如今就来看看是怎么实现的

在这里插入图片描述 咱们建立了一个新的文件element.js

// element.js
class Element {
    constructor(type,props){
        this.type = type
        this.props = props
    }

}
function createElement(type,props={},...children){
    props.children = children || [];
    return new Element(type,props)
}

export {
    Element,
    createElement
}
复制代码

咱们 定义了一个 Element 类 ,而后在createElement方法里建立了这个类的对象, 而且return出去了

没错,这个对象就是上面所说的给定类型的ReactElement元素,也就是下面这张图所显示的 在这里插入图片描述

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

咱们前面尚未实现react.js

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

// react.js
 import {createElement} from "./element"
 const React = {
    createElement
 }
 export default React
复制代码

10. 实现NativeUnit

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

// Unit.js
import {Element} from "./element" // 新增代码
class Unit{
    constructor(element){
        this._currentElement = element
    }
    getMarkUp(){
        throw Error("此方法应该被重写,不能直接被使用")
    }
}
class TextUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid
        return `<span data-reactid=${reactid}>${this._currentElement}</span>`
    }
}

function createUnit(element){
    if(typeof element === 'string' || typeof element === "number"){
        return new TextUnit(element)
    }
    // 新增代码
    if(element instanceof Element && typeof element.type === "string"){
        return new NativeUnit(element)
    }
}

export {
    createUnit
}
复制代码

好了,如今咱们来实现NativeUnit类,其实主要就是实现NativeUnit的getMarkUp方法

class NativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
    }
}
复制代码

要明确的一点是,NativeUnit 的getMarkUp方法,是要把 在这里插入图片描述 这样一个element 对象转化为 真实的dom的

所以,咱们能够这样完善getMarkUp方法

class NativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
        let tagStart = `<${type} `
        let childString = ''
        let tagEnd = `</${type}>`
        for(let propName in props){
            if(/^on[A-Z]/.test(propName)){ // 添加绑定事件
                
            }else if(propName === 'style'){ // 若是是一个样式对象

            }else if(propName === 'className'){ // 若是是一个类名

            }else if(propName === 'children'){ // 若是是子元素

            }else { // 其余 自定义的属性 例如 reactid
                tagStart += (` ${propName}=${props[propName]} `)
            }
        }
        return tagStart+'>' + childString +tagEnd
    }
}
复制代码

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

咱们测试下,如今有没有 把ul 渲染出来

// index.js
import React from './react';
import ReactDOM from './react-dom';

var li1 = React.createElement('li', {}, 'First');
var li2 = React.createElement('li', {}, 'Second');
var li3 = React.createElement('li', {}, 'Third');
var ul = React.createElement('ul', {className: 'list'}, li1, li2, li3);
console.log(ul);
ReactDOM.render(ul,document.getElementById('root'))
复制代码

在这里插入图片描述 发现确实成功渲染出来了,可是 属性和 子元素尚未,这是由于咱们 还没实现 具体 的功能。

如今咱们来实现事件绑定 功能

class NativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
        let tagStart = `<${type} data-reactid="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for(let propName in props){
        	// 新增代码
            if(/^on[A-Z]/.test(propName)){ // 添加绑定事件
                let eventName = propName.slice(2).toLowerCase(); // 获取click
                $(document).delegate(`[data-reactid="${this._reactid}"]`,`${eventName}.${this._reactid}`,props[propName])
            }else if(propName === 'style'){ // 若是是一个样式对象
               
            }else if(propName === 'className'){ // 若是是一个类名
                
            }else if(propName === 'children'){ // 若是是子元素
               
            }else { // 其余 自定义的属性 例如 reactid
                
            }
        }
        return tagStart+'>' + childString +tagEnd
    }
}
复制代码

在这里,咱们是用了事件代理的模式,之因此用事件代理,是由于这些标签元素还没被渲染到页面上,但咱们又必须提早绑定事件,因此须要用到事件代理

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

class NativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
        let tagStart = `<${type} data-reactid="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for(let propName in props){
            if(/^on[A-Z]/.test(propName)){ // 添加绑定事件
                ...
            }else if(propName === 'style'){ // 若是是一个样式对象
                let styleObj = props[propName]
                let styles = Object.entries(styleObj).map(([attr, value]) => {
                    return `${attr.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)}:${value}`;
                }).join(';')
                tagStart += (` style="${styles}" `)
            }else if(propName === 'className'){ // 若是是一个类名
                
            }else if(propName === 'children'){ // 若是是子元素
               
            }else { // 其余 自定义的属性 例如 reactid
              
            }
        }
        return tagStart+'>' + childString +tagEnd
    }
}
复制代码

这里 其实就是把

{style:{backgroundColor:"red"}}
复制代码

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

而后把backgroundColor 经过正则 变化成background-color

而后再拼接到tagStart中。

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

class NativeUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        let { type, props } = this._currentElement;
        let tagStart = `<${type} data-reactid="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for (let propName in props) {
            if (/^on[A-Z]/.test(propName)) { // 添加绑定事件
              	...
            } else if (propName === 'style') { // 若是是一个样式对象
                ...
            } else if (propName === 'className') { // 若是是一个类名
                tagStart += (` class="${props[propName]}"`)
            } else if (propName === 'children') { // 若是是子元素
               ...
            } else { // 其余 自定义的属性 例如 reactid
                ...
            }
        }
        return tagStart + '>' + childString + tagEnd
    }
}
复制代码

为何这么简单呢? 由于只须要把

className: 'list'
复制代码

中的className变化成 class就能够了。OMG!!

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

class NativeUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        let { type, props } = this._currentElement;
        let tagStart = `<${type} data-reactid="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for (let propName in props) {
            if (/^on[A-Z]/.test(propName)) { // 添加绑定事件
                ...
            } else if (propName === 'style') { // 若是是一个样式对象
                ...
            } else if (propName === 'className') { // 若是是一个类名
                ...
            } else if (propName === 'children') { // 若是是子元素
                let children = props[propName];
                children.forEach((child, index) => {
                    let childUnit = createUnit(child); // 多是字符串 ,也多是原生标签,也多是自定义属性
                    let childMarkUp = childUnit.getMarkUp(`${this._reactid}.${index}`)
                    childString += childMarkUp;
                })
            } else { // 其余 自定义的属性 例如 reactid
                
            }
        }
        return tagStart + '>' + childString + tagEnd
    }
}
复制代码

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

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

class NativeUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        let { type, props } = this._currentElement;
        let tagStart = `<${type} data-reactid="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for (let propName in props) {
            if (/^on[A-Z]/.test(propName)) { // 添加绑定事件
               ...
            } else if (propName === 'style') { // 若是是一个样式对象
               ...
            } else if (propName === 'className') { // 若是是一个类名
               ...
            } else if (propName === 'children') { // 若是是子元素
                ...
            } else { // 其余 自定义的属性 例如 reactid
                tagStart += (` ${propName}=${props[propName]} `)
            }
        }
        return tagStart + '>' + childString + tagEnd
    }
}
复制代码

其余属性直接就拼上去就行了哈哈哈

好了。如今咱们已经完成了NativeUini的getMarkUp方法。咱们来测试一下是否成功了没有吧! 在这里插入图片描述 害,不出所料地成功了。

11. 完成React.Component

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

// index.js
class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    render(){
        let p = React.createElement('p',{style:{color:'red'}},this.state.number);
        let button = React.createElement('button',{},"+")
        return React.createElement('div',{id:'counter'},p,button)
    }
}
let element = React.createElement(Counter,{name:"计时器"})
ReactDOM.render(element,document.getElementById('root'))
复制代码

咱们发现自定义组件好像须要继承React.Component。这是为何呢?

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

知道这个缘由后,咱们就能够先简单地实现React.Component了

// component.js
class Component{
    constructor(props){
        this.props = props
    }
}

export {
    Component
}
复制代码

而后再引入react中便可

// react.js
 import {createElement} from "./element"
 import {Component} from "./component"
 const React = {
    createElement,
    Component
 }
 export default React
复制代码

跟 处理NativeUnit同样,先经过createUnit判断element是属于什么类型,若是是自定义组件就 return CompositeUnit

// Unit.js
import { Element } from "./element" // 新增代码
import $ from "jquery"
class Unit {
    constructor(element) {
        this._currentElement = element
    }
    getMarkUp() {
        throw Error("此方法应该被重写,不能直接被使用")
    }
}
class TextUnit extends Unit {
    
}

class NativeUnit extends Unit {
   
}

function createUnit(element) {
    if (typeof element === 'string' || typeof element === "number") {
        return new TextUnit(element)
    }
    if (element instanceof Element && typeof element.type === "string") {
        return new NativeUnit(element)
    }
    // 新增代码
    if(element instanceof Element && typeof element.type === 'function'){
        return new CompositeUnit(element)
    }

}


export {
    createUnit
}
复制代码

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

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

class CompositeUnit extends Unit{
    getMarkUp(reactid){
      this._reactid = reactid
      let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter
      let componentInstance = new Component(props);
      let renderElement = componentInstance.render();
      let renderUnit = createUnit(renderElement);
      return renderUnit.getMarkUp(this._reactid)
    }
}
复制代码

咦,好简短 啊,不过 没那么 简单,可是让 个人三寸不烂之舌来说解一下,包懂

此时的_currentElement 是:

{
	type:Counter,
	props:{}
}
复制代码

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

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

class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    render(){
        let p = React.createElement('p',{style:{color:'red'}},this.state.number);
        let button = React.createElement('button',{},"+")
        return React.createElement('div',{id:'counter'},p,button)
    }
}
let element = React.createElement(Counter,{name:"计时器"})
ReactDOM.render(element,document.getElementById('root'))
复制代码

可想而知 ,经过new Counter就得到了Counter的实例

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

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

而后就把这个ReactElement元素对象传给createUnit,得到一个具备getMarkUp的renderUnit 对象, 而后就能够执行renderUnit.getMarkUp(this._reactid)得到真实dom,就能够返回了。

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

let renderUnit = createUnit(renderElement);
复制代码

以前,咱们是在处理自定义组件Counter。

而到了

let renderUnit = createUnit(renderElement);
复制代码

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

好了,测试一下在这里插入图片描述 发现确实成功了。

12. 实现 componentWillMount

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

// index.js
import React from './react';
import ReactDOM from './react-dom';

class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    componentWillMount(){
        console.log("阳光你好,我是componentWillMount");
    }
    render(){
        let p = React.createElement('p',{style:{color:'red'}},this.state.number);
        let button = React.createElement('button',{},"+")
        return React.createElement('div',{id:'counter'},p,button)
    }
}
let element = React.createElement(Counter,{name:"计时器"})
ReactDOM.render(element,document.getElementById('root'))
复制代码

咱们知道componentWillMount 实在组件渲染前执行的,因此咱们能够在render以前执行这个生命周期函数

class CompositeUnit extends Unit{
    getMarkUp(reactid){
      this._reactid = reactid
      let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter
      let componentInstance = new Component(props);
      componentInstance.componentWillMount && componentInstance.componentWillMount() // 添加生命周期函数
      let renderElement = componentInstance.render();
      let renderUnit = createUnit(renderElement);
      return renderUnit.getMarkUp(this._reactid)
    }
}
复制代码

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

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

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

答案是父组件先执行。

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

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

13. 实现componentDidMount

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

因此,咱们能够在返回组件的真实dom以前 就监听 一个mounted事件,这个事件执行componentDidMount方法。

class CompositeUnit extends Unit{
    getMarkUp(reactid){
      this._reactid = reactid
      let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter
      let componentInstance = new Component(props);
      componentInstance.componentWillMount && componentInstance.componentWillMount()
      let renderElement = componentInstance.render();
      let renderUnit = createUnit(renderElement);
      $(document).on("mounted",()=>{
          componentInstance.componentDidMount &&  componentInstance.componentDidMount()
      })
      return renderUnit.getMarkUp(this._reactid)
    }
}
复制代码

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

// react-dom.js
import {createUnit} from './unit'
import $ from "jquery"
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
    let unit = createUnit(element)
    let markUp = unit.getMarkUp(ReactDOM.rootIndex);// 用来返回HTML标记
    $(container).html(markUp)
    $(document).trigger("mounted")
}

export default ReactDOM
复制代码

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

测试一下,成功了没有哈 在这里插入图片描述 啊,一如既往的成功,可能好奇的你问我为何每次测试都成功,那是由于,不成功也被我调试到成功了。

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

class CompositeUnit extends Unit{
    getMarkUp(reactid){
      this._reactid = reactid
      let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter
      let componentInstance = this._componentInstance = new Component(props); // 把 实例对象 保存到这个 当前的 unit
      componentInstance._currentUnit = this // 把 unit 挂到 实例componentInstance 
      componentInstance.componentWillMount && componentInstance.componentWillMount()
      let renderElement = componentInstance.render();
      let renderUnit = this._renderUnit = createUnit(renderElement); // 把渲染内容对象也挂载到当前 unit
      $(document).on("mounted",()=>{
          componentInstance.componentDidMount &&  componentInstance.componentDidMount()
      })
      return renderUnit.getMarkUp(this._reactid)
    }
}
复制代码

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

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

另外,咱们也经过

componentInstance._currentUnit = this // 把 unit 挂到 实例componentInstance 
复制代码

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

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

14. 实现setState

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

// index.js
import React from './react';
import ReactDOM from './react-dom';
import $ from 'jquery'
class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    componentWillMount(){
        console.log("阳光你好,我是componentWillMount");
        $(document).on("mounted",()=>{
            console.log(456);
            
        })
    }
    componentDidMount(){
        setInterval(()=>{
            this.setState({number:this.state.number+1})
        },1000)
    }
    render(){
        
        return this.state.number
    }
}
let element = React.createElement(Counter,{name:"计时器"})
ReactDOM.render(element,document.getElementById('root'))
复制代码

前面说到,setState方法是从Component组件继承过来的。因此咱们给Component组件添加setState方法

// component.js
class Component{
    constructor(props){
        this.props = props
    }
    setState(partialState){
        // 第一个参数是新的元素,第二个参数是新的状态
        this._currentUnit.update(null,partialState)
    }
}

export {
    Component
}
复制代码

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

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

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,而且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
    }
    getMarkUp(reactid){
     ...
    }
}
复制代码

咱们首先 更换了_currentElement的值,这里为何会有 有或者没有nextElement的状况呢?

(主要就是由于,若是 _currentElement 是 字符串或者数字的话,那么它就须要 传nextElement 来替换掉旧的 _currentElement 。而若是不是字符串或者数字的话,是不须要传的。而CompositeUnit 一定是组件的,因此不用传nextElement )。

接着,咱们 经过下面这句代码获取了最新的state,而且更新了组件的state

let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
复制代码

获取 最新的 props跟获取state的方式不同,props是跟_currentElement 绑定在一块儿的,因此获取最新的props是经过

let nextProps = this._currentElement.props
复制代码

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

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,而且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        // 下面要进行比较更新
        // 先获得上次渲染的unit
        let preRenderedUnitInstance = this._renderUnit;
        // 经过上次渲染的unit获得上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 获得最新的渲染元素
        let nextRenderElement = this._componentInstance.render()

    }
    getMarkUp(reactid){
     	
    }
}
复制代码

咱们先获得上次渲染的unit,再经过上次渲染的unit获得上次渲染的元素preRenderElement ,

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

接下来就能够进行比较这两个元素了

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

若是不是进行深度比较就很是简单

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

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,而且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        // 下面要进行比较更新
        // 先获得上次渲染的unit
        let preRenderedUnitInstance = this._renderUnit;
        // 经过上次渲染的unit获得上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 获得最新的渲染元素
        let nextRenderElement = this._componentInstance.render()
        // 若是新旧两个元素类型同样,则能够进行深度比较,若是不同,直接干掉老的元素,新建新的
        if(shouldDeepCompare(preRenderElement,nextRenderElement)){

        }else{
            this._renderUnit = createUnit(nextRenderElement)
            let nextMarkUp = this._renderUnit.getMarkUp(this._reactid)
            $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp)
        }

    }
    getMarkUp(reactid){
     
    }
}
复制代码

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

function shouldDeepCompare(){
    return false
}
class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,而且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        // 下面要进行比较更新
        // 先获得上次渲染的unit
        let preRenderedUnitInstance = this._renderUnit;
        // 经过上次渲染的unit获得上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 获得最新的渲染元素
        let nextRenderElement = this._componentInstance.render()
        // 若是新旧两个元素类型同样,则能够进行深度比较,若是不同,直接干掉老的元素,新建新的
        if(shouldDeepCompare(preRenderElement,nextRenderElement)){

        }else{
            this._renderUnit = createUnit(nextRenderElement)
            let nextMarkUp = this._renderUnit.getMarkUp(this._reactid)
            $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp)
        }

    }
    getMarkUp(reactid){
     
    }
}
复制代码

在这里插入图片描述

发现确实成功了。

若是能够进行深度比较呢?

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,而且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        // 下面要进行比较更新
        // 先获得上次渲染的unit
        let preRenderedUnitInstance = this._renderUnit;
        // 经过上次渲染的unit获得上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 获得最新的渲染元素
        let nextRenderElement = this._componentInstance.render()
        // 若是新旧两个元素类型同样,则能够进行深度比较,若是不同,直接干掉老的元素,新建新的
        if(shouldDeepCompare(preRenderElement,nextRenderElement)){
            // 若是能够进行深度比较,则把更新的nextRenderElement传进去
            preRenderedUnitInstance.update(nextRenderElement)
            
        }else{
            this._renderUnit = createUnit(nextRenderElement)
            let nextMarkUp = this._renderUnit.getMarkUp(this._reactid)
            $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp)
        }

    }
    getMarkUp(reactid){
      
    }
}
复制代码

若是能够深度,就执行

preRenderedUnitInstance.update(nextRenderElement)
复制代码

这是什么意思?

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

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

在这里插入图片描述

这个字符串的 unit

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

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

因此咱们须要实现 TextUnit 的update 方法

class TextUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        return `<span data-reactid=${reactid}>${this._currentElement}</span>`
    }
    update(nextElement){
        debugger
        if(this._currentElement !== nextElement){
            this._currentElement = nextElement
             $(`[data-reactid="${this._reactid}"]`).html(nextElement)
        }
    }
}
复制代码

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

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

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

function shouldDeepCompare(){
    return true
}
复制代码

在这里插入图片描述 一如既往成功

15. 实现shouldComponentUpdate方法

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

shouldComponentUpdate(nextProps, nextState) {
  return nextState.someData !== this.state.someData
}
复制代码

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

而在 仍是上面,实现 update的过程当中,咱们已经获得了nextState 和nextProps

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        。。。
        // 获取新的状态,而且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        // 下面要进行比较更新
        。。。

    }
    getMarkUp(reactid){
     
    }
}
复制代码

因此,咱们能够在update里执行shouldComponentUpdate方法,来肯定要不要从新渲染组件

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,而且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        if(this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps,nextState)){
            return;
        }
        // 下面要进行比较更新
        // 先获得上次渲染的unit
        let preRenderedUnitInstance = this._renderUnit;
        // 经过上次渲染的unit获得上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 获得最新的渲染元素
        let nextRenderElement = this._componentInstance.render()
        // 若是新旧两个元素类型同样,则能够进行深度比较,若是不同,直接干掉老的元素,新建新的
        if(shouldDeepCompare(preRenderElement,nextRenderElement)){
            // 若是能够进行深度比较,则把更新的工做交给上次渲染出来的那个Element元素对应的unit来处理
            preRenderedUnitInstance.update(nextRenderElement)

        }else{
            this._renderUnit = createUnit(nextRenderElement)
            let nextMarkUp = this._renderUnit.getMarkUp(this._reactid)
            $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp)
        }

    }
    getMarkUp(reactid){
     
    }
}
复制代码

16. 实现componentDidUpdate生命周期函数

so Easy。

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

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        
        if(this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps,nextState)){
            return;
        }
   
        if(shouldDeepCompare(preRenderElement,nextRenderElement)){
            // 若是能够进行深度比较,则把更新的工做交给上次渲染出来的那个Element元素对应的unit来处理
            preRenderedUnitInstance.update(nextRenderElement)
            this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate()
        }else{
            this._renderUnit = createUnit(nextRenderElement)
            let nextMarkUp = this._renderUnit.getMarkUp(this._reactid)
            $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp)
        }

    }
    getMarkUp(reactid){
     
    }
}
复制代码

17. 实现shouDeepCompare

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

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

其余状况都return false

function shouldDeepCompare(oldElement,newElement){
    if(oldElement != null && newElement != null){
        let oldType = typeof oldElement
        let newType = typeof newElement
        if((oldType === 'string' || oldType === "number")&&(newType === "string" || newType === "number")){
            return true
        }
        if(oldElement instanceof Element && newElement instanceof Element){
            return oldElement.type === newElement.type
        }
    }
    return false
}
复制代码

文章首发于公众号《前端阳光》,项目已经放到github:github.com/Sunny-lucki… 以为能够的话,给个star鼓励下哈啊哈

相关文章
相关标签/搜索