React学习总结


React是FaceBook(脸书)公司研发的一款JS框架(MVC)

React的脚手架

React是一款框架 : 具备自己开发的独立思想(MVC : Model View Controller)

  • 划分组件开发
  • 基于路由的SPA单页面开发
  • 基于ES6来编写代码(最后部署上线的时候,我们需要把ES6编译成ES5 =>基于Babel来完成编译)
  • 可能用到Less/Sass等,我们也需要使用对应的插件把他们进行预编译
  • 为了优化性能(减少http请求次数),我们需要把JS或者CSS进行合并压缩
  • ………….
  • webpack来完成以上页面组件合并、JS / CSS编译加合并等工作

前端工程化开发

  • 基于框架的组件化开发 / 模块化开发
  • 基于webpack的自动部署

但是配置webpack是一个相对复杂的工作,我们需要自己安装许多的包,还需要自己写相对复杂的配置文件… 如果我们有一个插件,基于它可以快速构建一套完整的自动化工程项目结构,那么有助于提高开发的效率=>脚手架
VUE : vue-cli
REACT : create-react-app

create-react-app 的使用

1.把模块安装在全局(目的:可以使用命令),mac电脑安装的时候,前面需要加sudo,否则没有权限
npm install creact-react-app -g
2.基于脚手架命令,创建出一个基于react的自动化 / 工程化项目目录,和npm发包时候的命名规范一样,项目名称中不能出现:大写字母 / 中文 / 特殊符号(-或者_是可以的)等
create-react-app [项目名称]

脚手架生成目录中的一些内容

  • node_modules : 当前项目中依赖的包都安装在这里

    • .bin : 本地项目中可执行命令,在package.json的scripts中配置对应的脚本即可(react-scripts命令)
  • public : 存放的是当前项目的HTML页面(单页面应用放一个index.html即可,多页面根据自己需求放置需要的页面)

      <!--
           在react框架中,所有的逻辑都是在JS中完成的(包括页面结构的创建)如果想给当前的页面导入一些css样式或者image图片等内容,我们有两种方式:
           1.在JS中基于ES6 Module模块规范使用import导入,这样webpack在编译合并页面的时候,会把导入的资源文件等插入到页面的结构中(绝对不能再JS管控的结构中通过相对目录./或者../导入资源,因为在webpack编译的时候,地址就不再是之前的相对地址了)
           2.如果不想在JS中导入(JS中导入的资源都会基于webpack编译),我们也可以把资源手动的在HTML中导入,但是HTML最后也要基于webpack编译,带入的地址也不建议写相对地址,而是使用%PUBLIC_URL%/ 写成绝对路径
        <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
        <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
      -->
  • src : 项目结构中最主要的目录,因为后期所有的JS、路由、组件等都是放到这里面(包括需要编写的CSS或者图片等)

  • .gitignore : git提交时候忽略文件的目录配置项

  • package.json : 当前项目的配置清单
    ```javascript
    “dependencies”: {
    “react”: “^16.4.1”,
    “react-dom”: “^16.4.1”,
    “react-scripts”: “1.1.4”
    },
    基于脚手架生成工程目录,自动帮我们安装了三个模块,react/react-dom/react-scripts
    react-scripts集成了webpack需要的内容:

  • babel

  • css

  • eslink

  • webpack

  • 其他
    没有less/sass的处理内容(项目中使用less,需要自己额外安装)

    “scripts”: {
    “start”: “react-scripts start”,
    “build”: “react-scripts build”,
    “test”: “react-scripts test –env=jsdom”,
    “eject”: “react-scripts eject”
    }
    可执行的脚本”npm run start / yarn start”
    start:开发环境下,基于webpack编译处理,最后可以预览当前开发的项目成果(在webpack中,安装了webpack-dev-server插件,基于这个插件会自动创建一个web服务[端口号默认是3000],webpack会帮我们自动打开浏览器并且展示我们的页面,并且能够监听我们代码的改变,如果代码改变了,webpack会自动重新编译并且刷新浏览器来完成重新渲染)
    build:项目需要部署到服务器上,我们先执行yarn build,把项目整体编译打包(完成后会在项目中生成一个build文件夹,这个文件夹中包含了所有编译后的内容,我们把它上传到服务器即可);而且在服务器上进行部署的时候,不需要安装任何模块,因为webpack已经把需要的内容都打包到一个JS中了

React脚手架的深入剖析

creact-react-app脚手架为了让结构目录清晰,把安装的webpack及配置文件都集成在了react-scripts模块中,放到了node_modules中

但是真实项目中,我们需要在脚手架默认安装的基础上,额外安装一些我们需要的模块,例如 : react-router-dom / axios 及 less / less-loader….

情况一 : 如果我们安装其他的组件,但是安装成功后不需要修改webpack配置项,此时我们直接的安装并且调取使用即可

情况二 : 我们安装的插件是基于webpack处理的,也就是需要把安装的模块配置到webpack当中(重新修改webpack配置项)

  • 首先需要把隐藏到node_modules中的配置项暴露到项目
    yarn eject:暴露到项目中
    

首先会提示确认是否执行eject操作,这个操作是不可逆转的,一旦暴露出来配置项,就无法再隐藏回去了

如果当前的项目基于git管理,在执行eject的时候,如果还有没有提交到历史区的内容,需要先提交到历史区,然后再eject才可以,否则报错

一旦暴露后,项目中多了两个文件夹
config:存放到的是webpack的配置文件
webpack.config.dev.js:开发环境下的配置项(yarn start)
webpack.config.prod.js:生产环境下的配置项(yarn build)
scripts:存放的是可执行脚本的JS文件
start.js:yarn start执行的JS
bulid.js:yarn build执行的JS
package.json中的内容也改了


例如:配置less
-    yarn add less less-loader 
-    less是开发和生产环境下都需要配置的

{loader: require.resolve(‘less-loader’) }



  - 再去修改对应的配置项即可

我们预览项目的时候,也是先基于webpack编译,把编译后的内容放到浏览器中运行,所以如果项目中使用了less,我们需要修改webpack配置项,在配置项中加入less的编译工作,这样后期预览项目,首先基于webpack把less编译为css,然后再呈现在页面中.

### 设置环境变量

set HTTPS=true&&npm start 开启HTTPS协议模式
set PORT=6666&&npm start 修改端口号


### react&&react-dom

[渐进式框架]

- 一种最流行的框架设计思想,一般框架中包含很多内容,这样导致框架的体积过于臃肿,会拖慢加载速度,真实项目中,我们使用一个框架,不一定用到所有的功能,此时我们应该把框架的功能进行拆分,用户想用什么,让其自由组合即可

- 全家桶 : 渐进式框架N多部分的组合
- Vue全家桶 : vue-cli / vue / vue-router / vuex / axios(fatch) / vue element(vant) 
- React全家桶 : create-react-app / react / react-dom / react-router(-dom) / redux / react-redux / axios(fatch) / ant / dva / mobx...

> 1.react : react框架的核心部分,提供了Component类可以供我们进行组件开发,提供了钩子函数(生命周期函数 : 所有的生命周期函数都是基于回调函数完成的) 
>  
> 2.react-dom : 把JSX语法(React独有的语法)渲染为真实DOM(能够放到页面中展示的结构都叫做真实的DOM)的组装件 


### 把JSX(虚拟DOM)变为真实的DOM

**`JSX语法`**

> ReactDOM.render([jsx],[container],[callback]); 把JSX渲染到页面中
>  - jsx:react虚拟元素
>  - container:容器,我们想把元素放到页面中的哪个容器中
>  - callback:当把内容放到页面中呈现触发的回调函数

JSX(javascript+XML(HTML))

- 和之前拼接的HTML字符串类似,都是把HTML结构代码和JS代码或者数据混合在一起了,但是它不是字符串
-  1.不建议我们把JSX直接渲染到body中,而是放在自己创建的一个容器中,一般我们都放在一个id为root的div中即可
-  2.在JSX中出现的{}是存放JS的,但是要求JS代码执行完成需要有返回结果(JS表达式)
  - 不能直接放一个对象数据类型的值(对象(除了给style赋值)、函数都不行,数组里面如果没有对象,都是基本值是可以的或者是JSX元素)
  -  可以是基本数据类型的值(布尔类型什么都不显示,null、undefined也是JSX元素,代表的是空)
  -  循环判断的语句都不支持,但是支持三元运算符
- 3.循环数组创建JSX元素(一般都基于数组的map方法完成迭代),需要给创建的元素设置唯一的key值(当前本次循环内唯一即可)
- 4.只能出现一个根元素
- 5.给元素设置样式类用的是className而不是class
-  6.style中不能直接的写样式字符串,需要基于一个样式对象来遍历赋值

```javascript
let data = [
        { name:"张三",age:18},
        {name:"李四",age:20}
    ],
root = document.querySelector("#root");
ReactDOM.render(<div id="box">Hello world!
        <ul>
            {data.map((item,index)=>{
                let {name, age} = item;
                return <li key={index}>
                    <span>{name}</span>
                    <span>{age}</span>
                </li>
            })}
        </ul>
</div>, root);

JSX渲染机制

  • 1.基于babel中的语法解析模块(babel-preset-react)把JSX语法编译为react.createElement(…)结构
  • 2.执行createElement
    React.createElement(type, props, children);
    创建一个对象(虚拟DOM)
    属性:
    type:"h1" : 标签名
    props:{
    id:"titleBox",
    className:"title",
    style:{...},
    children:"jiatengda" =>存放的是元素中的内容
    }
    ref:null
    key:null
    ...
    __proto__:Object.prototype
  • 3.reactDOM.render(JSX语法最后生成的对象,容器),基于render方法把生成的对象对台创建DOM元素,插入到执行的容器中

自己写createElement&&render

function createElement(type, props, children) {
    props = props || {};
    //创建一个对象,设置一些默认属性值
    let obj = {
        type: null,
        props: {
            children: ""
        },
        ref: null,
        key: null
    };
    //用传递的type和props覆盖原有的默认值
    // obj = {...obj, type, props};//=>type:type,props:props
    obj = {...obj, type, props: {...props, children}};
    //把ref和key提取出来(并且删除props中的属性)
    "key" in obj.props ? (obj.key = obj.props.key, delete obj.props.key) : null;
    "ref" in obj.props ? (obj.ref = obj.props.ref, delete obj.props.ref) : null;
    return obj;
}
function render(obj, container, callback) {
    let {type, props} = obj || {},
        newElement = document.createElement(type);
    for (let attr in props) {
        if (props.hasOwnProperty(attr)) break;//不是私有的直接结束遍历
        if (!props[attr]) continue;//如果当前属性没有值,直接不处理即可
        let value = props[attr];
        //className的处理
        if (attr === "className") {
            newElement.setAttribute("class", value);
            continue;
        }
        //style
        if (attr === "style") {
            if (value === "") continue;
            for (let styKey in value) {
                if (!value.hasOwnProperty(styKey)) break;
                newElement['style'][styKey] = value[styKey];
            }
            continue;
        }
        //children
        if (attr === "children") {
            if (typeof value === "string") {
                let text = document.createTextNode(value);
                newElement.appendChild(text);
            }
            continue;
        }
        newElement.setAttribute(attr, value);//基于setAttribute可以让设置的属性表现在HTML的结构上
    }
    container.appendChild(newElement);
    callback && callback();
}

React组件

不管是VUE还是REACT框架,设计之初都是期望我们按照组件/模块管理的方式来构建程序的,也就是把程序划分为一个个的组件来单独处理

  • 有助于多人协作开发
  • 我们开发的组件可以被复用
  • ……

React中创建组件有两种方式:

  • 函数声明式组件
  • 基于继承component类来创建组件

src->components : 这个文件夹中存放的就是开发的组件

知识点 : createElement在处理的时候,遇到一个组件,返回的对象中,type就不再是字符串标签名了,而是一个函数(类),但是属性还是存在props中

{
    type:Dialog
    props:{
        lx:1,
        con:"xxx",
        children:一个值||一个数组
    }
}

render渲染的时候,我们需要做处理,首先判断type的类型,如果是字符串,就创建一个元素标签,如果是函数或者类,就把函数执行,把props中的每一项(包含children) 传递给函数

在执行函数的时候,把函数中return的JSX转换为新对象(createElement),把这个对象返回;紧接着render按照以往的渲染方式,创建DOM元素,插入到指定的容器中即可

基于继承component类来创建组件

基于createElement把JSX转化为一个对象,当render渲染这个对象的时候,遇到type是一个函数或者类,不是直接创建元素,而是先把方法执行

如果就是函数式声明组件,就把它当做普通方法执行(方法中的this是undefined),把函数返回的JSX元素(也是解析后的对象)进行渲染

如果是类声明式组件,会把当前类new它执行,创建类的一个实例(当前本次调取的组件就是它的实例),执行contructor之后,会执行this.render(),把render返回的JSX拿过来渲染,所以类声明式组件,必须有一个render的方法,方法中需要返回一个JSX元素

但是不管是哪一种方式,最后都会把解析出来的props属性对象作为实参传递给对应的函数或者类

总结
创建组件有两种方式,函数式创建类式

[函数式]

  • 1.操作很简单
  • 2.功能也简单,简单的调取,简单的返回

[创建类式]

  • 1.操作相对复杂一点,但是也可以实现更为复杂的业务功能
  • 2.能够使用声明周期函数操作业务
    • 3.函数式可以理解为静态组件(组件中的内容调取的时候就已经固定了,很难再修改),而类这种方式,可以基于组件内容的状态来动态更新渲染的内容

组件的属性和状态

React中有两个非常重要的概念

  • 1.组件的属性 : [只读] 调取组件的时候传递进来的信息
  • 2.组件的状态 : [读写] 自己在组件中设定和规划的(只有类声明式组件才有状态的管控,函数式声明组件不存在状态的管理)
function Clock() {
    return <section>
        <h3>当前北京时间为:</h3>
        <div style={{color:"lightblue",fontWeight:"bold"}}>{new Date().toLocaleString()}</div>
    </section>
}
window.setInterval(()=>{
    //每隔一秒钟重新调取组件,然后渲染到页面中
    ReactDOM.render(<Clock/>, root)
}, 1000);

所谓函数式组件是静态组件 : 和执行普通方法一样,调取一次组件,就把组件的内容获取到,插入到页面当中,如果不重新调取组件,显示的内容是不会发生任何改变的

真实项目中只有调取组件,组件中的内容不会再次改变的情况下,我们才可能使用函数式组件

组件的状态类似于Vue中的数据驱动 : 我们数据绑定的时候是基于状态值绑定,当修改组件内部状态后,对应的JSX元素也会跟着重新渲染(差异渲染 : 值把数据改变的部分重新渲染,基于dom-diff算法完成的)

当代框架最重要的核心思想就是数据操控视图(视图改变数据),告别jQuery手动操作DOM的时代,我们以后只需要改变数据,框架会帮我们重新渲染改变视图,从而减少直接操作DOM,提高性能,也有助于开发效率

class Clock extends React.Component {
    constructor() {
        super();
        //初始化组件的状态(都是对象类型的):要求我们在constructor中需要把后期使用的状态信息全部初始化一下(约定俗成)
        this.state = {
            time: new Date().toLocaleString()
        };
    }
    componentDidMount() {
        //React声明周期函数之一:第一次组件渲染完成后触发(我们在这里只需要间隔1秒把state状态中的time数据改变,这样React会自动帮我们把组件中部分内容重新渲染)
        //React中虽然下面方式可以修改状态,但是并不会通知React重新渲染页面,所以不能这么操作和修改状态
            //this.state.time = new Date().toLocaleString();
        setInterval(() => {
            //修改组件的状态
            //1.修改部分状态:会用我们传递的对象和初始化的state进行匹配,只把我们传递的属性进行修改,没有传递的依然保留原始的状态信息(部分无法修改)
            //2.当状态修改完成,会通知React,把组件JSX中的部分元素重新进行渲染
            this.setState({
                time: new Date().toLocaleString()
            },()=>{
                //当通知React把需要重新渲染的JSX元素渲染完成后,执行的回调操作(类似于声明周期函数中的componentDidUpdate,项目中一般使用钩子函数,而不是这个回调)
                //=>设置回调函数是因为setState是一个异步操作,基于async和await可以把异步的变为类似于同步
            });
        }, 1000);
        }
    render() {
        return <section>
            <h3>当前北京时间为:</h3>
            <div style={{color: "lightblue", fontWeight: "bold"}}>
                {/*获取组装件的状态信息*/}
                {this.state.time}
                </div>
        </section>
    }
}
ReactDOM.render(<Clock/>, root);

JSX中的事件绑定

render(){
//this:当前类的实例  this.support.bind(this)
     <button className="btn btn-success" onClick={this.support}>支持</button>
}
support(){
//this:undefined
//ev.target:通过事件源可以获取当前操作的元素(一般很少操作,一般框架是数据驱动所有DOM的改变)
}

如果能让方法中的this变成当前类的实例就好了,这样可以操作属性和状态等信息

support=ev=>{
    //this:继承上下文中的this(实例),真实项目中,给JSX元素绑定的事件方法一般都是箭头函数,目的是为了保证函数中的this还是实例
}

受控组件 : 数据管控的组件
非受控组件 : 不受数据管控的组件

在React中

  • 1.基于数据驱动(修改状态数据,React帮助我们重新渲染视图)完成的组件叫做受控组件

  • 2.通过ref操作DOM实现视图更新的,叫做非受控组件

真实项目中尽量多使用受控组件

VUE : [MVVM] 数据更改视图也跟着更改,视图更改数据也跟着更改(双向数据绑定)
REACT : [MVC] 数据更改视图跟着更改(原本是单向数据绑定,但是我们可以自己构建出双向的效果)

React中的生命周期函数

所谓生命周期函数(钩子函数) : 描述一个组件或者程序从创建到销毁的过程,我们可以在过程中间基于钩子函数来完成一些自己的操作,例如 : 在第一次渲染完成做什么,或者第二次即将重新渲染之前做什么等

[基本流程]
constructor 创建一个组件
|
componentWillMount 第一次渲染之前
|
render 第一次渲染
|
componentDidMount 第一次渲染之后
[修改流程]
当组件的状态数据发生改变(setState)或者传递给组件的属性发生改变(重新调用组件传递不通的属性都会引发render重新渲染(差异渲染)
shouldComponentUpdate : 是否允许组件重新渲染(允许则执行后面函数,不允许直接结束即可)
|
componentWillUpdate : 重新渲染之前
|
render 第二次及以后重新渲染
|
componentDidUpdate : 重新渲染之后
[componentWillReceiveProps]父组件把传递给子组件的属性 发生改变后触发的钩子函数

[卸载]
原有渲染的是不消失的,只不过以后不能基于数据改变视图了
componentWillUnmount : 卸载组件之前(一般不用)

function queryData() {
    return new Promise(resolve => {
        setTimeout(()=>{
            resolve(200);
        },3000)
    });
}
class A extends React.Component{
    // static default = {};//这个是第一个执行的,执行完成(给属性设置默认值后)才向下执行
    constructor(){
        super();
        console.log("1-constructor");
        this.state={
            n: 1
        };
    }
    /*componentWillMount(){
        console.log("2-willMount: 第一次渲染之前",this.refs.div);//undefined
        //在willMount中,如果直接的setState修改数据,会把状态信息改变后,然后render和didMount;但是如果setState是放到一个异步操作中完成(例如:定时器或者从服务器获取数据),也是先执行render和did,然后在执行这个异步操作修改状态,紧接着走修改的流程(这样和放到didMount中没有区别),所以我们一般吧数据请求放到did中处理
        //=>真实项目中的数据绑定,一般第一次组件渲染,我们都是绑定的默认数据,第二次才是绑定的从服务器获取的数据(有些需求我们需要根据数据是否存在判断显示隐藏)
        this.setState({n: 2})
    }*/

    async componentWillMount(){
        console.log("2-willMount");
     let res =  await queryData();
     this.setState({n: res})
    }

    componentDidMount(){
        console.log("4-didMount: 第一次渲染之后", this.refs.div);//div
        /*
         * 真实项目中在这个阶段一般做如下处理:
         *    1.控制状态信息更改的操作
         *    2.从服务器获取数据,然后修改状态信息,完成数据绑定
         */
        setInterval(() => {
            this.setState({n: this.state.n + 1})
        }, 5000);
    }
    shouldComponentUpdate(nextProps,nextState) {
        //在这个钩子函数中,我们获取的state不是最新修改的,而是上一次的state
        //例如:第一次加载完成,五秒后,我们基于setState把b修改为2,但是此处获取的还是1
        //但是这个方法有两个参数
        //nextProps:最新修改的属性
        //nextState:最新修改的状态信息

        console.log("5-是否允许更新,函数返回true允许,反之false不允许");
        return nextState.n > 3 ? false : true;

    }

    componentWillUpdate(nextProps,nextState) {
        // console.log(this.state.n);//这里获取的是更新之前的(和should相同,也有两个参数存储最新的信息-)
        console.log("6-组件更新之前")
    }
    componentDidUpdate(){
        // console.log(this.state.n);这里获取的状态是更新之后的
        console.log("8-组件更新之后,因为7是render")
    }


    componentWillReceiveProps(nextProps, nextState) {
        console.log("组件属性改变 ", this.props.n, nextProps.n);
        //属性改变也会触发子组件重新渲染,继续完成修改这套流程
    }

    render() {
        console.log("render");
        return <div ref="div" style={{textAlign:"center",fontSize:"30px"}}>{this.state.n}</div>
    }

}

ReactDOM.render(<A/>, root);

文章作者: Jia
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Jia !
  目录