深入理解React(二) —— 数据流和事件原理

  • Post author:
  • Post category:其他





这个,叫做竹笕,是中日传统禅文化中常见的庭院装饰品,它的构造可简单可复杂,但原理很简单,比如这个竹笕,水从竹笕顶部入口流入内部,并按照固定的顺序从上向下依次流入各个小竹筒,然后驱动水轮转动。对于强迫症患者来说,观赏竹笕的绝对是一种很享受的过程的最爱,你会发现这些小玩意竟然能这么流畅的协调起来,好神奇。


如果竹笕是一个组件的话,那么水就是组件的数据流。

在React中,数据流是自上而下单向的从父节点传递到子节点,所以组件是简单且容易把握的,他们只需要从父节点提供的props中获取数据并渲染即可。如果顶层组件的某个prop改变了,React会递归地向下遍历整棵组件数,重新渲染所有使用这个属性的组件。





这个是前面看到的 KM 热点问题组件,拥有一个叫做 articles 的属性。

在组件内部,可以通过this.props来访问props,props是组件唯一的数据来源,对于组件来说:

props永远是只读的。

不要尝试在组件内部调用setProps方法来修改props,如果你不小心这么做了,React会报错并给出非常详细的错误提示。

组件的属性类型如果不进行声明和验证,那么很可能使用者传给你的属性值或者类型是无效的,那会导致一些意料之外的故障。好在React已经为我们提供了一套非常简单好用的属性校验机制——


React有一个PropTypes属性校验工具,经过简单的配置即可。当使用者传入的参数不满足校验规则时,React会给出非常详细的警告,定位问题不要太容易。


PropTypes包含的校验类型包括基本类型、数组、对象、实例、枚举——




以及对象类型的深入验证等等。如果内置的验证类型不满足需求,还可以通过自定义规则来验证。


如果某个属性是必须的,在类型后面加上 isRequired 就可以了。

React的一大创新,就是把每一个组件都看成是一个状态机,组件内部通过state来维护组件状态的变化,这也是state唯一的作用。


state一般和事件一起使用,我们先看state,然后看看state和事件怎样结合。


这是一个简单的开关组件,开关状态会以文字的形式表现在按钮的文本上。


首先看render方法,返回了一个button元素,给button注册了一个事件用来处理点击事件,在点击事件中对state的on字段取反,并执行 this.setState() 方法设置on字段的新值。一个开关组件就完成了。




组件渲染完成后,必须有UI事件的支持才能正常工作。


React通过将事件处理器绑定到组件上来处理事件。


React事件本质上和原生JS一样,鼠标事件用来处理点击操作,表单事件用于表单元素变化等,Rreact事件的命名、行为和原生JS差不多,不一样的地方是React事件名区分大小写。


比如这段代码中,Article组件的section节点注册了一个onClick事件,点击后弹出alert。


有时候,事件的处理器需要由组件的使用者来提供,这时可以通过props将事件处理器传进来。


这个是刚才那个Article组件的使用者,它提供给Article组件的props中包含了一个onClick属性,这个onClick指向这个组件自身的一个事件处理器,这样就实现了在组件外部处理事件回调。


这是一个React组件实现组件可交互所需的流程,render()输出虚拟DOM,虚拟DOM转为DOM,再在DOM上注册事件,事件触发setState()修改数据,在每次调用setState方法时,React会自动执行render方法来更新虚拟DOM,如果组件已经被渲染,那么还会更新到DOM中去。


这些是React目前支持的事件列表。


React的组件拥有一套清晰完整而且非常容易理解的生命周期机制,大体可以分为三个过程:初始化、更新和销毁,在组件生命周期中,随着组件的props或者state发生改变,它的虚拟DOM和DOM表现也将有相应的变化。


首先是初始化过程,这里会着重讲,需要充分理解。


组件类在声明时,会先调用 getDefaultProps() 方法来获取默认props值,这个方法会且只会在声明组件类时调用一次,这一点需要注意,它返回的默认props由所有实例共享。


在组件被实例化之前,会先调用一次实例方法 getInitialState() 方法,用于获取这个组件的初始state。


实例化之后就是渲染,componentWillMount方法会在生成虚拟DOM之前被调用,你可以在这里对组件的渲染做一些准备工作,比如计算目标容器尺寸然后修改组件自身的尺寸以适应目标容器等等。


接下来就是渲染工作,在这里你会创建一个虚拟DOM用来表示组件的结构。对于一个组件来说,render 是唯一一个必须的方法。render方法需要满足这几点:


1.只能通过 this.props 或 this.state 访问数据


2.只能出现一个顶级组件


3.可以返回 null、false 或任何 React 组件


4.不能对 props、state 或 DOM 进行修改


需要注意的是,render 方法返回的是虚拟DOM。

渲染完成以后,我们可能需要对DOM做一些操作,比如截屏、上报日志、或者初始化iScroll等第三方非React插件,可以在 componentDidMount() 方法中做这些事情。当然,你也可以在这个方法里通过 this.getDOMNode() 方法取得最终生成DOM节点,然后对DOM节点做爱做的事情,但需要注意做好安全措施,不要缓存已经生成的DOM节点,因为这些DOM节点随时可能被替换掉,所以应该在每次用的时候去读取。

组件被初始化完成后,它的状态会随着用户的操作、时间的推移、数据更新而产生变化,变化的过程是组件声明周期的另一部分 ——


更新过程。

当组件已经被实例化后,使用者调用 setProps() 方法修改组件的数据时,组件的 componentWillReceiveProps() 方法会被调用,在这里,你可以对外部传入的数据进行一些预处理,比如从props中读取数据写入state。

默认情况下,使用者调用组件的 setProps() 方法后,React会遍历这个组件的所有子组件,进行“灌水”,将props从上到下一层一层传下去,并逐个执行更新操作,虽然React内部已经进行过很多的优化,这个过程并不会花费多少时间,但是程序员里永远不缺乏长期性能饥渴的同学,不用担心,React有一个能够解决你性能饥渴的办法——shouldComponentUpdate()。


有时候,props发生了变化,但组件和子组件并不会因为这个props的变化而发生变化,打个比方,你有一个表单组件,你想要修改表单的name,同时你能够确信这个name不会对组件的渲染产生任何影响,那么你可以直接在这个方法里return false来终止后续行为。这样就能够避免无效的虚拟DOM对比了,对性能会有明显提升。


如果这个时候有同学仍然饥渴难耐,那么你可以尝试 不可变数据结构(用过mongodb的同学应该懂)。


组件在更新前,React会执行componentWillUpdate() 方法,这个方法类似于前面看到的 componentWillMount()方法,唯一不同的地方只是这个方法在执行的时候组件是已经渲染过的。需要注意的是,不可以在这个方法中修改props或state,如果要修改,应当在 componentWillReceiveProps() 中修改。


然后是渲染,React会拿这次返回的虚拟DOM和缓存中的虚拟DOM进行对比,找出【最小修改点】,然后替换。


更新完成后,React会调用组件的componentDidUpdate 方法,这个方法类似于前面 componentDidMount 方法,你仍然可以在这里可以通过 this.getDOMNode() 方法取得最终的DOM节点。

香港电影结尾经常看到一个剧情,就是英雄打败了坏人,然后警察出来擦屁股——


警察偶尔还能立功,而 componentWillUnmount 最可怜,他除了擦屁股什么也做不了。


你可以在这个方法中销毁非React组件注册的事件、插入的节点,或者一些定时器之类。这个过程可能容易出错,比如绑定了事件却没销毁,这个React可帮不了你,你自己约的炮,含着泪也要打完。




两节内容讲了上手React所必备的知识。


后面讲价值。


直出有多快我就不多说了。


因为有虚拟DOM的存在,React可以很容易的将虚拟DOM转换为字符串,这便使我们可以只写一份UI代码,同时运行在node里和和浏览器里。


在node里将组件HTML渲染为一段HTML一句话即可。


不过围绕这个renderToString我们还要做一些准备工作。代码有点多,大家做好心理准备。


这是一个express的路由方法,在这里:


1.从后台server或数据库等来源拉取数据


2.引入要渲染的React组件


3.调用React.renderToString()方法来生成HTML


4.最后发送HTML和数据给浏览器

这里为了方便描述,没有写拉取数据的代码,大家自行脑补。

需要注意的是这里的JSON字符串中可能出现结尾标签或HTML注释,可能会导致语法错误,这里需要进行转义。

页面的示例代码本来打算用大家更熟悉的HTML,但发现代码量太多了PPT里一页放不下,所以换成了jade代码,没用过jade的同学也顺便了解一下,我也顺便给jade打个广告。


这个页面做了X个事:


1.将前面在action里生成的HTML写到#container元素里


2.引入必须的JS文件


3.获取action提供的数据


4.渲染组件

这就是React的服务端渲染,组件的代码前后端都可以复用。


<有没有没理解清楚的同学?>

是不是感觉React挺牛逼的?大家以为React就这么点能耐吗?





React能够用一套代码同时运行在浏览器和node里,而且能够以原生App的姿势运行在iOS和Android系统中,即拥有了web迭代迅速的特性,又拥有原生App的体验。


这个姿势叫做 React-Native。


这是React和React-Native在github上的数据,可以看出React-Native也是相当热门——因为React-Native能够使React的价值最大化,这个价值是什么呢——对业务来说,意味着不需要为了做终端版本就招聘和前端等量人力的终端开发,同时意味着我们成为全栈工程师有了一个捷径。


了解iOS开发的同学都知道,水果公司对应用上架的审核效率实在让人无力吐槽,很多团队上一个版本还没审核结束,下一个版本就已经做好了。而React-Native支持从网络拉取JS,这样iOS应用也能够像web一样实现快速迭代了。


这个是react-native的调试过程

作为一个没写过一句Object-C代码的web前端开发,我只用了一天时间就上手了react-native,然后用了半天时间做出了一个简单的demo页面,可以看到react-native的生产效率还是非常高的。

单元测试顾名思义,是对各个模块进行最小范围的测试,容易。


我们来演示一个checkbox的单元测试过程。


看代码

因为虚拟DOM的存在,使得react的代码很容易做好单元测试,这是上面那段代码的测试用例,通过karma执行后即可看到结果。

所以你可能需要这些东西





课后练习


(如果你已经看到这里了,为何不再花1分钟思考一下上面3个问题)