原文出處

前置知识

1. 模块化与组件化的理解

模块
  • 理解:向外提供特定功能的js程序, 一般就是一个js文件
  • 为什么要拆成模块:随着业务逻辑增加,代码越来越多且复杂
  • 作用:复用js, 简化js的编写, 提高js运行效率
组件
  • 理解:用来实现局部功能效果的代码和资源的集合(html/css/js/image等等)
  • 为什么要用组件: 一个界面的功能更复杂
  • 作用:复用编码, 简化项目编码, 提高运行效率
模块化
  • 当应用的js都以模块来编写的, 这个应用就是一个模块化的应用
组件化
  • 当应用是以多组件的方式实现, 这个应用就是一个组件化的应用

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

Snipaste_2023-09-13_13-25-20.png

一、组件的使用

React 是基于组件的方式进行用户界面开发的. 组件可以理解为对页面中某一块区域的封装。

ab4a0efe5ad04ffd90c35cb602cc43d1~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.png 注意事项

  1. 组件名称首字母必须大写,用以区分组件和普通标签。
  2. jsx语法外层必须有一个根元素

1.函数式组件

//1.先创建函数,函数可以有参数,也可以没有,但是必须要有返回值 返回一个虚拟DOM
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
//2.进行渲染
ReactDOM.Render(<Welcom name = "yunmu" />,document.getElementById("div"));

上面的代码经历了以下几步

  1. 我们调用 ReactDOM.render() 函数,并传入 <Welcome name="yunmu" /> 作为参数。
  2. React 调用 Welcome 组件,并将 {name: 'yunmu'} 作为 props 传入。
  3. Welcome 组件将 Hello, yunmu 元素作为返回值。
  4. React DOM 将 DOM 高效地更新为 Hello,yunmu

2.类式组件

f0252cf61d814a03b938b06c38161ada~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.png

class MyComponent extends React.Component {
    state = {isHot:true}
    render() {
        const {isHot} = this.state
        return <h1 onClick={this.changeWeather}>今天天气很{isHot?'炎热':'凉爽'}</h1>
        //this.changeWeather = this.changeWeather.bind(this);
    }
    changeWeather = () => {
        const isHot = this.state.isHot
        this.setState({isHot:!isHot})
    }
}
ReactDOM.render(<MyComponent/>,document.querySelector('.test'))

组件自定义方法中由于开启了严格模式,this 指向 undefined 如何解决

  • 通过 bind 改变 this 指向
  • 推荐采用箭头函数,箭头函数的 this 指向

执行了ReactDOM.render(<MyComponent/>.......之后,发生了什么?

  1. React解析组件标签,找到了MyComponent组件。
  2. 发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法。
  3. 将render返回的虚拟DOM转为真实DOM,随后呈现在页面中。

3.其他知识

包含表单元素的组件分为非受控租价与受控组件

受控组件

使 React 的 state 成为“唯一数据源” 表单控件中的值由组件的 state 对象来管理,state对象中存储的值和表单控件中的值时同步状态的


class Login extends React.Component{
    //初始化状态
    state = {
        username:'', //用户名
        password:'' //密码
    }

    //保存用户名到状态中
    saveUsername = (event)=>{
        this.setState({username:event.target.value})
    }

    //保存密码到状态中
    savePassword = (event)=>{
        this.setState({password:event.target.value})
    }

    //表单提交的回调
    handleSubmit = (event)=>{
        event.preventDefault() //阻止表单提交
        const {username,password} = this.state
        alert(`你输入的用户名是:${username},你输入的密码是:${password}`)
    }

    render(){
        return(
            <form onSubmit={this.handleSubmit}>
                用户名:<input onChange={this.saveUsername} type="text" />
                密码:<input onChange={this.savePassword} type="password"/>
                <button>登录</button>
            </form>
        )
    }
}
非受控组件

表单元素的值由 DOM 元素本身管理。

class Login extends React.Component {
    handleSubmit = (event) => {
        event.preventDefault()  //阻止表单提交
        const { username, password } = this
        alert(`你输入的用户名是:${username},你输入的密码是:${password}`)
    };
    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                用户名:
                <input ref={(c) => (this.username = c)} type="text" />
                密码:
                <input ref={(c) => (this.password = c)} type="password" />
                <button>登录</button>
            </form>
        );
    }
}

二、组件实例的三大核心属性

1.state

  • state是组件对象最重要的属性, 值是对象(可以包含多个key-value的组合)
  • 组件被称为"状态机", 通过更新组件的state来更新对应的页面显示(重新渲染组件)

45589ddf756d409a9421778398ea8ccf~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.png

在类式组件中定义state

  • 在构造器中初始化state
  • 在类中添加属性state来初始化

使用的时候通过this.state调用state里的值

修改 state

在类式组件的函数中,直接修改state

this.state.weather = '凉爽'

页面的渲染靠的是render函数 这时候会发现页面内容不会改变,原因是 React 中不建议 state不允许直接修改,而是通过类的原型对象上的方法 setState()

setState()
this.setState(partialState, [callback]);
  • partialState: 需要更新的状态的部分对象
  • callback: 更新完状态后的回调函数

更新的状态的两种写法

写法一:

this.setState({
    weather: "凉爽"
})

写法二:

// 传入一个函数,返回x需要修改成的对象,参数为当前的 state
this.setState(state => ({count: state.count+1});
  • setState是一种合并操作,不是替换操作

  • 在执行 setState操作后,React 会自动调用一次 render()

  • render() 的执行次数是 1+n (1 为初始化时的自动调用,n 为状态更新的次数)

注意:在组件生命周期或React合成事件中,setState是异步 在setTimeout或者原生dom事件中,setState是同步

2. props

在调用组件时可以向组件内部传递数据,在组件中可以通过 props 对象获取外部传递进来的数据。

类式组件中使用
class Person extends React.Component {
    render() {
        // const { name, age, sex } = this.props;
        return (
            <ul>
                <li>姓名:{this.props.name}</li>
                <li>性别:{this.props.sex}</li>
                <li>年龄:{this.props.age}</li>
            </ul>
        );
    }
}
ReactDOM.render(
    <Person name="yunmu" age={19} sex="男" />,
    document.getElementById("test1")
);

const p = { name: "linmeimei", age: 18, sex: "女" };
ReactDOM.render(
    // 这里的{}是代表分隔符  这里只是...p
    <Person {...p} />,
    document.getElementById("test2")
);
对props进行限制

组件内部不要修改props数据(单向数据流)

class Person extends React.Component {
    render() {
        // const { name, age, sex } = this.props;
        return (
            <ul>
                <li>姓名:{this.props.name}</li>
                <li>性别:{this.props.sex}</li>
                <li>年龄:{this.props.age}</li>
            </ul>
        );
    }
}

// 对标签属性进行类型、必要性的限制
Person.propTypes = {
    name:PropTypes.string.isRequired, // 限制name必传,且为字符串
    sex:PropTypes.string, // 限制sex为字符串
    age:PropTypes.number, // 限制age为数值
    speak:PropTypes.func, // 限制speak为函数
}

//指定默认标签属性值
Person.defaultProps = {
    sex:'男', // sex默认值为男
    age:18  // age默认值为18
}

ReactDOM.render(
    <Person name="yunmu" age={19} sex="男" />,
    document.getElementById("test1")
);
函数式组件中的使用

函数在使用props的时候,是作为参数进行使用的(props)

function Person (props){
    // const { name,age,sex } = props
    return (
        <ul>
            <li>姓名:{props.name}</li>
            <li>性别:{props.sex}</li>
            <li>年龄:{props.age}</li>
        </ul>
    )
}
Person.propTypes = {
    name:PropTypes.string.isRequired, // 限制name必传,且为字符串
    sex:PropTypes.string,// 限制sex为字符串
    age:PropTypes.number,// 限制age为数值
}

//指定默认标签属性值
Person.defaultProps = {
    sex:'男',//sex默认值为男
    age:18 //age默认值为18
}
//渲染组件到页面
ReactDOM.render(<Person name="yunmu"/>,document.getElementById('test1'))
组件的children

通过 props.children 属性可以获取到在调用组件时填充到组件标签内部的内容。

<Person>组件内部的内容</Person>
const Person = (props) => {
    return (
    	<div>{props.children}</div>
    );
}
单向数据流

在React中, 关于数据流动有一条原则, 就是单向数据流动, 自顶向下, 从父组件到子组 单向数据流特性要求我们共享数据要放置在上层组件中 子组件通过调用父组件传递过来的方法更改数 当数据发生更改时, React会重新渲染组件树 单向数据流使组件之间的数据流动变得可预测. 使得定位程序错误变得简单

9cfdaa341c0a47a9807f31ccb04f549f~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.png

双向数据绑定

双向数据绑定是指,组件中更新了状态,DOM 状态同步更新 DOM 更改了状态,组件中同步更新。 组件 <=> 视图。 要实现双向数据绑定需要用到表单元素和 state 状态对象。

class App extends Component {
    constructor () {
        this.state = {
            name: "张三"
        }
        this.nameChanged = this.nameChanged.bind(this)
    }
    nameChanged (event) {
        this.setState({name: event.target.value});
    }
    render() {
        return (
            <div>
                <div>{this.state.name}</div>
                <Person name={this.state.name} changed={this.nameChanged}/>
            </div>
        )
    }
}
const Person = props => {
	return <input type="text" value={props.name} onChange={props.changed}/>;
}

3.refs

在我们正常的操作节点时,需要采用DOM API 来查找元素,但是这样违背了 React 的理念,因此有了refs 有三种操作refs的方法,分别为:

  • 字符串形式
  • 回调形式
  • createRef形式
字符串形式refs

不推荐使用,在严格模式下报错。

class Demo extends React.Component{
    //展示左侧输入框的数据
    showData = ()=>{
        const {input1} = this.refs
        alert(input1.value)
    }
    
    //展示右侧输入框的数据
    showData2 = ()=>{
        const {input2} = this.refs
        alert(input2.value)
    }

    render(){
        return(
            <div>
                <input ref="input1" type="text" placeholder="点击按钮提示数据"/>&nbsp;
                <button onClick={this.showData}>点我提示左侧的数据</button>&nbsp;
                <input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
            </div>
        )
    }
}
回调函数形式的ref

组件实例的ref属性传递一个回调函数c => this.input1 = c 这样会在实例的属性中存储对DOM节点的引用,使用时可通过this.input1来使用

class Input extends Component {
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input = input)} />
        <button onClick={() => console.log(this.input)}>button</button>
      </div>
    )
  }
}
回调ref中回调执行次数的问题
  • 更新过程会执行两次 第一次传入参数为null 第二次传入参数是DOM元素
  • 定义成class的绑定函数即可以避免上述问题
saveInput = (c)=>{
    this.input1 = c;
    console.log('@',c);
}

<button onClick={this.showInfo}>点我提示输入的数据</button>
createRef 形式(推荐写法)

React 给我们提供了一个相应的API,它会自动的将该 DOM 元素放入实例对象中

class Input extends Component {
    // React.createRef调用后可以返回一个容器,该容器可以存储被ref所标识的节点
    inputRef = React.createRef()
    focusInput = () => {
        //this.inputRef.current就是对应的元素节点
        this.inputRef.current.focus()
    }
    render() {
        return (
            <div>
                <input type="text" ref={this.inputRef} />
            </div>
        )
    }
}

4. 事件处理

  1. React 使用的是自定义事件,通过onXxx属性指定事件处理函数(注意大小写),而不是原生的 DOM 事件(为了更好的兼容)
  2. React 的事件是通过事件委托方式处理的(为了更加的高效)
  3. 可以通过事件的 event.target获取发生的 DOM 元素对象(减少 refs的使用)
class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            message: "你好啊",
            counter: 100,
        };

        this.btnClick = this.btnClick.bind(this);
    }

    render() {
        return (
            <div>
                {/* 1.方案一: bind绑定this(显示绑定) */}
                <button onClick={this.btnClick}>按钮1</button>

                {/* 2.方案二: 定义函数时, 使用箭头函数 */}
                <button onClick={this.increment}>+1</button>

                {/* 3.方案三(推荐): 直接传入一个箭头函数, 在箭头函数中调用需要执行的函数*/}
                <button  onClick={() => {this.decrement("yun");}} > -1</button>
            </div>
        );
    }
    // 1.方案一: bind绑定this(显示绑定) 
    btnClick() {
        console.log(this.state.message);
    }
    // 2.方案二: 定义函数时, 使用箭头函数 
    increment = () => {
        console.log(this.state.counter);
    };
    // 3.方案三(推荐): 直接传入一个箭头函数, 在箭头函数中调用需要执行的函数
    decrement(name) {
        console.log(this.state.counter, name);
    }
}

三、函数高级

纯函数

  • 一类特别的函数: 只要是同样的输入(实参),必定得到同样的输出(返回)
  • 必须遵守以下一些约束
    • 不得改写参数数据
    • 不会产生任何副作用,例如网络请求,输入和输出设备
    • 不能调用Date.now()或者Math.random()等不纯的方法
  • redux的reducer函数必须是一个纯函数

高阶函数

  • 接受一个或多个函数作为输入
  • 输出一个函数

函数作为参数

// filter
function filter (array, fn) {
    let results = []
    for (let i = 0; i < array.length; i++) {
        if (fn(array[i])) {
           results.push(array[i])
        }
    }
    return results
}

函数作为返回值

function once (fn) {
    let done = false
    return function () {
        if (!done) {
            done = true
            return fn.apply(this, arguments)
        }
    }
}

let pay = once(function (money) {
    console.log(`支付:${money} RMB`)
})
// 只会支付一次
pay(5)
pay(5)
pay(5)

使用高阶函数的意义

  • 抽象可以帮我们屏蔽细节,只需要关注与我们的目标
  • 高阶函数是用来抽象通用的问题
// 面向过程的方式
let array = [1, 2, 3, 4]
for (let i = 0; i < array.length; i++) {
    console.log(array[i])
}

// 高阶高阶函数
let array = [1, 2, 3, 4]
forEach(array, item => {
    console.log(item)
})
let r = filter(array, item => {
    return item % 2 === 0
})

常用高阶函数 forEachmapfiltereverysomefind/findIndexreducesort ...

函数柯里化

  • 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
  • 然后返回一个新的函数接收剩余的参数,返回结果

使用柯里化的意义

参数复用

我们先看一段短短的代码,这段代码中,实现了输入输出个人信息的功能,通过printInfo函数将参数拼接返回,这实际上很简单,但是当用很多很多的用户信息时,需要一直传递着个人信息这个参数,这样显然是不合理的

function printInfo(inf, name, age) {
    return `${inf}:${name}${age}`
}
const myInfo1 = myInfo('个人信息', 'yunmu', '19')
console.log(myInfo1); // 个人信息:yunmu19

下面我们通过柯里化技术来解决

function printInfoCurry(inf) {
    return (name, age) => {
        return `${inf}:${name}${age}`
    }
}
let myInfoName = myInfoCurry('个人信息')
const myInfo1 = myInfoName('yunmu', '19')
const myInfo2 = myInfoName('linmeimei','19')
console.log(myInfo2); // 个人信息:yun19
console.log(myInfo1); // 个人信息:yunmu19

提前返回

这个特性是用来对浏览器的监听事件兼容性做一些判断并初始化,解决有些浏览器对addEventListener存在的兼容性问题,所以在使用之前做一次判断,之后就可以省略了

const whichEvent = (function () {
    if (window.addEventListener) {
        return function (ele, type, listener, useCapture) {
            ele.addEventListener(type, function (e) {
                listener.call(ele, e)
            }, useCapture)
        }
    } else if (window.attachEvent) {
        return function (ele, type, handler) {
            ele.attachEvent('on' + type, function (e) {
                handler.call(ele, e)
            })
        }
    }
})()

总结

  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
  • 这是一种对函数参数的'缓存'
  • 让函数变的更灵活,让函数的粒度更小
  • 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能

偏函数

当一个函数有很多参数时,调用该函数就需要提供多个参数 如果可以减少参数的个数,就能简化该函数的调用,降低调用该函数的难度

实现3个数求和

function sum(a, b, c){
	return a + b + c;
}
sum(1, 2, 3) // 6

在调用时我们需要传入3个参数,好像有些许麻烦,下面我们用偏函数的做法

创建一个新的partial函数,这个新函数可以固定住原函数的部分参数,从而减少调用时的输入的参数,让我们的调用更加简单

function sum(a, b, c) {
    return a + b + c
}

function partial(sum, c) {
    return function (a, b) {
        return sum(a, b, c)
    }
}
let partialSum = partial(sum, 3)

偏函数就是固定了函数的一个或多个参数,返回一个新的函数接收剩下的参数,以此来简化函数的调用。

AOP 面向切面编程

当我们需要使用一个公共函数,并且需要在这个函数执行前后添加自己的逻辑,通常我们的做法不能是直接修改这个函数,因为它是公共函数,这时候我们可以通过AOP的方法利用高阶函数和原型链的特点进行处理 把一些与业务无关的功能抽离出来,通过"动态植入"的方法,掺入到业务逻辑模块中。这样做的好处是保证业务逻辑模块的纯净和高内聚,其次可以方便的复用功能模块

面我们要实现在函数执行前输出提示信息

function say(who) {
    console.log(who + ':函数执行了');
}
Function.prototype.before = function(callback) {
    return (...args) => {
        callback()
        this(...args)
    }
}
let whoSay = say.before(function() {
    console.log('你要被调用了');
})
whoSay('yun')
// 你要被调用了
// yun:函数执行了

如果需要实现后置通知,只需要callback()往下放就可以了

所以在调用公共函数时,传入我们需要执行提前执行的函数,在内部函数执行前先调用该函数

使用柯里化完善受控组件

class Login extends React.Component {
    state = {
        username: "", //用户名
        password: "", //密码
    };

    //保存表单数据到状态中
    saveFormData = (dataType) => {
        return (event) => {
            this.setState({ [dataType]: event.target.value });
        };
    };

    //表单提交的回调
    handleSubmit = (event) => {
        event.preventDefault(); //阻止表单提交
        const { username, password } = this.state;
        alert(`你输入的用户名是:${username},你输入的密码是:${password}`);
    };
    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                用户名:
                <input onChange={this.saveFormData("username")} type="text" />
                密码:
                <input onChange={this.saveFormData("password")} type="password"/>
                <button>登录</button>
            </form>
        );
    }
}

不用函数柯里化传递参数

class Login extends React.Component{
    state = {
        username:'', //用户名
        password:'' //密码
    }

    //保存表单数据到状态中
    saveFormData = (dataType,event)=>{
        this.setState({[dataType]:event.target.value})
    }

    //表单提交的回调
    handleSubmit = (event)=>{
        event.preventDefault() //阻止表单提交
        const {username,password} = this.state
        alert(`你输入的用户名是:${username},你输入的密码是:${password}`)
    }
    render(){
        return(
<form onSubmit={this.handleSubmit}>
  用户名:
   <input onChange={event => this.saveFormData('username',event) } type="text" />
   密码:
   <input onChange={event => this.saveFormData('password',event) } type="password" />
    <button>登录</button>
</form>
        )
    }
}