Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

带你找出react中,回调函数绑定this最完美的写法! #16

Open
flytam opened this issue Mar 9, 2020 · 0 comments
Open
Labels
react New feature or request

Comments

@flytam
Copy link
Owner

flytam commented Mar 9, 2020

带你找出react中,回调函数绑定this最完美的写法!(文末 有惊喜)

相信每一个人写过react的人都对react组件的的this绑定有或多或少的了解

在我看来,有若干种this写法,我们通过本文,一步步找优缺点,筛选出最完美的react this写法!(有点小激动)

1、远古时代 React.createClass

说实话,在我接触react的时候,这种写法就只在相关文章见到了。React.createClass会自动绑定所有函数的this到组件上

React.createClass({
    fn() {
        //  this 指向组件本身
        console.log(this);
    },
    render() {
        return <div onClick={this.fn}></div>;
    }
});

react 0.13开始就已经支持class声明组件了。react 16已经废弃了这种写法,这里就不讨论了。直接淘汰

2、错误示范

class App extends React.Component {
    fn() {
        console.log(this);
    }
    render() {
        return <div onClick={this.fn}></div>;
    }
}

这种写法,最终打印this是指向undefined。原因在于上面的事件绑定函数调用可以看作如下。

// 伪代码
onClick = app.fn;
onClick();

onClick进行调用时,this的上下文是全局,由于是在es module中,全局this指向undefined,所以这个错误示范的事件处理函数中的this不是指向组件本身的

3、利用proposal-class-public-fields直接绑定箭头函数

class App extends React.Component {
    fn = () => {
        console.log(this);
    };
    render() {
        return <div onClick={this.fn}></div>;
    }
}

目前proposal-class-public-fields仍处于提案阶段,需要借助@babel/plugin-proposal-class-properties这个 babel 插件在浏览器中才能正常工作

经过babel转换,等价于以下的代码

class App extends React.Component {
    constructor(props) {
        super(props);
        this.fn = () => {
            console.log(this);
        };
    }
    render() {
        return <div onClick={this.fn}></div>;
    }
}

可以看出,32从最大的区别在于,3fn直接绑定在实例的属性上(2是绑定在原型的方法上),并利用箭头函数继承父级this作用域达到了this绑定的效果。

优点:代码十分简洁,不需要手动写bind、也不需要在constructor中进行额外的操作

缺点:很多文章都提到这是一种完美写法,但其实每一个实例在初始化的时候都会新建一个新事件回调函数(因为绑定在实例的属性上,每个实例都有一个fn的方法。本质上,这是一种重复浪费),所以其实并不是很完美

4、Constructor中使用 bind

class App extends React.Component {
    constructor(props) {
        super(props);
        this.fn = this.fn.bind(this);
    }
    fn() {
        console.log(this);
    }
    render() {
        return <div onClick={this.fn}></div>;
    }
}

优点:几乎等价于3

缺点:代码写起来比较繁琐,需要在constructor中,手动绑定每一个回调函数

5、在render中进行bind绑定

class App extends React.Component {
    fn() {
        console.log(this);
    }
    render() {
        return <div onClick={this.fn.bind(this)}></div>;
    }
}

优点:fn函数多次实例化只生成一次,存在类的属性上,类似于4,写法上比4稍微好一点。

缺点:this.fn.bind(this)会导致每次渲染都是一个全新的函数,在使用了组件依赖属性进行比较、pureComponent、函数组件React.memo的时候会失效。

最关键的是5的写法会被6全方面吊打完爆

6、箭头函数内联写法

class App extends React.Component {
    fn() {
        console.log(this);
    }
    render() {
        return <div onClick={() => fn()}></div>;
    }
}

优点:

1、写法简洁

2、与2-5的写法相比,6写法最大的最大好处就是传参灵活

3、全面吊打写法4,相同的缺点,但是多了传参数灵活。如果需要渲染一个数组,并且数组根据不同项,事件处理不一样时,2-5就很尴尬了

const arr = [1, 2, 3, 4, 5];
class App extends React.Component {
    fn(val) {
        console.log(val);
    }
    render() {
        return (
            <div>
                {arr.map(item => (
                    // 采用 6的写法,要打印数组这一项就很方便
                    <button onClick={() => this.fn(item)}>{item}</button>
                ))}
            </div>
        );
    }
}

网上看多文章都在使用3的方案的时候推荐使用闭包传参实现该效果

const arr = ["1", "2", "3", "4", "5"];
class App extends React.Component {
    fn = val => () => {
        console.log(val);
    };
    render() {
        return (
            <div>
                {arr.map(item => (
                    // 每次也生成了全新的函数了
                    <button onClick={this.fn(item)}>{item}</button>
                ))}
            </div>
        );
    }
}

经过前面的分析。使用这种写法,还不如直接使用6的内联写法,两种每次都是返回全新的函数,而且,少了一次返回闭包函数的开销。

缺点: 每次渲染都是一个全新的函数,类似于5的缺点,在使用了组件依赖属性进行比较、pureComponent、函数组件React.memo的时候会失效

7、函数组件的useCallback

虽然函数组件无this一说法,但既然讲到react回调函数,还是提一下

hook出现之前,函数组件是不能保证每次的回调函数都是同一个的,(虽然可以把回调提到函数作用域外固定,但都是一些 hack 的方法了)

const App = () => {
    // 每次都是全新的
    return <div onClick={() => console.log(2333)}></div>;
};

有了hook。我们便可以使用useCallback固定住回调

const App = () => {
    const fn = useCallback(() => console.log(2333), []);
    // 每次都是固定
    return <div onClick={fn}></div>;
};

有没有发现。其实很类似class组件的将回调挂在class上,嗯,这就hook强大的地方,利用了react fiber,挂在了它的memorizeState上,实现了能在多次渲染中保持(这就不展开讲了)。缺点还是和上面提过的,参数传递不方便,如渲染数组

8、(最完美)的写法?

当然,如果不使用内联写法又获取到参数行不行呢。当然也是可以的,利用元素的自定义属性data-属性传递参数

const arr = ["1", "2", "3", "4", "5"];
class App extends React.Component {
    constructor(props) {
        super(props);
        this.fn = this.fn.bind(this);
    }
    fn(e) {
        // 1 2 3 4 5
        console.log(e.target.dataset.val);
    }
    render() {
        return (
            <div>
                {arr.map(item => (
                    // 每次也生成了全新的函数了
                    <button data-value={item} onClick={this.fn}>
                        {item}
                    </button>
                ))}
            </div>
        );
    }
}

orz! 这是最完美写法了吧!不考虑代码繁琐的情况下,既正确绑定了this,又不会多次实例化函数,又能渲染数组。。

其实还是错误的...data-xxx属性只能传递string类型的数据,因为是附加给html的,react会进行一步JSON.stringify的操作,如果你传递一个对象,打印出来是value: "[object Object]"。果然,就算是为了获取字符串参数,也不推荐这种写法。可以,但没必要!

9、最省事的写法?

有一位大佬写了一个 babel 插件babel-plugin-react-scope-binding的插件,能够实现 将2的错误示范自动转化内联函数,更牛逼的是还能传参。介绍。确实是最省事的写法,不过很容易引起歧义,也有上面提到的问题

好吧,感谢你看到这里,废话连篇一篇文章,其实似乎并没有找回完美的写法。。。

下面说说本人的一些愚见吧

在平时写代码中,在render没有非常大的开销情况下(也没有依赖组件的某些属性进行性能优化、没使用 pureComponent), 会优先使用纯内联的写法(无论是函数组件还是 class 组件)。因为重新创建函数开销我觉得不是特别大的,并且内联我觉得还有最大的好处就是,看到一个事件调用,不需要再点到事件函数调用的地方...减少了飞来飞去的情况,而且上面也提到,内联传递参数是非常方便的。在实在遇到性能问题,再考虑优化。无需为了优化而优化

最近春招季,看完这篇文章,虽然还是找不出最完美的react绑定事件写法,但是面试官提起react绑定事件的几种区别时,相信大家都能答出来了。。。。

@flytam flytam changed the title 带你找出react中,回调函数绑定this最完美的写法!(文末 有惊喜) 带你找出react中,回调函数绑定this最完美的写法! Mar 9, 2020
@flytam flytam added the Go label Mar 9, 2020
@flytam flytam added react New feature or request and removed Go labels Jun 9, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
react New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant