React基础篇(二)

225 阅读19分钟

React基础.png

注:!!!

由于内容较多,分2部分,前半部分:React基础篇(一)

二、React组件化开发

11.使用ES6装饰器语法简化高阶组件的使用

1.下载插件包: @babel/plugin-proposal-decorators

2.配置插件: config-overrides.js中: addDecoratorsLegacy()

const {addDecoratorsLegacy} = require('customize-cra');

module.exports = override(
	...,
  	// 添加装饰器的配置
  	addDecoratorsLegacy()
);

3.使用:

​ 1)装饰connect高阶组件: @connect(null, {search})

​ 2)装饰withRouter高阶组件: @withRouter

​ 3)装饰器语法的本质:

//装饰器语法,让MyClass多了一个a属性
@demo
class MyClass { }
function demo(target) {
  target.a = 1;
}

等同于

//正常语法,让MyClass多了一个a属性
class MyClass { }
function demo(target) {
  target.a = 1;
}
MyClass = demo(MyClass)

12.组件的生命周期 (旧)(了解!)

1-理解

​ 1)组件从创建到死亡它会经历一些特定的阶段。

​ 2)React组件中包含一系列勾子函数(生命周期回调函数), 会在特定的时刻调用。

​ 3)我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作。

2-组件的生命周期 (旧)

1.初始化阶段: 由ReactDOM.render()触发---初次渲染

​ 1) constructor(): 创建组件对象的初始化方法

​ 2) componentWillMount(): 组件即将被装载、渲染到页面上

​ 3) render(): 组件在这里生成虚拟的DOM节点

​ 4) componentDidMount(): 组件真正在被装载之后 => 常用

​ 一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息、只执行一次。

2.更新阶段: 由组件内部this.setSate()或父组件render触发

​ 1) componentWillReceiveProps(): 组件将要接收到属性的时候调用

​ 2) shouldComponentUpdate(): 组件接受到新属性或者新状态的时候(可以返回false,接收数据后不更新,阻止render调用重绘)

​ 3) componentWillUpdate(): 组件即将更新不能修改属性和状态

​ 4) render(): 组件重新描绘 => 必须使用的一个

​ 5) componentDidUpdate(): 组件已经更新

3.卸载组件: 由ReactDOM.unmountComponentAtNode()触发

​ 1) componentWillUnmount(): 组件即将销毁 => 常用

​ 一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息、只执行一次。

图解:

react生命周期(旧).png

实例:

需求: 定义一个求和的组件

点击按钮,上方显示的总和+1

<div id="test"></div>
<script type="text/babel">
    //创建组件
    class Count extends React.Component{
        //构造器
        constructor(props){
            console.log('Count---constructor');
            super(props)
            //初始化状态
            this.state = {count:0}
        }

        //加1按钮的回调
        add = ()=>{
            //获取原状态
            const {count} = this.state
            //更新状态
            this.setState({count:count+1})
        }

        //卸载组件按钮的回调
        death = ()=>{
            ReactDOM.unmountComponentAtNode(document.getElementById('test'))
        }

        //强制更新按钮的回调
        force = ()=>{
            this.forceUpdate()
        }

        //组件将要挂载的钩子
        componentWillMount(){
            console.log('Count---componentWillMount');
        }

        //组件挂载完毕的钩子
        componentDidMount(){
            console.log('Count---componentDidMount');
        }
    
        //组件将要卸载的钩子
        componentWillUnmount(){
            console.log('Count---componentWillUnmount');
        }
    
        //控制组件更新的“阀门”
        shouldComponentUpdate(){
            console.log('Count---shouldComponentUpdate');
            return true
        }
    
        //组件将要更新的钩子
        componentWillUpdate(){
            console.log('Count---componentWillUpdate');
        }
    
        //组件更新完毕的钩子
        componentDidUpdate(){
            console.log('Count---componentDidUpdate');
        }
    
        render(){
            console.log('Count---render');
            const {count} = this.state;
            return(
                <div>
                    <h2>当前求和为:{count}</h2>
                    <button onClick={this.add}>点我+1</button>
                    <button onClick={this.death}>卸载组件</button>
                    <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
                </div>
        	)
        }
    }

    //测试父组件render
    //父组件A
    class A extends React.Component{
        //初始化状态
        state = {carName:'奔驰'}
    
        changeCar = ()=>{
            this.setState({carName:'奥拓'})
        }
    
        render(){
            return(
                <div>
                    <div>我是A组件</div>
                    <button onClick={this.changeCar}>换车</button>
                    <B carName={this.state.carName}/>
    			</div>
    		)
    	}
    }

    //子组件B
    class B extends React.Component{
        //组件将要接收新的props的钩子
        //此钩子有坑,第一次接收props不会调用
        componentWillReceiveProps(props){
            console.log('B---componentWillReceiveProps',props);
        }
    
        //控制组件更新的“阀门”
        shouldComponentUpdate(){
            console.log('B---shouldComponentUpdate');
            return true
        }
        //组件将要更新的钩子
        componentWillUpdate(){
            console.log('B---componentWillUpdate');
        }
    
        //组件更新完毕的钩子
        componentDidUpdate(){
            console.log('B---componentDidUpdate');
        }
    
        render(){
            console.log('B---render');
            return(
                <div>我是B组件,接收到的车是:{this.props.carName}</div>
    		)
    	}
    }

    //渲染组件
    ReactDOM.render(<Count/>,document.getElementById('test'))
    // ReactDOM.render(<A/>,document.getElementById('test'))
</script>

13.组件的生命周期 (新) - 16.4之后(重点!)

1-组件的生命周期 (新)

1.初始化阶段: 由ReactDOM.render()触发---初次渲染

​ 1) constructor()

​ 2) getDerivedStateFromProps()

​ 3) render()

​ 4) componentDidMount() => 常用 ​ 一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息、只执行一次。

3.更新阶段: 由组件内部this.setSate()或父组件重新render触发

​ 1) getDerivedStateFromProps()

​ 2) shouldComponentUpdate()

​ 3) render()

​ 4) getSnapshotBeforeUpdate()

​ 5) componentDidUpdate()

4.卸载组件: 由ReactDOM.unmountComponentAtNode()触发

​ 1) componentWillUnmount() => 常用 ​ 一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息、只执行一次。

2-新旧生命周期的区别

​ 1) 新生命周期废弃了3个钩子(componentWillMount,componentWillUpdate,componentWillReceiveProps),

​ 现在使用会出现警告,下一个大版本需要加上UNSAFE_前缀才能使用,以后可能会被彻底废弃,不建议使用。

UNSAFE_componentWillMount(){
	console.log('---componentWillMount---')
}

UNSAFE_componentWillUpdate(){
	console.log('---componentWillUpdate---')
}

UNSAFE_componentWillReceiveProps(){
	console.log('---componentWillReceiveProps---')
}

​ 2) 新提出了2个钩子 getDerivedStateFromProps 和 getSnapshotBeforeUpdate

图解:

react生命周期(新).png

实例:

需求: 定义一个求和的组件

点击按钮,上方显示的总和+1

<div id="test"></div>
<script type="text/babel">
	//创建组件
	class Count extends React.Component{
        //构造器
        constructor(props){
            console.log('Count---constructor');
            super(props)
            //初始化状态
            this.state = {count:0}
        }

        //加1按钮的回调
        add = ()=>{
            //获取原状态
            const {count} = this.state
            //更新状态
            this.setState({count:count+1})
        }
    
        //卸载组件按钮的回调
        death = ()=>{
            ReactDOM.unmountComponentAtNode(document.getElementById('test'))
        }
    
        //强制更新按钮的回调
        force = ()=>{
            this.forceUpdate()
        }
    		
        //(了解)若state的值在任何时候都取决于props,那么可以使用getDerivedStateFromProps
        //此方法不是实例调用的,必须写static
        //此方法必须返回state对象(跟状态内容一模一样)或null
        static getDerivedStateFromProps(props,state){
            console.log('getDerivedStateFromProps',props,state);
            return null;
            //返回由props得到的state的对象
            // return props;
        }
    
        //在更新之前获取快照
        //此方法必须返回snapshot value(快照值)或null
        getSnapshotBeforeUpdate(){
            console.log('getSnapshotBeforeUpdate');
            return 'atguigu'
        }
    
        //组件挂载完毕的钩子
        componentDidMount(){
            console.log('Count---componentDidMount');
        }
    
        //组件将要卸载的钩子
        componentWillUnmount(){
            console.log('Count---componentWillUnmount');
        }
    
        //控制组件更新的“阀门”
        shouldComponentUpdate(){
            console.log('Count---shouldComponentUpdate');
            return true;
        }
    
        //组件更新完毕的钩子
        componentDidUpdate(preProps,preState,snapshotValue){
            console.log('Count---componentDidUpdate',preProps,preState,snapshotValue);
        }
    		
        render(){
            console.log('Count---render');
            const {count} = this.state
            return(
                <div>
                    <h2>当前求和为:{count}</h2>
                    <button onClick={this.add}>点我+1</button>
                    <button onClick={this.death}>卸载组件</button>
                    <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
                </div>
        	)
        }
    }
    	
    //渲染组件
    ReactDOM.render(<Count count={199}/>,document.getElementById('test'));
</script>

扩展:getSnapShotBeforeUpdate的使用场景

  • getSnapshotBeforeUpdate有什么用?———— 在页面真正发生更新之前,保留下原来页面中DOM的信息

  • 注:

    1.此方法必须配合componentDidUpdate使用。

    2.只有返回的是DOM相关的值,才有意义。

    3.componentDidUpdate会收到三个参数:preProps,preState,snapshotValue

实例:

<div id="test"></div>
<script type="text/babel">
	class NewsList extends React.Component{
        state = {newsArr:[]};

        componentDidMount(){
            setInterval(() => {
                //获取原状态
                const {newsArr} = this.state;
                //模拟一条新闻
                const news = '新闻'+ (newsArr.length+1);
                //更新状态
                this.setState({newsArr:[news,...newsArr]});
            }, 1000);
        }
    
    	//获取快照在更新之前
        getSnapshotBeforeUpdate(){
            return this.refs.list.scrollHeight;
        }

        componentDidUpdate(preProps,preState,height){
            this.refs.list.scrollTop += this.refs.list.scrollHeight - height;
            // this.refs.list.scrollTop += 30;
        }

        render(){
            return(
                <div className="list" ref="list">
                {
                    this.state.newsArr.map((n,index)=>{
                        return <div key={index} className="news">{n}</div>
                    })
        		}
                </div>
        	)
    	}
    }
    ReactDOM.render(<NewsList/>,document.getElementById('test'))
</script>

14.虚拟DOM、DOM diff算法与key的作用(重难点!)

1) 虚拟DOM深入

1.本质是Object类型的对象(一般对象), 准确的说是一个对象树(倒立的)。

2.虚拟DOM保存了真实DOM的层次关系和一些基本属性,与真实DOM一一对应

3.如果只是更新虚拟DOM, 页面是不会重绘的。

2). DOM diff算法的基本步骤(重点!)

1.用JS对象树表示DOM树的结构, 然后用这个树构建一个真正的DOM树插到文档当中。

2.当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。

3.把差异应用到真实DOM树上,视图就更新了。

3) 进一步理解(重点!)

  • Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。

  • 可以类比 CPU 和 硬盘,既然硬盘这么慢,我们就在它们之间加个缓存, 既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。

  • CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

图解:

diff算法原理图.png

4) 验证diff算法的存在

<div id="test"></div>
<script type="text/babel">
	class Time extends React.Component {
		state = {date: new Date()}

        componentDidMount () {
            setInterval(() => {
                this.setState({
                    date: new Date()
                })
            }, 1000)
        }
    
        render () {
            return (
                <div>
                    <h1>hello</h1> //直接复用,不更新
                    <input type="text"/> //直接复用,不更新
                    <span>
                        现在是:{this.state.date.toTimeString()}
        				//此处的input不会更新,直接复用,因为diff比较的最小粒度是标签
    					<input type="text"/>
            		</span>
        		</div>
        	)
        }
    }
    
    ReactDOM.render(<Time/>,document.getElementById('test'))
</script>

5) React中key的作用(重点!)

1). react/vue中的key有什么作用?(key的内部原理是什么?)

2). 为什么遍历列表时,key最好不要用index?

解答:

1.虚拟DOM中key的作用:

​ 1). 简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用。

​ 2). 详细的说: 当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,

​ 随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:

​ a. 旧虚拟DOM中找到了与新虚拟DOM相同的key:

​ (1).若虚拟DOM中内容没变, 直接使用之前的真实DOM

​ (2).若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM

​ b. 旧虚拟DOM中未找到与新虚拟DOM相同的key

​ 根据数据创建新的真实DOM,随后渲染到到页面

2.用index作为key可能会引发的问题:

​ 1) 若对数据进行:逆序添加、逆序删除等破坏顺序操作:

​ 会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。

​ 2) 如果结构中还包含输入类的DOM:

​ 会产生错误DOM更新 ==> 界面有问题。

​ 3) 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,

​ 仅用于渲染列表用于展示,使用index作为key是没有问题的。

3.开发中如何选择key?

​ 1) 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。

​ 2) 如果确定只是简单的展示数据,用index也是可以的。

扩展: diff中key的比较过程

1.使用index索引值作为key

初始数据:

{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},

初始的虚拟DOM:

<li key=0>小张---18<input type="text"/></li>
<li key=1>小李---19<input type="text"/></li>

更新后的数据:

{id:3,name:'小王',age:20},
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},

更新数据后的虚拟DOM:

<li key=0>小王---20<input type="text"/></li>
<li key=1>小张---18<input type="text"/></li>
<li key=2>小李---19<input type="text"/></li>

2.使用id唯一标识作为key

初始数据:

{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},

初始的虚拟DOM:

<li key=0>小张---18<input type="text"/></li>
<li key=1>小李---19<input type="text"/></li>

更新后的数据:

{id:3,name:'小王',age:20},
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},

更新数据后的虚拟DOM:

<li key=3>小王---20<input type="text"/></li>
<li key=1>小张---18<input type="text"/></li>
<li key=2>小李---19<input type="text"/></li>

实例

<div id="test"></div>
<script type="text/babel">
	class Person extends React.Component{

		state = {
			persons:[
				{id:1,name:'小张',age:18},
				{id:2,name:'小李',age:19},
			]
		}
	
		add = ()=>{
			const {persons} = this.state;
			const p = {id:persons.length+1,name:'小王',age:20};
			this.setState({persons:[p,...persons]});
		}
	
		render(){
			return (
				<div>
					<h2>展示人员信息</h2>
					<button onClick={this.add}>添加一个小王</button>
					<h3>使用index(索引值)作为key</h3>
					<ul>
						{
							this.state.persons.map((personObj,index)=>{
								return <li key={index}>{personObj.name}---{personObj.age}<input type="text"/></li>
							})
						}
					</ul>
					<hr/>
					<hr/>
					<h3>使用id(数据的唯一标识)作为key</h3>
					<ul>
						{
							this.state.persons.map((personObj)=>{
								return <li key={personObj.id}>{personObj.name}---{personObj.age}<input type="text"/></li>
							})
						}
					</ul>
				</div>
			)
		}
	}
	
	ReactDOM.render(<Person/>,document.getElementById('test'));
</script>

15.使用React开发者工具调试

  • chrome应用商店安装React Developer Tools

  • 浏览器调试工具在用react开发的网页中会出现Components选项,用于开发者进行组件调试

16.React性能优化

16.1组件优化 - 类组件

1.Component的2个问题

1)只要执行setState(), 即使不改变状态数据, 组件也会重新render() => 效率低

  • 如:this.setState({})

2)只当前组件重新render(), 就会自动重新render子组件,即使子组件没有用到父组件的任何数据 => 效率低

2.原因

  • Component中的 shouldComponentUpdate() 总是返回 true

3.效率高的做法

  • 只有当组件的state或props数据发生改变时才重新render()

4.解决

1)方法1 :重写shouldComponentUpdate(nextProps, nextState)方法

  • 比较新旧 state 或 props 数据, 如果有变化才返回 true, 如果没有返回 false

2)方法2 :将类继承PureComponent

  • PureComponent重写了shouldComponentUpdate(), 只有state或props数据有变化才返回true

  • 但只是进行state和props数据的浅比较, 如果只是数据对象内部数据变了, 返回false

  • 所以不要直接修改state数据, 而是要产生新数据

注:

  • 项目中一般使用PureComponent来优化

实例

import React, { PureComponent } from 'react'
import './index.css'

export default class Parent extends PureComponent {

	state = {carName:"奔驰c36",stus:['小张','小李','小王']}

	addStu = ()=>{
		/* const {stus} = this.state
		stus.unshift('小刘')
		this.setState({stus}) */

		const {stus} = this.state
		this.setState({stus:['小刘',...stus]})
	}

	changeCar = ()=>{
		//this.setState({carName:'迈巴赫'})

		const obj = this.state
		obj.carName = '迈巴赫'
		console.log(obj === this.state);
		this.setState(obj)
	}

	/* shouldComponentUpdate(nextProps,nextState){
		// console.log(this.props,this.state); //目前的props和state
		// console.log(nextProps,nextState); //接下要变化的目标props,目标state
		return !this.state.carName === nextState.carName
	} */

	render() {
		console.log('Parent---render');
		const {carName} = this.state
		return (
			<div className="parent">
				<h3>我是Parent组件</h3>
				{this.state.stus}&nbsp;
				<span>我的车名字是:{carName}</span><br/>
				<button onClick={this.changeCar}>点我换车</button>
				<button onClick={this.addStu}>添加一个小刘</button>
				<Child carName="奥拓"/>
			</div>
		)
	}
}

class Child extends PureComponent {

	/* shouldComponentUpdate(nextProps,nextState){
		console.log(this.props,this.state); //目前的props和state
		console.log(nextProps,nextState); //接下要变化的目标props,目标state
		return !this.props.carName === nextProps.carName
	} */

	render() {
		console.log('Child---render');
		return (
			<div className="child">
				<h3>我是Child组件</h3>
				<span>我接到的车是:{this.props.carName}</span>
			</div>
		)
	}
}

16.2高阶组件memo - 函数式组件优化

1.memo()包裹函数组件,返回一个优化后的新组件

import React, { PureComponent, memo } from 'react';

// Header
const MemoHeader = memo(function Header() {
  console.log("Header被调用");
  return <h2>我是Header组件</h2>
})

class Banner extends PureComponent {
  render() {
    console.log("Banner render函数被调用");
    return <h3>我是Banner组件</h3>
  }
}

const MemoProductList = memo(function ProductList() {
  console.log("ProductList被调用");
  return (
    <ul>
      <li>商品列表1</li>
      <li>商品列表2</li>
      <li>商品列表3</li>
      <li>商品列表4</li>
      <li>商品列表5</li>
    </ul>
  )
})

// Main
class Main extends PureComponent {
  render() {
    console.log("Main render函数被调用");
    return (
      <div>
        <Banner/>
        <MemoProductList/>
      </div>
    )
  }
}

// Footer
const MemoFooter = memo(function Footer() {
  console.log("Footer被调用");
  return <h2>我是Footer组件</h2>
})

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    }
  }

  render() {
    console.log("App render函数被调用");
    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <MemoHeader/>
        <Main/>
        <MemoFooter/>
      </div>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}

17.Fragment

1.使用

<Fragment><Fragment>  只能设置key属性

或

<></>  不能传任何属性

2.作用

  • 可以不用必须有一个真实的DOM根标签了

3.特点

  • 此组件不会加载到dom中

实例

export default class Demo extends Component {
	render() {
		return (
			<Fragment key={1}>
				<input type="text"/>
				<input type="text"/>
			</Fragment>
		)
	}
}

18.Portals的使用

  • 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上的)。
  • Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
    • ReactDOM.createPortal(child, container);
    • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
    • 第二个参数(container)是一个 DOM 元素;
  • 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:
  • 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的。

实例:

1.默认情况

render() {
    return (
        //React挂载了一个新的div,并且把子元素渲染其中
        <div>
            {this.props.children}
        </div>
    );
}

2.Portals的使用

render() {
    return ReactDOM.createPortal(
        this.props.children,
        domNode
    );
}

Modal组件案例

  • 开发一个Modal组件,它可以将它的子组件渲染到屏幕的中间位置:
    • 步骤一:修改index.html添加新的节点
    • 步骤二:编写这个节点的样式
    • 步骤三:编写组件代码
//index.css
#modal {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

//index.html
<div id="root"></div>
<div id="modal"></div>

//app.js
import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';
import "./css/index.css";

class Modal extends PureComponent {
  render() {
    return ReactDOM.createPortal(
      this.props.children,
      document.getElementById("modal")
    )
  }
}

class Home extends PureComponent {
  render() {
    return (
      <div>
        <h2>Home</h2>
        <Modal>
          <h2>Title</h2>
        </Modal>
      </div>
    )
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Home/>
      </div>
    )
  }
}

19.StrictMode

  • StrictMode 是一个用来突出显示应用程序中潜在问题的工具。
    • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
    • 它为其后代元素触发额外的检查和警告;
    • 严格模式检查仅在开发模式下运行;它们不会影响生产构建;
  • 可以为应用程序的任何部分启用严格模式:
    • 不会对 Header 和 Footer 组件运行严格模式检查;
    • 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查;
<Header />
<React.StrictMode>
    <div>
    	<ComponentOne />
        <ComponentTwo />
    </div>
</React.StrictMode>
<Footer />
  • 严格模式检查的是什么?

    • 1.识别不安全的生命周期

    • 2.使用过时的ref API

    • 3.使用废弃的findDOMNode方法

      • 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了
    • 4.检查意外的副作用

      • 这个组件的constructor会被调用两次;

      • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;

      • 在生产环境中,是不会被调用两次的;

    • 5.检测过时的context API

      • 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的;

      • 目前这种方式已经不推荐使用

三、React中的样式

1.组件化天下的CSS

  • 整个前端已经是组件化的天下:

    • 而CSS的设计就不是为组件化而生的,所以在目前组件化的框架中都在需要一种合适的CSS解决方案。
  • 在组件化中选择合适的CSS解决方案应该符合以下条件:

    • 可以编写局部css:css具备自己的具备作用域,不会随意污染其他组件内的原生;
    • 可以编写动态的css:可以获取当前组件的一些状态,根据状态的变化生成不同的css样式;
    • 支持所有的css特性:伪类、动画、媒体查询等;
    • 编写起来简洁方便、最好符合一贯的css风格特点;
    • ...

2.React中的CSS

  • 事实上,css一直是React的痛点,也是被很多开发者吐槽、诟病的一个点。

  • 在这一点上,Vue做的要确实要好于React:

    • Vue通过在.vue文件中编写 < style >< /style > 标签来编写自己的样式;

    • 通过是否添加 scoped 属性来决定编写的样式是全局有效还是局部有效;

    • 通过 lang 属性来设置你喜欢的 less、sass等预处理器;

    • 通过内联样式风格的方式来根据最新状态设置和改变css;

    • ...

  • Vue在CSS上虽然不能称之为完美,但是已经足够简洁、自然、方便了,至少统一的样式风格不会出现多个开发人员、多个项目采用不一样的样式风格。

  • 相比而言,React官方并没有给出在React中统一的样式风格:

    • 由此,从普通的css,到css modules,再到css in js,有几十种不同的解决方案,上百个不同的库;

    • 大家一致在寻找最好的或者说最适合自己的CSS方案,但是到目前为止也没有统一的方案;

3.内联样式

  • 内联样式是官方推荐的一种css样式的写法:

    • style 接受一个采用小驼峰命名属性的 JavaScript 对象,而不是 CSS 字符串;

    • 并且可以引用state中的状态来设置相关的样式;

  • 内联样式的优点:

    • 1.内联样式, 样式之间不会有冲突

    • 2.可以动态获取当前state中的状态

  • 内联样式的缺点:

    • 1.写法上都需要使用驼峰标识

    • 2.某些样式没有提示

    • 3.大量的样式, 代码混乱

    • 4.某些样式无法编写(比如伪类/伪元素)

  • 所以官方依然是希望内联合适和普通的css来结合编写;

import React, { PureComponent } from 'react'

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      color: "purple"
    }
  }

  render() {
    const pStyle = {
      color: this.state.color,
      textDecoration: "underline"
    }

    return (
      <div>
        <h2 style={{fontSize: "50px", color: "red"}}>我是标题</h2>
        <p style={pStyle}>我是一段文字描述</p>
      </div>
    )
  }
}

4.普通的css

  • 普通的css我们通常会编写到一个单独的文件,之后再进行引入。

  • 这样的编写方式和普通的网页开发中编写方式是一致的:

    • 如果我们按照普通的网页标准去编写,那么也不会有太大的问题;

    • 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;

    • 但是普通的css都属于全局的css,样式之间会相互影响;

  • 这种编写方式最大的问题是样式之间会相互层叠掉;

// Profile/style.css
.profile .title {
  color: yellow;
}

// Profile/index.js
import React, { PureComponent } from 'react';
import './style.css';

export default class Profile extends PureComponent {
  render() {
    return (
      <div className="profile">
          <h2 className="title">我是Profile的标题</h2>
          <ul className="settings">
            <li>设置列表1</li>
            <li>设置列表2</li>
            <li>设置列表3</li>
          </ul>
      </div>
    )
  }
}


// Home/style.css
.home .title {
  font-size: 30px;
  color: red;
}

.home .banner {
  color: orange;
}

// Home/index.js
import React, { PureComponent } from 'react';
import './style.css';

export default class Home extends PureComponent {
  render() {
    return (
      <div className="home">
        <h2 className="title">我是home的标题</h2>
        <div className="banner">
          <span>轮播图</span>
        </div>
      </div>
    )
  }
}


// App/style.css
#app > .title {
  color: blue;
}

// App/index.js
import React, { PureComponent } from 'react';
import './style.css';

import Home from '../home';
import Profile from '../profile';

export default class App extends PureComponent {
  render() {
    return (
      <div id="app">
        App
        <h2 className="title">我是App的title</h2>
        <Home/>
        <Profile/>
      </div>
    )
  }
}

5.css modules

  • css modules并不是React特有的解决方案,而是所有使用了类似于webpack配置的环境下都可以使用的。

    • 但是,如果在其他项目中使用,那么我们需要自己来进行配置,比如配置webpack.config.js中的modules: true等。
  • React的脚手架已经内置了css modules的配置:

    • .css/.less/.scss 等样式文件都修改成 .module.css/.module.less/.module.scss 等;

    • 之后就可以引用并且进行使用了;

  • css modules确实解决了局部作用域的问题,也是很多人喜欢在React中使用的一种方案。

  • 但是这种方案也有自己的缺陷:

    • 引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的;

    • 所有的className都必须使用{style.className} 的形式来编写;

    • 不方便动态来修改某些样式,依然需要使用内联样式的方式;

  • 如果你觉得上面的缺陷还算OK,那么你在开发中完全可以选择使用css modules来编写,并且也是在React中很受欢迎的一种方式。

// Profile/style.module.css
.title {
  color: yellow;
}

.setting {

}

.settingItem {

}

// Profile/index.js
import React, { PureComponent } from 'react';
import style from './style.module.css';

export default class Profile extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      color: "purple"
    }
  }

  render() {
    return (
      <div className="profile">
        <h2 className={style.title} style={{color: this.state.color}}>我是Profile的标题</h2>
        <ul className={style.settings}>
          <li className={style.settingItem}>设置列表1</li>
          <li>设置列表2</li>
          <li>设置列表3</li>
        </ul>
      </div>
    )
  }
}


// Home/.module.css
.title {
  font-size: 30px;
  color: red;
}

.banner {
  color: orange;
}

// Home/index.js
import React, { PureComponent } from 'react';
import homeStyle from './style.module.css';

export default class Home extends PureComponent {
  render() {
    return (
      <div className="home">
        <h2 className={homeStyle.title}>我是home的标题</h2>
        <div className={homeStyle.banner}>
          <span>轮播图</span>
        </div>
      </div>
    )
  }
}


// App/.module.css
.title {
  color: blue;
}

// App/index.js
import React, { PureComponent } from 'react';
import appStyle from './style.module.css';

import Home from '../home';
import Profile from '../profile';

export default class App extends PureComponent {
  render() {
    return (
      <div id="app">
        App
        <h2 className={appStyle.title}>我是App的title</h2>
        <Home/>
        <Profile/>
      </div>
    )
  }
}

6.认识CSS in JS(推荐!)

  • 实际上,官方文档也有提到过CSS in JS这种方案:

    • “CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义;

    • 注意此功能并不是 React 的一部分,而是由第三方库提供。 React 对样式如何定义并没有明确态度;

  • 在传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离。

    • 但是在前面的学习中,我们就提到过,React的思想中认为逻辑本身和UI是无法分离的,所以才会有了JSX的语法。

    • 样式呢?样式也是属于UI的一部分;

    • 事实上CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态;

    • 所以React有被人称之为 All in JS;

  • 当然,这种开发的方式也受到了很多的批评:

7.认识styled-components(推荐!)

  • 批评声音虽然有,但是在我们看来很多优秀的CSS-in-JS的库依然非常强大、方便:

    • CSS-in-JS通过JavaScript来为CSS赋予一些能力,包括类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用、动态修改状态等等;

    • 依然CSS预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点;

    • 所以,目前可以说CSS-in-JS是React编写CSS最为受欢迎的一种解决方案;

  • 目前比较流行的CSS-in-JS的库有哪些呢?

    • styled-components

    • emotion

    • glamorous

  • 目前可以说styled-components依然是社区最流行的CSS-in-JS库,所以我们以styled-components的讲解为主;

  • 安装styled-components:

    • yarn add styled-components

8.styled-components原理 - ES6标签模板字符串

  • ES6中增加了模板字符串的语法,这个对于很多人来说都会使用。

  • 但是模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。

  • 一个普通的JavaScript的函数:

  • 正常情况下,我们都是通过 函数名() 方式来进行调用的,其实函数还有另外一种调用方式:

// 1.模板字符串的基本使用
const name = "why";
const age = 18;
const message = `my name is ${name}`;

// 2.标签模板字符串: 可以通过模板字符串的方式对一个函数进行调用
function test(...args) {
    console.log(args);
}

// test("aaa", "ccc");

// test`aaaa`;
test`my name is ${name}, age is ${age}`;

test`
    font-size: 15px;
    color: red;
`
  • 如果我们在调用的时候插入其他的变量:

    • 模板字符串被拆分了;

    • 第一个元素是数组,是被模块字符串拆分的字符串组合;

    • 后面的元素是一个个模块字符串传入的内容;

  • 在styled component中,就是通过这种方式来解析模块字符串,最终生成我们想要的样式的

9.styled的基本使用

  • styled-components的本质是通过函数的调用,最终创建出一个组件

    • 这个组件会被自动添加上一个不重复的class;

    • styled-components会给该class添加相关的样式;

  • 另外,它支持类似于CSS预处理器一样的样 式嵌套:

    • 支持直接子代选择器或后代选择器,并且直接编写样式;

    • 可以通过&符号获取当前元素;

    • 直接伪类选择器、伪元素等;

10.props、attrs属性

1.props可以穿透

<HYInput type="password" color={this.state.color}/>

2.props可以被传递给styled组件

  • 获取props需要通过${}传入一个插值函数,props会作为该函数的参数;

  • 这种方式可以有效的解决动态样式的问题;

3.添加attrs属性

const HYInput = styled.input.attrs({
  placeholder: "coderwhy",
  bColor: "red"
})`
  background-color: lightblue;
  border-color: ${props => props.bColor};
  color: ${props => props.color};
`

11.styled高级特性

1.支持样式的继承

2.styled设置主题

12.styled-components的综合案例

// Profile/index.js
import React, { PureComponent } from 'react';
import styled from 'styled-components';

/**
 * 特点:
 *  1.props穿透
 *  2.attrs的使用
 *  3.传入state作为props属性
 */

//2.attrs的使用
const HYInput = styled.input.attrs({
  placeholder: "coderwhy",
  bColor: "red"
})`
  background-color: lightblue;
  border-color: ${props => props.bColor};
  color: ${props => props.color};
`

export default class Profile extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      color: "purple"
    }
  }

  render() {
    return (
      <div>
        <input type="password"/>
        {/* 
            1.props穿透
            3.传入state作为props属性
        */}
        <HYInput type="password" color={this.state.color}/>
        <h2>我是Profile的标题</h2>
        <ul>
          <li>设置列表1</li>
          <li>设置列表2</li>
          <li>设置列表3</li>
        </ul>
      </div>
    )
  }
}


// Home/style.js
import styled from 'styled-components';

export const HomeWrapper = styled.div`
  font-size: 12px;
  color: red;

  .banner {
    background-color: blue;

    span {
      color: #fff;

      &.active {
        color: red;
      }

      &:hover {
        color: green;
      }

      &::after {
        content: "aaa"
      }
    }

    /* .active {
      color: #f00;
    } */
  }
`

export const TitleWrapper = styled.h2`
  text-decoration: underline;
  color: ${props => props.theme.themeColor};
  font-size: ${props => props.theme.fontSize};
`


// Home/index.js
import React, { PureComponent } from 'react';

import { 
  HomeWrapper,
  TitleWrapper
} from "./style";

export default class Home extends PureComponent {
  render() {
    return (
      <HomeWrapper>
        <TitleWrapper>我是home的标题</TitleWrapper>
        <div className="banner">
          <span>轮播图1</span>
          <span className="active">轮播图2</span>
          <span>轮播图3</span>
          <span>轮播图4</span>
        </div>
      </HomeWrapper>
    )
  }
}


// App/index.js
import React, { PureComponent } from 'react';
import Home from '../home';
import Profile from '../profile';
import styled, { ThemeProvider } from 'styled-components';

const HYButton = styled.button`
  padding: 10px 20px;
  border-color: red;
  color: red;
`

// const HYPrimaryButton = styled.button`
//   padding: 10px 20px;
//   border-color: red;
//   color: #fff;
//   background-color: green;
// `

//样式继承
const HYPrimaryButton = styled(HYButton)`
  color: #fff;
  background-color: green;
`

export default class App extends PureComponent {
  render() {
    return (
      //styled设置主题
      <ThemeProvider theme={{themeColor: "red", fontSize: "30px"}}>
        <Home />
        <hr />
        <Profile />
        <HYButton>我是普通的按钮</HYButton>
        <HYPrimaryButton>我是主要的按钮</HYPrimaryButton>
      </ThemeProvider>
    )
  }
}

13.React中添加class

1.React在JSX给了我们开发者足够多的灵活性,你可以像编写JavaScript代码一样,通过一些逻辑来决定是否添加某些class:

  • 原生React中添加class方法
{/* 原生React中添加class方法 */}
<h2 className={"foo bar active title"}>我是标题1</h2>
<h2 className={"title" + (isActive ? " active": "")}>我是标题2</h2>
<h2 className={["title", (isActive ? "active": "")].join(" ")}>我是标题3</h2>

2.使用第三方的库:classnames ,来简化写法

  • 类似于vue中添加class方法
const {isActive} = this.state;
const isBar = false;
const errClass = "error";
const warnClass = 10;

{/* classnames库添加class */}
<h2 className="foo bar active title">我是标题4</h2>
<h2 className={classNames("foo", "bar", "active", "title")}>我是标题5</h2>
<h2 className={classNames({"active": isActive, "bar": isBar}, "title")}>我是标题6</h2>
<h2 className={classNames("foo", errClass, warnClass, {"active": isActive})}>我是标题7</h2>
<h2 className={classNames(["active", "title"])}>我是标题8</h2>
<h2 className={classNames(["active", "title", {"bar": isBar}])}>我是标题9</h2>