React 是 facebook 开源出来的一套前端方案,官网在 https://reactjs.org 。javascript
先看一个简单的样子:html
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> </head> <body> <h1>Hello</h1> <div id="place"></div> <script type="text/javascript"> var attribute = { className: 'text', style: {color: 'red'}, id: 'me', href: 'http://' + 's.zys.me' }; var inner = React.createElement('a', attribute, 'React'); var instance = React.createElement('h1', null, '字符串在中间', inner); ReactDOM.render(instance, $('#place')[0]); </script> </body> </html>
从上面能够看出,它的使用其实跟其它不少框架的方式差很少,即,找到一个 DOM 节点,而后在这个节点上作事,相似 jQuery 的 $(dom).xxx({})
。前端
在 React.createElement
中,第一个参数是标签名,第二个是 config ,其中包括了节点属性(class
由于关键词问题改用 className
) ,第三个及之后,是子级节点,或者说子级元素。java
上面两个 createElement
获得的最终结果,是:node
<h1> 字符串在中间 <a class="text" style="color: red" id="me" href="http://s.zys.me">React</a> </h1>
固然,这里的重点,不是 React.createElement
,而是它返回的那个 instance
,事实上,从源码 https://unpkg.com/react@16.2.0/umd/react.development.js 中看的话,能够看出它返回的是一个 ReactElement
,这个 ReactElement
就是整个 React 体系于众不一样的地方,不然它直接返回一个 DOM Element 就行了。react
最后一行的 ReactDOM.render
就是处理 ReactElement
的, ReactDOM
的源码在 https://unpkg.com/react-dom@16.2.0/umd/react-dom.development.js , 这个东西本身实现了一套挂节点的树结构(所谓的 Virtual DOM),中间隔了一层再去处理真实的 DOM 渲染。这么作的好处是,由于本身有一套完整的树结构的,因此,能够在上面实现不少在原始 DOM 结构上不能作,或者不方便作的事。好比,异构渲染之类的。固然,局限的地方也很明显,本质上仍是“节点”,其实没有一个往上的抽象,暴露的细节仍是不少的,这种状况下,东西能够作得比较细,可是付出的成本也比较大,我是这样预测的。jquery
最开始,已经简单演示了针对像 a
h1
这类原生 DOM 元素的 ReactElement
定义, 天然,“能接受一个肯定值的地方也应该能够接受一个函数”是 js 的一个惯例吧:webpack
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> </head> <body> <div></div> <script type="text/javascript"> function Component(props) { if(props.tag === 'h1') { return React.createElement('h1', null, '大标题'); } else { return React.createElement('span', null, '普通文本'); } } var instance = React.createElement(Component, {tag: 'h1'}); ReactDOM.render(instance, $('div')[0]); </script> </body> </html>
上面代码的过程,大概是:git
React.createElement(type, config, children)
返回 ReactElement()
。ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props)
,上面 config
的一些东西,会补为这里的几个参数,及 props
里的东西。ReactElement()
的调用会返回一个 element
,这是一个比较单纯的结构体,里面会有 type
key
ref
props
_owner
$$typeof
等内容。ReactDOM.render(element, container, callback)
是对 renderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback)
的调用, element
也转为 children
的统一律念了。DOMRender
(从 reactReconciler
来) 的 updateContainer(element, container, parentComponent, callback)
,最外层的调用, container
就是一个 newRoot
,前面的 children
概念这里又变成 element
了。container
,接着到 scheduleTopLevelUpdate(current, element, callback)
, current
是从 container
里取的。scheduleTopLevelUpdate
是会做 scheduleWork(current, expirationTime)
,应该是处理一些同步的事。还有 构造一个 update
的结构体,而后 insertUpdateIntoFiber(current, update)
, element
被放到 update
中的 partialState: { element: element }
。insertUpdateIntoFiber(fiber, update)
, 新的 element
在 update
中,而 fiber
是 container
的范畴。这里只是处理“两个队列”的状态,干活的仍是在 scheduleWork()
中。current
和 element
就分开了,current
继续进入 scheduleWork(fiber, expirationTime)
进行下一步处理。而 element
只在 insertUpdateIntoFiber
中更新于队列状态。scheduleWork()
里实际上会用 scheduleWorkImpl(fiber, expirationTime, isErrorRecovery)
,这里开始迭代 fiber
,离真实的节点建立还有一半的路要走吧,中间又各类处理,最后 createInstance()
会建立真实的节点。反正,我从源码中找了一长串,仍是没搞明白 createElement()
第一个参数的 type
能够是,应该是一个什么东西。github
从官方的文档来看 https://reactjs.org/docs/react-api.html#createelement ,这个 type
能够是三类东西:
div
, span
。React component
, React 组件。React fragment
,这好像是 v16.2.0 加入的新机制,简单来讲它使 React 能够支持直接渲染“一串”节点,而以前,只能直接渲染“一个”节点,若是是一串的需求,那么在外面须要包一个节点。我理解大概就是下面这个样子吧:<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> </head> <body> <div></div> <script type="text/javascript"> function Component(props) { return ['1', React.createElement('h1', null, props.text)]; } var instance = React.createElement(Component, {text: '啊啊啊'}); ReactDOM.render(instance, $('div')[0]); </script> </body> </html>
React 组件有两种类型,一种是简单的“函数式组件”,另外一种是复杂点的“类组件”。
函数式组件就是接受 props
,而后返回一个 React element
:
function Component(props) { return React.createElement('h1', null, props.text); }
类组件,是继承 React.Component
,而后重写一些方法:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://unpkg.com/react@16.2.0/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16.2.0/umd/react-dom.development.js"></script> </head> <body> <div></div> <script type="text/javascript"> class MyComponent extends React.Component { render(){ return React.createElement('h1', null, this.props.text); } } var instance = React.createElement(MyComponent, {text: '哈哈'}); ReactDOM.render(instance, $('div')[0]); </script> </body> </html>
前面提过,若是你想经过 createElement
构造一个相似:
<h1> 字符串在中间 <a class="text" style="color: red" id="me" href="http://s.zys.me">React</a> </h1>
那么你须要的 js 大概是:
var attribute = { className: 'text', style: {color: 'red'}, id: 'me', href: 'http://' + 's.zys.me' }; var inner = React.createElement('a', attribute, 'React'); var instance = React.createElement('h1', null, '字符串在中间', inner); ReactDOM.render(instance, $('#place')[0]);
看起来是比较麻烦的,于 React 中引入了一个新的东西,JSX ,即 JavaScript XML ,简单来讲,就是 javascript 和 XML 的混写,上面的代码能够写成:
var inner = <a className="text" style={ {color: 'red'} } id="me" href={'http://' + 's.zys.me'}>React</a>; var instance = <h1>字符串在中间 {inner}</h1>; ReactDOM.render(instance, $('#place')[0]);
甚至是:
var inner = <a className="text" style={ {color: 'red'} } id="me" href={'http://' + 's.zys.me'}>React</a>; ReactDOM.render(<h1>字符串在中间 {inner}</h1>, $('#place')[0]);
能够看出,比原来直接写 createElement
的方式要简洁得多, JSX 除了支持直接写 XML 风格的标签,还可使用 {}
处理表达式。不过, JSX 并非浏览器的标准,要让它跑在浏览器,须要在发布前专门编译到纯 js ,平时测试,也能够直接用 babel 来跑:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script crossorigin src="https://unpkg.com/babel-standalone@6.15.0/babel.js"></script> </head> <body> <h1>Hello</h1> <div id="place"></div> <script type="text/babel"> var inner = <a className="text" style={ {color: 'red'} } id="me" href={'http://' + 's.zys.me'}>React</a>; ReactDOM.render(<h1>字符串在中间 {inner}</h1>, $('#place')[0]); </script> </body> </html>
注意,上在的 script
的 type
写的是 text/babel
哦。
babel 也有命令行的工具,先安装:
npm install -g babel npm install -g babel-cli npm install -g babel-preset-react
而后对于一个 demo.jsx
:
var inner = <a className="text" style={ {color: 'red'} } id="me" href={'http://' + 's.zys.me'}>React</a>; ReactDOM.render(<h1>字符串在中间 {inner}</h1>, $('#place')[0]);
可使用 babel 编译它:
babel --presets=react demo.jsx
就能够看到标准的 js 输出了:
var inner = React.createElement( "a", { className: "text", style: { color: 'red' }, id: "me", href: 'http://' + 's.zys.me' }, "React" ); ReactDOM.render(React.createElement( "h1", null, "\u5B57\u7B26\u4E32\u5728\u4E2D\u95F4 ", inner ), $('#place')[0]);
babel 默认输出的到标准输出,能够经过 -o
输出到指定文件。
平时的前端项目,通常把 babel 和 webpack 结合使用,在构建过程处理 JSX 格式。
JSX 中的名字,若是是自定义组件,名字开头须要大写,小写的是 DOM 标签。
名字中,能够带名字空间:
<MyComponent.A.B />
也能够是变量值:
let My = [MyComponent][0]; return <My />
可是,不能带表达式(下面是错误的):
<[MyComponent][0] />
提示一下, JSX 不是模板,它的行为,更像是“宏”。本质,仍是 js 代码。 宏是没有运行时能力的,天然没法求值表达式。
前面已经提到过 props 了, props 通常处理初始化后就不改变的数据,对于一个组件中要变化的数据,须要放在 state 中处理,由于 React 专门设计了组件的 setState()
来维护 state (不要直接更改 state ,而是使用 setState()
) ,同时 setState()
还处理节点渲染相关的事,经过函数调用,来实现“数据到展示”的单向绑定。
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script crossorigin src="https://unpkg.com/babel-standalone@6.15.0/babel.js"></script> </head> <body> <div id="place"></div> <script type="text/babel"> class MyComponent extends React.Component { constructor(props) { super(props); this.state = {date: (new Date()).valueOf()}; setInterval(() => this.setState({date: (new Date()).valueOf()}), 1000); } render(){ return <h1>Hello {this.props.name} {this.state.date}</h1>; } } ReactDOM.render(<MyComponent name="哈哈" />, $('#place')[0]); </script> </body> </html>
setState()
也能够接受一个函数,这个函数的参数第一个是当前的 state ,另外一个是 props :
class MyComponent extends React.Component { constructor(props) { super(props); this.state = {date: (new Date()).valueOf()}; setInterval(() => this.setState((prevState, props) => ({date: prevState.date + 1000})), 1000); } render(){ return <h1>Hello {this.props.name} {this.state.date}</h1>; } } ReactDOM.render(<MyComponent name="哈哈" />, $('#place')[0]);
React 本身维护了一个节点状态的树结构,而且,同时也本身实现了一套事件机制,只是这套事件,跟浏览器自身的区别不是很大:
class MyComponent extends React.Component { constructor(props) { super(props); this.state = {date: (new Date()).valueOf()}; setInterval(() => this.setState((prevState, props) => ({date: prevState.date + 1000})), 1000); } handleClick = () => { console.log(this); } render(){ return <h1 onClick={this.handleClick}>Hello {this.props.name} {this.state.date}</h1>; } } ReactDOM.render(<MyComponent name="哈哈" />, $('#place')[0]);
注意那个 handleClick
的写法:
handleClick = () => { console.log(this); }
若是不用“箭头函数”把 this
固定住了,那么在使用时就须要显式绑定:
handlerClick(){ console.log(this); } render(){ return <h1 onClick={this.handleClick.bind(this)}>Hello {this.props.name} {this.state.date}</h1>; }
React 本身的事件,是“驼峰式”的名字,好比 onClick
,而 onclick
则是浏览器本身的事件了。
其它支持的完整的事件列表,见: https://reactjs.org/docs/events.html
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script crossorigin src="https://unpkg.com/babel-standalone@6.15.0/babel.js"></script> </head> <body> <div id="place"></div> <script type="text/babel"> class MyComponent extends React.Component { constructor(props) { super(props); this.state = {itemList: [{name: 'abc', id: 'a'}, {name: 'oiii', id: 'b'}] }; } handleClick(o){ return (e) => { console.log(o); } } render(){ return <div> {this.state.itemList.map( (o) => (<div key={o.id} onClick={this.handleClick(o)}>{o.name}</div>) ) } </div> } } ReactDOM.render(<MyComponent name="哈哈" />, $('#place')[0]); </script> </body> </html>
对于列表的渲染, React 要求节点上须要添加 key
属性,不然,会报 warn (是的,不会报错)。若是没有明确的 id 之类的标识,可使用序列的索引值 index 。这个 key
属性的做用, React 会用于标识节点,以便在数据变动时,对应地能够追踪到节点的变动状况。
对于一个 React 组件,准备说,“类组件”,它在渲染及销毁过程当中,每一个状态转换时, React 都预留了相关的 Hook 函数,这些函数,一共 10 个,能够分红 4 类:
constructor(props)
componentWillMount()
render()
componentDidMount()
,从这里开始, setState()
会触发从新的 render()
。componentWillReceiveProps(nextProps)
, props 改变时触发,能够因为父组件的变化引发。 setState()
通常不会触发这个过程。shouldComponentUpdate(nextProps, nextState)
,若是返回 false
,则使 React “尽可能” 不要从新渲染组件,注意,只是 “尽可能” 而已。若是渲染上有性能问题,则应该考虑其它解决方案。componentWillUpdate(nextProps, nextState)
,上面的 shouldComponentUpdate()
若是返回 false
,则这个过程必定不会触发,现时,第一次初始化不会触发这个过程。render()
componentDidUpdate(prevProps, prevState)
,第一次初始化不会触发这个过程。componentWillUnmount()
componentDidCatch(error, info)
这几个方法的一些细节:
componentWillReceiveProps
setState()
。props
没变化时也可能被调用。componentWillUpdate
setState()
, 不然会再次触发变化流程,形成死循环。shouldComponentUpdate()
为 false
时不会调用。componentDidUpdate
props
的相应变化后再决定是否做新的远程调用。除了上面讲的过程 Hook 函数,及前面用过的 setState()
,组件还有一个 forceUpdate(callback)
方法,它的调用能够触发 render()
,而且跳过 shouldComponentUpdate()
。
另外,在“类”层面,有两个属性, defaultProps
用于定义默认的 props , displayName
,用于定义调试时显示的名字。
试试各个过程的表现吧:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script> </head> <body> <div></div> <script type="text/babel"> class MyComponent extends React.Component { constructor(props){ super(props); this.state = {name: '初始化'}; let that = this; setTimeout(function(){ console.log('2s go ...'); that.setState({name: '修改了的'}); }, 2000); console.log('constructor'); } componentWillMount(){ console.log('componentWillMount'); } componentDidMount(){ console.log('componentDidMount'); } componentWillReceiveProps(nextProps){ console.log('componentWillReceiveProps'); } shouldComponentUpdate(nextProps, nextState){ console.log('shouldComponentUpdate'); return true; } componentWillUpdate(nextProps, nextSate){ console.log('componentWillUpdate'); } componentDidUpdate(prevProps, prevState){ console.log('componentDidUpdate'); } componentWillUnmount(){ console.log('componentWillUnmount'); } componentDidCatch(error, info){ console.log('componentDidCatch'); } render(){ console.log('render'); if(this.props.error){throw Error()}; return <h1>{this.state.name}</h1>; } } ReactDOM.render(<MyComponent />, $('div')[0]); setTimeout(function(){ console.log('5s go ...'); ReactDOM.render(<MyComponent />, $('div')[0]); }, 5000); setTimeout(function(){ console.log('10s go ...'); ReactDOM.render(null, $('div')[0]); }, 10000); </script> </body> </html>
上面的代码会输出:
constructor componentWillMount render componentDidMount 2s go ... shouldComponentUpdate componentWillUpdate render componentDidUpdate 5s go ... componentWillReceiveProps shouldComponentUpdate componentWillUpdate render componentDidUpdate 10s go ... componentWillUnmount
上面过程看起来都很好理解的。
少的那个 componentDidCatch
是用来抓子组件错误的:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script> </head> <body> <div></div> <script type="text/babel"> class ErrorWrapper extends React.Component { constructor(props){ super(props); this.state = {error: null}; } componentDidCatch(error, info){ console.log('componentDidCatch'); console.log(error, info); this.setState({error: {error: error, info: info}}); } render(){ if(!!this.state.error) { return <h1>Error Here</h1> } else { return this.props.children || ''; } } } class MyComponent extends React.Component { render(){ throw Error(); } } ReactDOM.render(<ErrorWrapper><MyComponent /></ErrorWrapper>, $('div')[0]); </script> </body> </html>
上面代码占, MyComponent
的错误,会触发 ErrorWrapper
的 componentDidCatch
,参数中的 error
是一串调用栈字符串, info
是一个结构,里面有一个 componentStack
字符串记录了组件序列。
React 对于 Component 的处理,前面介绍得差很少了,这里说的外围函数,是指尚未脱离 DOM 的那几个工具,好比前面用到的 ReactDOM.render(child, container)
就是一个典型。
ReactDOM.unmountComponentAtNode(container)
, 从一个节点中删除组件。ReactDOM.findDOMNode(component)
,找到组件“渲染的内容”所在的 DOM 节点,注意,不是组件本身所在的节点。ReactDOM.createPortal(child, container)
,在 render()
中,取代返回 ReactElement
,而在一个确认的位置渲染出组件。(好比“弹层”的那种状况)前 2 个方法是比较好理解的:
class MyComponent extends React.Component { render(){ return <h1 onClick={this.clickHandler}>哈哈哈</h1>; } clickHandler = () => { var dom = ReactDOM.findDOMNode(this); console.log(dom); ReactDOM.unmountComponentAtNode($('div')[0]); } } ReactDOM.render(<MyComponent />, $('div')[0]);
ReactDOM.createPortal(child, container)
通常用在须要本身建立并维护真实 DOM 节点的地方:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script> </head> <body> <div></div> <script type="text/babel"> class MyComponent extends React.Component { constructor(props){ super(props); this.dom = document.createElement('div'); } componentDidMount(){ $('body').append($(this.dom)); } componentWillUnmount(){ $(this.dom).remove(); } clickHandler = () => { var dom = ReactDOM.findDOMNode(this); console.log(dom); ReactDOM.unmountComponentAtNode($('div')[0]); } render(){ return ReactDOM.createPortal(<div onClick={this.clickHandler}>{this.props.children}</div>, this.dom); } } ReactDOM.render(<MyComponent><h1>弹出来的东西</h1></MyComponent>, $('div')[0]); </script> </body> </html>
上面的代码演示了 ReactDOM.createPortal
的使用,须要注意的是,即便一个组件的 render()
实现,不是 ReactElement
而是 ReactDOM.createPortal
,组件自己,仍是须要依附于父组件,或者 ReactDOM.render()
的,即坑位在占住,这种时候,就有点“它在那里,它又不在那里”的感受。
React 的实现的基础是本身实现的一套“树状节点结构”,而后再把这个结构放到浏览器中用真实的 DOM 展示出来。那么这中间,本身搞的一套,与浏览器环境下的一套,之间确定会有少量的差别的,虽然 React 连事件这种都本身实现了一遍。
首先,对于节点的属性来讲, React 的属性都是“驼峰式”的名字,与 DOM 本身的一些全小写属性名不一样,好比, onclick
变成了 onClick
, tabindex
变成了 tabIndex
, SVG 中的属性 React 也支持。
属性名中的 class ,由于关键词因素,在 React 中变成了 className 。
style 在 React 中做了扩展实现的,给它一个对象值的话,能够获得正确的样式。
onChange 事件在 React 中的实现也做了加强,能够实时触发相应的回调。
label 标签的 for
属性,在 React 中可使用 htmlFor
代替。
dangerouslySetInnerHTML
是一个特殊的属性,能够用于直接添加原始 HTML 内容:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script> </head> <body> <div></div> <script type="text/babel"> class MyComponent extends React.Component { getHtml = () => { return {__html: '<h1 style="color: red">哈哈哈</h1>'}; } render(){ return <div dangerouslySetInnerHTML={this.getHtml()} /> } } ReactDOM.render(<MyComponent />, $('div')[0]); </script> </body> </html>
ref
也是 React 中的一个特殊属性,用于回调真实 DOM 节点的引用:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script> </head> <body> <div></div> <script type="text/babel"> class MyComponent extends React.Component { clickHandler = () => { console.log(this.h1); } render(){ return <h1 ref={(node) => {this.h1 = node}} onClick={this.clickHandler}>哈哈</h1> } } ReactDOM.render(<MyComponent />, $('div')[0]); </script> </body> </html>
ref
能够用于控制焦点,或者在节点上使用浏览器的其它一些 API ,好比音频,视频等。
React 由于有本身的一套树状态,因此,理论上它在测试方面有着得天独厚的优点,由于即便是在浏览器环境,不涉及 DOM 相关 API 的行为,只须要在那个树状态中的去操做,并检查操做后的状态就能够达到测试目的。固然,咱们也不用关心 React 的一些实现细节,如今说测试,会有两方面的内容。一方面是 React 提供一些测试相关的工具方法,只是工具方法,若是你要创建完整的测试体系,还须要本身解决断言库,测试用例组件与调度等问题。另外一方面,是看看非浏览器的渲染,若是能够彻底摆脱浏览器环境,完成大部分的测试工做,那将是很美好的一件事。
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script> <script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom-test-utils.development.js"></script> </head> <body> <div></div> <script type="text/babel"> class MyComponent extends React.Component { constructor(props){ super(props); this.state = {n: 0}; } clickHandler = () => { this.setState((s) => ({n: s.n + 1})); } render(){ return <h1 onClick={this.clickHandler}>{ this.state.n }</h1> } } let component = ReactTestUtils.renderIntoDocument(<MyComponent />); console.log(component); let node = ReactDOM.findDOMNode(component) console.log(component.state); ReactTestUtils.Simulate.click(node); console.log(component.state); console.log(ReactTestUtils.isElement(<MyComponent />)); console.log(ReactTestUtils.isElementOfType(<MyComponent />, MyComponent)); console.log(ReactTestUtils.isDOMComponent(ReactTestUtils.renderIntoDocument(<h1>xx</h1>))); console.log(ReactTestUtils.isCompositeComponent(component)); console.log(ReactTestUtils.isCompositeComponentWithType(component, MyComponent)); console.log(ReactTestUtils.findRenderedDOMComponentWithTag(component, 'h1')); console.log(ReactTestUtils.findRenderedComponentWithType(component, MyComponent)); </script> </body> </html>
引入额外的资源文件,相关的方法在浏览器是彻底能够用的。
不过,完成的测试体系的话,还须要搭配其它工具来完成, React 的相关 API 只是提供了获取信息的方法。
在 nodejs 环境,可使用 react-test-renderer
来完成对相关组件的处理,并按树结构遍历结果:
const React = require('react'); const TestRenderer = require('react-test-renderer'); class MyComponent extends React.Component { constructor(props){ super(props); this.state = {number: 0}; } render(){ return <h1 onClick={ () => {this.setState((s) => ({number: s.number + 1}))} }>{this.state.number}</h1>; } } const testRenderer = TestRenderer.create(<MyComponent />); testRenderer.root.findByType('h1').props.onClick(); console.log(testRenderer.root.findByType('h1').children); console.log(testRenderer.root.instance.state);
这部分完整的 API 在 https://reactjs.org/docs/test-renderer.html 。
在非浏览器环境下,“模拟点击”这种操做是没有意义的,可是 onClick
自己并无说必定是“点击”,它只是指明了一个回调函数而已,浏览环境下是点击触发,那么纯编程环境下,直接调用便可。
Redux 并非 React 必不可少的一部分,它只是随着 React 出来的一套模式,而且带了一个这个模式的实现: https://redux.js.org/ 。 简单来讲,这个模式是着眼于“组件状态及组件间通讯”的,换句话,它定义了一种“相对独立的组件句柄”。
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>Redux</title> <script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script> </head> <body> <script type="text/javascript"> var store = Redux.createStore(function reducer(state, action){ console.log('here', state); if(action.type === 'HAHA'){ return 'OK' } return 'ERROR'; }); console.log(store.getState()); store.subscribe(function render(){ console.log('subscribe'); console.log(store.getState()); }) store.dispatch({type: 'HAHA'}); </script> </body> </html>
Redux 的流程自己很是简单,如上所示,经过一个 reducer
初始化一个 store
, 这个 store
先 subscribe
一些动做,而后使用时在须要的时候 dispatch
出一个 action
就行了。 dispatch
的对应逻辑是 reducer
中的事,而 reducer
执行完就轮到 subscribe
的回调了,就是这样的一个执行循环。
React-Redux 是 Redux 在 React 上的一个实现,直观点说,就是结合了 React 的 Component 机制的一套 Redux 流程。它的核心点,我我的的理解是下面几点:
props
以后,它本身就能够彻底独立工做的,里面尽可能不包含业务逻辑。而“通讯组件”的做用,就是在“纯组件”之上,加入具体业务逻辑,去完成各“纯组件”的调度。State -> Props
和 Dispatch -> Props
的这个过程。props
控制,不管是相应的数据输入,仍是一些事件回调的输出。状态性的一些东西,则由上层的“通讯组件”,经过 state
和 dispatch
来维护,上层通讯组件的状态性的数据,会更新到“纯组件”的 props
上,来完成页面重渲染。具体代码:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/redux.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-redux.min.js"></script> </head> <body> <div id="app"></div> <script type="text/babel"> const Title = props => ( <h1 onClick={props.onClick}>{props.name}</h1> ) const mapStateToProps = state => { return {...state}; } const mapDispatchToProps = dispatch => { return { onClick: id => { dispatch({type: "CLICK"}); } } } const VisibleTitle = ReactRedux.connect( mapStateToProps, mapDispatchToProps )(Title); let store = Redux.createStore( (state, action) => { if(action.type === 'CLICK'){ return {name: '点击以后的'}; } return {name: '初始化的'}; }); const App = () => ( <ReactRedux.Provider store={store}> <VisibleTitle /> </ReactRedux.Provider> ) ReactDOM.render(<App />, document.getElementById('app')); </script> </body> </html>
上面的组件实现的效果很简单,就是点击以后,改变文本。可是通过 React-Redux 分拆以后,感受复杂了好多。不过,直观地也能够看出,分拆以后, Title
这个组件变“纯”了,它的行为一目了解,没有什么“点击以后,改变文本”这类逻辑,只有单纯的接受输入,而后渲染组件。操做上的逻辑,则由外层的 VisibleTitle
来完成。
VisibleTitle
经过它的 state
状态性数据,来控制下面的 Title
的渲染。 state
则是经过 Redux 的 store
来承接的。进一步, state
到 Title
的 props
,可能只是 Title
须要输入的一部分(数据部分),另外一部分涉及“反馈”到 state
变化的 props
,则由 Redux 的 dispatch - action
来承接。
说的这两部分,也就对应着 mapStateToProps
和 mapDispatchToProps
,一个处理数据,一个调度 dispatch
的执行。而后使用 connect
这个现成的方法生成包裹在 Title
之上的 VisibleTitle
。
固然,事情到这里尚未结束,再上层,还有一个 store
, Provider
是 React-Redux 提供的东西,目的是给里面的组件提供 store
。而获得 store
,又须要 Reducer
, dispatch
出的 action
,及 state
的变化其实是在这里处理。
Redux 的设计中,全部的状态变化是经过 dispatch
这一个点完成的(而且这个 API 又很是简单),天然,围绕这一个点,就能够很容易完成“套圈”。
let next = store.dispatch; store.dispatch = function(action){ console.log(action); next(action); }
这都彻底不须要特别设计,你本身就能够随便搞。
不过从 Monkeypatching 做为切入,官方的 https://redux.js.org/docs/advanced/Middleware.html 这份文档写得很是好,按部就班,推荐一读。
最后,官方 API 在这上面的支持,或者说推荐做法,是经过高阶函数定义 Middleware ,而后使用 applyMiddleware
挂到 store
里面去。
const logger = store => next => action => { console.log('dispatching', action, store.getState()); let result = next(action); console.log('next state', store.getState()); return result }
这是定义,使用时:
import { createStore, combineReducers, applyMiddleware } from 'redux' let store = createStore( reducer, applyMiddleware(logger) )
用前面的简单例子能够试试:
function logger(store) { return function(next){ return function(action){ console.log('dispatching', action, store.getState()); var result = next(action); console.log('next state', store.getState()); return result } } } var store = Redux.createStore(function reducer(state, action){ if(action.type === 'HAHA'){ return 'OK' } return 'ERROR'; }, Redux.applyMiddleware(logger)); store.subscribe(function render(){ console.log('sub', store.getState()); }) store.dispatch({type: 'HAHA'});
了解了中间件,再来看异步流程,就没什么新问题了。
在 Redux 中,state
的维护是在 reducer
中的,从前面的例子能够看出, reducer
自己的设计,是没有考虑异步状况的,它的结构: (state, action) => newState
是同步的。
若是咱们在某个 dispatch
以后,须要异步获取数据(事实上这很常见),并把这个东西更新到 store
中去,咱们就须要额外作点事,比较很容易想到的一个办法:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>Redux</title> <script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script> </head> <body> <script type="text/javascript"> var store = Redux.createStore(function reducer(state, action){ if(action.type === 'HAHA'){ setTimeout(function(){ store.dispatch({type: 'FETCH', payload: '123'}); }, 3000); return 'LOADING'; } if(action.type === 'FETCH'){ return action.payload; } return 'ERROR'; }); store.subscribe(function render(){ console.log('sub', store.getState()); }) store.dispatch({type: 'HAHA'}); </script> </body> </html>
简单来讲,在 dispatch
以后,咱们异步发出请求,并在当前至少有对 dispatch
的引用,而后返回一个中间状态的 state
,甚至是没有任何更改的 state
。在异步响应以后,再次 dispatch
,同时把异步响应的数据放到 action
中传递出去。
这种方式,虽然绕了一点,可是自己没毛病,不过要多定义出来一个 dispatch
类型,是很烦人就是了。
考虑前面讲过的“中间件”机制,咱们很容易对 dispatch
的行为做一些扩展,加入对异步的支持。
先看“中间件”的样子:
const logger = store => next => action => { console.log('dispatching', action, store.getState()); let result = next(action); console.log('next state', store.getState()); return result }
很显然,对于 dispatch
传入的 action
这个东西,咱们能够先判断它的特征,或者说状态,根据既定规则决定在何时做 next(action)
。
事实上, redux-promise
的代码就几行:
import { isFSA } from 'flux-standard-action'; function isPromise(val) { return val && typeof val.then === 'function'; } export default function promiseMiddleware({ dispatch }) { return next => action => { if (!isFSA(action)) { return isPromise(action) ? action.then(dispatch) : next(action); } return isPromise(action.payload) ? action.payload.then( result => dispatch({ ...action, payload: result }), error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); } ) : next(action); }; }
作的事,就是当 dispatch
出去的东西,里面有一个 then
成员,而且这个成员是一个函数的话,那么 reducer
会返回这个函数的调用结果,而且调用这个函数时传入的是当前的 dispatch
。
简单演示一下大概是:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>Redux</title> <script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script> </head> <body> <script type="text/javascript"> var store = Redux.createStore(function reducer(state, action){ if(!!action.then){ return action.then(store.dispatch.bind(store)); } return 'GOT: ' + action.payload; }); store.subscribe(function render(){ console.log('sub', store.getState()); }) store.dispatch({type: '123', payload: 'null'}); setTimeout(function(){ store.dispatch({type: 'async', then: function(dispatch){ setTimeout(function(){ dispatch({type: 'xxx', payload: 'async data'}); }, 3000); }}); }, 2000); </script> </body> </html>
前面一个例中,有一个 mapStateToProps
,把例子改一点点看看:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/redux.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-redux.min.js"></script> </head> <body> <div id="app"></div> <script type="text/babel"> const getName = (name, count) => { console.log('getName'); return name + count; } const Title = props => ( <div> <h1 onClick={props.onClick}>{props.name}</h1> <h1 onClick={props.onOtherClick}>另外的 {props.name}</h1> </div> ) const mapStateToProps = (state, props) => { console.log('mapStateToProps'); return { name: getName(state.name, state.count) } } const mapDispatchToProps = dispatch => { return { onClick: () => { dispatch({type: "CLICK"}); }, onOtherClick: () => { dispatch({type: "OTHER_CLICK"}); } } } const VisibleTitle = ReactRedux.connect( mapStateToProps, mapDispatchToProps )(Title); let store = Redux.createStore( (state = {name: '初始值', count: 0}, action) => { if(action.type === 'CLICK'){ return { ...state, name: '点击以后的', count: state.count + 1}; } if(action.type === 'OTHER_CLICK'){ return { ...state, other: '什么东西'}; } return { ...state }; }); const App = () => ( <ReactRedux.Provider store={store}> <VisibleTitle /> </ReactRedux.Provider> ) ReactDOM.render(<App />, document.getElementById('app')); </script> </body> </html>
上面的例子,简单来讲,“纯组件”须要一个 name
来显示,而这个 name
的来自于外套组件的 store
中的 name
和 count
连在一块儿的。
执行这个例子,能够看出一个重复执行的问题,当你点击第二行“另外的”那块文本时,是作的 onOtherClick
回调,这个过程并不会更改 count
值,也就是说,“纯组件”须要的 name
并不会由于这个操做而有所改变。可是, getName()
这个函数却仍是一遍又一遍地执行了,这就是所谓的重复执行的问题。(若是这样的函数不少,会影响到页面性能的,特别是这些函数的逻辑有一点复杂的时候)
其实看到这里,我个以为 React 中的这个问题仍是很是尴尬的,它本身内部的虚拟节点,靠本身的 diff 算法实现高效的更新策略,可是外面的 state
自己的维护又成了可能的问题。
回到重复执行的问题,它并无什么本质的解决办法,只可能进一步拆分 getName()
,分离变量的状况下,做一些执行层面的优化,简单说,先做变量是否变化的判断,而后再决定是否继续执行。这样用额外判断的成本,换取可能的省略后继执行的效益。大概是:
const getName = ( () => { let lastName = null; let lastCount = null; let lastResult = null; return (name, count) => { console.log('getName'); if(name === lastName && count === lastCount){ return lastResult } lastName = name; lastCount = count; lastResult = name + count; return lastResult; } })();
这个样子,像是中间加了一个缓存层。
把这个形式封装一下,就是说先把“变量”的获取提取出来,而后加一个中间缓存层。官方推荐的实现是 reselect ,作的事就是这样的: https://github.com/reactjs/reselect 。
用 reselect 把前面的代码调整一下,就是:
<!DOCTYPE html> <html lang="zh-cmn-Hans"> <head> <meta charset="utf-8" /> <title>React</title> <script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/redux.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/react-redux.min.js"></script> <script crossorigin src="https://s.zys.me/js/react/reselect.min.js"></script> </head> <body> <div id="app"></div> <script type="text/babel"> const getName = Reselect.createSelector( [ (state, props) => {console.log('reselect'); return state.name}, (state, props) => state.count ], (name, count) => { console.log('getName'); return name + count } ); const Title = props => ( <div> <h1 onClick={props.onClick}>{props.name}</h1> <h1 onClick={props.onOtherClick}>另外的 {props.name}</h1> </div> ) const mapStateToProps = (state, props) => { console.log('mapStateToProps'); return { name: getName(state, props) } } const mapDispatchToProps = dispatch => { return { onClick: () => { dispatch({type: "CLICK"}); }, onOtherClick: () => { dispatch({type: "OTHER_CLICK"}); } } } const VisibleTitle = ReactRedux.connect( mapStateToProps, mapDispatchToProps )(Title); let store = Redux.createStore( (state = {name: '初始值', count: 0}, action) => { if(action.type === 'CLICK'){ return { ...state, name: '点击以后的', count: state.count + 1}; } if(action.type === 'OTHER_CLICK'){ return { ...state, other: '什么东西'}; } return { ...state }; }); const App = () => ( <ReactRedux.Provider store={store}> <VisibleTitle /> </ReactRedux.Provider> ) ReactDOM.render(<App />, document.getElementById('app')); </script> </body> </html>
Reselect.createSelector
是一个高阶函数,它接受的第一个参数,是一个列表,里面就是分拆出来的,获取“变量”的方法,第二个参数就是接受变量的主要逻辑。由于变量已经明确拆分出来的,因此天然地,针对变量做判断并应用缓存机制就好办了。
这里注意一下,虽然表面上 getName()
的调用少了,可是却额外多了 reselect
的调用,本质上来讲,只是改善的重复调用的损耗,并无避免它。