React学习笔记-基础使用,hook及原理

222 阅读10分钟

观前提醒

本文是自主学习react的一些小积累,如有错误望大家指正,感谢~

背景

随着vue和react技术栈的普及,这两门技术栈其实已经成为普通前端工程师需要必备的技能点了,vue提供了较为规范的框架cli,但是在项目的不断升级过后,其实对一些额外能力的拓扑和工程化这方面也有了天花板的限制,vue2的旧项目存量代码改动起来可能牵一发而动全身,即便是组件相互独立,组件内部也可能不完全解耦,这是业务决定的,React技术栈依赖丰富的社区维护,相对架构和能力可以自定义维护,并且也提供了一套完整的hook供开发者使用。大型项目尽可能使用react来搭建,中小型两种皆可。

本文无鼓吹任何框架,业务维度上,框架只有最适合,没有最佳。

使用体验

两者虽稍有区别,但实现和使用理念完全不同,vue可能是即开即用的小工具箱,与html和css,js打配合,非常适合新手前端入门;react则是需要开发者熟悉js基础原理和一些中高级前端的语法知识,是浏览器渲染友好型的UI库(来自官网的定义),采用函数式编程和特色的jsx/tsx进行编译,虽与之前前端的习惯有所不同,但函数式编程无疑是较为舒适的,对一些传值,作用域,复用等的理解也可以在此基础上简明扼要的延伸。

但需要提醒的是,两者间并无明确的界限,只是使用上的观感有区别,以及在部分业务场景下可能哪一个更加便捷,二者可以相互转化。

安装react

安装采用官网推荐的脚手架create-react-app,一键生成react文件目录,推荐node版本高于8.10,这里我采用的是16.0+。

npx create-react-app my-app
cd my-app
npm start

在此基础上,可能有typescript的书写需求,为避免后续自己添加环境变量的繁琐,可以使用:

npx create-react-app my-app --template typescript

jsx语法

创建组件时react支持用ES6的class或者函数式编程,个人比较习惯用函数式编程,不同于vue绑定html和css,react支持函数在最后return时直接写dom。

function Home() {
  return (
    <div>
      我是主页,测试
    </div>
  );
};

列表渲染与条件渲染

不同于vue提供了v-for api供列表渲染,React提倡使用原生的js api去渲染列表。

const myList = [
    {
        id: 1,
        name: 'a'
    },
    {
        id: 2,
        name: 'b'
    },
    {
        id: 1,
        name: 'c'
    }
]

function Children(props) {
    return (
        <div>
            <h2>我是子</h2>
            <div>
                {myList.map(item => 
                    <div>
                        id: {item.id}
                        name: {item.name}
                    </div>
                )}
            </div>
        </div>
    );
}

父子组件传值

vue的父传子,子传父使用props和emit/on来解决,若不采用任何hook,react也可同样采用props的思想,函数直接通过传参即可获取父传子参数,子可通过回调函数的形式传递给父值。

// 父传子
function Father() {
  return (
    <div>
        我是主页,测试
        <Children name="父值"></Children> // 通过属性方式传值
    </div>
  );
};

function Children(props) {
    return (
        <div>
            <h1>我是子</h1>
            <div>{props.name}</div> // 这里会显示父值
        </div>
    );
}

// 子传父,回调函数
function Father() {
  const [msg, setMsg] = useState<string>("父值"); // react里jsx更改值使用异步setstate
  function sendMsg() {
    setMsg('信息已发送')
  }
  return (
    <div>
        我是主页,测试
        <Children msg={msg} sendMsg={sendMsg}></Children>
    </div>
  );
};

function Children(props) {
    return (
        <div>
            <h1>我是子</h1>
            <div>{props.msg}</div>
            <button onClick={props.sendMsg}>点击发送信息</button>
        </div>
    );
}

HOC

HOC意为高阶组件,是在原有组件的基础上对组件能力进行拓扑,可以视作是对组件能力的补充。高阶组件包裹着的组件,返回时也视作一个组件。另比较重要的一点,HOC是纯函数,没有副作用。

import React from 'react';

function Children(props) {
    return (
        <div>
            <h2>我是子</h2>
            <div>{props.msg}</div>
            <button onClick={props.myConsole}>打印</button>
        </div>
    );
}

const withMyHeader = ComposeComponent => props => {
    function myConsole() {
        console.log('具备打印能力')
    }
    return(
        <header>
            <h1>套上了标题</h1>
            <ComposeComponent {...props} myConsole={myConsole}></ComposeComponent>
        </header>
    );
};

export default withMyHeader(Children);

image.png 使用高阶组件可以有效复用一些常用业务能力,如组件登录验证能力等。

常用hook

小tips:一些常用的hook能够帮助我们在不同业务场景中游刃有余,框架的最终落地场景是业务本身,所以做技术评估的时候也要看下选型哪个更方便(而非高大上)。

useState

useState返回一个值以及修改这个值的函数,直接地,对于基础类型我们可以直接传入修改后的值进行修改;但当遇到引用类型如数组或对象时,可以借助...扩展运算符保持不修改部分的完整性。

应用

const [msg, setMsg] = useState('默认值'); // 这里括号内可以传默认值
setMsg('修改!') // 如没有其他setstate操作,这里msg值会改变成“修改”

const [obj, setObj] = useState({name: '小黑', age: 18});
setObj(obj => {
    ...obj,
    age: 20
}) // 修改了age为20

为什么是setState?

state作为组件的私有属性,直接去改变值是无意义的,这是由React设计的特性(immutability)决定的。如果是一些简单的值改变还好,但当场景和数据一旦复杂,如果仍然随心所欲的修改数据,可能会导致数据的不一致性。

setState异步问题

若setState是同步,不仅会产生冒泡重复渲染的性能浪费,还会导致依赖state的effect执行。因此,React将单次所有的setState看做一次事务(Transaction),合并批处理后更新调用。

官方回应setState存在批处理:

image.png

源码中的合并操作,实际是更新队列的实现:

// 源码目录 react/packages/react/src/ReactBaseClasses.js  57行
Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  // 这里可以看到一个插入更新队列的操作,实际是创建update和调度update的api
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

// 源码目录 react/packages/react-reconciler/src/ReactFiberClassComponent.old.js 196行
enqueueSetState(inst, payload, callback) {
    ...
    // 将update都放入队列中
    enqueueUpdate(fiber, update);
    // 启动调度
    scheduleUpdateOnFiber(fiber, lane, eventTime);
    ...
}

useEffect

useEffect接收一个副作用的函数,副作用指的是函数产生的对函数外改变状态的操作影响,官方文档举例如下:

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

在浏览器完成布局与绘制之后,传给useEffect的函数会延迟调用,因此像一些接口数据的渲染初始化等等可能会阻塞浏览器渲染的操作均可以放在useEffect中。

应用

const [form,setForm] = useState({
    status:0,
    username: 'xiaohei',
    password: '123456',
    title: ''
});
// 初始化接口数据
useEffect(() => {
    testApi({id:123}).then(res => { // testApi是一个mock接口
        setForm((form)=>({
            ...form,
            title: res.data.title
        }))
    })
})

useEffect的第二个参数

默认情况下,effect会在每轮组件渲染完成后执行,如果依赖改变,effect也会被重复创建。在上述代码中,我们希望的是接口初始化只初始一次,但现实可能会初始一次以上,造成不必要的重复请求资源浪费。 为了减少这一操作,我们可以给useEffect传入第二个参数,只有当第二个参数改变时,该副作用函数才会执行。由此,代码优化为:

// 初始化接口数据
useEffect(() => {
    testApi({id:123}).then(res => { // testApi是一个mock接口
        setForm((form)=>({
            ...form,
            title: res.data.title
        }))
    })
}, []) // 因为空数组内不存在任何依赖项,所以可以视作该副作用只执行一次

该hook还可以充当像vue一样watch的角色,监听某个值的变化进行回调:

const [val,setVal] = useState("");
useEffect(() => {
    console.log('val变化了,进行一些依赖改变的副作用操作')
}, [val]) // 这里的数组里可以监听多个值

useContext

父子组件传值可以依赖props,但如果父子组件嵌套多层的传值依赖props就会显得非常冗余。React提供了useContext可以解决该问题,该函数接收一个context对象,并返回这个对象的当前值,借此可以实现数据的全局共享。

应用

import React,{useState, createContext, useContext} from 'react';
const form = {
  name: 'xiaohei',
  age: 18
};
const FormContext = createContext(form);

function Father() {
  const [msg, setMsg] = useState<string>("父值");
  function sendMsg() {
    setMsg('信息已发送')
  }
  return (
    <div>
        我是主页,测试
        <FormContext.Provider value={form}>
          <Children msg={msg} sendMsg={sendMsg}></Children>
        </FormContext.Provider>
    </div>
  );
};

function Children(props) {
  const form = useContext(FormContext);
  return (
      <div>
          <h1>我是子</h1>
          <div>{props.msg}</div>
          <button onClick={props.sendMsg}>点击发送信息</button>
          <div>昵称:{form.name}</div> // 显示xiaohei
      </div>
  );
}

不足之处与补正

如果我们维护一个context对象数据流,并发布到各个子组件中,在每次改动对象值时,都会引起其他子组件不必要的渲染,这是因为我们还没有对引用类型做深层次的比较,只能浅层的认为该值改变了。

在class component的时代我们可以用PureComponent来进行浅比较,或者使用shouldComponentUpdate进行深层次的比较。在函数式编程的时代,React团队提供了memo这一高阶组件供大家使用,可以有效优化该问题。当然,如果是为了更细粒度的比较,还可以引用useMemo来支持。

memo与useMemo

memo函数支持传递两个形参,第一个形参表示组件,第二个形参传入比较传入组件props的函数。当第二个参数传入为空时,memo默认只进行浅比较组件前后两次变化的props。

function Children(props) {
    ...
}

function areEqual(pre, next) {
    if(pre.xxx === next.xxx) return true;
    return false;
}

export default React.memo(Children, areEqual); // 最后导出组件相当于HOC包一层组件的形式

useMemo函数支持传递两个形参,第一个形参为函数,第二个形参为数组,useMemo会根据判断数组里的参数是否变化来执行第一个参数即传入的函数。

function Children(props) {
    // 当props.count发生改变的时候,会自动触发事件,否则则不出发该回调
    useMemo(() => doSomething(props.count), [props.count])
    function doSomething(count){
        console.log('count change:', count);
    }
    return(
    <div>
        <div>
          <div>{props.count}</div>
          <button onClick={props.onClick}>
            +1
          </button>
        </div>
    </div>
    );
}

function Father() {
    const [form, setForm] = useState({count:0, name: 'xiaohei'});
    function changeCount(){ 
        setForm({
            ...form,
            count: form.count + 1,
        })
    };
    function changeName(){ 
        setForm({
            ...form,
            name: 'xiaohong',
        })
    };
    return(
        <div>
            <Children count={form.count} onClick={changeCount}></Children>
            <button onClick={changeName}>切换名字</button>
        </div>
    )
}

useMemo与useEffect

useMemo和useEffect在用法和传参上近乎相同,但区别是useMemo在dom渲染之前执行,强调比较,避免重复渲染;而useEffect在dom渲染之后执行,强调依赖带来的副作用。

useCallback

useCallback返回一个memorized回调函数,第一个参数传递函数,第二个参数传递依赖数组。仅当依赖数组发生变化时,useCallback里的函数才会重新渲染,换言之,useCallback是对函数做的『持久化』操作。

应用

在业务需求中,数据的依赖往往比较错综复杂,容易『牵一发而动全身』。指的是当某个props或者state发生变化时,我们可能只期望依赖该值的子组件和父组件重新渲染,但如果是引用类型的数值发生变化的话,会导致其他使用该值其他属性的子组件也重新渲染。

这个时候可以利用useCallback和memo打配合:

const Children = memo(({onClick}) => {
    console.log('子组件渲染')
    return(
        <div>
          <div>
            {/* <div>{props.count}</div> */}
            <button onClick={onClick}>
              +1
            </button>
          </div>
        </div>
    );
})

function Father() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('xiaohei');
  const changeCount = () => { 
    setCount( count + 1 );
  };
  const changeName = useCallback(() => { 
      setName('xiaohong')
  }, []); // 函数缓存
  return(
      <div>
          <Children onClick={changeName}></Children>
          <button onClick={changeCount}>父组件:+1</button>
      </div>
  )
}

这样即使父组件重新渲染,依赖该值的子组件也不会再重新渲染,相当于父子做了两层保险工作。

useRef

useRef用于创建一个dom实例以便操作dom,使用useRef时可以默认传一个初始值,也可以传入null。用函数进行dom操作时,若已经挂载到dom元素,则useRef默认创建一个current属性与dom绑定。传入dom对象后,无论形式发生什么改变(如改变宽高,改变标签等),useRefs的.current属性不会触发重新渲染,如需要监听可以用useCallback进行回调操作。

应用

function Father() {
  const refInput = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    refInput.current.value = "ref传入值";
  };
  return (
    <div>
        <input ref={refInput}></input>
        <button onClick={onButtonClick}>click me</button>
    </div>
  );
};

总结

react的使用和vue有互通之处,但react更推行函数式编程,以及引入副作用和依赖的主要概念,可以看到hook基本围绕着这两大理念进行运用。