Skip to content

面向组件编程

state 属性

state 是组件对象最重要的属性, 值是对象。组件被称为"状态机", 通过更新组件的 state 来更新对应的页面显示(重新渲染组件)

html
<script type="text/babel">
  class MyComponent extends React.Component {
    constructor(props) {
      this.state = { name: "jack", isMan: true };
      this.changeFunc = this.changeFunc.bind(this);
    }

    render() {
      const { name, isMan } = this.state;
      return (
        <h1 onClick={this.changeFunc}>
          我的名字是{name},性别是{isMan ? "男" : "女"}
        </h1>
      );
    }
    // 自定义方法
    changeFunc() {
      const { isMan } = this.state;
      this.setState({ isMan: !isMan });
    }
  }
  //渲染虚拟dom到页面
  ReactDOM.render(<MyComponent />, document.getElementById("test"));
</script>
  • 简化写法:
    类中直接写赋值语句,可以给实例对象添加一个属性,和构造器中定义效果一样
html
<script type="text/babel">
  class MyComponent extends React.Component {
    state = { name: "jack", isMan: true };

    render() {
      const { name, isMan } = this.state;
      return (
        <h1 onClick={this.changeFunc}>
          我的名字是{name},性别是{isMan ? "男" : "女"}
        </h1>
      );
    }
    // 自定义方法
    changeFunc = () => {
      const { isMan } = this.state;
      this.setState({ isMan: !isMan });
    };
  }
  //渲染虚拟dom到页面
  ReactDOM.render(<MyComponent />, document.getElementById("test"));
</script>

注意点

  1. 组件 render 方法中this为组件实例对象
  2. state状态数据不可以直接修改,要通过setState()
  3. 组件中定义的方法 this 指向问题
    • 通过箭头函数
    • 通过函数对象bind()强制绑定this

props 属性

每个组件对象都有props属性,组件标签的属性都保存在props中。组件内部不要修改props数据

基本使用

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

  // 类似于标签属性传值
  ReactDOM.render(<Person name="Tom" age={19} sex="男" />, document.getElementById("test"));
</script>

批量传递 props

html
<script type="text/babel">
  // ...
  const obj = { name: "James", age: 21, sex: "女" };
  ReactDOM.render(<Person {...obj} />, document.getElementById("test"));
</script>

限制 props

在 React15.5 版本之前, React 自带 PropTypes 属性,可以直接使用:

js
Person.propTypes = {
  name: React.PropTypes.string.isRequired,
  age: React.PropTypes.number,
};

在 React15.5 版本以后, 需要单独引入prop-types库:

html
<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>

<script type="text/babel">
  class Person extends React.Component {
    render() {
      const { name, age, sex } = this.props;
      return (
        <div>
          姓名:{name}, 性别:{sex}, 年龄:{age}
        </div>
      );
    }
  }

  // 对标签类型和必要性限制
  Person.propTypes = {
    name: PropTypes.string.isRequired,
    sex: PropTypes.string,
    age: PropTypes.number,
    speak: PropTypes.func, // 限制 speak 为函数
  };
  // 指定默认值
  Person.defaultProps = {
    sex: "male",
    age: 19,
  };

  ReactDOM.render(
    <Person name="Tom" sex="male" age={24} speak={speak} />,
    document.getElementById("test")
  );

  function speak() {
    console.log("speaking...");
  }
</script>

props 简写

通过static 关键字在类的内部定义限制属性, 定义的属性只能类调用, 实例对象不能调用

html
<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>

<script type="text/babel">
  class Person extends React.Component {
    static propTypes = {
      name: PropTypes.string.isRequired,
      sex: PropTypes.string,
      age: PropTypes.number,
      speak: PropTypes.func,
    };
    static defaultProps = {
      sex: "male",
      age: 19,
    };

    render() {
      // ...
    }
  }

  ReactDOM.render(
    <Person name="Tom" sex="male" age={41} speak={speak} />,
    document.getElementById("test")
  );

  function speak() {
    console.log("speaking...");
  }
</script>

函数式组件使用 props

html
<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script src="https://unpkg.com/prop-types@15.8.1/prop-types.js"></script>
<script type="text/babel">
  function Person(props) {
    const { name, age, sex } = props;
    return (
      <ul>
        <li>姓名:{name}</li>
        <li>年龄:{age}</li>
        <li>性别:{sex}</li>
      </ul>
    );
  }

  Person.propTypes = {
    name: PropTypes.string.isRequired,
    age: PropTypes.number,
    sex: PropTypes.string,
  };
  Person.defaultProps = {
    sex: "男",
    age: 38,
  };
  const p1 = { age: "23", sex: "男" };

  ReactDOM.render(<Person {...p1} />, document.getElementById("test"));
</script>

refs 属性

通过定义ref可以给标签添加标识, 从而能够获取标签属性和对其进行操作

字符串形式 ref

官方不推荐使用, 未来版本或被移除

html
<script type="text/babel">
  class Demo extends React.Component {
    showData = () => {
      const { input1 } = this.refs;
      alert(input1.value);
    };

    render() {
      return (
        <div>
          <input ref="input1" type="text" placeholder="点击按钮提示数据" />
          <button onClick={this.showData}>点我提示左侧的数据</button>
        </div>
      );
    }
  }

  ReactDOM.render(<Demo />, document.getElementById("test"));
</script>

回调形式 ref

理解 : 给标签节点命名, 并挂载到组件实例自身

html
<script type="text/babel">
  class Demo extends React.Component {
    // 点击事件
    clickTrigger = () => {
      alert(this.input1.value);
    };

    render() {
      return (
        <div>
          {/* el是当前标签, 命名为input1
           * 由于是箭头函数, this为 render 中的 this, 即组件实例
           * 所以相当于往组件实例上挂载了该标签属性
           */}
          <input ref={(el) => (this.input1 = el)} type="text" placeholder="请输入" />
          &nbsp;
          <button onClick={this.clickTrigger}>点击触发</button>
        </div>
      );
    }
  }
  //渲染虚拟dom到页面
  ReactDOM.render(<Demo />, document.getElementById("test"));
</script>

createRef 形式 ref

通过调用 React.createRef 返回一个容器, 该容器可以存储被 ref 标识的节点, 该容器只能存储一个节点, 后定义的会覆盖掉之前的

html
<script type="text/babel">
  class Demo extends React.Component {
    // 创建容器
    myRef1 = React.createRef();
    clickTrigger = () => {
      // current是react定义的固定格式
      alert(this.myRef1.current.value);
    };

    render() {
      return (
        <div>
          <input ref={this.myRef1} type="text" placeholder="请输入" />
          &nbsp;
          <button onClick={this.clickTrigger}>点击触发</button>
        </div>
      );
    }
  }
  //渲染虚拟dom到页面
  ReactDOM.render(<Demo />, document.getElementById("test"));
</script>

事件处理

react 中的事件处理

  1. 通过 onXxx 属性指定事件处理函数
    • React 使用自定义事件,而非原生 DOM 事件,保证更好兼容性
    • React 事件会委托给组件最外层元素, 以保证更高效
  2. 通过 event.target 得到发生事件的 dom 元素对象, 避免了过渡使用 ref
js
class Demo extends React.Component {
  showData2 = (event) => {
    alert(event.target.value);
  };

  render() {
    return (
      <div>
        <input onBlur={this.showData2} type="text" placeholder="请输入" />
      </div>
    );
  }
}

受控组件&非受控组件

  • 非受控组件: 现用现取, 需要使用时, 直接获取标签属性的值
html
<script type="text/babel">
  class Demo extends React.Component {
    handleSubmit = (e) => {
      e.preventDefault();
      const { username, password } = this;
      // 需要时获取数据值
      alert(`用户名:${username.value},密码:${password.value}`);
    };

    render() {
      return (
        <div>
          <form onSubmit={this.handleSubmit}>
            用户名:
            <input type="text" name="username" ref={(c) => (this.username = c)} />
            <br />
            <br />
            密码:
            <input type="password" name="password" ref={(c) => (this.password = c)} />
            <button>登录</button>
          </form>
        </div>
      );
    }
  }
  //渲染虚拟dom到页面
  ReactDOM.render(<Demo />, document.getElementById("test"));
</script>
  • 受控组件: 类似 vue 的双向绑定, 维护数据在 state 中, 需要时从 state 中拿取
html
<script type="text/babel">
  class Demo extends React.Component {
    state = {
      username: "",
      password: "",
    };

    changeName = (e) => {
      this.setState({
        username: e.target.value,
      });
    };

    changePassword = (e) => {
      this.setState({
        password: e.target.value,
      });
    };
    handleSubmit = (e) => {
      e.preventDefault();
      const { username, password } = this.state;
      alert(`用户名:${username},密码:${password}`);
    };

    render() {
      return (
        <div>
          <form onSubmit={this.handleSubmit}>
            用户名:
            <input type="text" name="username" onChange={this.changeName} />
            <br />
            密码:
            <input type="password" name="password" onChange={this.changePassword} />
            <button>登录</button>
          </form>
        </div>
      );
    }
  }
  //渲染虚拟dom到页面
  ReactDOM.render(<Demo />, document.getElementById("test"));
</script>

尽量使用受控组件,因为非受控组件会使用大量的 ref

函数柯里化

一个函数接收函数 A 作为参数,运行后能够返回一个新的函数。并且这个新的函数能够处理函数 A 的剩余参数

js
//普通函数
function add(a, b, c) {
  return a + b + c;
}
add(1, 2, 3); //6

//手动柯里化后的函数,其参数可以逐步单个传入,得到相同结果。
function _add(a) {
  return function (b) {
    return function (c) {
      return a + b + c;
    };
  };
}

_add(1)(2)(3); //6

通过函数柯里化可以优化表单代码, 将saveUsernamesavePassword 合并为一个函数

jsx
class Login extends React.Component {
  state = {
    username: "",
    password: "",
  };

  saveFormData = (dataType) => {
    // 作为回调给 onChange 调用, 所以此处能拿到 onchange 的 event 事件
    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}>
        用户名:
        {/* 将函数的返回值交给 onChange */}
        <input onChange={this.saveFormData("username")} type="text" name="username" />
        密码:
        <input onChange={this.saveFormData("password")} type="password" name="password" />
        <button>登录</button>
      </form>
    );
  }
}

生命周期

生命周期官方文档

旧版生命周期

旧生命周期

三个阶段

  1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
  2. 更新阶段: 由组件内部this.setSate()或父组件重新render触发
  3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发

新版生命周期

新生命周期

更新内容

  • 即将废弃 componentWillMountcomponentWillReceivePropscomponentWillUpdate
  • 新增(很少使用) getDerivedStateFromPropsgetSnapshotBeforeUpdate

常用三个钩子

  • render: 初始化渲染和更新渲染
  • componentDidMount: 进行初始化,如开启定时器、发送网络请求、订阅消息
  • componentWillUnmount: 进行收尾,如关闭定时器、取消订阅消息

虚拟 DOM 和 Diff 算法

diff 预设限制

  • 只对同级元素进行 diff
  • 不同元素标签产生不同的树, 即如果元素由 div 变成 p,那么 React 会删除 div 及其子孙节点,新建 p 及其子孙节点
  • 可以使用key标识虚拟 DOM

使用key标识进行比较的规则

  • 旧虚拟 DOM 找到和新虚拟 DOM 相同的 key
    • 如果内容没有改变, 直接复用
    • 如果内容改变,则生成新的真实 DOM, 替换掉之前的真实 DOM
  • 旧虚拟 DOM 未找到和新虚拟 DOM 相同的 key:根据数据创建新的真实 DOM
js
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>
        <button onClick={this.add}>添加</button>
        <ul>
          {this.state.persons.map((item, index) => {
            return (
              <li key={index}>
                {item.name}---{item.age}
                <input type="text" />
              </li>
            );
          })}
        </ul>
      </div>
    );
  }
}

使用index 作为key可能出现的问题

  • 若对数据进行逆序添加、逆序删除等破坏顺序的操作,index值会重新顺序生成。
    当根据key值再比对时, 由于内容不同导致真实DOM重新更新无法复用。 页面虽显示正常, 但会影响更新效率
html
<!-- 初始虚拟DOM -->
<li key="0">小张---18</li>
<li key="1">小李---28</li>

<!-- 往顶部添加一条数据后的虚拟DOM -->
<li key="0">小王---14</li>
<li key="1">小张---18</li>
<li key="2">小李---28</li>
  • 如果结构中包含输入类的 DOM(如 input 输入框), 由于input 标签结构没变会按照index继续复用, 但其value 值可能有着更新前的输入, 最终会导致错误的 DOM 更新
html
<!-- 初始虚拟DOM -->
<li key="0">小张---18<input type="text" /></li>
<li key="1">小李---28<input type="text" /></li>

<!-- 更新数据后的虚拟DOM -->
<li key="0">小王---14<input type="text" /></li>
<li key="1">小张---18<input type="text" /></li>
<li key="2">小李---28<input type="text" /></li>