如何在React中做到jQuery-free

简介:


前言

前些天在订阅的公众号中看到了以前阮一峰老师写过的一篇文章,「如何做到 jQuery-free?」。这篇文章讨论的问题,在今天来看仍不过时,其中的一些点的讨论主要是面向新内核现代浏览器的标准 DOM API,很可惜的是在目前的开发环境下,我们仍然无法完全抛弃 IE,大部分情况下我们至少还要兼容到 IE 8,这一点使我们无法充分践行文章中提到的一些点,而本文也正是首次启发,顺着阮老师文章的思路来讨论如何在 React 中实战 IE8-compatible 的 jQuery-free。

首先我们仍要说的是,jQuery 是现在最流行的 JavaScript 工具库。在 W3techs 的统计中,目前全世界 70.6% 的网站在使用他,而 React 甚至还不到 0.1%,但 React 一个值得注意的趋势是,他在目前顶级流量网站中的使用率是最高的,比例达到了 16%。这一趋势也表明了目前整个前端界的技术趋势,但 70.6% 的数字也在告诉我们,jQuery 在 JS 库中的王者地位,即使使用了React,也可能因为各种各样的原因,还要和 jQuery 来配合使用。但 React 本身的体积已经让我们对任何一个重库产生了不适反应,为了兼容 IE8,我们仍然需要使用 1.x 的 jQuery 版本,但当时设计上的缺陷使得我们无法像 lodash 那样按需获取。而 React 和 jsx 的强大,又使得我们不需要了 jQuery 的大部分功能。从这个角度来看,他臃肿的体积让开发者更加难以忍受,jQuery-free 势在必行。

下面就顺着阮老师当年的思路,来讨论如何使用 React 自带的强大功能,和一些良心第三方库屏蔽兼容性,来取代 jQuery 的主要功能,做到 jQuery-free。

(注:React 15.x 版本已经不再兼容 IE8,因此本文讨论的 React 仍是 0.14.x 的版本,同时为了易于理解,本文也基本上以 ES6 class 的方式来声明组件,而不采用 pure function。)

一、选取 DOM 元素

在 jQuery 中,我们已经熟悉了使用 sizzle 选择器来完成 DOM 元素的选取。而在 React 中,我们可以使用 ref 来更有针对性的获取元素。

 
  1. import React from 'react'
  2. class Demo extends React.Compoent { 
  3.  
  4.     getDomNode() { 
  5.         return this.refs.root; // 获取 Dom Node 
  6.     } 
  7.     render() { 
  8.         return ( 
  9.             <div ref="root">just a demo</div> 
  10.         ); 
  11.     } 

这是最简单的获取 node 的方式,如果有多层结构嵌套呢?没有关系。

 
  1. import React from 'react'
  2. class Demo extends React.Compoent { 
  3.  
  4.     getRootNode() { 
  5.         return this.refs.root; // 获取根节点 Dom Node 
  6.     } 
  7.     getLeafNode() { 
  8.         return this.refs.leaf; // 获取叶节点 Dom Node 
  9.     } 
  10.     render() { 
  11.         return ( 
  12.             <div ref="root"
  13.                 <div ref="leaf">just a demo</div> 
  14.             </div> 
  15.         ); 
  16.     } 

如果是组件和组件嵌套呢?也没关系,父组件仍然可以拿到子组件的根节点。

 
  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3. class Sub extends React.Compoent { 
  4.     render() { 
  5.         return ( 
  6.             <div>a sub component</div> 
  7.         ); 
  8.     } 
  9. class Demo extends React.Compoent { 
  10.  
  11.     getDomNode() { 
  12.         return this.refs.root; // 获取 Dom Node 
  13.     } 
  14.      
  15.     getSubNode() { 
  16.         return ReactDOM.findDOMNode(this.refs.sub); // 获取子组件根节点 
  17.     } 
  18.     render() { 
  19.         return ( 
  20.             <div ref="root"
  21.                 <Sub ref="sub" /> 
  22.             </div> 
  23.         ); 
  24.     } 

上面使用了比较易懂的 API 来解释 Ref 的用法,但里面包含了一些现在 React 不太推荐和即将废弃的方法,如果用 React 推荐的写法,我们可以这样写。

 
  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3. class Sub extends React.Compoent { 
  4.     getDomNode() { 
  5.         return this.rootNode; 
  6.     } 
  7.     render() { 
  8.         return ( 
  9.             <div ref={(c) => this.rootNode = c}>a sub component</div> 
  10.         ); 
  11.     } 
  12. class Demo extends React.Compoent { 
  13.  
  14.     getDomNode() { 
  15.         return this.rootNode; // 获取 Dom Node 
  16.     } 
  17.      
  18.     getSubNode() { 
  19.         return this.sub.getDomNode(); // 获取子组件根节点 
  20.     } 
  21.     render() { 
  22.         return ( 
  23.             <div ref={(c) => this.rootNode = c}> 
  24.                 <Sub ref={(c) => this.sub = c} /> 
  25.             </div> 
  26.         ); 
  27.     } 

有人可能会问,那子组件怎么拿父组件的 Dom Node 呢,从 React 的单向数据流角度出发,遇到这种情况我们应该通过回调通知给父组件,再由父组件自行判断如何修改 Node,其实父组件拿子组件的 Node 情况也很少,大多数情况下我们是通过 props 传递变化给子组件,获取子组件 Node,更多的情况下是为了避开大量重新渲染去修改一些Node的属性(比如 scrollLeft)。

二、DOM 操作

jQuery 中提供了丰富的操作方法,但一个个操作 DOM 元素有的时候真的很烦人并且容易出错。React 通过数据驱动的思想,通过改变 view 对应的数据,轻松实现 DOM 的增删操作。

 
  1. class Demo extends React.Compoent { 
  2.     constructor(props) { 
  3.         super(props); 
  4.         this.state = { 
  5.             list: [1, 2, 3], 
  6.         }; 
  7.         this.addItemFromBottom = this.addItemFromBottom.bind(this); 
  8.         this.addItemFromTop = this.addItemFromTop.bind(this); 
  9.         this.deleteItem = this.deleteItem.bind(this); 
  10.     } 
  11.      
  12.     addItemFromBottom() { 
  13.         this.setState({ 
  14.             list: this.state.list.concat([4]), 
  15.         }); 
  16.     } 
  17.      
  18.     addItemFromTop() { 
  19.         this.setState({ 
  20.             list: [0].concat(this.state.list), 
  21.         }); 
  22.     } 
  23.      
  24.     deleteItem() { 
  25.         const newList = [...this.state.list]; 
  26.         newList.pop(); 
  27.         this.setState({ 
  28.             list: newList, 
  29.         }); 
  30.     } 
  31.      
  32.     render() { 
  33.         return ( 
  34.             <div> 
  35.                 {this.state.list.map((item) => <div>{item}</div>)} 
  36.                 <button onClick={this.addItemFromBottom}>尾部插入 Dom 元素</button> 
  37.                 <button onClick={this.addItemFromTop}>头部插入 Dom 元素</button> 
  38.                 <button onClick={this.deleteItem}>删除 Dom 元素</button> 
  39.             </div> 
  40.         ); 
  41.     } 

三、事件的监听

React 通过根节点代理的方式,实现了一套很优雅的事件监听方案,在组件 unmount 时也不需要自己去处理内存回收相关的问题,非常的方便。

 
  1. import React from 'react'
  2. class Demo extends React.Component { 
  3.     constructor(props) { 
  4.         super(props); 
  5.         this.handleClick = this.handleClick.bind(this); 
  6.     } 
  7.     handleClick() { 
  8.         alert('我是弹窗'); 
  9.     } 
  10.     render() { 
  11.         return ( 
  12.             <div onClick={this.handleClick}>点击我弹出弹框</div> 
  13.         ); 
  14.     } 

这里有一个小细节就是 bind 的时机,bind 是为了保持相应函数的上下文,虽然也可以在 onClick 那里 bind,但这里选择在 constructor 里 bind 是因为前者会在每次 render 的时候都进行一次 bind,返回一个新函数,是比较消耗性能的做法。

但 React 的事件监听,毕竟只能监听至 root component,而我们在很多时候要去监听 window/document 上的事件,如果 resize、scroll,还有一些 React 处理不好的事件,比如 scroll,这些都需要我们自己来解决。事件监听为了屏蔽差异性需要做很多的工作,这里像大家推荐一个第三方库来完成这部分的工作,add-dom-event-listener,用法和原生的稍有区别,是因为这个库并不旨在做 polyfill,但用法还是很简单。

 
  1. var addEventListener = require('add-dom-event-listener'); 
  2. var handler = addEventListener(document.body, 'click'function(e){ 
  3.   console.log(e.target); // works for ie 
  4.   console.log(e.nativeEvent); // native dom event 
  5. }); 
  6. handler.remove(); // detach event listener 

另一个选择是 bean,达到了 IE6+ 级别的兼容性。

四、事件的触发

和事件监听一样,无论是 Dom 事件还是自定义事件,都有很优秀的第三方库帮我们去处理,如果是 DOM 事件,推荐 bean,如果是自定义事件的话,推荐 PubSubJS。

五、document.ready

React 作为一个 view 层框架,通常情况下页面只有一个用于渲染 React 页面组件的根节点 div,因此 document.ready,只需把脚本放在这个 div 后面执行即可。而对于渲染完成后的回调,我们可以使用 React 提供的 componentDidMount 生命周期。

 
  1. import React from 'react'
  2. class Demo extends React.Component { 
  3.     constructor(props) { 
  4.         super(props); 
  5.     } 
  6.      
  7.     componentDidMount() { 
  8.         doSomethingAfterRender(); // 在组件渲染完成后执行一些操作,如远程获取数据,检测 DOM 变化等。 
  9.     } 
  10.     render() { 
  11.         return ( 
  12.             <div>just a demo</div> 
  13.         ); 
  14.     } 

六、attr 方法

jQuery 使用 attr 方法,获取 Dom 元素的属性。在 React 中也可以配合 Ref 直接读取 DOM 元素的属性。

 
  1. import React from 'react'
  2. class Demo extends React.Component { 
  3.     constructor(props) { 
  4.         super(props); 
  5.     } 
  6.      
  7.     componentDidMount() { 
  8.         this.rootNode.scrollLeft = 10; // 渲染后将外层的滚动调至 10px 
  9.     } 
  10.     render() { 
  11.         return ( 
  12.             <div  
  13.                 ref={(c) => this.rootNode = c}  
  14.                 style={{ width: '100px', overflow: 'auto' }} 
  15.             >  
  16.                 <div style={{ width: '1000px' }}>just a demo</div> 
  17.             </div> 
  18.         ); 
  19.     } 

但是,在大部分的情况下,我们完全不需要做,因为 React 的单向数据流和数据驱动渲染,我们可以不通过 DOM,轻松拿到和修改大部分我们需要的 DOM 属性。

 
  1. import React from 'react'
  2. class Demo extends React.Component { 
  3.     constructor(props) { 
  4.         super(props); 
  5.         this.state = { 
  6.             link: '//www.taobao.com'
  7.         }; 
  8.         this.getLink = this.getLink.bind(this); 
  9.         this.editLink = this.editLink.bind(this); 
  10.     } 
  11.      
  12.     getLink() { 
  13.         alert(this.state.link); 
  14.     } 
  15.      
  16.     editLink() { 
  17.         this.setState({ 
  18.             link: '//www.tmall.com'
  19.         }); 
  20.     } 
  21.      
  22.     render() { 
  23.         return ( 
  24.             <div> 
  25.                 <a href={this.state.link}>跳转链接</a> 
  26.                 <button onClick={this.getLink}>获取链接</button> 
  27.                 <button onClick={this.editLink}>修改链接</button> 
  28.             </div> 
  29.         ); 
  30.     } 
  31.      

七、addClass/removeClass/toggleClass

在 jQuery 的时代,我们通常靠获取 Dom 元素后,再 addClass/removeClass 来改变外观。在 React 中通过数据驱动和第三库classnames 修改样式从未如此轻松。

 
  1. .fn-show { 
  2.     display: block; 
  3. .fn-hide { 
  4.     display: none; 
  5. }
 
  1. import React from 'react'
  2. import classnames from 'classnames'
  3. class Demo extends React.Component { 
  4.     constructor(props) { 
  5.         super(props); 
  6.         this.state = { 
  7.             show: true
  8.         }; 
  9.         this.changeShow = this.changeShow.bind(this); 
  10.     } 
  11.      
  12.     changeShow() { 
  13.         this.setState({ 
  14.             show: !this.state.show,  
  15.         }); 
  16.     } 
  17.      
  18.     render() { 
  19.         return ( 
  20.             <div> 
  21.                 <a  
  22.                     href="//www.taobao.com"  
  23.                     className={classnames({ 
  24.                         'fn-show': this.state.show, 
  25.                         'fn-hide': !this.state.show, 
  26.                     })} 
  27.                 > 
  28.                     跳转链接 
  29.                 </a> 
  30.                 <button onClick={this.changeShow}>改变现实状态</button> 
  31.             </div> 
  32.         ); 
  33.     } 
  34.      

八、css

jQuery 的 css 方法用于设置 DOM 元素的 style 属性,在 React 中,我们可以直接设置 DOM 的 style 属性,如果想改变,和上面的 class 一样,用数据去驱动。

 
  1. import React from 'react'
  2. class Demo extends React.Component { 
  3.     constructor() { 
  4.         super(props); 
  5.         this.state = { 
  6.             backgorund: 'white'
  7.         }; 
  8.         this.handleClick = this.handleClick.bind(this); 
  9.     } 
  10.      
  11.     handleClick() { 
  12.         this.setState({ 
  13.             background: 'black'
  14.         }); 
  15.     } 
  16.      
  17.     render() { 
  18.         return ( 
  19.             <div  
  20.                 style={{ 
  21.                     background: this.state.background, 
  22.                 }} 
  23.             > 
  24.                 just a demo 
  25.                 <button>change Background Color</button> 
  26.             </div> 
  27.         ); 
  28.     } 

九、数据存储

比起 jQuery,React 反而是更擅长管理数据,我们没有必要像 jQuery 时那样将数据放进 Dom 元素的属性里,而是利用 state 或者 内部变量(this.xxx) 来保存,在整个生命周期,我们都可以拿到这些数据进行比较和修改。

十、Ajax

Ajax 确实是在处理兼容性问题上一块令人比较头疼的地方,要兼容各种形式的 Xhr 不说,还有 jsonp 这个不属于 ajax 的功能也要同时考虑,好在已经有了很好的第三方库帮我们解决了这个问题,这里向大家推荐 natty-fetch,一个兼容 IE8 的fetch 库,在 API 设计上向 fetch 标准靠近,而又保留了和 jQuery 类似的接口,熟悉 $.ajax 应该可以很快的上手。

十一、动画

React 在动画方面提供了一个插件 ReactCSSTransitionGroup,和它的低级版本 ReactTransitionGroup,注意这里的低级并不是退化版本,而是更加基础的暴露更多 API 的版本。

这个插件的灵感来自于 Angular 的 ng-animate,在设计思路上也基本保持一致。通过指定 Transition 的类名,比如 example ,在元素进场和退场的时候分别加上对应的类名,以实现 CSS3 动画。例如本例中,进场会添加 example-enter 和 example-enter-active到对应的元素 ,而在退场 example-leave 和 example-leave-active 类名。当然你也可以指定不同的进场退场类名。而对应入场,React 也区分了两种类型,一种是 ReactCSSTransitionGroup 第一次渲染时(appear),而另一种是 ReactCSSTransitionGroup 已经渲染完成后,有新的元素插入进来(enter),这两种进场可以使用 prop 进行单独配置,禁止或者修改超时时长。具体的例子,在上面给出的链接中有详细的例子和说明,因此本文不再赘述。

但这个插件最多只提供了做动画的方案,如果我想在动画进行的过程中做一些其他事情呢?他就无能为力了,这时候就轮到 ReactTransitionGroup 出场了。ReactTransitionGroup 为他包裹的动画元素提供了六种新的生命周期:componentWillAppear(callback), componentDidAppear(), componentWillEnter(callback), componentDidEnter(),componentWillLeave(callback), componentDidLeave()。这些 hook 可以帮助我们完成一些随着动画进行需要做的其他事。

但官方提供的插件有一个不足点,动画只是在进场和出场时进行的,如果我的组件不是 mount/unmount,而只是隐藏和显示怎么办?这里推荐一个第三方库:rc-animate,从 API 设计上他基本上是延续了 ReactCSSTransitionGroup 的思路,但是通过引入 showProp这一属性,使他可以 handle 组件显示隐藏这一情况下的出入场动画(只要将组件关于 show/hide 的属性传给 showProp 即可),同时这个库也提供自己的 hook,来实现 appear/enter/leave 时的回调。

如果你说我并不满足只是进场和出场动画,我要实现类似鼠标拖动时的实时动画,我需要的是一个 js 动画库,这里向大家推荐一个第三方库:react-motion , react-motion 一个很大的特点是,有别以往使用贝塞尔曲线来定义动画节奏,引入了刚度和阻尼这些弹簧系数来定义动画,按照作者的说法,与其纠结动画时长和很难掌握的贝塞尔表示法,通过不断调整刚度和阻尼来调试出最想要的弹性效果才是最合理的。Readme 里提供了一系列的很炫的动画效果,比如这个 draggable list。Motion 通过指定 defaultStyle、style,传回给子组件正在变化中的 style,从而实现 js 动画。

 
  1. <Motion defaultStyle={{x: 0}} style={{x: spring(10)}}> 
  2.   {interpolatingStyle => <div style={interpolatingStyle} />} 
  3. </Motion> 

总结

本文的灵感来源于阮老师两年前的文章,这篇的实战意义更大于对未来技术的展望,希望能够给各位正在使用 React 开发系统的同学们一点启发。


作者:紅白

来源:51CTO

相关文章
|
3月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
92 0
|
3月前
|
存储 前端开发 JavaScript
【第34期】一文学会React组件传值
【第34期】一文学会React组件传值
31 0
|
3月前
|
前端开发
【第31期】一文学会用React Hooks组件编写组件
【第31期】一文学会用React Hooks组件编写组件
34 0
|
3月前
|
存储 前端开发 JavaScript
【第29期】一文学会用React类组件编写组件
【第29期】一文学会用React类组件编写组件
30 0
|
3月前
|
前端开发 开发者
【第26期】一文读懂React组件编写方式
【第26期】一文读懂React组件编写方式
28 0
|
3月前
|
资源调度 前端开发 JavaScript
React 的antd-mobile 组件库,嵌套路由
React 的antd-mobile 组件库,嵌套路由
40 0
|
2月前
|
存储 前端开发 中间件
React组件间的通信
React组件间的通信
15 1
|
2月前
|
前端开发 应用服务中间件 数据库
react服务端组件
react服务端组件
21 0
|
2月前
|
前端开发 JavaScript
快速上手React:从概述到组件与事件处理
快速上手React:从概述到组件与事件处理
|
3月前
|
前端开发 JavaScript API
React组件生命周期
React组件生命周期
74 1