React 初识
React
We built React to solve one problem: building large applications with data that changes over time.
- 声明式的,用于构建用户界面的 JavaScript 库
- 组合模型,using composition instead of inheritanc
- 单向响应的数据流
- JSX,语法糖,类型检查,执行速度快(尽可能减少与DOM直接操作的次数)
核心
- 组件
- 虚拟DOM:解决jQuery操作真实DOM慢的问题
- 响应式UI
普通的现代构建管道通常包括
- 包管理器(package manager):如npm或Yarn。它可以利用大量的第三方软件包生态系统,并轻松安装或更新它们
- 打包工具(bundler):如webpack或Browserify。它允许编写模块化代码并将他们打包成为一个小包,以实现加载性能的优化,节省加载时间
- 编译器(compiler):如Babel。它可以在编写现代JavaScript代码的同时兼容旧版本浏览器
常用库概览
react.js:React的核心库 react-dom.js:提供与DOM相关的操作功能 Browser.js:将JSX语法转为JavaScript语法(耗时)
元素
Elements are the smallest building blocks of React apps.
组件
Small and Isolated pieces of code,模版即组件,组件即HTML自定义标签,是包含了模板代码的一种特殊的HTML标签类型。
- 函数式组件
- 类组件
当组件第一次渲染到DOM时,在React中称为挂载(mounting);当组件产生的DOM被销毁时,在React中称为卸载(unmounting)。
所有React组件都必须是纯函数,并禁止修改其自身props 。在JSX回调中必须注意this的指向,提供3种方法:
- 在构造函数中显式绑定:.bind(this)
- 使用箭头函数:onClick={(e) => this.handleClick(e)}
- 保持使用ES5风格:createReactClass
推荐第1种。箭头函数每次渲染时都创建一个不同的回调。多数情况下没问题,然而如果这个回调被作为prop(属性)传递给下级组件,这些组件可能需要额外的重复渲染。
setState
- (可能是)异步执行
- 构造函数是唯一可以初始化
this.state
的地方
// ok,当前值不依赖上一次的值 this.setState({name: 'Hello'}); // 提供callback方式1 this.setState((prevState, props) => { return { ...prevState, name: props.name }; }); // 或 this.setState((prevState, props) => ({ counter: prevState.counter + props.increment })); // 或 this.setState(function(prevState, props) { return { counter: prevState.counter + props.increment }; }); // 提供callback方式2 this.setState({ name: 'qwer' }, ()=>{ console.log(this.state.name); //qwer });
在状态更新、渲染完成后,会触发对回调函数的执行。有关信息参见:react - setState;
状态提升(Lifting State Up)
state创建有2种:
- 当组件以class类创建,state可以是class的属性值,也可以在构造函数通过this.state赋值来创建
- 当组件以函数创建,state需通过函数的getIntialState函数的返回值来创建
在React中,共享state(状态)是通过将其移动到需要它的组件的最接近的共同祖先组件来实现,使React的state成为 “单一数据源原则” 。
在一个React应用中,对于任何可变的数据都应该循序“单一数据源”原则,依赖从上向下的数据流向。
对于UI中的错误,使用React开发者工具来检查props,向上遍历树,直到找到负责更新状态的组件,跟踪到bug的源头。
具体参见:React Developer Tools
将参数传递给事件处理程序
- arrow functions:箭头函数方式,参数e作为React事件对象作为第二个参数进行显式传递
- Function.prototype.bind:bind方式,事件对象以及更多的参数将会被隐式传递
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button> <button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
对于第2种方法,方法定义的形式
deleteRow(id, event) {...}
当需要从子组件中更新父组件的state时,要在父组件创建事件句柄 (handleChange) ,并作为prop (updateStateProp) 传递到子组件。
var Content = React.createClass({ render: function() { return <div> <button onClick = {this.props.updateStateProp}>点我</button> <h4>{this.props.myDataProp}</h4> </div> } }); var HelloMessage = React.createClass({ getInitialState: function() { return {value: 'Hello Runoob!'}; }, handleChange: function(event) { this.setState({value: '菜鸟教程'}) }, render: function() { var value = this.state.value; return <div> <Content myDataProp = {value} updateStateProp = {this.handleChange}></Content> </div>; } }); ReactDOM.render( <HelloMessage />, document.getElementById('example') );
综上,对于显式bind方式,通式如下
handleclick(要传的参数,event){...} onClick = {this.handleclick.bind(this,要传的参数)}
组件钩子函数生命周期
。。。
高阶组件
高阶组件是一个函数,接受一个组件并返回一个新的组件
const EnhancedComponent = higherOrderComponent(WrappedComponent);
- 高阶组件既不会修改输入组件,也不会通过继承来复制行为。
- 通过包裹的形式,高阶组件将原先的组件组合在容器组件中。
- 高阶组件是纯函数,没有副作用。
- 高阶组件最好是通过将输入组件包裹在容器组件的方式来使用组合。
错误边界
Error Boundaries 是React组件,它可以在子组件树的任何位置捕获JavaScript错误、记录这些错误,并显示一个备用UI,而不是使整个组件树崩溃。但是,仅可以捕获其子组件的错误,无法捕获其自身的错误。对以下情况无能为力:
- 事件处理:事件处理器内部的错误,采用try{}catch(){}捕获即可
- 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
- 服务端渲染
- 错误边界自身抛出来的错误(而不是其子组件)
钩子函数:componentDidCatch(error, info)
类似JS的catch(){}方法,如果一个错误边界无法渲染错误信息,则错误会向上冒泡至最接近的错误边界。
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { error: null, errorInfo: null }; } componentDidCatch(error, errorInfo) { // Catch errors in any components below and re-render with error message this.setState({ error: error, errorInfo: errorInfo }) // You can also log error messages to an error reporting service here } render() { if (this.state.errorInfo) { // Error path return ( <div> <h2>Something went wrong.</h2> <details style={{ whiteSpace: 'pre-wrap' }}> {this.state.error && this.state.error.toString()} <br /> {this.state.errorInfo.componentStack} </details> </div> ); } // Normally, just render children return this.props.children; } }
ref属性
用来绑定到render()输出的任何组件上,允许引用render()返回的相应的支撑实例(Backing Instance)。
简言之,用于从组件获取真实的DOM结点。
- 处理focus、文本选择或者媒体播放
- 触发强制动画
- 集成第三方DOM库
- 访问<input type="file">表单要提交处理的文件
组件并不是真实的DOM节点,而是存在于内存之中的一种数据结构,叫做虚拟DOM (virtual DOM)。只有当它插入文档以后,才会变成真实的DOM 。根据 React 的设计,所有的DOM变动,都先在虚拟DOM上发生,然后再将实际发生变动的部分,反映在真实DOM上,这种算法叫做 DOM diff,可以极大提高网页的性能表现。
场景:获取组件new出来的实例,绕过父子组件通信的约束,直接操作组件new出来的实例
- 通过 ref 属性获取真实的React元素实例
- 通过 ReactDOM.findDOMNode(组件真实实例) 返回真实的DOM结构
通过ref属性获取到React组件new出来的真实实例,前置条件:
- 自定义组件
- 通过class来定义
真实DOM结点结构 = ReactDOM.findDOMNode(组件真实实例)
this.props.children属性
表示组件的所有子结点,其值有三种可能:
- undefined:当前组件没有子节点
- object:有且仅有有一个子节点
- array:有多个子节点
React提供工具方法:React.Children,智能处理(容错) this.props.children。
React.Children.map(children, function[(thisArg)]) React.Children.forEach(children, function[(thisArg)]) React.Children.count(children) React.Children.only(children) React.Children.toArray(children)
条件渲染组件
To do this return null instead of its render output:从组件的render方法返回null不会影响组件生命周期方法的触发。
function WarningBanner(props) { if (!props.warnsInfoMsg) { return null; } return ( <div className="warning"> Warning! {this.props.warnsInfoMsg} </div> ); }
模块加载机制
CommonJS规范
- fs:文件系统模块,负责读写文件,支持stream和pipe(自动流式读写)
- http:web服务器模块,提供request和response对象分别封装http请求和响应
- cypto:加密解密模块,哈希算法,数字证书
// 模块对外提供变量或函数方法 module.exports = { key: value ... }; // 引入模块 const module = require('./Module.js');
context
上下文,提供通过组件树传递数据的方法,在组件间共享数据,避免通过中间元素传递 props。
- 使用props参数传递方式
class App extends React.Component { render() { return <Toolbar theme="dark" />; } } function Toolbar(props) { // The Toolbar component must take an extra "theme" prop // and pass it to the ThemedButton. This can become painful // if every single button in the app needs to know the theme // because it would have to be passed through all components. return ( <div> <ThemedButton theme={props.theme} /> </div> ); } function ThemedButton(props) { return <Button theme={props.theme} />; }
- 使用context:Stick to cases where the same data needs to be accessed in many components at multiple levels.
// Context lets us pass a value deep into the component tree without explicitly threading it through every component. // Create a context for the current theme (with "light" as the default). const ThemeContext = React.createContext('light'); class App extends React.Component { render() { // Use a Provider to pass the current theme to the tree below. // Any component can read it, no matter how deep it is. // In this example, passing "dark" as the current value. return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } // A component in the middle doesn't have to pass the theme down explicitly anymore. function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } function ThemedButton(props) { // Use a Consumer to read the current theme context. // React will find the closest theme Provider above and use its value. // In this example, the current theme is "dark". return ( <ThemeContext.Consumer> {theme => <Button {...props} theme={theme} />} </ThemeContext.Consumer> ); }
其中,ThemeContext 是 {Provider, Consumer} 对象
const {Provider, Consumer} = React.createContext(defaultValue); <Provider value={/* some value */}> <Consumer> {value => /* render something based on the context value */} </Consumer>
仔细体会,该方式类似发布订阅模式,provider生产数据,Consumer消费数据。
具体参见:Context - React;
编程思想
页面设计流程
- 组件拆解:自下而上,由内而外,单一职责原则
- 组件组合:各组件组合成静态页面框架
- 确定UI状态(state)的最小但完整集合表示
- 确定state位置:状态提升,公共父级组件
- 组件通信交互:回调函数(反向数据流)
-
- 父组件将方法名作为参数传递给子组件
- 在子组件上触发的事件调用父组件的方法以更新父组件的状态
注:不要在子组件上直接调用父组件方法。子组件应该封装一个事件句柄,在句柄内调用回调函数。
关于确定 UI state
- 是否通过props(属性)从父级传入? 若是,它可能不是state(状态)。
- 是否永远不会发生变化? 若是,它可能不是state(状态)。
- 是否可以由组件中其他的state(状态)或props(属性)计算得出?若是,则它不是state(状态)。
具体参见:React 编程思想;
学习了下React 核心开发者出品的 React 设计思想 ,简单总结如下:
-
变换(Transformation):纯函数
-
抽象(Abstraction):函数/组件调用
-
组合(Composition):组合思想,而非继承
-
状态(State):不可变性,setState()
-
Memoization:记忆缓存
-
列表(Lists)
-
连续性(Continuations)
-
代数效应(Algebraic Effects):context
经验避坑
关于React注释
- 在标签外的的注释不能使用花括号
- 在标签内部的注释需要花括号
ReactDOM.render( /*注释 */ <h1>xxxx {/*注释*/}</h1>, document.getElementById('root') );
关于Html与React
- 在React的.js文件中,标签中属性和事件必须驼峰格式
- 为组件添加属性时,原生Html中的关键字class和for必须写成className和htmlFor
环境判断
根据浏览器和Node环境提供的全局变量名称来判断当前环境:
if (typeof(window) === 'undefined') { console.log('node.js'); } else { console.log('browser'); }
关于规范
- 添加属性Object.assign()
- 扩展运算符(...)拷贝数组
- Array.from():将类数组对象转为数组
- 所有配置项都应该集中在1个对象中,作为最后1个参数,布尔值不可以直接作为参数。
- 使用帕斯卡式命名构造函数或类
具体参见:Airbnb React/JSX Style Guide;
参考: