React + React路由 学习总结

1,184 阅读26分钟

一、学习收获

1、React无状态组件和有状态组件

2、React内部组件之间的通讯

3、React组件生命周期

4、React原理

5、React路由

二、React组件

1、无状态组件和有状态组件

  • 函数组件又叫做无状态组件,类组件又叫做有状态组件
  • 状态(state)即数据
  • 函数组件没有自己的状态,只负责数据展示(静态的)
  • 类组件有自己的状态,负责更新UI,让页面能够跟用户进行交互(动态)

比如:计数器中的值从 0开始,点击按钮,对值进行加1操作,计数器中的值就是 状态。

2、组件中的props特点

  • 可以给组件传递任意类型的数据

  • props是只读的对象,只能读取属性的值,无法修改对象

  • 注意:使用类组件时,如果写了构造函数,应该将props传递给super(),否则,无法在构造函数中获取到props。

    class Hello extends React.Component {
      constructor() {
        super();
        console.log(this.props); // 这里拿不到props,想要拿到,需要在构造函数和super中写入props参数
      }
      // render中的 this.props能够拿到
      render() {
        return (
          <div>
            <span>姓名:{this.props.name}</span>
            <span>年龄:{this.props.age}</span>
          </div>
        );
      }
    }
    
    ReactDOM.render(
      <Hello name="zhangsan" age={19} />,
      document.getElementById("root")
    );
    

3、组件之间通讯的三种方式

3.1、父组件传递数据给子组件

  • 父组件中要提供传递的state数据
  • 在父组件中,给子组件标签添加属性,值为state中的数据
  • 子组件中通过props接收父组件中传递的数据
// 父组件
class Parent extends React.Component {
  state = {
    ParentName: "张"
  };
  render() {
    return (
      <div>
        父组件姓名:{this.state.ParentName}
        <Children1 name={this.state.ParentName} />
      </div>
    );
  }
}

// 子组件

class Children1 extends React.Component {
  render() {
    console.log(this.props);
    return <div>子组件接收到父组件的数据:{this.props.name}</div>;
  }
}
ReactDOM.render(<Parent />, document.getElementById("root"));

3.2、子组件传递数据给父组件

思路:利用回调函数,父组件提供回调函数,子组件调用回调函数,并且把将要传递的数据作为回调函数的参数进行传递。

  • 父组件提供一个回调函数(用于接收数据)
  • 在父组件中,给子组件标签中添加属性,值为父组件中的回调函数(this.回调函数名)
  • 在子组件中声明一个函数,然后在子组件的函数中,通过该props调用父组件的回调函数
  • 将子组件的数据作为参数传递给父组件的回调函数
  • 在子组件中触发点击事件(或者其它事件),执行子组件中声明的函数
class Parent2 extends React.Component {
  state = {
    childName: " "
  };
  // 父组件提供一个回调函数(用于接收数据)
  ParentHandle = data => {
    console.log(data);
    this.setState({
      childName: data
    });
  };
  render() {
    return (
      <div>
        子组件传过来的值:{this.state.childName}
        <Children2 fn={this.ParentHandle} />
      </div>
    );
  }
}

class Children2 extends React.Component {
  state = {
    childName: "张三丰"
  };
  childHandle = () => {
    // 子组件通过该props调用回调函数
    // 将子组件的数据作为参数传递给回调函数
    this.props.fn(this.state.childName);
  };
  render() {
    return (
      <div>
        {/* 在子组件中触发点击事件(或者其它事件),执行子组件中声明的函数 */}
        <button onClick={this.childHandle}>点击我,把名字传递给父组件</button>
      </div>
    );
  }
}
ReactDOM.render(<Parent2 />, document.getElementById("root"));

3.3、兄弟组件之间的通讯

思想:把状态提升到兄弟组件最近的父组件中,让状态变成共享状态。(状态提升)

  • 将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态
    • 公共父组件要提供共享状态
    • 公共父组件要提供操作共享状态的方法
  • 兄弟组件中只要通过props接收状态或者调用操作状态的方法
class Parent3 extends React.Component {
  // 公共父组件要提供共享状态
  state = {
    count: 0
  };
  // 公共父组件要提供操作共享状态的方法
  parentHandle = () => {
    this.setState({
      count: this.state.count + 1
    });
  };
  render() {
    return (
      <div>
        <Child1 childCount={this.state.count} />
        <Child2 childFn={this.parentHandle} />
      </div>
    );
  }
}

class Child1 extends React.Component {
  render() {
    // 兄弟组件中只要通过props接收状态
    return <h1>当前计数器的值:{this.props.childCount}</h1>;
  }
}

class Child2 extends React.Component {
  render() {
    return (
      // 兄弟组件中只要通过props调用操作状态的方法
      <button
        onClick={() => {
          this.props.childFn();
        }}
      >
        加1
      </button>
    );
  }
}

ReactDOM.render(<Parent3 />, document.getElementById("root"));

4、Context

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props,(能够跨组件传递数据,解决了组件嵌套很深,传递数据繁琐的问题)

// Context (跨组件传递数据,解决嵌套层级较深的组件传值)

// 使用React.createContext 创建 Provider对象和 Consumer对象
// Provider对象:用来提供数据
// Consumer对象:用来消费数据
const { Provider, Consumer } = React.createContext();

class App extends React.Component {
  render() {
    return (
      <Provider value="color">
        <div>
          <Node />
        </div>
      </Provider>
    );
  }
}
class Node extends React.Component {
  render() {
    return (
      <div>
        <SubNode />
      </div>
    );
  }
}
class SubNode extends React.Component {
  render() {
    return (
      <div>
        <Child />
      </div>
    );
  }
}
class Child extends React.Component {
  render() {
    return <Consumer>{data => <div>{data}</div>}</Consumer>;
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

5.1 children属性

  • children属性:表示组件标签的子节点。当组件标签有子节点时,props就会有该属性
  • children属性与普通的props一样,值可以是任意值(文本、React元素、组件、函数)

children作为文本节点

class App extends React.Component {
  render() {
    return (
      <div>
        <Node>我是子节点</Node>
      </div>
    );
  }
}

class Node extends React.Component {
  render() {
    console.log(this.props);
    return (
      <div>
        组件中的文本内容是
        {this.props.children}
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));

image-20211022215322305

children作为组件

// children 作为组件
class App extends React.Component {
  render() {
    console.log(this.props);
    return <div>{this.props.children}</div>;
  }
}

class Node extends React.Component {
  render() {
    return <div>我是Node组件</div>;
  }
}

ReactDOM.render(
  <App>
    <Node />
  </App>,
  document.getElementById("root")
);

image-20211022215400427

children作为函数

// children作为函数
class App extends React.Component {
  render() {
    console.log(this.props);
    return <div>{this.props.children()}</div>;
  }
}

const handle = () => {
  console.log("hello");
};

ReactDOM.render(<App>{handle}</App>, document.getElementById("root"));

image-20211022215441565

5.2 props 校验

前提:

  • 对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据
  • 如果传入的数据格式不对,可能会导致组件内部报错
  • 关键问题:组件的使用者不知道明确的错误原因

解决办法:

props 校验:允许在创建组件的时候,就制定 props 的类型、格式等

作用:捕获使用组件时因 props 导致的错误,给出明确的错误提示信息,增加组件的健壮性。

步骤:

  • 安装 prop-types包, yarn add prop-typesnpm i prop-types
  • 导入 prop-types包 import PropTypes from "prop-types";
  • 使用组件名.propTypes = {} 来给组件的props添加校验规则
  • 校验规则通过PropTypes对象来指定

函数形式

// 函数形式
const App = props => {
  const arr = props.colors;
  const lis = arr.map((item, index) => <li key={index}>{item}</li>);
  return (
    <div>
      <ul>{lis}</ul>;
    </div>
  );
};
// 注意:这里的 propTypes 中的开头的p是小写的。
App.propTypes = {
  // 意:这里的PropTypes 的 P 是大写的,是引入上面的。
  colors: PropTypes.array
};
ReactDOM.render(
  <App colors={["red", "pink"]} />,
  document.getElementById("root")
);

组件形式

// Props 校验
// 1、安装和导入 prop-types包
// 2、使用组件名.propTypes = {} 来给组件的props添加校验规则
// 3、校验规则通过PropTypes对象来指定

import PropTypes from "prop-types";

class App extends React.Component {
  //   state = {
  //     arr: this.props.colors
  //   };
  render() {
    const arr = this.props.colors;
    return (
      <div>
        <ul>
          {/* {this.state.arr.map((item, index) => (
            <li key={index}>{item}</li>
          ))} */}
          {arr.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
      </div>
    );
  }
}
// 注意:这里的PropTypes,跟函数形式不太一样。
App.defaultProps = {
  colors: PropTypes.array
};

ReactDOM.render(
  <App colors={["red", "pink"]} />,
  document.getElementById("root")
);

5.3 props的默认值

使用场景:比如:分页功能,用来显示条数

// props的默认值
const App = (props) => {
  return (
    <div>
      <h1>props的默认值是:{props.PageSize}</h1>
    </div>
  );
};
App.defaultProps = {
  PageSize: 30,
};
ReactDOM.render(<App />, document.getElementById("root"));

三、React组件的生命周期

组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的整个过程。

生命周期的每个阶段总是伴随着一些方法的调用,这些方法就是生命周期的钩子函数。

钩子函数的作用:为开发人员在不同阶段操作组件提供了时机。

只有类组件才有生命周期,函数组件没有生命周期。

image-20211022215514025

1、挂载阶段的钩子函数

钩子函数触发时机作用
constructor创建组件时,最先执行1、初始化state
2、为事件处理程序绑定this
render每次组件渲染都会触发1、渲染UI<
2、注意:在render钩子函数中,不能调用setState()
因为setSate(),能够更新state,也能更新UI,只要调用setSate(),就会调用render更新UI,如果再render中再调用setSate()这样就会出现递归更新,导致报错)
componentDidMount组件挂载(完成DOM渲染)后执行1、发送网络请求
2、DOM操作

2、更新阶段的钩子函数

执行时机
1、组件接收到新的props会触发render,更新UI
2、调用setSate()会触发render,更新UI
3、调用forceUpdate()会强制触发render,更新UI

以上任意一种变化,组件都会重新渲染

钩子函数触发时机作用
render每次组件渲染都会触发1、渲染UI
componentDidUpdate组件更新(完成DOM渲染)后执行1、发送网络请求
2、DOM操作
3、注意:如果要调用setState()必须放在一个if条件中,因为在此钩子中调用 setState()也会递归更新
componentDidUpdate(prevProps){
	console.log(prevProps)
	console.log(this.props)
}
if(prevProps.name !=== this.props.name){
   	this.setState({})
}

3、卸载阶段的钩子函数

钩子函数触发时机作用
componentWillUnmount组件卸载(从页面消失)执行清理,比如清理定时器

四、React组件复用

React组件复用:把多个组件中部分功能相似或者相同的状态或者逻辑进行复用。(复用:state和操作state的方法)。

复用的方式:

  • render props模式
  • 高阶组件(HOC)

1、render props模式

// 组件的复用
// render props模式
class Mouse extends React.Component {
  state = {
    x: 0,
    y: 0,
  };
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };
  // 监听鼠标的移动事件
  componentDidMount() {
    window.addEventListener("mouseover", this.handleMouseMove);
  }
  render() {
    // return null;
    return this.props.render(this.state);
  }
}

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>render props 模式</h1>
        <Mouse
          render={(mouse) => {
            return (
              <div>
                鼠标的位置{mouse.x} {mouse.y}
              </div>
            );
          }}
        />
				
 				<Mouse
          render={(mouse) => {
            return (
              <div>
                <img
                  src={logo}
                  alt=""
                  style={{
                    position: "absolute",
                    left: mouse.x - 96,
                    top: mouse.y - 96,
                  }}
                />
              </div>
            );
          }}
        /> 

      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

用children替代render(推荐)

import logo from "./assets/logo192.png";
// 组件的生命周期

// class App extends React.Component {
//   constructor() {
//     super();
//   }

// }
// Props 校验
// 1、安装和导入 prop-types包
// 2、使用组件名.propTypes = {} 来给组件的props添加校验规则
// 3、校验规则通过PropTypes对象来指定

import PropTypes from "prop-types";
// 组件的复用
// render props模式
class Mouse extends React.Component {
  state = {
    x: 0,
    y: 0,
  };
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };
  // 监听鼠标的移动事件
  componentDidMount() {
    window.addEventListener("mouseover", this.handleMouseMove);
  }
  componentWillUnmount() {
    window.removeEventListener("mouseover", this.handleMouseMove);
  }
  render() {
    // return null;
    // return this.props.render(this.state);
    return this.props.children(this.state);
  }
}
Mouse.propTypes = {
  children: PropTypes.func.isRequired,
};

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>render props 模式</h1>
        {/* <Mouse
          render={(mouse) => {
            return (
              <div>
                鼠标的位置{mouse.x} {mouse.y}
              </div>
            );
          }}
        /> */}

        {/* <Mouse
          render={(mouse) => {
            return (
              <div>
                <img
                  src={logo}
                  alt=""
                  style={{
                    position: "absolute",
                    left: mouse.x - 96,
                    top: mouse.y - 96,
                  }}
                />
              </div>
            );
          }}
        /> */}

        <Mouse>
          {(mouse) => {
            return (
              <div>
                鼠标的位置{mouse.x} {mouse.y}
              </div>
            );
          }}
        </Mouse>
        <Mouse>
          {(mouse) => {
            return (
              <div>
                <img
                  src={logo}
                  alt=""
                  style={{
                    position: "absolute",
                    left: mouse.x - 96,
                    top: mouse.y - 96,
                  }}
                />
              </div>
            );
          }}
        </Mouse>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

2、高阶组件(HOC)

目的:实现状态逻辑复用

高阶组件(HOC,Higher-Order Components)是一个函数,接收一个要包装的组件,返回一个增强后的组件

高阶组件内部创建一个类组件,在这个类组件中,提供复用的状态逻辑代码,通过props将复用的状态传递给被包装组件WrappedComponent

const EnhancedComponent = higherOrderComponent(WrappedComponent);

高阶组件的使用步骤

1、创建一个函数,名称以hoc开头

2、指定函数参数,参数应该以大写字母开头(作为要渲染的组件)

3、在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回该类组件。

4、在该组件中,渲染参数组件,同时将状态通过props传递给参数组件

5、调用高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中

// 高阶组件
// 1、创建一个函数,名称以hoc开头
// 2、指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
const hocMouse = (WrappedComponent) => {
  // 3、在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回该类组件。
  class Mouse extends React.Component {
    // 鼠标状态
    state = {
      x: 0,
      y: 0,
    };
    handleMouseMove = (e) => {
      this.setState({
        x: e.clientX,
        y: e.clientY,
      });
    };
    // 控制鼠标状态的逻辑
    componentDidMount() {
      window.addEventListener("mousemove", this.handleMouseMove);
    }
    // 解绑事件
    componentWillUnmount() {
      window.removeEventListener("mousemove", this.handleMouseMove);
    }
    render() {
      return (
        <div>
         {/* 4、在该组件中,渲染参数组件,同时将状态通过props传递给参数组件 */}
          <WrappedComponent {...this.state}></WrappedComponent>
        </div>
      );
    }
  }
  // 返回类组件
  return Mouse;
};
// 测试高阶组件
const PagePosition = (props) => {
  return (
    <div>
      鼠标当前位置:x:{props.x},y:{props.y}
    </div>
  );
};
// 图片定位
const PageCatPosition = (props) => (
  <img
    src={logo}
    alt=""
    style={{
      position: "absolute",
      left: props.x - 96,
      top: props.y - 96,
    }}
  />
);
// 5、调用高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
const MousePage = hocMouse(PagePosition);
const PageCat = hocMouse(PageCatPosition);

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>高阶组件的运用</h1>
        {/* 渲染增强后的组件 */}
        <MousePage></MousePage>
        <PageCat></PageCat>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

高阶组件传递props

在改动代码:<WrappedComponent {...this.state} {...this.props}></WrappedComponent>

增加 {...this.props}

const hocMouse = (WrappedComponent) => {
  // 在内部创建类组件,在类组件中提供复用状态逻辑
  class Mouse extends React.Component {
    // 鼠标状态
    state = {
      x: 0,
      y: 0,
    };
    handleMouseMove = (e) => {
      this.setState({
        x: e.clientX,
        y: e.clientY,
      });
    };
    // 控制鼠标状态的逻辑
    componentDidMount() {
      window.addEventListener("mousemove", this.handleMouseMove);
    }
    // 解绑事件
    componentWillUnmount() {
      window.removeEventListener("mousemove", this.handleMouseMove);
    }
    render() {
      console.log(this.props);
      return (
        <div>
          <WrappedComponent {...this.state} {...this.props}></WrappedComponent>
        </div>
      );
    }
  }
  Mouse.displayName = `hocMouse${getDisplayName(WrappedComponent)}`;
  // 返回类组件
  return Mouse;
};

const getDisplayName = (WrappedComponent) => {
  return WrappedComponent.displayName || WrappedComponent.name || "Component";
};
// 测试高阶组件
const PagePosition = (props) => {
  console.log(props);
  return (
    <div>
      鼠标当前位置:x:{props.x},y:{props.y}
    </div>
  );
};
// 图片定位
const PageCatPosition = (props) => (
  <img
    src={logo}
    alt=""
    style={{
      position: "absolute",
      left: props.x - 96,
      top: props.y - 96,
    }}
  />
);
const MousePage = hocMouse(PagePosition);
const PageCat = hocMouse(PageCatPosition);
class App extends React.Component {
  render() {
    return (
      <div>
        <h1>高阶组件的运用</h1>
        {/* 渲染增强后的组件 */}
        <MousePage a="1"></MousePage>
        <PageCat></PageCat>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

五、React原理

1、React的工作原理

1)、UI = f(data){}

UI 就是界面 ,这个界面就是函数执行的结果,是按照函数式编程理念来的,我们所做的东西就是要实现这个函数,改变UI,是通过改变函数中的参数data才行的,改变data,让data来驱动这个函数,这个函数来影响(产生)UI。所以在React中,无论你实现一个组件也好,或者完成其它功能,都是要实现一个函数,改变函数中的data,让data来驱动这个函数,这个函数来影响(产生)UI,也就是说你通过改变data来改变UI,这才是React响应式的理念。

data 就是 props和state

开发者 只需要维护可变的数据 state (what) ,让 react 框架帮助我们处理 DOM 操作(what)。

React 通过 diffing 算法计算如何更新视图。而 diffing 算法有个 的假设前提,开发人员会提供给长列表的每个子项一个 ID,帮助算法进行对比,如下图所示:

image.png

通过key保证节点的唯一性,避免了重复的渲染,可以通过上图来进行说明,React认为,如果key都相同,那么就不需要重新的计算并且渲染了,只要key不相同了(比如 新插入了一个 x 这个节点),就需要单独渲染这个 x 节点就行了。

在同一组组件中key的两个要素

1、key 在兄弟节点中是唯一的。

2、key的值是稳定的,key的值必须是固定的,不要总是改变key的值,在实际开发中,key的值不要设置数组的下标索引值,因为这样是不稳定的值,例如上图所示,之前 B的索引值是 1,但是通过改变之后,索引值就变成了 2。

3、三元表达式不需要使用key来进行标记,因为无论三元表达式的条件成立与否,都会有一个对应的值,在对应的位置进行占位。

2)、完成的渲染流程

初始化的渲染流程分为 3 步。

第一步,开发者使用 JSX 语法写 React,babel 会将 JSX 编译为浏览器能识别的 React JS 语法。这一步,一般配合 webpack 在本地进行。

第二步,执行 ReactDOM.render 函数,渲染出虚拟DOM。

第三步,react 将虚拟DOM,渲染成真实的DOM。

页面更新的流程同样也是 3 步。

第一步,当页面需要更新时,通过声明式的方法,调用 setState 告诉 react什么数据变了。

第二步,react 自动调用组件的 render 方法,渲染出虚拟 DOM。

第三步,react 会通过 diffing 算法,对比当前虚拟 DOM 和需要更新的虚拟 DOM 有什么区别。然后重新渲染区别部分的真实 DOM。

ReactRender

  1. 什么是模块化:从 代码 的角度,去分析问题,把我们编程时候的业务逻辑,分割到不同的模块中来进行开发,这样能够方便代码的重用
  2. 什么是组件化:从 UI 的角度,去分析问题,把一个页面,拆分为一些互不相干的小组件,随着我们项目的开发,我们手里的组件会越来越多,最后,我们如果要实现一个页面,可能直接把现有的组件拿过来进行拼接,就能快速得到一个完整的页面, 这样方便了UI元素的重用组件是元素的集合体

3)、什么是虚拟DOM(Virtual DOM)

虚拟DOM(VDOM)是一种编程概念,是指虚拟的视图被保存在内存中,并通过诸如ReactDOM这样的库与“真实”的DOM保持同步。这个过程被称为reconciliation。

这种编程方法使用了React的声明式API:你需要告诉React你想让视图处于什么状态,React则负责确保DOM与该状态相匹配。因此你在构建你的应用时不必自己去完成属性操作、事件处理、DOM更新,React会替你完成这一切。

由于“虚拟DOM”更像一种模式而不是特定的技术,有时候我们也会用它表示其他的意思。在React的世界中,由于 “虚拟DOM” 和React元数都是用于表示视图的对象,因此常常被关联在一起。然而React也使用被称为“fibers”的对象来存放组件树的附加信息。在React中,它们也被认为是“虚拟DOM”实现的一部分。

4)、影子DOM(Shadow DOM)和虚拟DOM(Virtual DOM)是相同的概念吗?

不,它们并不是相同的概念。影子DOM是一种浏览器技术,主要被设计用来为Web组件中的变量和CSS提供封装。虚拟DOM是由JavaScript库在浏览器API之上实现的一种概念。

5)、Diff算法

  • tree diff:新旧DOM树,逐层对比的方式,就叫做 tree diff,每当我们从前到后,把所有层的节点对比完后,必然能够找到那些 需要被更新的元素;
  • component diff:在对比每一层的时候,组件之间的对比,叫做 component diff;当对比组件的时候,如果两个组件的类型相同,则暂时认为这个组件不需要被更新,如果组件的类型不同,则立即将旧组件移除,新建一个组件,替换到被移除的位置;
  • element diff:在组件中,每个元素之间也要进行对比,那么,元素级别的对比,叫做 element diff;
  • key:key这个属性,可以把 页面上的 DOM节点 和 虚拟DOM中的对象,做一层关联关系;

fiber是React 16中新的引擎。它的主要目的是使虚拟DOM能够进行增量渲染。

React Fiber 是对 React 核心算法的重新实现。

React Fiber 的目标是提高其对动画,布局和手势等领域的适用性。它的最重要的特性是 incremental rendering(增量渲染):它能够将渲染 work 拆分成多块并将这些任务块分散到多个帧执行。

其他的核心特性还包括当新的更新到来时暂停、中止或恢复 work 的能力;为不同类型的更新设置优先级的能力;以及新的并发原语。

6)、什么是 reconciliation?

  • reconciliation

    React 用来比较两棵树的算法,它确定树中的哪些部分需要被更新。

    • update

      用来渲染 React 应用的数据的改变。通常是 setState 的结果。最终结果将是一次重新渲染

    React 的 API 的核心理念是将更新想象成为对整个应用的重新渲染。这允许开发者声明式地推导,而不用担心应用如何从一个状态高效的过渡到另一个状态(A 到 B,B 到 C,C 到 A 等等)。

    实际上,对于每个更改重新渲染整个应用程序只适用于最简单的应用程序。在实际的应用中,对性能会有非常大的开销。

    Reconciliation 是被大家广泛知晓的 "virtual DOM" 背后的算法。

    • 不同的组件类型被认为将生成本质上不同的树。React 将不尝试去进行差异比较,而是直接完全替换旧的树。
    • 对于列表的差异比较使用 key 来优化性能。Keys 应当是稳定、可预测且唯一的。
  • Scheduling(调度)

    Scheduling

    • scheduling

      决定 work 何时应该被执行的过程。

    • work

      任何计算结果都必须被执行。Work 通常是更新的结果(e.g. setState).

      在 React 目前的实现中,React 递归地遍历树并且调用 render 方法在 event loop 的一次迭代中更新整棵树。不过在将来,它将延迟一些更新来防止掉帧。

      这是 React 设计中一个公共的主题。一些受欢迎的库实现了“推”的方法,计算会在新数据到来的时候被执行。但是 React 坚持“拉”的方法,计算能够被推迟直到需要的时候。

      React 不是一个通用的数据处理库。它只是设计来构建用户界面的库。我们认为知晓哪些计算是现在就相关的,哪些不是是在应用程序中有独一无二位置的内容。

      如果一些东西在屏幕外,我们可以推迟任何与其相关的逻辑。如果数据到来的速度比帧率块,那么我们可以合并并批量更新。我们可以对来自用户与界面互动的工作(如一个按钮点击的动画)和相对不太重要的背后工作(如远程加载数据渲染新的内容)进行优先级的排序来阻止掉帧。

    总结一下核心点如下:

    • 在 UI 中,并非所有的更新都需要立即生效。实际上,这样做是浪费的,可能会造成掉帧从而影响用户体验。
    • 不同类型的更新有不同的优先级。一个动画更新相比于来自数据的更新通常需要更快地被执行。
    • 一个以“推”为基础的方案要求应用程序(你,工程师)来决定如何调度工作。而一个以“拉”为核心的方案允许框架(如:React)更智能,来为你做这些决定。

    iber 的主要目标是让 React 能够享受到调度带来的好处。明确地说,我们需要能够做到以下几件事:

    • 暂停任务并能够在之后恢复任务。
    • 为不同的任务设置不同的优先级。
    • 重新使用之前完成的任务。
    • 如果不在需要则可以终止一个任务。

    为了做到这些事,我们首先需要一个方式来拆分这些任务为一个个任务单元。从某种意义上来说,这些任务单元就是 fiber。一个 fiber 代表了一个 任务单元.

    view = fn(data)
    

    因此渲染 React 应用程序就类似于调用一个包含其他函数调用的函数。

2、一切皆为组件

2.1 组件

组件之间通过props 得到任何数据,都是只读的,不能重新赋值

jsx语法中的标签,并不是直接渲染到了页面上,而是先转换成了React.createElement 这样的js代码,然后再通过render渲染到了页面中

es6 class 类中 静态方法挂载到了constructor构造器中,实例方法挂载到了原型对象中

class Person {
    constructor(name,age){
        this.name = name
        this.age = age
    }
    // say 是实例方法
    say(){
        console.log('您好')
    }
    
    // info 是静态方法
    static info = 123 
}

class Chinese extends Person {
    // 使用extends 关键字实现继承,子类的constructor构造函数中,必须显示调用super()方法,这个super表示父类中constructor的引用
    constructor(name,age,color,lang) {
        super(name,age)
    }
}

var p1 = new Person('zs',12)
console.log(p1)

var c1 = new Person('ls',12,'yellow','汉语')
console.log(c1)
c1.say()
console.log(Chinese.info)

react无状态组件和有状态组件的区别 无状态组件:无状态组件(Stateless Component)是最基础的组件形式,由于没有状态的影响所以就是纯静态展示的作用。一般来说,各种UI库里也是最开始会开发的组件类别。如按钮、标签、输入框等。它的基本组成结构就是属性(props)加上一个渲染函数(render)。由于不涉及到状态的更新,所以这种组件的复用性也最强。

有状态组件:在无状态组件的基础上,如果组件内部包含状态(state)且状态随着事件或者外部的消息而发生改变的时候,这就构成了有状态组件(Stateful Component)。有状态组件通常会带有生命周期(lifecycle),用以在不同的时刻触发状态的更新。这种组件也是通常在写业务逻辑中最经常使用到的,根据不同的业务场景组件的状态数量以及生命周期机制也不尽相同。

2.1 什么时候用 无状态组件?什么时候用有状态组件?

1、如果一个组件,只需要通过外界传递过来的props,渲染固定的页面,就适合使用function创建出来的无状态组件。

2、如果一个组件需要存放自己的私有数据,同时需要在组件的不同阶段执行不同的业务逻辑,就适合使用有状态组件。

在React中,我们通常通过props和state来处理两种类型的数据。props是只读的,只能由父组件设置。state在组件内定义,在组件的生命周期中可以更改。基本上,无状态组件使用props来存储数据,而有状态组件(也称为智能组件)使用state来存储数据。

总的来说:无状态函数式写法 优于React.createClass,而React.Component优于React.createClass。能用React.Component创建的组件的就尽量不用React.createClass形式创建组件。

在React中创建组件有三种方式:

  • ES5写法:React.createClass
  • ES6写法:React.Component
  • 无状态的函数写法,又称为纯组件

3、声明式编程

声明式与命令式的区别

  • 命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
  • 声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。

jQuery是命令式的编程,jQuery让你去使用它的API,去做一些事情,如果jQuery的API改变了,我们写的的代码也要改变,因为API发生变化了,我们需要修改之前的API,所以这样的设计是很不友好的。

React中的写的code不大可能调用系统级别的API,相反,我们是实现一些函数,去响应,我们写的代码,只是声明我们要做什么,怎么去做,就是React来完成了,我们不用去关心,即使React的API改变了,也不会影响。

4、setState() 说明

  • setState() 是异步更新数据的

  • 可以多次调用setState(),但是只会触发一次render(重新渲染)

class App extends React.Component {
  state = {
    num: 0,
  };
  handleAdd = () => {
    // 异步,多次调用this.setState,里面的num不能累加
    this.setState({
      num: this.state.num + 1,
    });
    this.setState({
      num: this.state.num + 1,
    });
    console.log(this.state.num);
  };
  render() {
    return (
      <div>
        <h2>当前的值为:{this.state.num}</h2>
        <button onClick={this.handleAdd}>+1</button>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));
  • 推荐语法
class App extends React.Component {
  state = {
    num: 0,
  };
  handleAdd = () => {
    // 推荐使用该语法:该语法也是异步的,但是多次调用this.setState,里面的num是累加的。
    // state:表示最新的state
    // props: 表示最新的props
    this.setState((state, props) => {
      return {
        num: state.num + 1,
      };
    });
    this.setState((state, props) => {
      return {
        num: state.num + 1,
      };
    });
    console.log(this.state.num);
  };
  render() {
    return (
      <div>
        <h2>当前的值为:{this.state.num}</h2>
        <button onClick={this.handleAdd}>+1</button>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));
  • setState() 的第二个参数

setState({},callback)

class App extends React.Component {
  state = {
    num: 0,
  };
  handleAdd = () => {
    // 推荐使用该语法:该语法也是异步的,但是多次调用this.setState,里面的num是累加的。
    // state:表示最新的state
    // props: 表示最新的props
    this.setState(
      (state, props) => {
        return {
          num: state.num + 1,
        };
      },
      () => {
        console.log(this.state.num); // 这里的num 跟页面中num值是同步的。
      }
    );

    console.log(this.state.num); // 0
  };
  // componentDidUpdate钩子函数的执行,提前于  this.setState执行
  componentDidUpdate() {
    console.log("componentDidUpdate", this.state.num); // 这里的num 跟页面中num值是同步的。
  }
  render() {
    return (
      <div>
        <h2>当前的值为:{this.state.num}</h2>
        <button onClick={this.handleAdd}>+1</button>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

结论:

  • componentDidUpdate钩子函数的执行,提前于 this.setState执行,this.setState的第二个参数的执行效果跟componentDidUpdate钩子函数的执行效果都是一样的。

5、组件更新机制

5.1 setState() 两个作用

  • 修改state
  • 更新UI(组件)

5.2 组件更新过程

父组件更新时,也会更新当前父组件中所有的子组件,不会影响父组件的父组件更新,也不会影响父组件的兄弟组件更新。

5.3 避免不必要的更新渲染

组件的更新机制:父组件的更新会引起子组件的更新。

上面的问题:当子组件没有任何变化时也会进行更新。

解决办法:使用钩子函数shouldComponentUpdate(nextProps, nextState)

使用shouldComponentUpdate()可以让React知道当前状态或属性的改变是否不影响组件的输出,通过返回值决定该组件是否重新渲染,默认返回ture表示重新渲染,返回false时不会重新渲染,而且该方法并不会在初始化渲染或当使用forceUpdate()时被调用。

使用 shouldComponentUpdate(nextProps, nextState) 中的 nextState,避免不必要的更新渲染

class App extends React.Component {
  state = {
    num: 0,
  };
  handleAdd = () => {
    this.setState(() => {
      return {
        num: Math.floor(Math.random() * 3),
      };
    });
  };
  shouldComponentUpdate(nextProps, nextState) {
    // nextState : 最新状态
    // this.state: 当前状态
    console.log("nextState", nextState, "this.state", this.state);
    /* if (nextState.num == this.state.num) {
      return false;
    }
    return true; */
    // 以上简写成
    return nextState.num !== this.state.num;
  }
  render() {
    console.log("重新渲染");
    return (
      <div>
        <h2>当前的值为{this.state.num}</h2>
        <button onClick={this.handleAdd}>加1</button>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

触发时机:更新阶段的钩子函数,组件重新渲染前执行(shouldComponentUpdate --> render)

使用 shouldComponentUpdate(nextProps, nextState) 中的 nextProps,避免不必要的更新渲染

class App extends React.Component {
  state = {
    num: 0,
  };
  handleAdd = () => {
    this.setState(() => {
      return {
        num: Math.floor(Math.random() * 3),
      };
    });
  };
  render() {
    return (
      <div>
        <NumData num={this.state.num}></NumData>
        <button onClick={this.handleAdd}>加1</button>
      </div>
    );
  }
}
class NumData extends React.Component {
  shouldComponentUpdate(nextProps) {
    console.log("nextProps", nextProps, "this.props", this.props);
    return nextProps.num !== this.props.num;
  }
  render() {
    console.log("重新渲染");
    return <h2>当前的值为{this.props.num}</h2>;
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

5.4 纯组件(PureComponent)

PureComponent中自动实现了shouldComponentUpdate钩子函数,更加方便

PureComponent原理:纯组件内部通过分别对比props、state的值,来决定是否重新渲染组件

class App extends React.PureComponent {
  state = {
    num: 0,
  };
  handleAdd = () => {
    this.setState(() => {
      return {
        num: Math.floor(Math.random() * 3),
      };
    });
  };

  render() {
    console.log("重新渲染");
    return (
      <div>
        <h2>当前的值为{this.state.num}</h2>
        <button onClick={this.handleAdd}>加1</button>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));
class App extends React.Component {
  state = {
    num: 0,
  };
  handleAdd = () => {
    this.setState(() => {
      return {
        num: Math.floor(Math.random() * 3),
      };
    });
  };

  render() {
    console.log("重新渲染");
    return (
      <div>
        <NumData num={this.state.num}></NumData>
        <button onClick={this.handleAdd}>加1</button>
      </div>
    );
  }
}
class NumData extends React.PureComponent {
  render() {
    return <h2>当前的值为{this.props.num}</h2>;
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

纯组件内部对比的是 shallow compare(浅层对比),对于数值类型是可以,但是对于引用类型就会出现问题

注意:props或state属性值为引用类型时,应该创建新数据,不要直接修改原数据

class App extends React.PureComponent {
  state = {
    obj: {
      num: 0,
    },
  };
  handleAdd = () => {
    // 创建新的对象
    const newObj = { ...this.state.obj, num: Math.floor(Math.random() * 3) };
    this.setState(() => {
      return {
        obj: newObj,
      };
    });
  };

  render() {
    console.log("重新渲染");
    return (
      <div>
        <h2>当前的值为{this.state.obj.num}</h2>
        <button onClick={this.handleAdd}>加1</button>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

同时注意:也不要使用数组的push/unshift等直接修改当前数组的方法,这些都是在修改了原数组,没有创建新的数组,应该使用concat或者slice等这些返回新数组的方法。

this.setState({
list:[...this.state.list,{newData}]
})

六、React路由

路由是一套映射规则,在React中,是URL路径与组件的对应关系,其实就是配置路径与组件的配对,让用户从一个视图,导航到另外一个视图中

1、基本用法

// 引入React路由
import { BrowserRouter as Router, Route, Link } from "react-router-dom";

const About = () => {
  return <div>关于页面</div>;
};

class App extends React.Component {
  render() {
    return (
      // 使用Router组件包裹整个应用,Router组件只需要使用一次即可
      <Router>
        <div>
          <h2>React路由</h2>
          {/* 指定路由入口 */}
          <Link to="/about">跳转至关于页面</Link>
          {/* 指定路由出口 */}
          <Route path="/about" component={About}></Route>
        </div>
      </Router>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

2、编程式导航

class Login extends React.Component {
  handleLogin = () => {
    // 编程式导航 push跳转到某个页面
    this.props.history.push("/home");
  };
  render() {
    return (
      <div>
        <div>登录页面欢迎您</div>
        <br />
        <button onClick={this.handleLogin}>登录</button>
      </div>
    );
  }
}
const HomePage = (props) => {
  const handleBack = () => {
    // 编程式导航 go(-1)返回
    props.history.go(-1);
  };
  return (
    <div>
      <h2>首页</h2>
      <button onClick={handleBack}>退出</button>
    </div>
  );
};

const App = () => {
  return (
    <Router>
      <div>
        <h2>注册页面</h2>
        <Link to="/login">去登录</Link>
        <br />
        <br />
        <Route path="/login" component={Login} />
        <Route path="/home" component={HomePage} />
      </div>
    </Router>
  );
};
ReactDOM.render(<App />, document.getElementById("root"));

3、默认路由

const About = () => {
  return <div>关于页面</div>;
};

class App extends React.Component {
  render() {
    return (
      <Router>
        <div>
          <h2>React路由</h2>
          {/* 默认路由 */}
          <Route path="/" component={About}></Route>
        </div>
      </Router>
    );
  }
}
ReactDOM.render(<App />, document.getElementById("root"));

4、模糊匹配和精准匹配

在React中,React路由默认是模糊匹配模式,想要精准匹配如下所示,

给默认路由添加 exact属性

<Route exact path="/" component={About}></Route>