React底层原理以及在工作中碰到的细节问题

3,219 阅读17分钟

滴水能把石穿透,万事功到自然成——zZ先森

1.React脚手架__create-react-app

全局安装脚手架

$ npm install -g create-react-app
//或者
$ yarn add -g create-react-app

基于脚手架快速构建工程化项目

$ create-react-app xxx
//xxx:项目名称遵循npm包规范,使用小写字母、数字、横杠组合方式

如果电脑上安装了yarn,默认会基于yarn安装

安装最新版本脚手架

要求npm的版本号在5.2以上才可以,并且一步到位安装到局部

$ npx create-react-app my-app

脚手架项目目录

  • node_modules: 安装所需的所有模块。
  • public: 存放编译模板
    • index.html:

    在index.html中引入的公共资源文件即public中与index.html同级的资源,在导入的时候,前缀加上<%PUBLIC_URL%>(当前目录),在webpack打包编译的时候会加以处理。vue中是<%BASE_URL%>

    <head>
    ...
    <link rel="icon" href="`<%PUBLIC_URL%>/favicon.ico"/>
    ...
    </head>
    <body>
    //把最后编译完成的html放到id为root的盒子里
    <div id="root"></div>
    </body>
    
    • 公共资源: 基于srclink调入html中,这样的话webpack对此不做处理。

    存放一些不支持CommonJs规范、ES6Module规范,在逼不得已使用的时候,在这里直接导入。

  • src: 存放项目源码
    • index.js: 当前项目入口,建议新建为index.jsx 在新建组件的时候一般为.jsx文件,webpack中支持.jsx文件的解析和编译,vscode也就可以识别
    • api: 数据处理api
    • store: REDUX公共状态管理
    • assets: 存储公共资源
    • routes: 路由管理
    • untis: 公共的js模块
    • components 公共的组件
  • package.json: 默认的配置清单
    • 生产依赖项
      • react REACT框架的核心,提供了状态、属性、组件、生命周期等
      • react-dom 把JSX语法渲染成为真实的DOM,最后显示在浏览器中
      • react-scripts 包含了当前工程化项目中webpack配置的东西(嫌弃把webpack放到项目目录中看上去太丑,脚手架把所有webpack的配置项和依赖都隐藏到node_modules中了,react-scripts这个REACT脚本执行命令,会通知webpack打包编译)
    • scripts 当前项目可执行的脚本命令($ yarn xxx)
      • $ yarn start => 开发环境下启动项目(默认会基于WEBPACK-DEV-SERVER创建一个服务,用来随时编译和渲染开发的内容)
      • $ yarn build => 生产环境下,把编写内容打包编译,放到build文件目录下(服务器部署)
      • $ yarn eject => 把所有隐藏在node_modules中的webpack配置项都暴露出来(方便自己根据项目需求,二次更改webpack配置)

yarn eject(将配置文件暴露出来进行二次配置)

  • babel-preset-react-app 解析JSX语法的

  • scripts

    • start.js =>执行$ yarn start的入口文件
    • build.js =>执行 $ yarn build的入口文件

    如果执行yarn start/build 提示少模块,我们则少了谁就安装谁

    @babel/plugin-transform-react-jsx

    @babel/plugin-transform-react-jsx-source

    @babel/plugin-transform-react-jsx-self

  • config |- 这里存储的就是webpack的配置项

PACKAGE.JSON

配置端口号和本地域名和HTTPS协议

"scripts": {
	"startMac": "PORT=8081 node scripts/start.js",
	"start": "set PORT=8081&&set HOST=127.0.0.1&&set HTTPS=true&&node scripts/start.js",
	"build": "node scripts/build.js"
},

修改less的处理配置

  • 先安装less以及less-loader $ yarn add less less-loader

  • 配置config/webpack.config.js

const cssRegex = /\.(css|less)$/;


{
	test: cssRegex,
	exclude: cssModuleRegex,
	use: getStyleLoaders({
		importLoaders: 1,
		sourceMap: isEnvProduction && shouldUseSourceMap,
	}, "less-loader"),
	// Don't consider CSS imports dead code even if the
	// containing package claims to have no side effects.
	// Remove this when webpack adds a warning or an error for this.
	// See https://github.com/webpack/webpack/issues/6571
	sideEffects: true,
},

2.JSXJavaScript XML(HTML)基础语法

JSX编写出来的就是虚拟DOM

  • 每一个组件视图只能有一个根元素节点 :有必要的话,可以添加一个Fragment空文档标记标签<></>

    ReactDOM.render([JSX],[CONTAINER],[CALLBACK])

    • [CONTAINER]不建议是body或者是html并且指定一个元素容器
    • [CALLBACK]将虚拟DOM渲染到页面上,然后触发回调函数,一般不用
  • JSX语法中基于大括号来绑定动态数据值和JS表达式

    • nullundefined代表的是空元素
    • {}中不能写对象和函数等其他引用数据类型(除数组外),写数组的话会将其转换为字符串。
    • jsx虚拟DOM对象可以放在{}
  • 给JSX设置类名和样式

    • 给JSX元素设置样式类名为className
    • 给JSX元素设置样式,必须在大括号内放入对象、
    • 如果{}里放的是JS表达式,那么返回结果可以是新的JSX元素或者元素值
  • 动态绑定数据

    • 在JSX语法中基于大括号来绑定数据,并且大括号里面只能是空元素和数组,再者必须是JS表达式,所以可以在大括号中基于数组的一些方法来绑定数组中的一些数据,这些方法也必须是返回一个值。
    • 在JSX语法中,循环绑定数据的时候,要求在给每个循环的数据都要加一个key,因为key是DOM DIFF中重要的凭证,key值一般不设置为循环的索引,而是设置唯一不变的值
let name = "zZ",
    styObj = {color:'blue'},
    data=[
    {
        id:1,
        name:'shu'
    },
    {
        id:2,
        name:'gang'
    }
    ];
ReactDOM.render(<div className="box" style={styObj}>
<ul>
     data.map(item=>{
        return <li key={item.id}>
               <span>{item.name}</span>
               <span>{item.name}</span>
        </li> 
     })
</ul>
</div>)

此处的索引不能用索引的,主要目的是为了数据变动的时候,DIFF渲染的时候,渲染速度更快,且不容易产生组件的错误。

3.虚拟DOM到真实DOM

1.把JSX语法通过PABEL-PRESET-REACT-APP语法解析包变为CREATE-ELEMENT格式

Vue中通过VUE-LOADER解析template模板

ReactDOM.render(<div className="box" style={styObj}>
                     zZ是个好男孩
                    <span>优秀</span>
            </div>)

通过解析解析为以下格式:

React.createElement("div",{
    className:"box",
    style:{color:"blue"}
},"zZ\u662f\u4e2a\u597d\u7537\u5b69",React.creatElement("span",null,"\u4f18\u79c0"))
  • 每一个标签都会解析为一个CREATE-ELEMENT格式
  • 通过解析,实则是构建了一棵树,在执行编译的时候是从最底层的子节点开始从右到左、从下到上执行编译

2.执行React.createElement()

返回的的是一个对象:它就是一个虚拟DOM

  • ?typeof:Symbol(react.element),
  • key:null,
  • ref:null,
  • type:标签名/组件名
  • props:给元素标签上设置的属性,除key和ref
    • children:如果元素有子节点,才有children属性,并且值会根据子节点的类型而定。
      • 如果是单个子节点,属性值为字符串或者对象
      • 如果是多个子节点,属性值为数组

3.执行REACT-DOM.RENDER

render函数把执行React.createElement()返回的对象,变为真实的DOM,最后渲染到指定容器中,呈现到页面上。

4.虚拟DOM到真实DOM实现原理

模拟虚拟DOM如何到真实DOM,封装如下代码:

let React = {},
    ReactDOM={};
    //直接操作私有属性 将属性名和属性值暴露给回调函数
function each(obj,callback){
    Obiect.keys(obj).forEach(item=>{
         let val = obj[item];
         callback && callback(val,item)
    })
}
React.creatElement = function createElement(type,props,...children){
    let virtualDOM = {
        type,
        props:{},
        key:null,
        ref:null
    };
    //处理props
    if(props){
        each(props,(value,item)=>{
            if(item === "key"){
                virtualDOM.key = value;
                return
            }
            if(item === "ref"){
                virtualDOM.ref = value;
                return
            }
        })
        delete props["key"];
        delete props["ref"];
        virtualDOM.props = {...props}
    }
    //处理children
    if(children.length>0){
        virtualDOM.props.children = children.length===1 ? children[0] : children
    }
    
}
//把虚拟DOM转换为真实DOM
ReactDOM.render = function render(virtualDOM,container,callback){
    let {
        type,
        props
    }=virtualDOM;
    let element = document.createElement(type);
    each(props,(val,item)=>{
        if(item==="className"){
            element.className = val;
            return;
        }
        if(item==="style"){
            each(item,(value,key)=>{
                element.["style"]["key"] = value;
            })
            return;
        }
        if(item === "children"){
            let children = val;
            //统一作为数组
            children = Array,isArray(children) ? children : [children];
            each(children,(val,item)=>{
                if(typeof val === "string"){
                    element.appendChild(document.creatTextNode(val));
                    return 
                }
                render(val,element)
            })
            return
        }
        //将其他属性直接挂载到元素上
        element.setAttribute(item,val)
    })
    //放到指定的容器中
    container.appendChild(element);
    if(typeof callback === "function"){
        callback();
    }
}
export default {React,ReactDOM}

5.组件

React中的组件:每一个组件(类组件)都是一个单独的个体(数据私有,有完整的生命周期函数,有自己的视图)

组件命名可以以.jsx为后缀名,在create-react-app脚手架创建项目中,包含了对.JSX文件的处理

1.组件分类

  • 函数组件(静态组件): 一个函数返回JSX对象。通过HOOKS将其动态化
  • 类组件(动态组件):创建一个类,并继承React.Component/PureComponent,且必须要有一个render函数作视图的渲染,在函数中返回JSX
  • REACT HOOKS

2.动态组件和静态组件的区别

  • 静态组件没有自己的状态、生命周期函数等,所以组件一旦被渲染内容就固定了,但是可以多次使用,优势: 渲染速度快,开发维护简单。弊端: 静态化以及功能简单
  • 动态组件: 有自己的状态和生命周期函数,即便在渲染完成,还可以通过状态的改变重新渲染修改的部分,优势 功能强大,动态化管理数据,缺点运行速度较慢
  • 最常用的是HOOKS,根据业务的需求,将函数静态组件加以动态化,实现动态组件的状态、生命周期以及通过refs修改DOM元素。

3.调用组件

组件调用】在JSX语法中,ReactDOM.render进行处理组件的时候,当发现type不是标签字符串,则把当前的组件执行,如果是函数组件,则直接执行,如果是类组件,则进行new执行,并创建一个实例,并把父组件调用子组件传递进去的props都传递给函数或者实例来进行渲染。注意俩点:与vue不同的是单闭合和双闭合标签皆可props是只读的不能修改

函数组件】相对比vue插槽来说,如果在父组件调用子组件的时候,将属性、标签、其他组件通过props属性传递给子组件,也就是函数接收的参数为props。在React.createElement方法形成虚拟DOM的时候,将存储在JSX对象的props中,标签和组件都将存储在props属性名为children中,想要控制在不同的位置渲染的时候,有俩种方法第一种: React提供了一个遍历children的方法集合对象=>Children,Children中有①count/②forEach③map④only ⑤toArray五个方法。第二种: 利用索引来获取children数组中的每一项,放到不同位置。

类组件

【继承】:1. 原型继承 2.call继承 3.寄生组合式 继承 4.ES6中基于class实现继承 关键字extends

【super】类似于call继承,会把父类当作函数执行,让函数中的this是子类的实例。当前类中必须有constructor。且在调用组件的时候传递进来的属性,传递给constructor。在REACT中,在构建类组件的时候,类组件继承了React.Component,则在super执行的时候,相当于把React.Component当作普通函数执行,让方法中的THIS是当前实例。this=>{props:xxx,content:xxx,refs:{},updater:{...}} 即为了能让传递进来的属性挂载到实例上,则会给super传递props

REACT-DOM.RENDER渲染的时候,如果发现虚拟DOM中的TEPE是一个类组件,会创建这个类实例,并把它解析出来的props传递这个类,constructor就可以接收进来props。执行constructor之后才创建一个当前类的实例。虽然已将props传递给constructor,但是实例上并未挂载这些属性,基于this.props不能获取到值,但是可以直接使用形参中的props。当constructor执行完,REACT会帮我们继续处理,通过render方法把PROPS/CONTEXT...挂载到实例上,后期的生命周期函数都可以基于THIS.PROPS来获取和应用传递进来的属性值。 必须要有render函数,它返回的是当前组件要渲染的视图。

4.Component&PureComponent

类组件进行继承的时候,React提供了俩种: Component PureComponent。 俩个父类区别在于:PureComponent相对于Component,会默认创建一个钩子函数shouldComponentUpdate,如果我们手动添加shouldComponentUpdate则以我们添加的为主,shouldComponentUpdate运行原理是浅比较,给这个钩子函数传递进来的就是更新的新数据,通过判断类决定返回true还是false。

注意在修改的状态是数组或者其他引用数据类型的时候,注意其地址如果没有更新,其存储的值修改的话,照样是不渲染的。基于解构到新数组来给予新的地址加以区分,然后返回true,视图才会渲染。

import React from 'react';

export default class Vote extends React.PureComponent {
	state = {
		arr: [10, 20]
	};

	render() {
		return <div className="voteBox">
			{this.state.arr.map((item, index) => {
				return <span key={index}>
					{item}
				</span>;
			})}
			<button onClick={this.handle}>按钮</button>
		</div>;
	}
	handle = () => {
		let arr = this.state.arr;
		arr.push(30);
		this.setState({
			arr: [...arr]
	});
}

6.数据管控

我们把基于状态或者属性的更新来驱动视图渲染,叫受控组件。属性不能修改,但可以通过设置默认值、让父组件重新调用传递不同的属性、或者把属性值赋值给组件的状态,通过借刀杀人的方式修改属性。非受控组件: 不受状态管控,而是直接操作DOM。

属性——props

给属性设置规则需要第三方插件【prop-types】 设置的规则不会阻碍内容的渲染,不符合规则的在控制台报错。

  • Installation
$ npm install --save prop-types
//&
$yarn add prop-types
  • Importing
import PropTypes from "prop-types";
//&
var PropTypes = require("prop-types");
  • 设置默认值: 在props里面没有该属性值 则会走默认的值
static defaultProps = {
    title:"zZ很懒"
}

PropTypes.isRequired 必须传递 PropTypes.string/bool/number/func/object/symbol/node(元素节点)

element(JSX元素)

instanceOf(Xxx)(必须是某个类的实例)

oneOf(["News","photos"])(多个中的一个)

oneOfType([PropTypes.string,PropTypes.number,PropTypes.instanceOf(Message)])多个类型中的一个

static propTypes = {
    title"PropTypes.string.isRequired
}

状态——state(私有状态/公共状态REDUX)

1.在constructor构造函数中初始化,要求后期再组件中使用的状态都要在这里初始化一下

2.通过setState方法来通知视图重新渲染,setState(partialState,callback),在某些情况下是异步的,也可以理解在钩子函数中是局部异步的。需要等待周期函数执行完成,再去修改状态,保证了周期函数的稳定性,以及整体执行逻辑的完整性。有些情况是同步的,在设置定时器或者在绑定事件中执行setState,这个时候就是同步的,走修改状态的流程。

  • 异步情况:

    • 在钩子函数componentWillMount中设置setState,为了提高性能,React为了避免不必要的重新渲染,执行完componentWillMount,就立即修改了状态。实现了局部异步性。
    • 在钩子函数componentDidMount中设置setState,因为已经执行完render了,所以此时的异步的等到生命周期函数搜执行完再去修改状态。
  • 同步情况:

    • 设置定时器: 在修改状态外加一层定时器,本身就在异步操作里面,不用考虑定时器内部会影响到组件的生命周期函数的执行顺序以及逻辑。就按照修改状态的顺序来即可。
    • 绑定事件: 在给DOM绑定二级事件的时候,本身也是异步的,在触发某个操作去执行绑定的操作,所以此时的修改状态也不会去影响到组件正常的生命周期函数的执行逻辑。
  • setState【修改状态】过程=> shouldComponentUpdate -> WillUpdate -> render(重新渲染:重新构建虚拟的DOM对象->DOM DIFF(补丁包)->差异渲染) -> DidUpdate

  • 【partialState】 第一个参数部分状态对象【partialState】,修改初始化中的哪个状态,在setState中改谁即可,React在处理的时候把之前的状态和传递的partialState进行合并并替换,使用原生Object.assign(this.state,partialState)

  • 【callback】 在视图重新渲染完成之后执行。可以解决setState异步情况,在项目中,如果视图修改了数据且要从服务器上获取的最新数据,并传递修改后的参数,则可以在callback中进行axios请求。

3.批量更新状态

基于setState修改状态时的同步异步操作,React把当前异步修改状态的操作任务放到队列里,如果有来个任务修改同一个状态,则会以最后一个任务为主。和浏览器渲染更新机制一个道理。为了避免不必要的渲染,提高性能。

4.this.forceUpdate(): 强制更新,执行该方法就不会再走shouldComponentUpdate,直接修改状态

5.数据驱动机制比较

  • 【React】 :基于setState/forceUpate来手动添加修改并通知render重新渲染
  • 【vue】 :基于内置的Object.defineProperty/Proxy(3.0版本)的setter通知render执行。
this.state = {
    time: new Date().toLocaleString()
};
render(){
    return <div>
       <p>{this.state.time}</p>
    </div>
}
componentDidMount(){
    //第一次加载组件渲染完毕 等价于 VUE中的MOUNT
    //这种情况不会通知组件重新渲染
    //this.state.time = "1970年1月1日 0点0分0秒" 
    //每一次修改状态基于:setState方法 
   setInterval(()=>{
        this.setState({
        time: new Date().toLocaleString();
    },()=>{console.log("疫情很快结束!")})
   },1000);
}

6.refs :是一个对象,对象里面存储着有属性为ref的元素DOM,从而获得DOM来进行操作。 有俩种写法: 设置属性ref=xxXxx,基于this.refs.xxXxx获取DOM元素。 通过函数的模式,传递的参数是当前的DOM元素,然后将DOM元素直接挂载到实例上,后续通过设置的属性名来获取相应的DOM元素,进行操作DOM。

<p ref="timeBox">{new Date().toLoacaleString}</p>
//&
<p ref=DOM=>{
    this.timeBox = DOM
}>{new Date().toLoacaleString}</p>
  • React中的事件绑定

    • 原生DOM事件绑定: 基于refs或者通过直接挂载到实例上DOM元素在componentDidMount钩子函数中绑定事件
    • JSX自带的合成事件: onXxx

    React为了处理事件的兼容以及移动端事件的处理,所有的事件都是合成的(事件对象也是自己合成的)=》原理:事件委托,把每一种事件类型都在document上委托一下。

7.类组件的生命周期函数

  • getDefaultProps 获取属性的默认值和校验传递属性的类型
  • constructor getInitialState 在执行getInitialState之前,如果当前类组件有构造函数,则先执行该构造函数,从而初始化状态,把属性等信息挂载到实例上
  • componentWillMount 第一次渲染之前,可以在这里提前从服务器获取数据,进而把获取的数据重赋值给状态或者存放到redux中。
  • render渲染组件
  • componentDidMount() 第一次组件渲染完成,然后处于运行当中。可以获取DOM元素了。

【state改变】

  • shouldComponentUpdate 状态改变触发该钩子函数,此处可以判断该更新是否有必要,避免不必要的更新,从而得到优化,返回true或者false来控制是否执行下一个钩子函数,但是不论返回的哪个,状态都已经改了
  • componentWillUpdate在上一个钩子函数返回true执行,做一系列的更新前准备
  • render再次渲染,渲染状态改变的部分
  • componentDidUpdate状态更新完成后,到再次监听属性或者状态的改变。

【props改变】

  • componentWillReceiveProps属性值改变触发该钩子函数,获取到某一个具体属性的改变,然后触发shouldComponentUpdate,看看到底是否可以更新。

【卸载】

  • componentWillUnmount 组件卸载之前,做最后一步的收尾工作,然后组件的整个生命周期结束。

代码示意以及各个周期函数的参数

class Clock extends React.Component {
    static defaultProps = {
        time:"1970年1月1日 0点0分0秒"
    }
    static  propTypes = {
        time:"instanceOf(Date)"
    }
    constructor(props){
        super(props);
        this.state = {t:0}
    }
    componentWillMount(){
        console.log("--- 执行componentWillMount---")
    }
    render(){
        console.log("--- 执行render---")
        return <div>
            <h2>回溯到{this.state.time}</h2>
            <button Onclick={()=>{
                this.setState({t:this.state.t+1})
            }}>{this.state.t}</button>
        </div>
    }
    componentDidMount(){
        console.log("--- 执行componentDidMount---")
    }
    shouldComponentUpdate (nextProps,nextState) {
        console.log("--- 执行shouldComponentUpdate ---",nextProps,nextState)
    }
    componentWillUpdate(){
         console.log("--- 执行componentWillUpdate---")
    }
    //render重新渲染
    componentDidUpdate(){
         console.log("--- 执行componentDidUpdate---")
    }
    componentWillUnmount(){
        console.log("--- 执行componentWillUnmount---")
    }
}
React.render(<section>
<Clock></Clock>
</section>,document.getElementById("root"),_=>{alert("奥利给")})

总结

总结的比较细,相对于初学者更容易理解,如果哪里有不对的地方,希望各位江湖人士加以指正,哪里有不懂得地方可以留言,一起讨论。后续不断更新文章,谢谢各位!!