二十分钟掌握React核心理念,老鸟快速入门指南

817次阅读  |  发布于1年以前

小编前言

现在的前端招聘JD里大概率会有一条要求,“Vue,React 有其一经验”,引申的意思可能是:一个成熟且有着现代前端开发经验的开发者,要学会一门框架应该是成本很低的事情。人常说,框架都是相通的,是否如此呢?

确实,随着各家不停探索,框架的基本形态和功能设计日趋完善,最根本差异点最终演变成了框架开发者各自的理念差异,不同的理念让框架有了各自的设计模式和最佳实践,如果你要准备从现在开始学习掌握 React ,那么先理解 React 的设计理念至关重要。

这是一份适合有着前端开发经验,并习惯使用现代前端框架,却还没深入使用过 React 的老鸟快速入门指南,如果你还没有接触过前端框架,那建议直接在官方文档仔细从头看起。

目标

当我们决定要正式去学习,那我们起码要以掌握它为基本目标,一个有经验的开发者,看一篇技术文档,写一个 Hello world 轻而易举,你即使没仔细了解过 React 的设计理念,翻翻文档应该也能把简单的功能写出来。

但是,这不叫掌握,顶多叫能用,掌握是指你要在了解过所有功能之后,理解框架设计者的设计理念最佳实践,并最好懂得其实现的基本原理,就基本算是掌握了。

本文我会简单说说我理解的 React 设计理念、常识、和一些最佳实践,有些地方对照 Vue 会更容易理解,帮助你快速了解 React。文章略长,请耐心阅读。

理解理念:React 的野心与颠覆式创新

核心理念

React核心原理就是:当数据发生变化时,UI随之更新,就是所谓的数据驱动,之所以说 React 很有野心,是因为它完全抛弃了前端熟悉的开发模式,创造出一个全新的思路,试图颠覆前端工作者的开发方式,确实这是个很伟大的尝试。

实现方式

当然你说 Vue 同样是数据驱动,但不同的是 Vue 做了更上层的封装,Vue 设计了新的类似 HTML 的模板语法,通过选项属性为开发者提供编写逻辑和 state 的地方,再通过一个 viewModel,当监听到 state 变化时再去更新 view ,总结起来就是在前端现有开发习惯下,以“糖水”的方式“注入”了更多让开发变的更加容易的功能,但无疑设计上会变的非常的复杂。

而 React 选择了完全不同的一条路,React 选择让前端开发回归语言本身,从 js 自身找到解决方案,一次性解决以下的所有问题:

最终 React 选择了 js 中的 【函数】 去承载所有的功能,所以 React 的函数组件本质上真的就只是 js 中的普通函数而已,而非 .vue 这种专门需要复杂编译的新产物,一定要理解它就是一个普通函数。

那一个简单的函数是如何实现上述这么多复杂的功能的呢:

  1. 首先 React 将 HTML 与 js 相融合,我们可以在函数组件中直接写 DOM 语法,DOM 在 js 中成为了合法的结构,最终函数组件返回一个 DOM ,就是该函数作为一个 React 组件最终要渲染的 UI,这种写法是和传统开发分歧最大的地方,也是最不容易被开发者接受的地方,很多开发者习惯了单独开发 HTML 模板,第一次开发会有种 HTML 太过于散乱,不直观很难理解的感觉。

    但这正是 React 对开发方式的一种颠覆,让开发者抹平传统开发中 HTML 与 JS 之间巨大的割裂感,更专注于 js 逻辑。

export default function({foo}){
  const dom = <div>React dom 1</div>
  const dom = <div>React dom 2</div>
  if(foo){
    return dom1
  }else{
    return dom2
  }
}
  1. 第二是逻辑,函数本身天然就是可以直接写 js 执行逻辑的,所以将业务逻辑直接写在函数中即可,无需像 Vue 一样提供各种选项API,让开发者将不同的逻辑写进预置好的各种接口里
  2. 第三是组件的状态,需要通过逻辑改变状态从而触发页面的更新,但函数组件是一个纯函数,通过函数自身的执行去渲染页面,所以函数组件本身天然无法留存状态,所以 React 采用 hooks 的方式为组件提供状态,什么是 hooks 这个下面会将,最终实现就是:状态可以写在函数中,既不破坏纯函数的特性,又能在状态变更时使函数组件以最新的状态重新执行,更新页面
import { useState } from 'react'
export default function(){
// 当函数组件中逻辑更新data时,当前函数组件会重新执行,生成最新的dom区更新视图  
  const [data, setData] = useState(1)

 return <div>{data}</div>
}
  1. 第四是生命周期,因为现在大多传统框架架构都会引入生命周期的概念,以在组件的不同阶段去更新视图,但是 React 的函数组件完全摒弃了这一概念,它只是在特定的时机会触发整个函数组件的重新执行,自然会生成最新的视图,不需要做不同逻辑的处理。
  2. 最后是渲染,函数组件本身最终返回一个 DOM 结构,可以理解它本身就是执行的一个渲染逻辑,所以只需要让函数组件在需要更新的时候反复执行自身就行了。

总结

至此,React 几乎全部的核心功能就都通过一个简单函数实现了,所谓函数组件,几乎完全遵循 js 函数的特性,对于开发者来说几乎没有新的概念引入,这相比于 Vue 极大的降低的心智负担,谁不喜欢简洁而纯粹的东西呢

没有模板、没有生命周期、没有指令、没有各种各样的语法糖、没有复杂的执行过程,这是一个只有函数的世界,太优雅了!

写 Vue 时你可能会觉得以后可能随时会出个新功能,让开发变的更舒适,写 React 你会觉得,这就到头了,弟弟!

这玩意谁研究的呢你说,不得不说是个人才!

理念之争

再聊一个广大网友最津津乐道的问题,所以 React 与 Vue 到底谁更好呢?

关键版本

为了你更容易理解 React 的各种概念,在此我们先梳理下 React 重要的历史版本。

从 V16.8.0 开始,React 引入了 hooks 的概念,可以算是 React 里程碑式的更新,hooks 的引入弥补了之前函数组件的缺陷,函数组件因此大放异彩。

从此时开始 React 真正同时并存两种截然不同的开发方式,Class 组件与函数式组件,在22年最新 V18 新版的官方文档更新之前,React 还一直默认使用 Class 组件讲解 React ,所以你可能会因为选择哪种组件而觉得困惑。

Class组件 or 函数式组件

先快速回答常见的问题:

Class 组件和函数组件到底该用哪个?

回答:只需要用函数组件,class组件已经成为历史,可以完全抛弃了

已经写好 Class 组件需不需要改?

回答:不需要

能不能在老项目里写函数组件?

回答:可以,Class组件与函数组件完全可以共存,只要注意 React 版本就可以

React 组件演变顺序经过三个阶段:

  1. Class组件
  2. Class 与函数组件共存
  3. 函数式组件

为什么会有Class组件

在上面讲理念的段落,已经讲过,函数才是贯彻 React 思想最好的载体,所以函数作为组件是最符合的,可是在当时有个局限就是,函数组件内部无法留存状态,函数也更没办法设计一套生命周期,用函数作为组件有严重缺功能陷的,所以在当时选用 Class 作为组件的载体。

Class 组件我现在看起来依然觉得很难接受,原因有几点:

  1. 在现在的前端开发中,Class 真的很少被用到,尤其目前函数式编程盛行的情况下。
  2. 其次 Class 最大的特性就是继承和通过 Class 实例化出一个对象,但这两种特性跟 React 的组件化思想,都极其违和,完全用不到。

可以说 Class 只是为了实现组件基本功能而用,所以最初看到 React Class 组件时,反而觉得没有 .vue 单文件组件来的优雅和直接

函数式组件 + Hooks

但这一切问题都随着 hooks 的推出迎刃而解,彻底抛弃生命周期,并通过 hooks 引入一个不受函数组件重复执行影响的外部变量作为函数组件内的状态,当这个状态变更时,函数组件随之重新渲染,将最新的状态渲染到页面。

现在时间来到了2023年,函数组件 + Hooks 的开发方式广受好评,你已经没有任何理由再去使用 Class 组件了

Hooks

顺着上面说函数组件早期有很大的不足,就是不能留存状态与很难设计生命周期,那 Hooks 是如何解决的呢。

Hooks 设计

想象一下如果将一个纯函数作为组件,纯函数通过自身的重复执行来做到渲染与重复更新,需要在函数多次执行期间保存其中的状态,那我们肯定是需要这个函数之外的空间来存储状态,并且当这个状态被改变时,能监听到并触发函数组件的重新渲染。

Hooks 就是这种方式,字面意思就是钩子,Hooks 将函数钩到一个可能会变化的数据源上,当这个数据变化时,被钩在上面的函数会重新执行,生成新的结果。

内置 useState Hooks 解决状态的问题

useState 用法很简单,如下代码,引入后会在当前函数外声明一个变量:

最终复杂的视图渲染就在函数组件一遍又一遍简单的重复执行中完成了

而且 Hooks 虽然是将状态声明在函数外部,但写法上仍然是写在函数组件的内部的,这让人写起来并不会有函数组件的割裂感

import { useState } from 'react'
export default function(){
// 当函数组件中逻辑更新data时,当前函数组件会重新执行,生成最新的dom区更新视图  
  const [data, setData] = useState(1)

 return <div>{data}</div>
}

内置 useEffect Hooks 解决生命周期的问题

const [state1, setState1] = useState(1)
useEffect(()=>{
  //只有state1发生变化时才执行这段副作用代码
}, [state1])
useEffect(()=>{
  //只有state1发生变化时才执行这段副作用代码
  document.addEventListener('click', fn);
  return ()=>{
    document.removeEventListener('click', fn);
  }
}, [])

内置 useRef 函数组件的逃脱机制

最后还缺少一个非常重要的功能,就是在函数重复执行渲染过程中的数据共享,我们需要一个能从纯函数重复执行中逃脱,贯穿整个组件渲染的变量,有人说,useState 不就是做这个的吗?但是 state 是和渲染绑定的特殊状态,有以下绑定的特性:

  1. state 状态一定和视图渲染有关
  2. state 值的变更会触发函数组件重新执行
  3. state 变更后有一系列复杂的逻辑,要先触发渲染,再执行副作用,在最新的函数组件执行过程中才能拿到最新的state的值
export default function () {
    const [count, setCount] = useState(0);
    function click(){
        setCount(2); 
        console.log(count) // 打印 0
    }
    return <>
        <span onClick={click}>点击</span>
        <p>{count}</p>
    </>
} 

所以需要在一个复杂的时机才能拿到 state 最新的值,而我们需要一个与渲染无关的数据,能贯穿重复的函数执行,变更后不需要触发函数重新渲染,并且不需要在意此时函数的渲染过程是非常有必要的

useRef 就实现了这个功能,他在函数组件首次执行时创建,你可以在函数任何逻辑中直接更改 useRef 的值,它会立即同步更改,并贯穿重复的函数执行,无需任何心理负担。

除了能存储函数组件重复执行过程的共享数据,useRef 在存储DOM节点,和清理某次渲染过程产生的闭包逻辑有非常重要的意义

import { useState, useRef } from "react";

export default function Timer() {
 const [time, setTime] = useState(1)
  const timer = useRef(null);
  const domRef = useRef(null);

  const click = function(){
     // 需要再每次执行前清理掉之前的定时器,
      // 如果不使用 useRef ,函数组件重复渲染后无法找到上一次函数执行产生的定时器对象
     window.clearInterval(timer.current);
     // 延时器对象赋值给 useRef
      timer.current = window.setTimeout(() => {
      setTime(time + 1);
    }, 5000);
  }
  const getDOm = function(){
    // 我们可以毫无负担的获取到domRef节点,不受函数组件重复执行的影响
    console.log(domRef)
  }

  return (
    <div ref={domRef}>
      <p>{time}</p>
      <button onClick={click}>add</button>
      <button onClick={getDOm}>getDom</button>
    </div>
  );
}

useState useEffect useRef 一把梭?

跨组件通信 useContext

缓存 useCallback 与 useMemo

import { useState, useRef } from "react";

export default function ({a,b}) {
  // 函数内部计算,每次函数重新渲染都会重新计算,无论 a b 是否变化
  // 如果这里是非常占耗CPU的计算,可能会阻塞页面渲染
 const c = a + b

  // 每次函数重新渲染方法都会重新声明
  const click = function(){
     // 声明一个内部方法
  }

  return (
    <div>
      <p>{c}</p>
      <button onClick={click}>click</button>
    </div>
  );
}

总结

自定义 Hooks 带来的逻辑复用新形态

全文说了这么多,其中强调的最重要一条就是开发思维的转换,思维不转换你永远无法灵活的去使用,这里要重点提到 React 带来的另外一个给我们开发方式带来巨大的转变的特性:自定义 Hooks

在以前无论是 Vue 还是 React 组件内的逻辑复用都异常艰难,通常情况我们只能封装 js 自身的对象和方法,比如封装一个函数,但组件内部的功能却是无法封装的,比如我们只能封装一个普通方法,不可能封装出一个带有响应式的方法。

这个过程中 React 出现过一些组件内逻辑封装的设计模式,比如高阶组件HOC、混入Mixin,Vue 也曾采用过 Mixin ,但使用度很低现在也都被官方废弃了,因为他们的使用实在太过牵强,很多人宁愿复制代码,也不想使用他们,我就是其中一员。所以我们需要一个能将组件内逻辑再次封装复用的功能,自定义Hooks的推出就是解决了这一点。

比如我们要在多个组件中实现获取视口宽度的功能,在以前如果我们不想在每个组件中都写一套事件监听程序,那就需要在父组件中写一个监听程序,监听到变化后将视口宽度通过 props 传递给子组件,子组件才能响应式更新,而现在我们可以在自定义 Hooks 直接使用 useState 给组件返回一个响应式的 state。

// useWindowSize.js 
export default function useWindowSize (){
  const [size, setSize] = useState(getSize());
  useEffect(() => {
  const handler = () => {
      setSize(window.innerWidth)
    };
    window.addEventListener('resize', handler);
    return () => {
      window.removeEventListener('resize', handler);
    };
  }, []);

  return [size];
};

// jsx中使用
export default function(){
  const [size] = useWindowSize();
  if (size >1000 ) {
    return <SmallComponent />;
  }else{
    return <LargeComponent />;
  }
};

从此在 React 中,又多了一种新的封装形态,自定义 Hooks,让相同业务逻辑拆分的更清晰,降低代码的冗余,提高代码的复用程度

React 函数式编程的意义

在函数式编程中,函数是头等对象即头等函数,这意味着一个函数,既可以作为其它函数的输入参数值,也可以从函数中返回值,被修改或者被分配给一个变量。λ演算是这种范型最重要的基础,λ演算的函数可以接受函数作为输入参数和输出返回值。

比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。(来自维基百科)

文章的最后我想探讨下我对React 设计的理解。

很多人在从 Vue 转 React 觉得不太适应,或者从 Class 组件换成函数式组件觉得无法 get 到函数式组件设计的意义,我想这可能和编程习惯有关,传统前端开发方式我们受面向对象的编程思想影响颇深,我们常习惯在 类->继承->对象 的基础上去思考我们的开发方式,我们继承一个类,执行一个方法,改变一个属性,更新一个视图,我们在对象这个基础上,可以构建出很多复杂的功能。

但是 React 似乎更愿意在函数上做文章,React 用函数作为组件的基础,用纯函数简单的重复执行来替代复杂的视图更新流程,Hooks 也为是函数,useState 触发函数组件的执行并作为纯函数执行的不同输入,useEffect 将函数组件中所有的副作用从函数中隔离出去,自定义 Hooks 也是函数可无缝的和函数组件组合使用,所以如果你真的理解 【函数】 在 React 中的意义,那你就能体会到,React 看起来如此复杂的框架,归根结底只是通过函数的执行去渲染出一个视图而已,就是如此的简洁。

所以如果你能用函数式编程的思想去思考如何通过函数的执行实现你想要的功能,我想你才真正的掌握了 React 的精髓。

参考文献

  1. React 官方文档
  2. React Hooks 核心原理与实战
  3. 深入理解函数式编程

Copyright© 2013-2019

京ICP备2023019179号-2