React 事件绑定大概有以下几种,先给最终源码再逐步解析。
bind 方式
1 class Profile extends React.Component<{ name: string, age: number }, { linkd: number }> { 2 constructor(props: any) { 3 super(props); 4 this.state = { 5 linkd: 0 6 } 7 this.onLiked = this.onLiked.bind(this); 8 } 9 render() { 10 return ( 11 <div > 12 <h2 onClick={this.onLiked} >给我点赞</h2> 13 <h1>总点赞数 {this.state.linkd}</h1> 14 </div > 15 ); 16 } 17 onLiked() { 18 19 let liked = this.state.linkd; 20 21 liked++; 22 23 this.setState({ 24 linkd: liked 25 }); 26 } 27 }
第一步:定义事件处理方法
onLiked() { let liked = this.state.linkd; liked++; this.setState({ linkd: liked }); }
第二步:为h2标签绑定点击事件并指定回掉
<h2 onClick={this.onLiked} >给我点赞</h2>
第三布:在构造函数中为 onLiked 方法指定上下文对象
this.onLiked = this.onLiked.bind(this);
解析:
这是最为常规的处理方式,我们通过调用 bind 方法。
第一步,为当前实例 Profile 组件创建自有属性 onLiked 方法。也就是将原型链的 onLiked 指针给了一份给当前自有属性,bind() 函数会创建一个新函数(称为绑定函数)为 onLiked 方法指定当前上下文对象,保证 onLiked 函数中 this 不会丢失。理论上来说我们定义事件处理函数是作为 Profile 组件的原型链上因此 onLiked 方法中 this 的指向应该是 Profile 组件的实例,并且在 TypeScript 的静态类型检测中也不会有编译问题,但是我们运行发现如果仅做到前两步会报类型错误
Uncaught TypeError: Cannot read property 'state' of undefined
当我们查看 React 源码最终触发事件的方法时会发现
var invokeGuardedCallback = function (name, func, context, a, b, c, d, e, f) { this._hasCaughtError = false; this._caughtError = null; var funcArgs = Array.prototype.slice.call(arguments, 3); try { func.apply(context, funcArgs); } catch (error) { this._caughtError = error; this._hasCaughtError = true; } };
我们定义的事件处理函数 func 的指针被指向 context 对象,那么 context 对象是什么呢,通过找到 context 来源
function executeDispatch(event, simulated, listener, inst) { var type = event.type || 'unknown-event'; event.currentTarget = getNodeFromInstance(inst); ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); event.currentTarget = null; }
优点:
仅会给 onLiked 指定一次
render 方法渲染中,不会因为回掉处理函数指向的改变而进行多余的渲染
缺点:
每次定义回掉函数都需要在构造函数中指定指针
JSX 内嵌箭头函数方式
1 class Profile extends React.Component<{ name: string, age: number }, { linkd: number }> { 2 constructor(props: any) { 3 super(props); 4 this.state = { 5 linkd: 0 6 } 7 } 8 render() { 9 return ( 10 <div > 11 <h2 onClick={() => { this.onLiked() }} >给我点赞</h2> 12 <h1>总点赞数 {this.state.linkd}</h1> 13 </div > 14 ); 15 } 16 onLiked() { 17 18 let liked = this.state.linkd; 19 20 liked++; 21 22 this.setState({ 23 linkd: liked 24 }); 25 } 26 }
第一步:与 bind 方式一样定义事件处理方法
onLiked() { let liked = this.state.linkd; liked++; this.setState({ linkd: liked }); }
第二步:为 h2 标签绑定点击事件并指定回掉,不过这里我们在调用之前再嵌套一层箭头函数且回掉方法带上()执行语法
render() { return ( <div > <h2 onClick={() => { this.onLiked() }} >给我点赞</h2> <h1>总点赞数 {this.state.linkd}</h1> </div > ); }
对比一下最终生成 javascript 代码
Profile.prototype.render = function () { var _this = this; return (React.createElement("div", null, React.createElement("h2", { onClick: function () { _this.onLiked(); } }, "u7ED9u6211u70B9u8D5E"), React.createElement("h1", null, "u603Bu70B9u8D5Eu6570 ", this.state.linkd))); };
解析:
相比于 bind 方式,嵌套箭头函数避免了在构造函数中指定指针步骤,最终 React 调用回掉时的空指向这里不会有影响,由于变量函数作用域特性,每次 render 执行时都会为保存当前 Profile 对象指针,以此来保证 this 正确指向。但是缺点也很明显,React 在将虚拟的 DOM 对应生成真实 DOM 节点之前, React 会将虚拟的 DOM 树与先前的进行比较(Diff),计算出变化部分,再将变化部分作用到真实 DOM 上,实现最终界面的更新。方法在每次调用时,方法在定义时会保存一个作用域链,在方法每次调用时会创建一个全新的对象将该方法内局部变量作为属性保存到新对象并把新对象保存到最初作用域链上。因此每次 render 方法调用时都创建了一个局部变量来保存 this 指针。对于虚拟DOM来说,每次都是新的变量因此会进行不必要的渲染。
优点:
不需要每次在构造函数中指定
缺点:
没必要的性能损耗
作为属性的箭头函数方法
class Profile extends React.Component<{ name: string, age: number }, { linkd: number }> { constructor(props: any) { super(props); this.state = { linkd: 0 } } render() { return ( <div > <h2 onClick={this.onLiked} >给我点赞</h2> <h1>总点赞数 {this.state.linkd}</h1> </div > ); } onLiked = () => { let liked = this.state.linkd; liked++; this.setState({ linkd: liked }); } }
第一步:定义事件处理方法
onLiked = () => { let liked = this.state.linkd; liked++; this.setState({ linkd: liked }); }
第二步:为 h2 标签绑定点击事件并指定回掉
render() { return ( <div > <h2 onClick={this.onLiked} >给我点赞</h2> <h1>总点赞数 {this.state.linkd}</h1> </div > ); }
解析:
作为给予 TypeScript 或者 Babel 开发环境来说。这最简介且集合 bind 方式不会重复渲染等优点,同时也不必像 bind 一样每次去定义
优点:
简洁,同时保证执行效率额
缺点:
需要通过编译器或支持 ES6 的运行环境
注意:常规事件回掉处理中 context 指向对象为 undefined 但是由于 react 源码调用错综复杂,也不能排除在特殊场景中会为回掉方法指定非空的上下文对象。但是目前还没有碰到这种场景。