学习react

学习使用React

准备工作:首先先在html文件中引入如下的脚本:

  <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

 	<!-- 用于编译jsx -->
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
	
	<!-- 引入我们自己的脚本,需要打上type属性 -->
	<script src="./reactApp.js" type="text/babel"></script>

创建组件,class声明和 function声明

// reactApp.js
const e = React.createElement; //将渲染函数中的jsx转化为react元素

//使用class创建组件,必须要有render函数
class Demo extends React.Component {
  render() {
    return <h1>Hello Demo!</h1>;
  }
}

//使用函数创建组件
function Demo1() {
  return <h1>Hello Demo1!</h1>;
}

const app = document.querySelector("#app");

//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(Demo), app);

组件的生命周期的使用

下图为react组件的生命周期:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tVLAi64s-1627715673108)(/Users/user/Desktop/react-life-cycle.png)]

以上的图清晰的解释了挂载时、更新时和卸载是会触发的生命周期函数。以下的程序可以触发各个生命周期函数:

const e = React.createElement; //将渲染函数中的jsx转化为react元素

class Demo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    console.log("construstor");
  }
  add = () => {
    this.setState((state, props) => {
      return {
        count: state.count + 1,
      };
    });
  };
  static getDerivedStateFromProps(props, state) {
    console.log("getDerivedStateFromProps");
    //它应返回一个对象来更新 state
    return state;
  }
  componentDidMount() {
    console.log("componentDidMount");
  }
  shouldComponentUpdate() {
    console.log("shouldComponentUpdate");
    //返回false的话,使用setState则永远不会更新,也不会到达render阶段
    return true;
  }
  getSnapshotBeforeUpdate(preveProps, preveState) {
    console.log("getSnapshotBeforeUpdate");
    //应返回一个快照值(或 null)。
    return preveState;
  }
  componentDidUpdate() {
    console.log("componentDidUpdate");
  }
  componentWillUnmount() {
    console.log("componentWillUnmount");
  }
  render() {
    console.log("render");
    return (
      <div>
        <button onClick={this.add}>add</button>
        {this.state.count}
      </div>
    );
  }
}

function App() {
  return (
    <div>
      <Demo></Demo>
    </div>
  );
}

const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
let d = document.createElement("button");
d.innerText = "destroy";
d.onclick = function () {
  //卸载组件方法
  ReactDOM.unmountComponentAtNode(app);
};
document.body.appendChild(d);

个人理解的一些常用生命周期函数可以做的事情:

  • render:简单说就是需要在这个函数里返回要渲染的东西
  • constructor:应该要在其他语句前调用super(props),可以在这里初始化state,但是不应该在此处使用setState()。也可以在此处发起一些异步请求。
  • componentDidMount:此时可以获取到对应的DOM节点,可以在此处访问DOM节点。
  • componentWillUnmount:组件卸载及销毁前调用。可以在此处做一些收尾工作,如清除定时器和取消网络请求等。

组件的页面状态和属性的设置

props的设置和使用
const e = React.createElement; //将渲染函数中的jsx转化为react元素

class Demo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    console.log("construstor");
  }
  add = () => {
    this.setState((state, props) => {
      return {
        count: state.count + 1,
      };
    });
  };
  forceAdd = () => {
    this.setState({ count: this.state.count + 1 });
    //使用forceUpdate可强制重新渲染
    this.forceUpdate(() => {
      console.log("forceUpdate");
    });
  };
  static getDerivedStateFromProps(props, state) {
    console.log("getDerivedStateFromProps");
    //它应返回一个对象来更新 state
    return state;
  }
  componentDidMount() {
    console.log("componentDidMount");
  }
  shouldComponentUpdate() {
    console.log("shouldComponentUpdate");
    //返回false的话,使用setState则永远不会更新,也不会到达render阶段,
    //但是使用forceUpdate可以强制重新渲染
    return true;
  }
  getSnapshotBeforeUpdate(preveProps, preveState) {
    console.log("getSnapshotBeforeUpdate");
    //应返回一个快照值(或 null)。
    return preveState;
  }
  componentDidUpdate() {
    console.log("componentDidUpdate");
  }
  componentWillUnmount() {
    console.log("componentWillUnmount");
  }
  render() {
    console.log("render");
    return (
      <div>
        <button onClick={this.add}>add</button>
        <button onClick={this.forceAdd}>forceAdd</button>
        {this.state.count}
      </div>
    );
  }
}

function App() {
  return (
    <div>
      <Demo></Demo>
    </div>
  );
}

const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
let d = document.createElement("button");
d.innerText = "destroy";
d.onclick = function () {
  //卸载组件方法
  ReactDOM.unmountComponentAtNode(app);
};
document.body.appendChild(d);

state的设置和使用
const e = React.createElement; //将渲染函数中的jsx转化为react元素

//在hook未出现之前,只能在class中使用state
class Clock extends React.Component {
  constructor(props) {
    super(props);
    //只能在构造函数中初始化状态
    this.state = {
      date: new Date(),
      count: 0,
    };
  }
  getCount = () => {
    this.setState({
      date: new Date(),
    });
    this.setState((state, props) => {
      console.log(state, props);
      return {
        count: state.count + 1,
      };
    });
  };
  render() {
    //使用setState()函数来更新状态

    return (
      <div>
        {/* 使用state时,需要使用this */}
        <button onClick={this.getCount}>{this.state.count}次获取时间</button>
        <h1>It is {this.state.date.toLocaleTimeString("en-uk")}.</h1>
      </div>
    );
  }
}

function App() {
  return (
    <div>
      <Clock></Clock>
    </div>
  );
}

const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);

对于state的使用,需要注意的是:

  • 不要直接修改state,更新state只能使用setState()函数。
  • stateprops的更新可能是异步的,所以不要依赖他们的值来更新下一个状态。

state的深入学习:

组件间的通信

父组件向子组件通信
  • 使用props
  • 使用ref
const e = React.createElement; //将渲染函数中的jsx转化为react元素

class Child extends React.Component {
  render() {
    return (
      /**
       *  可以使用Refs回调给DOM元素或者组件添加ref属性,
       * 回调函数第一个参数即对应的元素或者组件
       */
      <h1 ref={this.props.h1Ref}>从父组件接收到的数据:{this.props.data}</h1>
    );
  }
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.child = React.createRef(); //使用React.createRef()创建一个Refs
    this.h1Ref = null;
  }
  getChildInfo = () => {
    console.log(this.child);
  };
  getChildH1Info = () => {
    console.log(this.h1Ref);
  };
  render() {
    //使用props与子组件进行通信
    //给子组件添加ref属性后,可以直接访问子组件的信息
    return (
      <div>
        <button onClick={this.getChildInfo}>getChildInfo</button>
        <button onClick={this.getChildH1Info}>getChildH1Info</button>
        <Child
          data={"传给子组件的数据"}
          ref={this.child}
          h1Ref={(el) => (this.h1Ref = el)}
        />
      </div>
    );
  }
}

function App() {
  return (
    <div>
      <Parent></Parent>
    </div>
  );
}

const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);

子组件向父组件通信
  • 调用父组件传进来的回调函数
const e = React.createElement; //将渲染函数中的jsx转化为react元素

class Child extends React.Component {
  render() {
    return (
      <button
        onClick={() => {
          // 使用回调函数向父组件通信
          this.props.sendMsg("来自子组件的消息");
        }}
      >
        发送消息给父组件
      </button>
    );
  }
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
  }
  sendMsg = (msg) => {
    console.log(msg);
  };
  render() {
    return (
      <div>
        {/* 向子组件传递回调函数 */}
        <Child sendMsg={this.sendMsg} />
      </div>
    );
  }
}

function App() {
  return (
    <div>
      <Parent></Parent>
    </div>
  );
}

const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);

跨组件通信
  • 使用Context
  • 使用Redux(暂时搁置)
const e = React.createElement; //将渲染函数中的jsx转化为react元素

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext("light");
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: "dark",
    };
    this.toggleTheme = () => {
      console.log("toggle");
      this.setState((state) => ({
        theme: state.theme === "dark" ? "light" : "dark",
      }));
    };
  }
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      //此处对context使用了动态值
      <ThemeContext.Provider value={this.state.theme}>
        <Toolbar toggleTheme={this.toggleTheme} />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
class Toolbar extends React.Component {
  render() {
    return (
      <div>
        <ThemedButton onClick={this.props.toggleTheme} />
      </div>
    );
  }
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  // 此处也可以在类的外部写一行"ThemeButton.contextType=ThemeContext"来替换
  static contextType = ThemeContext;
  render() {
    return <h1 {...this.props}>Theme:{this.context}</h1>;
  }
}

const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);

此处稍微忽略了一些在函数式组件上的应用:Context.Consumer、“在嵌套组件中更新Context”。

Context.Consumer的用法如下:

//我们可以在标签内使用一个函数来渲染一个依赖context的dom<MyContext.Consumer>  {value => /* 基于 context 值进行渲染*/}</MyContext.Consumer>

若要在嵌套组件中更新Context,这个可以参考官网文档,讲的不错。他是吧context设置成了一个对象,对象中有一个更新方法,这样嵌套组件就可以使用这个方法更新了。使用hook进行嵌套更新的例子可以看之后关于Context Hook的使用。

有一个稍微不懂的问题就是文档中关于Context的注意事项(已解决):当value的值为state中的值时,更新后,value仍然是同一个对象,所以Provide并不会重新渲染。

高阶组件

高阶组件的概念和高阶函数类似,一个函数接受一个组件作为参数,然后经过处理返回一个组件,那么这个函数就可以被称为高阶组件。自己写了一个简单的例子如下:

const e = React.createElement; //将渲染函数中的jsx转化为react元素//创建高阶组件的函数function hoc(WrappedComponent, fn) {  return class extends React.Component {    constructor(props) {      super(props);    }    render() {      // 将传递进来的props传递给被包裹的组件      return <WrappedComponent {...this.props} onClick={fn}></WrappedComponent>;    }  };}function C(props) {  console.log(props);  //点击事件可由hoc中的参数决定  return <h1 onClick={props.onClick}>{props.name}</h1>;}//创建一个高阶组件const Demo = hoc(C, () => {  console.log("wqq");});//创建一个高阶组件const Demo1 = hoc(C, () => {  console.log("wqq1");});class App extends React.Component {  render() {    return (      <div>        <Demo name={"WrappedComponent"} />        <Demo1 name={"WrappedComponent1"} />      </div>    );  }}const app = document.querySelector("#app");//渲染react元素并挂载到对应的dom上ReactDOM.render(e(App), app);

需要注意,不要改变原始组件,若修改了原始组件,若使用另一个hoc继续修改,那么高阶组件的效果可能会在你的预期之外。详细可见https://zh-hans.reactjs.org/docs/higher-order-components.html

只将相关的props传递给被包裹的组件。这是react官方提的一个约定。

以下是注意事项:

  • 不要在render方法中使用hoc。引用官方文档的一句话“如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。”,若我们在render函数中使用hoc,那每次render函数都会创建一个新的组件,注意是新的,这样以来react就会卸载前一个子树,然后重新渲染这个hoc。这样对性能有很大的影响。

  • 在hoc中,refs并不会被传递。可以通过使用React.forwardRef来解决这个问题。

  • hoc不会有原始组件的静态方法。我们可以在函数中将需要的方法拷贝下来:

    function enhance(WrappedComponent) {  class Enhance extends React.Component {/*...*/}  // 必须准确知道应该拷贝哪些方法 :(  Enhance.staticMethod = WrappedComponent.staticMethod;  return Enhance;}
    

"约定:最大化可组合性"看不出有什么。

const CommentWithRelay = Relay.createContainer(Comment, config);// React Redux 的 `connect` 函数const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

当时看不懂“最大化可组合性”的原因是:看不太懂以上两种写法的区别有什么。请教了一下导师后,清楚了一些。第一行这样写的原因可能是高阶组件初始化的时候,comment就需要使用到config。第二行的可能是,使用时,首先需要使用connect来初始化一些配置(这些配置可能与后面传进来的组件关系并不大),然后返回一个函数,这个函数再接受一个组件,执行后返回一个新组件。对组合性的理解就是:配置和组件的组合性,以上第二行的代码就是一种组合性。

函数式组件和 hook

hook的定义

首先看看官方定义:“Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。“。结合官方的定义,我的理解是之前我们写的几乎都是class组件,我们可以在class组件中使用生命周期函数、“refs”、"state"等,但是在函数组件上较难使用。于是官方就做了一些函数,这些函数能够让我们在函数组件中使用react特性,这些函数就叫做hook。

State Hook

使用"useState"这个hook挺简单的,例子如下:

const e = React.createElement; //将渲染函数中的jsx转化为react元素
const { useState } = React;
function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);

在class组件中,我们使用"this.state={}"来初始化我们的state,然后使用"this.setState()"来更新我们state。在hook中,我们使用"useState"来初始化我们需要的状态变量,传入的参数即初始值。然后我们通过数组解构得到我们需要用到的变量和更新变量的方法。更新方法和"this.setState()"作用都是差不多的,都可以更新state。

在hook api索引中的补充:

  • setState可以传入一个函数来更新,函数第一个参数为更新前的值,需要返回更新后的值。若更新前后值不变,则会跳过随后的渲染。
  • useState也可以接受一个函数来初始化初始值,初始值可在在该函数中进行复杂计算。函数需要返回一个值作为初始值。
Effect Hook

使用"useEffect"hook也是比较简单的,如下:

const e = React.createElement; //将渲染函数中的jsx转化为react元素
const { useState, useEffect } = React;
function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`You clicked1 ${count} times`);
    return () => {
      console.log("clean1");
    };
  });
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);

“useEffect"相当于"componentDidMOunt”、“componentDidUpdate"和"componentWillUnmount"三个生命周期函数的组合。组建每经历一次这三个生命周期,都会执行一次"useEffect"中的函数(我们将它称为effect)。值得注意的是,我们在"useEffect"中返回了一个函数,这个函数将在下一次执行effect之前执行。我们可以在这个返回的函数中进行一下一些清除操作,官网文档的例子是个不错的例子。在函数组件中,我们也可以调用多次"useEffect”。那么问题来了,执行顺序是怎么样的呢?我用了一个例子测试了一下:

const e = React.createElement; //将渲染函数中的jsx转化为react元素
const { useState, useEffect } = React;
function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`You clicked1 ${count} times`);
    return () => {
      console.log("clean1");
    };
  });
  useEffect(() => {
    console.log(`You clicked2 ${count} times`);
    return () => {
      console.log("clean2");
    };
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);

//点击按钮两次,打印结果如下:
You clicked1 0 times
You clicked2 0 times
clean1
clean2
You clicked1 1 times
You clicked2 1 times
clean1
clean2
You clicked1 2 times
You clicked2 2 times

执行effect的顺序是按照添加的先后顺序执行的,清除effect也是一样的顺序,但是执行effect之前是会先把所有的effect先清除完。

effect会在每次渲染的时候执行或者清除,这有时候会产生性能问题,"useEffect"提供了第二个参数,它是一个数组,里面存放state,若渲染前后state的值不变,则会跳过该effect,否则会执行和清除。使用例子如下:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

Context Hook

Context Hook使用注意的一点就是第一个参数必须为React.createContext 的返回值。以下的例子也展示了使用hook怎么嵌套更新context:

const e = React.createElement; //将渲染函数中的jsx转化为react元素
const { useState, useEffect, useContext } = React;

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee",
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222",
  },
};

const ThemeContext = React.createContext({
  theme: themes.light,
  change: () => {},
});

function App() {
  const [theme, setTheme] = useState(themes.light);

  return (
    //将setTheme传递进上下文
    <ThemeContext.Provider value={{ theme, changeTheme: setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const themeObj = useContext(ThemeContext);
  return (
    <button
      style={{
        background: themeObj.theme.background,
        color: themeObj.theme.foreground,
      }}
      //点击时则触发上下文的更新函数
      onClick={() => {
        themeObj.changeTheme(themes.dark);
      }}
    >
      I am styled by theme context!
    </button>
  );
}

const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);

Ref Hook

官方文档解释ref hook解释的不错,这里引用他的解释:“useRef 就像是可以在其 .current 属性中保存一个可变值的’盒子‘。“。useRef返回的值并不会随着渲染的更新而更新,所以可以用来保存一个你想在下一次更新后还想使用的变量。官网使用了一个指向dom的ref,这个大家都可以理解,以下我使用的是只想timeout的ref:

	//这个例子是给输入框加入一个防抖功能,inputVal是输入框的值,onInputChange是输入框的值更新了触发的函数
	const [inputVal, setInputVal] = useState(initValue || '');
  const timer = useRef(null);
  useEffect(() => {
    clearTimeout(timer.current);
    timer.current = setTimeout(() => {
      onInputChange(inputVal);
    }, 200);
  }, [inputVal]);
  // const [timer, setTimer] = useState(null);
  // useEffect(() => {
  //   clearTimeout(timer);
  //   setTimer(
  //     setTimeout(() => {
  //       onInputChange(inputVal);
  //     }, 200)
  //   );
  // }, [inputVal]);

把注释去掉,然后使用注释中的代码也可以更新timer。使用state和ref都可以更新timer,那么为什么会优先使用ref呢?state是保存组件的状态的,它更偏向于保存和视图相关的一些状态。而ref则是保存和代码逻辑相关的一些变量,就比如例子中的timer。这是一个原因。另外一个原因是,当 ref 对象内容发生变化时,useRef不会通知你。变更 .current 属性不会引发组件重新渲染。而变更state会引发组件重新渲染,这里timer的更新并不需要我们对视图进行更新,所以使用ref更佳。

hook规则

**不要在循环,条件或嵌套函数中调用hook。**具体为什么,可以去学习一下其原理。

自定义hook

可以把自定义hook简单理解为在一个函数中使用hook,这个函数就可以被称为自定义hook了。自定义hook可以提高代码的复用性。这里要注意一个约定:自定义hook必须以"use"开头,这便于react自动去检查自定义hook是否违反了hook的规则。以下是自己写的简单例子,不过官方文档的例子更能体现自定义hook的特点。

const e = React.createElement; //将渲染函数中的jsx转化为react元素
const { useState, useEffect } = React;

//自定义hook,记录数字的奇偶性
function useNumType(number) {
  const [type, setType] = useState(0);

  useEffect(() => {
    setType(number % 2);
  });

  return type ? "odd" : "even";
}

function App() {
  const [count, setCount] = useState(0);
  const type = useNumType(count);

  return (
    <div>
      <p>You clicked {count} times</p>
      <p>
        {count} is {type}
      </p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值