标签: React

1. 如果你是leader,做管理系统项⽬ Vue和React 怎么选择?

  • 评估项⽬成员的⽔平,如果成员js基础较好、编码能⼒较强则选择React,否则Vue。
  • 评估系统的⼤⼩,如果想构建⽣态系统,则选择React,如果要求快速,则选择Vue。当然现在Vue3.x在对Vue2的性能优化之后,开发大型复杂的项目的性能也更好了。
  • 评估系统运⾏环境,如果你想要⼀个同时适⽤于Web端和原⽣APP的框架,请选择React(RN)。当然现在Vue也有相应的原生框架weex。

技术没有哪个更好或者是更优秀,只有适合的才是最合适的。

另外关于大型项目,能否做大型项目关键在于项目组的业务划分、部门之间的协调效率上,因为大型项目不是一两个人,三五个人能够完成的。一个项目之所以称为大项目是在于它是公司大量部门协同合作下的产物。也就是说,解决了项目划分等问题,使用vue和react都是可以的。

2. Vue和React区别

两个语言有共同点和区别。由于是两种不同的语言,区别肯定很多,这里只是列举一些比较重要的点

共同点:

  • 都是 JavaScript 的 UI 框架
  • 都是优秀的 MVVM 前端框架(对MVC和MVVM的理解),但react严格上是只针对mvc的view层,vue则是完全的mvvm模式。
  • 不同于早期的 JavaScript 框架“功能齐全”,Reat 与 Vue 只有框架的骨架,其他的功能如路由、状态管理等都是框架附属的模块。
  • 都支持数据驱动视图,不操作真实dom就能自动渲染页面。
  • 都支持native源生的方案,react的RN(reactNative)和vue的weex
  • 都支持服务端渲染
  • 都支持组件化开发和虚拟dom(virtual Dom)
  • 都支持props进行父子组件间数据通信
  • 都比较容易上手,但VUE 相较于 React 更容易上手,主要是React的JSX语法和我们平时的习惯不同

区别:

1)数据绑定方式不同

vue实现了数据的双向绑定,react可以说是单向绑定。但两者都实现了单向数据流

2)组件写法及模板语法不同

react是jsx(html,css写进js),vue是html,css,js写在同一个文件里。Vue 使⽤的是web 开发者更熟悉的模板与特性,Vue的API跟传统web模板契合度更⾼,⽐如Vue的单⽂件组件是以模板+JavaScript+CSS的组合模式,对应用的升级更方便、更容易,可以更轻松地使用和修改现有应用程序。

3)数据更新方式不同(也就是反应式系统不同),这是最大的区别

Vue提供反应式的数据,当数据改动时,界⾯就会⾃动更新。react需要setState方法对数据进行更新,这一点比Vue 数据更加可控。

React 整体是函数式的思想,在 React 中是单向数据流,推崇数据不可变的思想

Vue 的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立 Watcher 来监听,当属性变化的时候,响应式的更新对应的虚拟 DOM。

所以,React 的性能优化需要手动去做,而Vue的性能优化是自动的,但是Vue的响应式机制也有问题,就是当 state 特别多的时候,Watcher 会很多。

4)DOM更新策略不同(diff算法)

react 会自顶向下全diff;Vue 会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。

在react中,当状态发生改变时,组件树就会自顶向下的全diff, 重新render页面, 重新生成新的虚拟dom tree, 新旧dom tree进行比较, 进行patch打补丁方式,局部跟新dom. 所以react为了避免父组件跟新而引起不必要的子组件更新, 可以在shouldComponentUpdate做逻辑判断,减少没必要的render, 以及重新生成虚拟dom,做差量对比过程.

在 vue中, 通过Object.defineProperty 把这些 data 属性 全部转为 getter/setter。同时watcher实例对象会在组件渲染时,将属性记录为dep, 当dep 项中的 setter被调用时,通知watch重新计算,使得关联组件更新。(Vue3.x的更新机制优化

Diff 算法借助元素的 Key 判断元素是新增、删除、修改,从而减少不必要的元素重渲染。

5)React 内部封装较少,Vue封装内容较多

react 本身做的事情很少,很多都得靠自己进行封装。这是由于React是函数式思想,组件写在函数中。但几乎所有的功能逻辑都需要自己手动编写,这无疑提升了项目成本。

而vue很多东西都是内置的,写起来方便一些。比如一些封装好的指令、依赖注入等。

但这也说明react编写自由度更高。

6)React更新机制需要更多人为操心

React只要调用setState就会触发render重新渲染,甚至视图什么数据都没使用,它不关心也不知道是哪个数据发生了改变。当组件或节点比较多的时候,更新数据可能会造成很多不必要的虚拟DOM的构建,而react-hooks的使用也会带来一些心智负担

Vue的更新无需人为操心,Vue的发布-订阅机制能够明确知道哪个值发生了改变,然后只重新渲染该部分即可。

前端框架火了已经有些年头了。像React、Vue、Angular等,都是视图层数据驱动框架

当然现在最火的两大框架就是React和Vue。而React又出了v16.8版本的Hooks新特性,Vue出了3.x版本。

争论React和Vue哪个更好没有意义。每个框架各有优势,但他们没有本质的区别。

  • Vue更注重视图的自动同步(双向数据绑定),且封装性更好(比如框架封装了完善的事件机制、绑定规范、指令操作等)。使用习惯更偏向前端人员。
  • React更注重组件及其状态的管理(单向数据绑定),更加侧重于逻辑(JS优先)。习惯更偏向于程序人员(比如原先是做PHP的更易上手)。React更加透明,没有封装过多的隐含逻辑(比如指令、事件、数据监听等),便于程序员了解程序的执行过程,但也较Vue使用更复杂。

抛开框架具体本身,宏观上来谈近年来前端框架的发展,我认为前端的发展思路永远是:在保证性能的基础上,提升程序员的开发效率为先。

如果一个框架本身的学习成本和使用成本过高,给开发过程带来很大的心智负担,让程序员对于语言或框架本身使用的关注度超过了对业务逻辑及数据的关注度,那么这个框架一定不是一个好的框架。

简便易用

像React、Vue等,在使用方面有一个最大的特点就是:简便易用!(相对于不使用框架)

以前有人问我说,原来是用原生的,用JQuery的,现在学React,学Vue难不难,复不复杂。

其实新的框架要比老的框架要更加简便易用,这个其实是一个很容易理解的问题。他如果比原来的那玩意还难用,那我们为啥要用他呢?

使用这些框架后,我们可以把精力都放到业务逻辑和数据关系上,对于程序员来说,精力是很有限的。如果让你过多的去处理那些coding层面的细枝末节的东西,那么大的结构,整体的流程方面就会关注不足,很容易拉低效率,延误工时。

视图自动更新

框架最大的优势实际上就是帮我们维护我们的视图层。视图说白了就是页面上的元素。

如果你数据发生了变化,框架会自动将视图进行更新,那么我们的思路就得以解放,精力就不用过多的放到视图上,从而有更多的时间去关注业务逻辑。而业务逻辑、数据等才是决定一个程序根本表现的东西。

虚拟DOM

这个技术实际上老生常谈了。在这里我也不去谈那些没用的什么原理上的东西。无非就是把DOM元素用JS对象的形式进行处理。在视图更新时,通过一系列的diff对比算法对其更新,只更新对应的元素,不会重绘整个页面。

1. 简化操作

虚拟DOM的方式可以帮助我们进行简化操作,使得我们摆脱元素的创建、选取和操作的噩梦。比如React的jsx语法和Vue的模板语法,最终会创建虚拟DOM节点。

在不使用框架前,我们的HTML和JS是分开的。而在虚拟DOM使得我们的HTML标签和JS合二为一(JS优先)。使得我们无需去获取元素。如有需要,可以直接操作标签本身。所见即所得。

2. 提升性能

DOM操作是JS操作中很慢且性能很差的操作。实测一次的DOM操作足够进行成百上千次的其他JS运算。所以在DOM操作非常频繁的,比如带有大的动画特效的页面会非常卡,很吃性能。

而使用视图框架,他不会直接操作真实DOM,而是操作一套虚拟的DOM。以最小化更新真实DOM,从而减小不必要的性能损耗。

基于组件的开发

在开发过程中,实际上存在许多需要多次复用的单元。框架的发展,越来越倾向于使用组件化的开发去把大的视图单元和逻辑分离开来,变成一个个的小功能。

在组件化的开发模式下,只要把一个个的小东西做好了,就可以用组件拼凑出一个大型的应用。

1. 便于大型应用开发

一个大型应用往往比较复杂,往往一眼看去无法直接入手。

大型应用工作任务所需人员众多,如果像原来一样,一大块一个整体,是很难去细化任务的。

如果使用组件,便于页面开发的细化,分工也会更加方便和明确。便于降低整个程序的复杂度。

一个组件可能很简单,但是多个组件组合在一起,那么功能就可能非常强大,威力无穷。

2. 便于功能的复用

一个应用其实是有很多重复的内容和单元的。如果按照原始的开发流程,除了一些业务逻辑,视图结构是不方便复用的。

而组件化的开发就解决了这一难题。

3. 便于使用第三方的组件和开发自己的组件库

你不仅可以组装自己开发的组件,而且还能够使用大量第三方开源的组件库。这也是使用框架的优势之一。

而且你也可以开发自己的组件库,便于后期相关项目视图的统一性。比如我们公司是做法律相关的应用,那么我们可以开发相应的组件库,使得后期所有应用得以统一标准

甚至你可以把自己的组件库开源,为社区做点贡献也是很不错的。

JS优先的设计原则

理论上来说,Vue和React等这些视图层数据驱动框架有一个设计原则就是JS优先。这是与一些古典的模板引擎相对比而言的。

我们知道,视图框架涵盖了模板引擎的功能,但这个功能与模板引擎还有所不同。

模板引擎

模板为主,JS为辅。因为在模板引擎(如EJS、Handlebars等)诞生的年代,JS相对还比较简单,只是浏览器的一个小的脚本语言。像做个小东西如轮播图、请求个数据、做个简单的表单校验。

所以模板为主,优先保障的是静态HTML的结构。js是附加的,是额外的,是添头。

模板引擎更适合于传统开发,偏向于一些比较简单的、静态的、逻辑很少的页面,不利于扩展。比如做个企业站首页,没什么交互功能,只用于展示。

视图框架

JS为主,模板为辅。模板作为JS的一种数据类型。这点在React上尤为明显。

逻辑优先,模板是js逻辑的一部分,可以任意扩展。因为js的灵活程度比HTML强太多了。

适用于现代开发,尤其是大型、复杂的逻辑。

丰富的应用场景

视图框架有非常丰富的应用场景,不仅仅用于开发Web。

可用于移动端开发。比如 React Native 可以开发移动原生应用,比普通混合式应用性能更高。

也可用于开发服务端渲染(SSR静态web应用)的应用。比如React衍生框架Next、Vue衍生框架Nuxt。前后台渲染可以使用同一框架相互结合。

甚至可以用于开发跨平台桌面应用和多端小程序,比如Taro、Electron、Tauri。

1. 原生属性的改写和差异

在 React 中,所有的 DOM 特性和属性(包括事件处理)都应该是小驼峰命名的方式。例如,与 HTML 中的 tabindex 属性对应的 React 的属性是 tabIndex,再比如readOnly和。例外的情况是 aria-* 和 data-* 属性以及自定义的属性,一律使用小写字母命名。比如, 可以用 aria-label 作为 aria-label

1)checked 和 defaultChecked

当 <input> 组件的 type 类型为 checkbox 或 radio 时,组件支持 checked 属性,这个属性在原生的DOM中也是有的。你可以使用它来设置组件是否被选中。这对于构建受控组件(controlled components)很有帮助。而 defaultChecked 则是非受控组件的的新增属性,用于设置组件首次挂载时是否被选中。

2)className

className 属性用于指定 CSS 的 class,此特性适用于所有常规 DOM 节点和 SVG 元素,如 <div>,<a> 及其它标签。

如果你在 React 中使用 Web Components(这是一种不常见的使用方式),请使用 class 属性代替。

3)dangerouslySetInnerHTML

innerHTML 的替换方案,类似于Vue中的v-html指令。通常来讲,使用代码直接设置 HTML 存在风险,因为很容易无意中使用户暴露于跨站脚本(XSS)的攻击。因此,你可以直接在 React 中设置 HTML,但当你想设置 dangerouslySetInnerHTML 时,需要向其传递包含 key 为 __html 的对象。

const root = ReactDOM.createRoot(document.getElementById('root'));

function createMarkup() {
  return {__html: '<h1 style="color:red" class="h">我是标题</h1>'};
}

function MyComponent() {
  return <div dangerouslySetInnerHTML={createMarkup()} />;
}

root.render(<MyComponent/>);

4)htmlFor

由于 for 在 JavaScript 中是保留字,所以 React 元素中使用了 htmlFor 来代替。

<label htmlFor="namedInput">Name:</label>
<input id="namedInput" type="text" name="name"/>

5)onChange

onChange 事件与预期行为一致:每当表单字段变化时,该事件都会被触发。我们故意没有使用浏览器已有的默认行为,是因为 onChange 在浏览器中的行为和名称不对应,并且 React 依靠了该事件实时处理用户输入。

6)selected

如果要将 <option> 标记为已选中状态,请在 select 标签的 value 中引用该选项的值,而非使用selected,这与原生Html完全不同。总的来说,这使得 <input type="text">, <textarea><select> 之类的标签都非常相似—它们都接受一个 value 属性,你可以使用它来实现受控组件。

你可以将数组传递到 value 属性中,以支持在 select 标签中选择多个选项:

<select multiple={true} value={['B', 'C']}>

7)style

不推荐将 style 属性作为直接设置元素样式的方式。在多数情况下,应使用 className 搭配外部 CSS 样式表中定义的 class。style 在 React中多用于添加动态计算的样式。

添加动态计算样式时,style 接受一个采用小驼峰命名属性的 JavaScript 对象(部分浏览器引擎前缀为大驼峰),而不是 CSS 字符串。这与 DOM 中 style 的 JavaScript 属性是一致的。

const divStyle = {
  color: 'blue',
  backgroundImage: 'url(' + imgUrl + ')',
  WebkitTransition: 'all', // note the capital 'W' here
  msTransition: 'all' // 'ms' is the only lowercase vendor prefix
};

function HelloWorldComponent() {
  return <div style={divStyle}>Hello World!</div>;
}

React 会自动添加 ”px” 后缀到内联样式为数字的属性后。如需使用 ”px” 以外的单位,请将此值设为数字与所需单位组成的字符串。

// Result style: '10px'
<div style={{ height: 10 }}>
  Hello World!
</div>

// Result style: '10%'
<div style={{ height: '10%' }}>
  Hello World!
</div>

8)value 和 defaultValue

<input><select> 和 <textarea> 组件支持 value 属性。你可以使用它为组件设置 value。这对于构建受控组件是非常有帮助。defaultValue 属性对应的是非受控组件的属性,用于设置组件第一次挂载时的 value。

2. 生命周期(常用)

1)constructor()

在 React 组件挂载之前,会调用它的构造函数。在为 React.Component 子类实现构造函数时,应在其他语句之前调用 super(props)。否则,this.props 在构造函数中可能会出现未定义的 bug。

通常,在 React 中,构造函数仅用于以下两种情况:

在 constructor() 函数中不要调用 setState() 方法。如果你的组件需要使用内部 state,请直接在构造函数中为 this.state 赋值初始 state

ps:函数式组件不存在构造函数。函数本身就是构造函数。

2)componentDidMount()

在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。

3)componentDidUpdate()

在更新后会被立即调用。首次渲染不会执行此方法。

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。

在这里调用 setState()时必须被包裹在一个条件语句里,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。

4)componentWillUnmount()

会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。

componentWillUnmount() 中不应调用 setState(),因为该组件在此之后将不会重新渲染。

5)static getDerivedStateFromError(error)

此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state。

一般用于构建一个用于外层嵌套的自定义组件,以便在子组件出现错误时降级显示此UI。

ps:getDerivedStateFromError() 会在渲染阶段调用,因此不允许出现副作用(这里只能是纯函数,不允许修改其余变量)。 如遇此类情况,请改用 componentDidCatch()

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染可以显降级 UI
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      // 你可以渲染任何自定义的降级  UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

6)componentDidCatch(error, info)

此生命周期在后代组件抛出错误后被调用。 它接收两个参数:

  1. error —— 抛出的错误。
  2. info —— 带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息。

ps:componentDidCatch() 会在“提交”阶段被调用,因此允许执行副作用。 它应该用于记录错误之类的情况:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染可以显示降级 UI
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // "组件堆栈" 例子:
    //   in ComponentThatThrows (created by App)
    //   in ErrorBoundary (created by App)
    //   in div (created by App)
    //   in App
    logComponentStackToMyService(info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      // 你可以渲染任何自定义的降级 UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

7)函数式组件中,useEffect() 可以取代一部分生命周期

可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdate 和 componentWillUnmount 这三个函数的组合。具体如下:

每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。

与 componentDidMount 或 componentDidUpdate 不同,React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得额外操作很方便。使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。

你也可以使用多个 effect。这会将不相关逻辑分离到不同的 effect 中。但你不可将effect hook放到条件判断中(所有的hooks都不可以)。

effect 只要传递数组作为 useEffect 的第二个可选参数(deps依赖数组)即可限制每次渲染都执行,只会在传入的依赖变化时才执行。需要重点注意的是:请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。

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

在每次渲染后都执行,类似于componentDidUpdate

useEffect(() => {
    document.title = `You clicked ${count} times`;
});

如果 effect 返回一个函数,React 将在执行组件卸载操作时调用它,类似于componentWillUnmount

useEffect(() => { 
    document.title = `You clicked ${count} times`;
    return ()=>{
        document.title = '';
    }
});

如果你的 effect 第二个参数传入一个空数组,则只会在组件挂载后(插入 DOM 树中)调用。类似于componentDidMount

useEffect(() => { 
    document.title = `You clicked ${count} times`; 
},[]);

3. 数据监听

useEffect 实现数据监听的效果,类似于其他框架中的watch

const [count,setCount] = useState(0)
useEffect(() => { 
    console.log('我是改变后的count',count)
}, [count]); 

1. 内置组件

除HTML原生标签外,React还有一些内置的组件。

1)<React.StrictMode>

严格模式。StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。

Ps:严格模式检查仅在开发模式下运行;它们不会影响生产构建

你可以为应用程序的任何部分启用严格模式。

2)<React.Fragment>

是一个类似于 Vue 中的 <template> 一样的空元素,它仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性。还有一种缩写语法<>something...</>,短语法无法传参。

Fragment 能够在不额外创建 DOM 元素的情况下,让 render() 方法中返回多个元素。

Ps:key 是唯一可以传递给 Fragment 的属性。未来我们可能会添加对其他属性的支持,例如事件。

import React, { Fragment } from 'react';

function ListItem({ item }) {
  return (
    <Fragment>
      <dt>{item.term}</dt>
      <dd>{item.description}</dd>
    </Fragment>
  );
}

function Glossary(props) {
  return (
    <dl>
      {props.items.map(item => (
        <ListItem item={item} key={item.id} />
      ))}
    </dl>
  );
}

// 或者
function Glossary(props) {
  return (
    <dl>
      {props.items.map(item => (
        // Fragments should also have a `key` prop when mapping collections
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

3)<React.Suspense>

可以指定加载Loading指示组件,以防其组件树中的某些子组件尚未具备渲染条件。也是唯一一个可支持Loading指示的组件。

React.lazy()引入的动态组件是其支持的唯一用例。也就是说,渲染 lazy 组件依赖该组件渲染树上层的 <React.Suspense> 组件。

lazy 组件可以位于 Suspense 组件树的深处——它不必包装树中的每一个延迟加载组件。最佳实践是将 <Suspense> 置于你想展示加载指示器(loading indicator)的位置,而 lazy() 则可被放置于任何你想要做代码分割的地方。

// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));

const Spinner = ()=>(<div>Loading...</div>)

function MyComponent() {
  return (
    // 显示 <Spinner> 组件直至 OtherComponent 加载完成
    <React.Suspense fallback={<Spinner />}>
      <div>
        <OtherComponent />
      </div>
    </React.Suspense>
  );
}

4)<Profiler>

测量一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分。

这个标签一般很少用。文档:https://zh-hans.reactjs.org/docs/profiler.html

这个元素在生产环境是被禁用的,因为比较吃性能。

2. 组件包含关系

组件之间的包含关系作用类似于Vue/Angular中的插槽(slot)。但这种包含关系相比于插槽更加容易理解。 React 元素本质就是对象(object),所以你可以把它们当作 props,像其他数据一样传递。在 React 中没有“插槽”这一概念的限制,你可以将任何东西作为 props进行传递。

注意:React 组件无继承。如果你想要在组件间复用功能,我们建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。

1) 使用 children prop

可将子组件传递到外层组件中,这使得别的组件可以通过 JSX 嵌套,将任意组件作为子组件传递:

// 传递子组件
function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

// 外层插槽,props.children为自有属性
function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

2) 不使用 children

自行约定所插入的位置:将所需内容传入 props,并使用相应的 prop。这类似于Vue的具名插槽。

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

// 直接将元素标签作为props属性进行传递
function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

3. 多组件对象导出

注意:组件名必须使用大写字母开头!

import React from 'react';

const MyComponents = {
  DatePicker: function DatePicker(props) {
    return <div>Imagine a {props.color} datepicker here.</div>;
  }
}

function BlueDatePicker() {
  return <MyComponents.DatePicker color="blue" />;
}

标签名只能使用点语法(<A.B>),不能使用其他表达式(错误语法:<A[b]>)。如果要解决动态标签问题, 需要首先使用表达式将标签名赋值给一个大写字母开头的变量:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  // 正确!JSX 类型可以是大写字母开头的变量。
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}

4. 组件通信

1)父子通信

父组件给子组件传递数据,使用属性进行传递,子组件通过props获取父组件传递过来的值。

export default function Accordion() {
  return (
      <Panel title="About" isActive={true}>
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
  );
}

function Panel({ title, children, isActive }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive && (
        <p>{children}</p>
      )}
    </section>
  );
}

2)子父通信

父组件将一个方法通过属性传递给子组件,子组件调用props中父组件传来的方法并传参,父组件即可在该方法中获取子组件传递过来的值。说白了父子子父之间的通信都是依赖props。这和其他的框架有所不同,不存在自定义事件及emit触发机制。实际上也更加简单。这也说明,React的props可以传递任何类型的数据。

import { useState } from "react";

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="About"
        index={activeIndex}
        isActive={activeIndex === 0}
        onShow={(val) => () => setActiveIndex(val)}
      >
        美国佐治亚州立大学生物医学研究所研究人员开展的一项新研究显示,一种新的通用流感疫苗用在小鼠身上可预防甲型和乙型流感病毒的不同变种。日前这项研究发表在《公共科学图书馆・病原体》杂志上。
      </Panel>
      <Panel
        title="Etymology"
        index={activeIndex}
        isActive={activeIndex === 1}
        onShow={(val) => () => setActiveIndex(val)}
      >
        “改革开放是当代中国发展进步的必由之路,是实现中国梦的必由之路。”党的十八大以来以习近平同志为核心的党中央,统筹国内国际两个大局,推动许多领域实现历史性变革、系统性重塑、整体性重构,领导全党全国人民开创了改革开放新局面。
      </Panel>
    </>
  );
}

function Panel({ title, index, children, isActive, onShow }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow(index === 0 ? 1 : 0)}>Show</button>
      )}
    </section>
  );
}

3)多组件共享状态

可以使用Redux等工具进行状态管理。

也可以将状态移动到组件的公共父级,然后通过props将其传递给它们。这被称为提升状态,利用hooks(useState()useReducer())封装状态是编写 React 代码时最常见的事情之一。

5. 高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。具体而言,高阶组件是参数为组件,在不改变传入的组件前提下,返回值为新组件的函数。类似于纯函数,没有副作用。

// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
  // ...并返回另一个组件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ...负责订阅相关的操作...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... 并使用新数据渲染被包装的组件!
      // 请注意,我们可能还会传递其他属性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

1. 元素属性及props

1)组件可以接受任意 props,包括基本数据类型,React 元素以及函数。

2)组件接到父组件传来的props是只读的,绝不能够被修改的。这也叫做单向数据流。

3)组件中的标签元素与原生的dom元素不同,其会被转义为对象(虚拟dom)。组件元素的属性与事件与原生的也不尽相同。比如class 在 JSX 中被写作 classNameonclick 在 JSX 中被写作 onClickfor 在 JSX 中被写作 htmlFor。这些新构建的属性和事件一般都是用小驼峰的书写形式。

4)不要将 props “镜像”给 state,请考虑直接使用 props。否则可能会产生BUG(当一个派生 state 值被 setState 方法更新时,你的prop是否需要更新?)。

5)属性可使用展开表达式将所有props一次性赋值:

function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}
// 等价于
function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

6)属性可以条件赋值

const Button = props => {
  const { kind, ...other } = props;
  const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
  return <button className={className} {...other} />;
};

const App = () => {
  return (
    <div>
      <Button kind="primary" onClick={() => console.log("clicked!")}>
        Hello World!
      </Button>
    </div>
  );
};

2. state 和 setState

1)不要直接修改 State,直接修改将不会重新渲染组件,而是应该使用 setState(),类似于小程序中的setData()。如this.state.comment = 'Hello';是错误的。这里也能看出,React的数据是单向绑定且单向流动。

2)Class组件中,构造函数(constructor)是唯一能够直接设置State的地方。但现在一般都使用函数式组件及hook方法取代Class组件了。

3)Class组件中,setState方法会浅合并对象(Object.assign)。而在hook方法中,useState 导出的set方法不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。

const [state, setState] = useState({});
setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});

3. Refs 和 Dom

1)Refs 创建

在React中也有refs,和Vue等其他框架一样,同样在标签中使用ref属性。Refs 允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。但非必要尽量避免能够使用props解决的问题而使用refs

在Class组件中使用React.createRef()来创建ref,或在函数式组件内部使用名为useRef()的hook来创建ref。一般情况下可以使用ref的current属性来访问该元素的dom节点或该自定义组件的实例。不可在函数组件父级中对函数组件使用ref,因为函数组件没有实例。

function MyFunctionComponent() {
   // 这里必须声明 textInput,这样 ref 才可以引用它
  const textInput = useRef(null);

  function handleClick() {
    textInput.current.focus();
  }
  
  // 这样做是正确的(在函数组件内部访问元素dom)
  return (
    <div>
      <input
        type="text"
        ref={textInput} />
      <input
        type="button"
        value="Focus the text input"
        onClick={handleClick}
      />
    </div>
  );
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }
  render() {
    // 这样做是错误的,无法访问的(在函数组件外部访问组件实例)
    return (
      <MyFunctionComponent ref={this.textInput} />
    );
  }
}

useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

2)回调 Refs

你可以在组件间传递回调形式的 Refs,也就是使用回调函数的形式创建Refs,这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。比如将回调函数通过props传递给子组件,这样父子组件都可以访问到子组件中的元素。

React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMount 或 componentDidUpdate 触发前,React 会保证 refs 一定是最新的。

回调Refs创建和使用

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;

    this.setTextInputRef = element => {
      this.textInput = element;
    };

    this.focusTextInput = () => {
      // 使用原生 DOM API 使 text 输入框获得焦点
      if (this.textInput) this.textInput.focus();
    };
  }

  componentDidMount() {
    // 组件挂载后,让文本框自动获得焦点
    this.focusTextInput();
  }

  render() {
    // 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
    // 实例上(比如 this.textInput)
    return (
      <div>
        <input
          type="text"
          ref={this.setTextInputRef}
        />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

父子组件共享Refs

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}
      />
    );
  }
}

在函数式组件中可以使用useCallback()来创建具有变化监听的回调Ref

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}
// 没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的变化通知到我们。
// 注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。
// 在此示例中,当且仅当组件挂载和卸载时,callback ref 才会被调用,因为渲染的 <h1> 组件在整个重新渲染期间始终存在。
// 如果你希望在每次组件调整大小时都收到通知,则可能需要使用 ResizeObserver 或基于其构建的第三方 Hook。

4. 元素中表达式渲染的一些问题

falsenullundefined, and true 是合法的子元素。但它们并不会被渲染。以下的 JSX 表达式渲染结果相同:

<div />

<div></div>

<div>{false}</div>

<div>{null}</div>

<div>{undefined}</div>

<div>{true}</div>

值得注意的是有一些 “falsy” 值(被类型转义),如数字 0,仍然会被 React 渲染。这个特点和其他的一些框架会有些出入。

例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,将会渲染为数字 0

// 如下会被渲染为数字0
<div>
  {props.messages.length &&
    <MessageList messages={props.messages} />
  }
</div>

// 需要渲染右侧组件,&& 之前的表达式应总是布尔值:
<div>
  {props.messages.length > 0 &&
    <MessageList messages={props.messages} />
  }
</div>