原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大多数教程专业、系统。
1.1 Introduction to React
1.1.1 What is React?
React IS A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES , INCLUDING
- React.js
- ReactRenders: ReactDOM / ReactServer / ReactCanvas
- Flux模式及其实现
- React Components
- React Native
- GraphQI + Relay
1.1.2 Basic concept in React
- React.js
React.js 是 React 的核心库,在应用中必须先加载核心库
<script src="http://facebook.github.io/react/js/react.js"</script>
<script src="http://facebook.github.io/react/js/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
- ReactDOM.js
DOM渲染器,为了在 web 页面中显示开发的组件,需要调用 ReactDOM.render 方法, 第一个参数是 React 组件,第二个参数为 HTMLElement。 - JSX
JSX 是 React 自定义的语法,最终 JSX 会转化为 JS 运行于页面当中 - Components
Component是 React 中的核心概念,页面当中的所有元素都是通过 React 组件来表达。 - Virtual DOM
React 抽象出来的虚拟 DOM 树,虚拟树是 React 高性能的关键。 - one-way reactive data flow
React 应用的核心设计模式,数据流向自顶向下
1.1.3 Features of React
- Component的组合模式
组合模式有时候又叫做部分-整体模式,它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
React 就是基于组合模式, 无论是应用等级还是一个表单亦或是一个按钮都视为一个组件, 然后基于组件的组合构建整个应用,这样的结构一直是前端界想要却迟迟不来的 web component。
基于组合模式的优点:
- 构建可重用的组件:组件的开发能够形成公司的组件库,每个业务的开发都能积累可重用的组件。
- 无学习障碍:天然符合 HTML 的结构, 对前端开发者来说几乎没有学习障碍。
- 具有弹性的架构:组合模式很简单却很有效,能够构建简单的页面也能构建大型的前端应用。
- 源码高可维护性:开发只是工作中的一部分,应用的上线才是噩梦的开始,很多大型应用因为复制的业务逻辑导致无法快速响应业务需求,可维护性低。
- One way data flow
Javascript 是脚本语言,不能像静态语言一样通过编译定位为题,想要清晰的定位到应用中的 bug 需要深入了解业务代码,对于大型前端应用来说,因为业务代码量很大且复杂,很难定位到 bug。 然而 React 的单向数据流的设计让前端 bug 定位变得简单, 页面的 UI 和数据的对应是唯一的,我们可以通过定位数据变化就可以定位页面展现问题。
- High efficiency
基于VirtualDOM算法可以让只有需要改变的元素才去重渲染 - Separate frame design
React.js 现在的版本已经将源码分开为 ReactDOM 和 React.js . 这就意味着 React 不仅仅能够在 web 端工作, 甚至可以在服务端(nodejs),Native 端运行。
与此同时, 我们可以自定义自己的渲染器, 实现比如 Three.js, Pixi.js, D3.js 的 React 方式渲染。
1.1.4 Where can React apply?
- web 端
- 原生应用
IOS、Android、Native 应用
这要归功于 facebook 开源的 React Native。 基于 React Native , 我们将可以使用 jsx 来实现具有原生应用性能的 UI 运行于 IOS 和 android 中,同时我们也可以通过 NW.js 或者 Electron 来实现基于 React 的桌面应用。 - 服务器端渲染
React 除了在 Web 和 Native 环境以外, 也可以通过 React 实现在服务器端渲染出 HTML。
1.2 JSX Grammar
1.2.1 Prepare running environment
- Babel REPL
Babel REPL
直接在 REPL 中写 JSX 语法,可以实时的查看编译后的结果。 - JSFiddle
在线模式 React Fiddle - Local development
1.2.2 JSX Grammar
创建 JSX 语法的本质目的是为了使用基于 xml 的方式表达组件的嵌套,保持和 HTML 一致的结构,语法上除了在描述组件上比较特别以外,其它和普通的 Javascript 没有区别。 并且最终所有的 JSX 都会编译为原生 Javascript。
- jsx componments
JSX 组件分为 HTML 组件和 React 组件
HTML 组件就是 HTML 中的原生标签, 如:
function render() {
return <p> hello, React World </p>
}
function render() {
return <ul>
<li>list item 1</li>
<li>list item 2</li>
</ul>
}
React 组件就是自定义的组件,如
// 定义一个自定义组件
var CustomComponnet = React.createClass({
render: function() {
return <div> custom component </div>
}
});
// 使用自定义组件
function render() {
return <p> <CustomComponent/> </p>
}
- properties of jsx components
和 html 一样,JSX 中组件也有属性,传递属性的方式也相同
对于 HTML 组件
function render() {
return <p title="title" >hello, React, world </p>
}
如果是 React 组件可以定义自定义属性,传递自定义属性的方式:
function render() {
return <p> <CustomComponent customProps="data"/> </p>
}
}
属性即可以是字符串,也可以是任意的 Javascript 变量, 传递方式是将变量用花括号, eg:
function render() {
var data = {a: 1, b:2};
return <p> <CustomComponent customProps={data}/> </p>
}
需要注意的地方上,属性的写法上和 HTML 存在区别,在写 JSX 的时候,所有的属性都是驼峰式的写法,主要是出于标准的原因,驼峰式是 Javascript 的标准写法,并且 React 底层是将属性直接对应到原生 DOM 的属性,而原生 DOM 的属性是驼峰式的写法,这里也可以理解为什么类名不是 class 而是 className 了, 又因为 class 和 for 还是 js 关键字,所以 jsx 中:
class => className
for => htmlFor
除此之外比较特殊的地方是 data-*
和 aria-*
两类属性是和 HTML 一致的。
- curly braces
- 显示文本
function render() {
var text = "Hello, World"
return <p> {text} </p>
}
- 运算
funtion render() {
var text = text;
var isTrue = false;
var arr = [1, 2, 3];
return <p>
{text}
{isTrue ? "true" : "false"}
{arr.map(function(it) {
return <span> {it} </span>
})}
</p>
}
- 限制规则
render 方法返回的组件必须是有且只有一个根组件,错误情况的例子
// 无法编译通过,JSX 会提示编译错误
function render() {
return (<p> .... </p>
<p> .... </p>)
}
- 组件命名空间
JSX 可以通过命名空间的方式使用组件, 通过命名空间的方式可以解决相同名称不同用途组件冲突的问题。如:
function render() {
return <p>
<CustomComponent1.SubElement/>
<CustomComponent2.SubElement/>
</p>
}
1.2.3 Understand JSX
- JSX 的编译方式
- 在 HTML 中引入 babel 编译器, 如上 Hello World 程序中一样。
- 离线编译 JSX,通过 babel 编译 JSX,细节我们将在第二章中讲解。
- JSX 到 JS 的转化
Hello World 程序转化为 JS 的代码如下:
var Hello = React.createClass({
displayName: 'Hello',
render: function() {
return React.createElement("div", null, "Hello ", this.props.name);
}
});
ReactDOM.render(
React.createElement(Hello, {name: "World"}),
document.getElementById('container')
);
1.3 React Components
1.3.1 Create a component
- 创建一个 React 组件的方法为,调用 React.createClass 方法,传入的参数为一个对象,对象必须定义一个 render 方法,render 方法返回值为组件的渲染结构,也可以理解为一个组件实例(React.createElement 工厂方法的返回值),返回值有且只能为一个组件实例,或者返回 null/false,当返回值为 null/false 的时候,React 内部通过 <noscript/> 标签替换
var MyComponent = React.createClass({
render: function() {
return <p>....</p>;
}
});
- Component命名空间
可以看出 React.createClass 生成的组件类为一个 Javascript 对象。 当我们想设置命名空间组件时,可以在组件下面添加子组件
MyComponent.SubComponent = React.createClass({...});
MyComponent.SubComponent.Sub = React.createClass({....});
- 无状态组件
除了可以通过 React.createClass 来创建组件以外,组件也可以通过一个普通的函数定义,函数的返回值为组件渲染的结果
function StatelessComponent(props) {
return <div> Hello {props.name} </div>
}
无状态组件能够优化性能,因为其内部不会维护状态,React 内部不会有一个对应的组件实例,并且也没有生命周期 hook
1.3.2 将组件渲染到 DOM 中
当创建好了组件过后,为了将组件渲染到 DOM 中显示出来,需要两个步骤:
- 在 HTML 中定义一个元素,设置 id 属性
- JSX 中调用 ReactDOM.render 方法, 第一个参数为 组件,第二个为刚才定义的 DOM 元素
<!-- 定义的 DOM 元素 -->
<div id="example"></div>
<script type="text/babel">
// 自定义组件
var MyComponent = React.createClass({
render: function() {
return <p>....</p>;
}
});
// 调用 render 方法
ReactDOM.render(
<MyComponent/>,
document.getElementById('example')
);
</script>
1.3.3 States of components
React 的渲染结果是由组件属性和状态共同决定的,状态和属性的区别是,状态维护在组件内部,属性是由外部控制,我们先介绍组件状态相关细节:
控制状态的 API 为:
-
this.state
:组件的当前状态 -
getInitialState
:获取组件的初始状态,在组件加载的时候会被调用一次,返回值赋予 this.state 作为初始值 -
this.setState
:组件状态改变时,可以通过 this.setState 修改状- setState 方法支持按需修改,如 state 有两个字段,仅当 setState 传入的对象包含字段 key 才会修改属性
- 每次调用 setState 会导致重渲染调用 render 方法
- 直接修改 state 不会重渲染组件
var Switch = React.createClass({
// 定义组件的初始状态,初始为关
getInitialState: function() {
return {
open: false
}
},
// 通过 this.state 获取当前状态
render: function() {
console.log('render switch component');
var open = this.state.open;
return <label className="switch">
<input type="checkbox" checked={open}/>
</label>
},
// 通过 setState 修改组件状态
// setState 过后会 React 会调用 render 方法重渲染
toggleSwitch: function() {
var open = this.state.open;
this.setState({
open: !open
});
}
})
1.3.4 Properties of components
前面已经提到过 React 组件可以传递属性给组件,传递方法和 HTML 中无异, 可以通过 this.props 获取组件属性
属性相关的 API 为:
-
this.props
: 获取属性值 -
getDefaultProps
: 获取默认属性对象,会被调用一次,当组件类创建的时候就会被调用,返回值会被缓存起来,当组件被实例化过后如果传入的属性没有值,会返回默认属性值 -
this.props.children
:子节点属性 -
propTypes
: 属性类型检查
// props.name 表示代办事项的名称
var TodoItem = React.createClass({
render: function() {
var props = this.props;
return <div className="todo-item">
<span className="todo-item__name">{props.name}</span>
</div>
}
});
ReactDOM.render(
<TodoItem name="代办事项1"/>,
document.getElementById('example'));
- children属性
组件属性中会有一个特殊属性 children ,表示子组件, 还是以上面一个组件为例子,我们可以换一种方式定义 name:
var TodoItem = React.createClass({
render: function() {
var props = this.props;
return <div className="todo-item">
<span class="todo-item__name">{props.children}</span>
</div>
}
});
ReactDOM.render(
<TodoItem>代办事项1</TodoItem>,
document.getElementById('example'));
- 属性类型检查
为了保证组件传递属性的正确性, 我们可以通过定义 propsType 对象来实现对组件属性的严格校验
var MyComponent = React.createClass({
propTypes: {
optionalArray: React.PropTypes.array,
optionalBool: React.PropTypes.bool,
optionalFunc: React.PropTypes.func,
optionalNumber: React.PropTypes.number,
optionalObject: React.PropTypes.object,
optionalString: React.PropTypes.string,
// 任何可以被渲染的包括,数字,字符串,组件,或者数组
optionalNode: React.PropTypes.node,
// React 元素
optionalElement: React.PropTypes.element,
// 枚举
optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),
// 任意一种类型
optionalUnion: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.instanceOf(Message)
]),
// 具体类型的数组
optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),
// 具体类型的对象
optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),
// 符合定义的对象
optionalObjectWithShape: React.PropTypes.shape({
color: React.PropTypes.string,
fontSize: React.PropTypes.number
}),
requiredFunc: React.PropTypes.func.isRequired,
requiredAny: React.PropTypes.any.isRequired,
// 自定义校验
customProp: function(props, propName, componentName) {}
}
});
- 属性传递的单向性
我们已经提到过 React 的单向数据流模式,数据的流动管道就是 props,流动的方向就是组件的层级自定向下的方向。所以一个组件是不能修改自身的属性的,组件的属性一定是通过父组件传递而来(或者默认属性)。 - 无状态组件属性
对于无状态组件,可以添加 .propTypes 和 .defaultProps 属性到函数上。
1.3.5 组件的嵌套组合
在JSX 实例子中,当我们循环输出 todo 列表的时候,React 会提示对于循环输出的组件,需要有一个唯一的 key 属性。这个问题的原因在于 React 的调和机制(Reconciliation)上。
- 什么叫调和?
在每次数据更新过后,React 会重新调用 render 渲染出新的组件结构,新的结构应用到 DOM 中的过程就叫做调和过程。 - 为什么需要调和?
假设我们有一个输入组件,这个时候我们正聚焦在输入框中,当修改值过后触发事件导致了数据改变,数据改变导致了重渲染, 这个时候输入框被替换成了新的 DOM。 这个过程对用户来说应该是无感知的,所以那原来的聚焦状态应该被保存, 那怎么做到的呢? DOM 都被替换了,输入状态,选择状态为什么还能保存。 我们先不急着知道 How,目前只需要知道这就是调和过程。
除了保存状态以外,调和过程还做了很多 DOM 优化。 比如输出一个数组的时候,数据新增加或者减少了一下,或者数组项值改变了,实际上我们没有必要删除原来的 DOM 结构,只需要修改 DOM 的值或者删除 DOM 就能实现重渲染。
这就是为什么要有 key 属性,key 属性能够帮助定位 DOM 与数组元素的关系,在重渲染的时候能够实现渲染优化。
1.4 Life circle of React Components
14.1 Components
React 中组件有自己的生命周期方法,简单理解可以为组件从出生(实例化) -> 激活 -> 销毁
生命周期 hook。通过这些 hook 方法可以自定义组件的特性。 除此之外,还可以设置一些额外的规格配置。
React.createClass
的参数对象中传入, 之前使用过了一些方法:render getInitialState getDefaultProps propTypes
1.4.2 mixins
Type: array mixins
mixins 可以理解为 React 的插件列表,通过这种模式在不同组件之间共享方法数据或者行为只需共享 mixin 就行,mixins 内定义的生命周期方法在组件的生命周期内都会被调用。
var MyMixin1 = {
componentDidMount: function() {
console.log('auto do something when component did mount');
}
};
var MyMixin2 = {
someMethod: function() {
console.log('doSomething');
}
};
var MyComponnet = React.createClass({
mixins: [MyMixin1, MyMixin2],
componentDidMount: function() {
// 调用 mixin1 共享的方法
this.someMethod();
}
});
1.4.3 statics
Type:object statics
statics可以定义组件的类方法
React 的组件是 面向对象OOP 的思维,MyComponent 是一个 class,class 分为类方法和实例方法,实例方法可以访问 this, 然而类方法不能,所以我们不能在 Class 中返回状态或者属性。
var MyComponent = React.createClass({
statics: {
customMethod: function(foo) {
return foo === 'bar';
}
}
});
MyComponent.customMethod('bar'); // true
1.4.4 displayName
Type: string displayName
为了显示调试信息,每个组件都会有一个名称,JSX 在转为 JS 的时候自动的设置 displayName,当然我们也可以自定义 displayName
// Input (JSX):
var MyComponent = React.createClass({ });
// Output (JS):
var MyComponent = React.createClass({displayName: "MyComponent", });
1.4.5 生命周期方法
1.4.6 componentWillMount
void componentWillMount()
- 条件:第一次渲染阶段在调用 render 方法前会被调用
- 作用:该方法在整个组件生命周期只会被调用一次,所以可以利用该方法做一些组件内部的初始化工作
1.4.7 componentDidMount
void componentDidMount()
- 条件:第一次渲染成功过后,组件对应的 DOM 已经添加到页面后调用
- 作用:这个阶段表示组件对应的 DOM 已经存在,我们可以在这个时候做一些依赖 DOM 的操作或者其他的一些如请求数据,和第三方库整合的操作。如果嵌套了子组件,子组件会比父组件优先渲染,所以这个时候可以获取子组件对应的 DOM。
1.4.8 componentWillReceiveProps(newProps)
void componentWillReceiveProps(
object nextProps
)
- 条件: 当组件获取新属性的时候,第一次渲染不会调用
- 用处: 这个时候可以根据新的属性来修改组件状态
componentWillReceiveProps: function(nextProps) {
this.setState({
likesIncreasing: nextProps.likeCount > this.props.likeCount
});
}
注意: 这个时候虽说是获取新属性,但并不能确定属性一定改变了,例如一个组件被多次渲染到 DOM 中,如下面:
var Component = React.createClass({
componentWillReceiveProps: function(nextProps) {
console.log('componentWillReceiveProps', nextProps.data.bar);
},
rener: function() {
return <div> {this.props.data.bar} </div>
}
});
var container = document.getElementById('container');
var mydata = {bar: 'drinks'};
ReactDOM.render(<Component data={mydata} />, container);
ReactDOM.render(<Component data={mydata} />, container);
ReactDOM.render(<Component data={mydata} />, container);
结果会输出两次 componentWillReceiveProps,虽然属性数据没有改变,但是仍然会调用 componentWillReceiveProps 方法。
1.4.9 shouldComponentUpdate(nextProps, nextState)
boolean shouldComponentUpdate(
object nextProps, object nextState
)
- 条件: 接收到新属性或者新状态的时候在 render 前会被调用(除了调用 forceUpdate 和初始化渲染以外)
- 用处: 该方法让我们有机会决定是否重渲染组件,如果返回 false,那么不会重渲染组件,借此可以优化应用性能(在组件很多的情况)。
1.4.10 componentWillUpdate
void componentWillUpdate(
object nextProps, object nextState
)
- 条件:当组件确定要更新,在 render 之前调用
- 用处:这个时候可以确定一定会更新组件,可以执行更新前的操作
- 注意:方法中不能使用 setState ,setState 的操作应该在 componentWillReceiveProps 方法中调用
1.4.11 componentDidUpdate
void componentDidUpdate(
object prevProps, object prevState
)
- 条件:更新被应用到 DOM 之后
- 用处:可以执行组件更新过后的操作
1.4.12 生命周期与单向数据流
我们知道 React 的核心模式是单向数据流,这不仅仅是对于组件级别的模式,在组件内部 的生命周期中也是应该符合单向数据的模式。数据从组件的属性流入,再结合组件的状态,流入生命周期方法,直到渲染结束这都应该是一个单向的过程,其间不能随意改变组件的状态。1.5 React & DOM
1.5.1获取DOM元素
DOM真正被添加到HTML中的hook为
- componentDidMount
- componentDidUpdate
在这两个 hook 函数中, 我们可以获取真正的 DOM 元素,React 提供的获取方法两种方式
- findDOMNode()
通过 ReactDOM 提供的 findDOMNode 方法, 传入参数
var MyComponent = React.createClass({
render: function() {
return <div> .... </div>
},
componentDidMount: function() {
var $root = ReactDOM.findDOMNode(this);
console.log($root);
}
})
需要注意的是此方法不能应用到无状态组件上
- Refs
上面的方法只能获取到 root 元素,那如果我的 DOM 有很多层级,我想获取一个子级的元素呢?React 提供了 ref 属性来实现这种需求。
每个组件实例都有一个 this.refs 属性,会自动引用所有包含 ref 属性组件的 DOM
var MyComponent = React.createClass({
render: function() {
return <div>
<button ref="btn">...</button>
<a href="" ref="link"></a>
</div>
},
componentDidMount: function() {
var $btn = this.refs.btn;
var $link = this.refs.link;
console.log($btn, $link);
}
})
1.5.2 DOM事件
- 绑定事件
在 React 中绑定事件的方式很简单,只需要在元素中添加事件名称的属性已经对应的处理函数,如:
var MyComponent = React.creatClass({
render: function() {
return <div>
<button onClick={this.onClick}>Click Me</button>
</div>
},
onClick: function() {
console.log('click me');
}
});
事件名称和其他属性名称一样,服从驼峰式命名。
- 合成事件(SyntheticEvent)
在 React 中, 事件的处理由其内部自己实现的事件系统完成,触发的事件都叫做 合成事件(SyntheticEvent),事件系统对浏览器做了兼容,其提供的 API 与原生的事件无异。
boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
DOMEventTarget target
number timeStamp
string type
和原生事件的区别在于,事件不能异步话,如:
function onClick(event) {
console.log(event); // => nullified object.
console.log(event.type); // => "click"
var eventType = event.type; // => "click"
setTimeout(function() {
console.log(event.type); // => null
console.log(eventType); // => "click"
}, 0);
this.setState({clickEvent: event}); // Won't work. this.state.clickEvent will only contain null values.
this.setState({eventType: event.type}); // You can still export event properties.
}
原因是在事件系统的内部实现当中, 一个事件对象可能会被重用(也就是事件做了池化 Pooling)。当一个事件响应函数执行过后,事件的属性被设置为 null, 如果想用保持事件的值的话,可以调用
event.persist()
这样,属性会被保留,并且事件也会被从池中取出。
- 事件捕获和冒泡
在 DOM2.0 事件分为捕获阶段和冒泡阶段,React 中通常我们注册的事件为冒泡事件,如果要注册捕获阶段的事件,可以在事件名称后加 Capture 如:
onClick
onClickCapture
- 支持事件列表
粘贴板事件 {
事件名称:onCopy onCut onPaste
属性:DOMDataTransfer clipboardData
}
编辑事件 {
事件名称:onCompositionEnd onCompositionStart onCompositionUpdate
属性:string data
}
键盘事件 {
事件名称:onKeyDown onKeyPress onKeyUp
属性: {
boolean altKey
number charCode
boolean ctrlKey
boolean getModifierState(key)
string key
number keyCode
string locale
number location
boolean metaKey
boolean repeat
boolean shiftKey
number which
}
}
// 焦点事件除了表单元素以外,可以应用到所有元素中
焦点事件 {
事件名称:onFocus onBlur
属性:DOMEventTarget relatedTarget
}
表单事件 {
事件名称:onChange onInput onSubmit
}
鼠标事件 {
事件名称:{
onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp
}
属性:{
boolean altKey
number button
number buttons
number clientX
number clientY
boolean ctrlKey
boolean getModifierState(key)
boolean metaKey
number pageX
number pageY
DOMEventTarget relatedTarget
number screenX
number screenY
boolean shiftKey
}
}
选择事件 {
事件名称:onSelect
}
触摸事件 {
事件名称:onTouchCancel onTouchEnd onTouchMove onTouchStart
属性:{
boolean altKey
DOMTouchList changedTouches
boolean ctrlKey
boolean getModifierState(key)
boolean metaKey
boolean shiftKey
DOMTouchList targetTouches
DOMTouchList touches
}
}
UI 事件 {
事件名称:onScroll
属性:{
number detail
DOMAbstractView view
}
}
滚轮事件 {
事件名称:onWheel
属性:{
number deltaMode
number deltaX
number deltaY
number deltaZ
}
}
媒体事件 {
事件名称:{
onAbort onCanPlay onCanPlayThrough onDurationChange onEmptied onEncrypted onEnded onError onLoadedData onLoadedMetadata onLoadStart onPause onPlay onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend onTimeUpdate onVolumeChange onWaiting
}
}
图像事件 {
事件名称:onLoad onError
}
动画事件 {
事件名称:onAnimationStart onAnimationEnd onAnimationIteration
属性:{
string animationName
string pseudoElement
float elapsedTime
}
}
渐变事件 {
事件名称:onTransitionEnd
属性: {
string propertyName
string pseudoElement
float elapsedTime
}
}
1.5.3 表单事件
在 React 中比较特殊的事件是表单事件,大多数组件都是通过属性和状态来决定的,但是表单组件如 input, select, option 这些组件的状态用户可以修改,在 React 中会特殊处理这些组件的事件。
- onChange 事件
和普通 HTML 中的 onChange 事件不同, 在原生组件中,只有 input 元素失去焦点才会触发 onChange 事件, 在 React 中,只要元素的值被修改就会触发 onChange 事件。
var MyComponent = React.createClass({
getInitialState: function() {
return {
value: ''
}
},
render: function() {
return <div onChange={this.onChangeBubble}>
<input value={this.state.value} onChange={this.onChange}/>
</div>
},
onChange: function(ev) {
console.log('change: ' + ev.target.value);
this.setState({
value: ev.target.value
});
},
// onChange 事件支持所有组件,可以被用于监听冒泡事件
onChangeBubble: function(ev) {
console.log('bubble onChange event', + ev.target.value);
}
})
- 交互属性
表单组件中能被用户修改的属性叫交互属性,包括:
value => <input> 和 <select> 组件
checked => <input type="checkbox|radio">
selected => <opiton>
- textarea
在 HTML 中,textarea 的值是像如下定义的:
<textarea name="" id="" cols="30" rows="10">
some value
</textarea>
而在 React 中, TextArea 的使用方式同 input 组件,使用 value 来设置值
var MyComponent = function() {
render: function() {
return <div>
<textarea value={...} onChange={...}/>
</div>
}
}
- select 组件
在 React 中 select 组件支持 value 值,value 值还支持多选
<select value="B">
<option value="A">Apple</option>
<option value="B">Banana</option>
<option value="C">Cranberry</option>
</select>
<select multiple={true} value={['B', 'C']}>
<option value="A">Apple</option>
<option value="B">Banana</option>
<option value="C">Cranberry</option>
</select>
- 受控组件
在 React 中表单组件可分为两类,受控与非受控组件,受控组件是包含了 value 值的,如:
render: function() {
return <input type="text" value="....."/>
}
为什么叫受控组件? 因为这个时候用户不能修改 input 的值, input 的值永远是 value 固定了的值。如果去掉 value 属性,那么就可以输入值了。
那如何修改受控组件的值呢? 如上面的例子中, 添加 onChange 事件,事件内修改 value 属性,value 属性的值会被设置到组件的 value 中。
- 非受控组件
没有 value 值的 input
render: function() {
return <input type="text"/>
}
可以通过 defaultValue 属性来设置默认值
render: function() {
return <input type="text" defaultValue="Default Value">
}
类似的对于 checkbox 有 defaultChecked 属性
需要注意的是,默认值只适用于第一次渲染,在重渲染阶段将不会适用。
- checkbox & radio
checkbox 和 radio 比较特殊, 如果在 onChange 事件中调用了 preventDefault ,那么浏览器不会更新 checked 状态,即便事实上组件的值已经 checked 或者 unchecked 了 。
var CheckBox = React.createClass({
getInitialState: function(){
return {
checked: false
}
},
render: function() {
return <div>
<input type="checkbox"
checked={this.state.checked}
onChange={this.onChange}/>
</div>
},
onChange: function(ev) {
this.setState({
checked: true
});
ev.preventDefault();
}
})
这个例子里边,checked 虽然更新为 true ,但是 input 的值 checked 为 false
那应如何处理 checkbox 呢?
- 避免调用 ev.preventDefault
- 在 setTimeout 中处理 checked 的修改
- 使用 click 事件
1.5.4 Style属性
在 React 中,可以直接设置 style 属性来控制样式,不过与 HTML 不同的是, 传入的 style 值为一个object, 对象的所有 key 都是驼峰式命名,eg:
render: function() {
var style = {
backgroundColor: 'red',
height: 100,
width: 100
}
return <div style={style}></div>
}
其中还可以看到不同的地方时,为了简写宽度高度值,可以直接设置数字,对应 100 -> 100px
。如果某些属性不需要添加 px 后缀,React 也会自动去除。
通过属性值驼峰式的原因是 DOM 内部访问 style 也是驼峰式。如果需要添加浏览器前缀瑞 -webkit-、-ms- 大驼峰(除了 ms ), 如:
var divStyle = {
WebkitTransition: 'all', // 'W' 是大写
msTransition: 'all' // 'ms' 为小写
};
在以前的前端开发方式是 样式结构和逻辑要分离, 而现在 React 中却有很多人推崇** inline **的样式。 在我看来因人而异,React 的这种模式也能做到样式模块化,样式重用(借用 Js 的特点)。并且因为 React 的实现方式,Inline 样式的性能甚至比 class 的方式高。
1.6 Flux
1.6.1 Flux 介绍
简单来讲,Flux 是 Facebook 引入到 React 中的一种前端架构,通过定义其核心单向数据流的方式,让 React 应用更加健壮。同时,这种应用架构也具有普适性,可以应用到其他任意前端项目中,甚至可以应用到客户端应用开发中,也就是说 Flux 更应该叫做一种架构模式(Pattern)。
1.6.2 MVC 架构之痛
MVC 的实现可能有很多种方式,比较灵活,但基本本质不会改变,只是三者间的数据传递方向可能会改变,即便是 MVP 模式也只是 MVC 的变种,所以为了统一我们且以下图的 MVC 方式来讨论。
- 概念
- Model: 负责保存应用数据,和后端交互同步应用数据
- View: 负责渲染页面 HTML DOM
- Controller: 负责连接 View 和 Model , Model 的任何改变会应用到 View 中,View 的操作会通过 Controller 应用到 Model 中
- 关系:Model, View, Controller 都是多对多关系。
- 流程
以 TODOMVC 为例子用户添加一个 todo 的交互流程:
View -> Action -> Controller -> Model -> View
- View -> Action: 添加按钮事件或者 input 输入的提交事件
- Action -> Controller: 控制器响应 View 事件
- Controller -> Model: 控制器依赖 Model, 调用 Model 添加 todo
- Model -> View: View 监听 Model 的改变添加 todo 事件,在 HTML 中添加一个新的 Todo 视图
- 问题
对于新增一个 todo ,需要编写一个视图渲染处理函数,函数内添加新项目到列表中。同理对于删除一个 todo,也会有一个处理函数。当业务逻辑变多过后,可能有很多模型需要做增删改的功能,与之对应的就是我们需要精心构建这么多的渲染处理函数。 这种局部更新模式是高性能的关键所在,但问题是:
- 更新逻辑复杂,需要编写大量的局部渲染函数
- 问题定位困难,页面的当前状态是有数据和这些局部更新函数确定的
- 如何解决
如果渲染函数只有一个,统一放在 App 控制器中,每次更新重渲染页面,这样的话:- 任何数据的更新都只用调用重渲染就行
- 数据和当前页面的状态是唯一确定的
重渲染也有弊端,会带来严重的性能问题,重渲染和局部渲染各有好坏,对 MVC 来说这是一个两难的选择,无法做到鱼和熊掌兼得。
1.6.3 Flux 架构
通过 React + Flux 就可以完美解决 MVC 的问题。简单来说在 Flux 架构中直接剥离了控制器层,MVC 架构变成了 MV + Flux 架构。
- 重渲染: 在 React 中每次渲染都是重渲染,且不影响页面性能,是因为重渲染的是 Virtual Dom。这就意味着完全不用去关系重渲染问题,增删改的渲染都和初始化渲染相同入口
- 数据和状态一致性: Store 的数据确定应用唯一的状态
- 概念
-
one way data flow
这是 Flux 架构的核心思想,从图中可以看到,数据的流向从action 到 view 的一个单向流。
-
Action
Action 可以理解为对应用数据修改的指令,任何修改应用数据的行为都必须需通过触发 action 来修改。Action 可以来自于 View,也可以来自服务端的数据更新。
-
Action creator
为了抽象 Action ,提供一些辅助的语义化的方法来创建 Action,这些辅助方法叫做 Action Creator。
- Stores
应用的数据中心,所有应用数据都存放在这里控制,同时包含数据的控制行为,可能包含多个 store. - Dispatcher
action 的控制者,所有 action 都会通过 dispatcher,由 dispatcher 控制 action 是否应该传入到 store 中,Dispatcher 是一个单例。 - View
页面的视图,对应 React 的 Component, 视图可以触发 action 到 dispatcher。
需要区别出一种叫控制器 View(Controller View)的类型,这种 View 可以知晓 store 数据,把 store 数据转化为自身的状态,在将数据传递给其他 view 。 并且可以监听 store 数据的改变,当 store 数据改变过后重新设置状态触发重渲染。 可以将控制器 View 对应 MVC 中的控制器,但是差别很大,控制器 View 唯一多做的事情就是监听 store 数据改变,没有其他任何业务处理逻辑。
- 流程
同样以 TODOMVC 的添加 todo 为例,Flux 中的流程为:
View -> Action(Action Creator -> Action) -> Dispatcher -> Store -> Controller View -> View
- View -> Action: 添加按钮事件或者 input 输入的提交事件,View 中将事件转化为 action, action 由 Action Creator 创建。
- Action -> Dispatcher: action 统一由 Dispatcher 分配
- Dispatcher -> Store: Dispatcher 分配 action 到 Store
- Store -> Controller View: 控制器 View 监听 store 的数据改变,将数据转化为自身属性
- Controller View -> View: 数据改变自动重渲染所有视图
与MVC的对比 - 渲染策略: 数据改变 Flux 自动渲染,MVC 手动编写更新函数
- 事件触发策略: Flux 中所有 action 交给 dispather 分配,MVC 中交给对应的控制器分配
- Flux 在核心策略上的不同是解决 MVC 架构问题的关键
1.6.4 理解 Flux 架构
Flux 架构是非常优雅简洁的,合理利用了一些优秀的架构思维
- 分而治之(Divide And Conquer)
数据的处理过程是 Store -> Controller View -> View。 所有数据来自于 Store,页面的渲染层级为 Store 将数据传入 Controller View, 再由 Controller View 传入子 View , 一直到 View 的叶子节点。
这个是一个典型的分而治之策略,将大的页面拆分为小的模块,再由小的模块拆分为小的组件,具体组件负者组件自身的问题,所有子组件都是自私的,不用关心“大家”,只用关心“小家”。 - 合而治之 - 中心化控制
Flux 把所有的 View 都视作愚民,Store 视作资源的拥有者为统治者,统治者需要提供资源(数据)给平民,但是如果平民企图对资源修改(Mutation),必须得先通知给统治者,让统治者决定是否做处理。
我们为 Flux 中的概念分配角色
View
: 平民
Action
: 资源修改操作
Dispatcher
: 审核官
Store
: 统治者
一个企图修改资源的操作可以描述为:
View Require Mutation -> Action -> Dispatcher -> Store -> Mutate Handler
平民提交 Mutation 请求,由审核官控制,审核通过后递交给统治者,统治者再分配给亲信做资源 Mutation
合而治之的策略也等于中心化控制策略, 作为统治者既要懂得放权利(资源的分配),也要懂得控制权利(资源的修改),这种收缩自如的合理性是 Flux 简洁的根本。
同时这种思维带来的优点如下:
- View 的独立性和简单性:View 自身的逻辑简单,不需要知道太多事情,只关心上级传来的数据,这种模式使得 View 是低耦合的,简洁的。
- 高可维护性:中心化控制知道所有对资源的操作,如果发生 bug, 可以很快定位问题
- 函数式编程思想
在 Flux 中数据的单向流动依赖于 View 的确定性,相同的数据传入相同的组件,得到的结果必然要相同,这是函数式编程的思想。
为了保证组件也能做到 “纯函数” 的特性,相同的属性会得到相同的渲染结果。 在写 React 组件的时候尽量准守一下约定:
1.尽量使用无状态组件
2.除了控制类组件以外其他组件避免使用组件状态
3.可以通过属性计算出来的状态不要用状态来表示
4.组件的渲染避免外部依赖,按照纯函数的方式写
函数式的优点也是无副作用组件的优点:
- 无耦合,可移植性强: 组件可重用性高
- 可测试性高:组件无依赖,可以很容易的单独测试组件