chenfei-hnu / Blog

个人整理的跟前端相关的文档 ( This is some front-end development related documentation )
9 stars 2 forks source link

React 学习总结 #21

Open chenfei-hnu opened 5 years ago

chenfei-hnu commented 5 years ago

React

其核心机制 就是跟踪组件的状态变化,并将更新的状态映射到到新的界面

背景:

1.MVC框架中频繁的DOM操作,随着项目变得复杂,开始逐步影响页面的整体性能,
  开发者也需要耗费更多的时间进行代码维护

2.基于此情况MVVC的引入双向数据绑定自动同步数据与视图更新,极大降低了我们维护DOM操作的成本

3.但如果每次有数据更改,还是进行全量更新,一样存在频繁操作dom的问题,因此出现了虚拟DOM,
及diff算法;先通过JS对象的比对计算,根据比对差异尽可能小的操作真实DOM

4.因为react只是一个视图层的框架,当组件之间关系变得复杂,组件之间的状态管理变得异常困难
  因此将状态管理交给   redux这样的数据层来统一管理

特点:

1.采用MVVC的声明式的开发方式,不需要关注具体的DOM操作,减少大量编码量

2.单向数据流,只允许父组件向子组件传递数据,不允许在子组件中直接修改父组件数据

3.视图层框架,仅处理视图和页面渲染方面的问题,使用其他数据层的框架辅助开发

4.函数式编程,由一个个函数组成,便于自动化测试和代码优化拆分

JSX介绍

JSX是通过描述HTML结构的信息来生成React元素的JS对象

1.将表达式包含在一个大括号里,以便在JSX中嵌入JavaScript表达式
<h1>hello, {formatName(user)}<h2>

2.JSX拆分为多行以提高可读性,用括号包含
(
    <h1>
        hello, {formatName(user)}
    </h1>
)

3.JSX表达式编译后成为常规JavaScript对象,所以可以在if语句和for循环中使用JSX
可以将其赋值给变量或作为返回值 return <h1>1<h1>

4.React DOM属性使用驼峰命名法约定而不是直接使用HTML属性,class在JSX中变为className

5.JSX中嵌入用户输入是安全的,默认渲染它们之前转义嵌入在JSX中的任何值

6.可以通过React.createElement()表示对象,检测明显的语法错误,同时也是JSX在react的内部实现
const element = React.createElement(
    'h1',
    {className: 'greeting'},
    'hello world'
);
部分元素属性与原生的区别
1.checked
在<input type="checkbox">或<input type="radio">中使用, 用来设置是否选中该组件

2.className
因为class是Javascript的关键字,所以在指定CSS类时,使用className属性

3.dangerouslySetInnerHTML
在React中替换浏览器DOM中使用的innerHTML
输入dangerouslySetInnerHTML并使用__html键传递一个对象,提醒自己这是危险的

4.htmlFor
点击自动聚焦到对应id的表单

5.onChange
React依赖此事件来实时处理用户输入

6.selected
selected属性在<option>中使用,您可以使用它来设置是否选择了组件

7.style
style属性接受一个驼峰命名法的JavaScript对象,更高效,并防止XSS安全漏洞

8.value
用它来设置元素的值

渲染元素

最终都是将React元素渲染到 root DOM节点,可以将它们都传递给ReactDOM.render()

const element = <h1>hello world</h1>;
ReactDOM.render(
    element,
    document.getElementById('root')
);

组件化和属性

1.组件名称始终使用首字母大写

2.引用多个组件或元素时必须包裹在一个根元素中返回

3.如果一个组件的一部分被使用了好几次,或者内部比较复杂,尽量将其优化为可重用的单独组件

4.组件不能修改自己的props

功能性组件

接收props对象作为参数,并返回一个JSX,也成为无状态组件
function Welcome(props) {
    return <h1>hello, {props.name}</h1>
}

类组件

ES6类来定义组件
class Welcome extends React.Component {
    render() {
        return <h1>hello, {this.props.name}</h1>;
    }
}

createClass

createClass本质上是一个工厂函数
const Contacts = React.createClass({
  render() {
    return (
      <div></div>
    );
  }
});

自定义的组件

const element = <Welcome name="zhangyatao" />;

State和生命周期

state 和 props 主要的区别在于 props 是不可变的,而 state 可以根据与用户交互来改变 因此组件需要定义 state 来更新和修改数据,并通过 props 来向子组件传递数据

1.当父组件的render函数被运行时,所有子组件的render函数都将被执行一次

2.当组件的state和props发生改变时,会自动运行render函数

3.几个组件需要访问相同的数据,数据应该提升到层级最近的共同父组件,而不是在不同组件之间去同步状态

4.setState第二个参数是一个回调函数,函数会在setState函数调用完成并且组件重新渲染完毕的时候被调用

5.setState 合并,在 合成事件 和 生命周期函数中多次连续调用会被优化为一次

6.使用 setState 而不是直接修改state,唯一可以分配this.state的地方是构造函数
state更新可能是异步的,用回调函数设置state
this.setState((prevState, props) => ({
    counter: prevState.counter + props.increment
}));

在构造函数中定义this.state

constructor(props) {
    super(props);
    this.state = {date: new Date()};
}

通常在组件类上声明特殊方法,以便在组件使用期间运行一些代码,这些方法称为生命周期函数


Reconciliation 阶段,可以被打断

componentWillMount 组件渲染前调用
getDerivedStateFromProps 
替代componentWillMount,更新组件时,该静态方法会在shouldComponentUpdate前执行,返回值将作为setState的参数
componentWillReceiveProps 在组件 prop 更新后被调用
componentWillUpdate组件接收到新的props或者state但还没有render时被调用
shouldComponentUpdate 在组件接收到新的props或者state时被调用

commit 阶段,不可被打断

componentDidMount组件安装完成
getSnapshotBeforeUpdate 
在组件完成更新后调用,一般用来获取组件更新之前的滚动条位置,返回值将作为第三个参数传递给componentDidUpdate
componentDidUpdate 在组件完成更新后调用
componentWillUnmount组件即将卸载
componentDidCatch render函数抛出错误,则会触发该函数

render()组件重新描绘

因为 Reconciliation 阶段是可以被打断的,所以 Reconciliation 阶段会执行的生命周期函数就可能会出现调用多次的情况,由此对于 Reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 以外,其他都应该避免去使用

当创建组件的实例并将其插入DOM时,会依次调用这些方法:

constructor()
componentWillMount()/static getDerivedStateFromProps
render()
componentDidMount()

更新可以由prop或者state的改变引起,在重新渲染组件时依次调用这些方法:

componentWillReceiveProps()/static getDerivedStateFromProps
shouldComponentUpdate()
componentWillUpdate()
render()
getSnapshotBeforeUpdate
componentDidUpdate()

当从DOM中删除组件时,将调用此方法:

componentWillUnmount()

事件处理

注意:

1.React事件使用驼峰命名法

2.绑定事件时用jsx表达式指向一个处理函数,而不是DOM中的字符串形式

3.JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都
  统一绑定在了 document 上,减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件

4.冒泡到document上的是React的合成事件而不是原生事件,必须显式调用preventDefault阻止默认行为

1.合成事件解决了浏览器之间的兼容问题,使框架具有跨浏览器开发的能力

2.对于原生事件,需要管理事件绑定及取消,分配大量的事件对象造成高额的内存分配,对于合成事件,内部已经实现事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池中复用对象,事件回调结束后,自动销毁事件对象上的属性

在JSX表达式中使用bind onClick={this.handleClick.bind(this)} 这种写法会在每次render的时候都插件一个函数 在构造函数使用 this.fn = this.fn.bind(this)这种写法在绑定事件过多时显得繁琐 使用箭头函数定义方法绑定this,不需要在构造函数中手动绑定this,也不需要担心组件重复渲染导致的函数重复创建的问题

根据条件选择性渲染元素

根据props state属性值判断显示组件内容

1.The user is <b>{isLoggedIn ? '已登录' : '未登录'}</b>

2.You {liked && <span>dis</span>}like this. Click to toggle.

3.render 中return null不显示元素

Lists和Keys的处理

const listItems = numbers.map((item,index) => <li key={item}>{item}</li>);
return (
    <ul>{listItems}</ul>
);

1.应该为列表的每一项提供一个属性key,react利用key来识别组件

2.需要保证某个元素的 key 在其同级元素中具有唯一性,在 React Diff 算法中React 会借助元素的 Key 值来对比元素,从而减少不必要的元素重渲染

表单处理

表单值由state控制的可输入表单元素被称为可控组件

<input type="text" value={this.state.value} onChange={this.change}/>
change(e) {
    this.setState({value: e.target.value});
}

深入理解JSX

1.JSX就是提供了一个React.createElement(component, props, ...children)函数的语法糖

2.如果一个标签没有子元素的话,你可以使用/>来自动闭合

3.可以传递任何JavaScript表达式作为Props,用{}括住它们就可以使用

4.传递一个字符串直接量时,它的值是经过html转义

5.如果你没有给Props传入一个值,那么它的默认值为true

6.如果你有一个对象的数据作为props,并且想在JSX中传递它,你可以使用...运算符整个传递到props
function App() {
    return <Greeting firstName="yatao" lastName="zhang" />;
}
function App() {
    const props = {firstName: 'yatao', lastName: 'zhang'};
    return <Greeting {...props} />;
}
JSX中的子元素和子组件

使用PropTypes进行类型检测

React.PropTypes返回的是一系列验证函数,用于确保接收的数据类似是否有效

class Greeting extends React.Component {
    render() {
        return (
            <h1>Hello {this.props.name}</h1>
        )
    };
}
Greeting.propTypes = {
    name: React.PropTypes.string.isRequired
};

还可以使用自定义函数检测

customProp: React.PropTypes.arrayOf(function(propValue, key) {
    if (!/matchme/.test(propValue[key])) {
      return new Error(
         `Invalid prop `
      );
    }
})

通过使用defaultProps属性来定义props的默认值

Greeting.defaultProps = {
    name: 'zhangyatao'
}

refs和DOM元素

通过ref属性设置回调函数,当在HTML元素上使用ref属性时,ref回调函数接收一个基础的DOM元素作为其参数

<input type="text" ref={input => {
    this.textInput = input;
}}/>

refs 是 React 提供给我们的安全访问 DOM元素或者某个类组件ClassComponent实例的句柄

使用ref回调函数是为类设置一个属性来访问DOM元素的常见模式, 建议将this.refs.myRefName模式的访问改为此模式

不能在函数组件FunctionComponent上使用refs属性,因为它们没有实例

React.forwardRef的应用场景

① 如果目标组件是一个 FunctionComponent,需要用 ref 去传递
② 作为库的开发者的话,使用该库的人是不知道库的组件类别的,那么当库组件类别是 FunctionComponent 的时候,使用者想用 ref 获取库组件
③ redux 中的 connect 方法将组件包装成高阶组件(HOC),获取包装前的组件实例
④ props 不能传递 ref

使用
(1) 函数组件FunctionComponent以接收到一个叫做forwardRef的props, 将其接收的 ref 属性转发到函数组件FunctionComponent内部的一个组件中
const Child = React.forwardRef((props, ref) => (  
    <button ref={ref}>    {props.children}  </button>
));

性能优化

1.使用生产环境的配置进行构建

2.shouldComponentUpdate 允许我们手动地判断是否要进行组件更新,根据应用场景设置函数的合理返回值能够帮我们避免不必要的更新

3.通常使用React.PureComponent替代改动shouldComponentUpdate,使用React.memo纯化一个函数组件,对props和state的所有字段之 间做浅比较,以确定组件是否应该更新,自动加载shouldComponentUpdate函数,减少不必要的render渲染次数

3.通过shouldComponentUpdate返回false,React不会尝试渲染,返回true,React元素返回的元素与之前渲染的元素有差异,则更新DOM

不使用ES6语法编写React应用

用React.createClass来创建一个组件

var Greeting = React.createClass({
    render: function() {
        return <h1>hello {this.props.name}</h1>;
    }
});

1.React.createClass,通过propTypes对象和getDefaultProps()方法来设置和获取props
2.getInitialState()返回初始化的state
3.React.createClass不需要给每个内部方法绑定this
4.一些不同的组件需要共享一些常见的功能(公共方法), React.createClass允许你使用一个mixins属性来实现

不使用JSX编写React应用

return <h1>hello zhangyatao</h1>被编译为
return React.createElement('h1', null, 'hello zhangyatao')

彻底理解React如何重新处理DOM(Diffing算法)

操作真实DOM的耗费的性能代价太高,因此React使用虚拟DOM和diff算法优化DOM操作 每次props或state改变重新render时,通过比对当前虚拟DOM树和之前虚拟DOM树,找出差异后尽可能小的改变真实DOM

Diff算法通过2种方式策略进行差异计算

  1. Tree Diff 两棵树分层比较,同一层进行节点遍历,当发现节点已经不存在时,则该节点及其子节点会被完全删除,如果出现了 DOM 节点跨层级的移动操作,也将被直接删除重新渲染 2.Element Diff 对于同一层的同组子节点通过key值进行遍历对比,来进行移动操作,再来进行删除新增节点的操作

页面渲染的步骤

1.通过React.Component的constructor或者createClass的getDefaultProps、getInitialState初始化组件对象

2.返回render中的JSX 模板

3.react调用 createElement(component,props,...children)创建JS对象,生成虚拟DOM树的JS对象

4.根据此JS对象生成真实DOM,渲染到页面中

5.state或者props发生改变,重新调用返回render中的JSX 模板

6.react调用 createElement生成当前虚拟DOM树的JS对象,与原虚拟DOM树进行Diff算法比对

7.Diff算法先进行根元素比对,当元素的类型发生变化时,直接删除其子元素并将整个组件全部重新生
  成真实DOM并渲染,旧的DOM树相关联的任何state也都将丢失

8.组件类型未变化时,React会对比两者的属性差异,然后保留相同的底层DOM元素,仅仅去更新那些被更改的属性

9.根据上面的判断逻辑对其子元素递归扫描,进行同级比对

10.对列表进行更新时,先将有key值的元素一一对应,如果有变化,仅更改或移动该元素

11.如果没有对应元素,则按元素在列表中的位置进行对比,还是按照元素类型再属性的方式进行判断,
  类型不一致则直接删除并创建真实DOM,一致仅更改元素并继续递归扫描子元素

建议:

1.注意保持DOM结构的稳定,尽可能少地动态操作DOM结构,尤其是移动操作,避免元素层级改变

2.使用 shouldComponentUpdate() 来减少组件不必要的更新

3.对于类似的结构应该尽量封装成组件,既减少代码量,又能减少component diff的性能消耗

4.对于列表结构,尽量减少节点移动的操作

5.key属性应该是稳定和唯一的,否则将导致性能降低以及子组件中的state丢失

通信

1. 父子通信

父组件通过 props 传递数据给子组件,子组件通过调用父组件通过prop传递过来的函数传递数据给父组件

父组件通过 props 传递数据,子组件不能直接修改 props, 必须通过调用父组件函数告知父组件修改数据

2. 兄弟组件通信

通过共同的父组件来管理状态和事件函数

3. 跨多层次组件通信

> 如果在一个组件中定义了contextTypes,则生命周期函数将多接收个参数,就是Context对象

如果contextTypes被定义为无状态功能性组件的属性,无状态功能性组件也能够引用Context
```ruby
function MyButton(props, context) {
     const children = props.children;
     return (
         <button style={{backgroundColor: context.color}}>
             {children}
         </button>
     );
}
MyButton.contextTypes = {
    color: React.PropTypes.string.isRequired
};

如果父组件在shouldComponentUpdate()返回false,子组件永远也不会更新,因此没有办法可靠地更新Context

React顶级API

Components

React组件可以让你将UI部分独立出来,成为可重用的部分,从而单独考虑分离出来的每一部分功能,可以通过 React.Component或者React.PureComponent来创建React组件

创建一个React元素

每个JSX元素都是React.createElement(component, props, children)的语法糖

createElement 函数是 JSX 编译之后使用的创建 React Element 的函数

React.createFactory()返回一个给定类型的React元素的函数

处理React元素

React.cloneElement()
传入一个React元素进行克隆并设置新的 Props

React.isValidElement()
验证一个对象是否是React元素

React.Children
提供了处理this.props.children中那些不透明的数据结构的一些工具函数

map
遍历子元素并对每项进行操作

forEach
遍历子元素并对每项进行操作并返回数组

count
返回children中的组件总数

only
返回children中的只出现一次的子元素

toArray
将子元素中的不透明数据结构作为一个一维数组返回
React.PropTypes

React.PropTypes.array

React.PropTypes.bool

React.PropType.func

React.PropTypes.number

React.PropTypes.object

React.PropTypes.string

React.PropTypes.symbol

React.PropTypes.node 验证prop是一个可以渲染的东西

React.PropTypes.element 验证prop是一个React元素

React.PropTypes.instanceOf() 验证prop是否是class的实例

React.PropType.oneOf() 将其视为枚举来验证prop是否受限于特定值

React.PropType.oneOfType() 验证prop是可以是多种类型之一的对象

React.PropType.arrayOf() 验证porp是一个特定类型的数组

React.PropType.objectOf() 验证prop是具有某个类型的属性值的对象。

React.PropTypes.shape() 验证prop是采取特定形状的对象,对象多属性判断

React.PropTypes.any() 任意类型的数据

以上的验查器默认都是可选的。你可以使用isRequired来标记一个必填属性

ReactDOM.unmountComponentAtNode(container) 从DOM中删除已安装(mounted)React组件,并清除其event handle和state

ReactDOM.findDOMNode(component)

findDOMNode是一个用于访问底层DOM节点的接口,如果此组件已装载到DOM中,则返回相应的html DOM元素

findDOMNode仅适用于已安装的组件

findDOMNode不能用于功能性组件

Redux

Redux是一个JavaScript的可预测的状态管理容器,另外它还能实现时间旅行,即action的回放 使用redux来更好地管理react应用中的state,解决组件间state传递的问题 整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一的 store

1. store:所有数据都来自redux store,它是一个统一管理应用的状态树

2. action:对象数据格式,通常至少有type和data属性,是对redux中任务的描述

3. reducer:通常是以纯函数形式存在,接收state和action对象两个参数,根据action类型执行不同的任务

4. dispatch:store提供的分发action的方法,传递一个action对象给reducer处理

步骤

1.createStore创建响应事件和返回state的store,reducer初始化默认数据并进行数据处理

2.页面渲染一个组件,组件初始化通过store.getState获取组件的初始数据state
  并通过store.subscribe(fn)订阅store数据的变化处理函数

3.用户触发组件的事件,组件封装一个包含事件类型和数值的action对象

4.使用store.dispatch(action)将事件告知store,store事件转交给reducer进行处理

5.reducer处理完毕后将state返回给store保存,数据变更触发组件的订阅函数,更新组件state数据重新渲染视图

优化

1.创建actionTypes和actionCreators,分别创建常量事件类型和定义创建action的函数,组件中直接调用函数

2.reducer中使用switch通过常量判断事件类型处理,加上default直接返回上一次的数据

纯函数:给定固定的输入,就一定会有固定的输出,不能修改参数,不能有异步操作和时间操作

使用中间件可以拓展诸如记录日志,添加监控,切换路由等功能,所以中间件本质上只是拓展了 store.dispatch方法

react-redux

基本开发思想是UI组件与容器组件相分离

UI组件只负责页面呈现,不处理数据,不维护状态

容器组件负责页面的逻辑处理,获取展示组件中的事件,更新状态,将显示组件和redux连接起来

subscribe和dispatch的逻辑都放到了容器组件,UI组件所有的数据和事件都是通过this.props获取

步骤

1.createStore创建响应事件和返回state的store,reducer初始化默认数据并进行数据处理

2.根组件中返回以Provider为子元素的JSX,Provider添加store属性,让里面的所有组件都有能力获取store

3.Provider内部的组件,通过export default connect(mapStateToProps,mapDispatchToProps)(ClassA)
  连接组件与store,mapStateToProps将store里面的数据映射到组件的props,mapDispatchToProps将对象中
  的自定义函数映射到组件的props,connect返回的结果就变成了一个容器组件

4.通过创建actionTypes和actionCreators,在mapDispatchToProps中,dispatch创建的action即可

5.reducer中使用switch对事件进行处理

6.可以使用对象解构赋值简化props,最后因为UI中只有render,将其改为无状态组件(功能性组件)提升性能

可以根据功能定义多个reducer进行代码拆分,最后通过combineReducers合并成一个,作为createStore的参数

redux中间件

中间件提供第三方插件的模式,自定义拦截 action -> reducer 的过程。变为 action -> middlewares -> reducer 这种机制可以让我们改变数据流,实现如异步action ,action 过滤,日志输出,异常报告等功能

redux-logger:提供日志输出

redux-thunk:处理异步操作,在action中允许返回函数,actionCreator返回一个带dipatch的函数,函数中执行异步操作,异步代码dipatch一个action
在返回的函数中使用axios请求数据,因为create-react-app底层也是一个node服务器,访问工程下一个一个路径会先找对应的路由,没有再去public目录下找对应的实际路径

redux-promise:处理异步操作,actionCreator 返回一个 Promise 对象

redux数据处理

使用combineReducers合并各个单独的reducer,取数据是也多加一层state.XXX.XXX
可以在模块目录创建index.js来引入其他模块作为统一的出口文件,因为引入的时候可以省略index.js减少层级
创建当面模块的actionCreator和constants,也可以直接通过出口文件直接导出

使用immutable对象的set方法,结合旧对象和新的属性生成新的对象返回
使用redux-immutabel获取combineReducer,将state变为immutable对象,使用getIn将数组作为参数链式取值

优化

lazy React.Memo useMeno 复杂计算封装useWorker