如何防止Unmounted类组件中的方法被意外调用



我正在开发一个React Redux应用程序,该应用程序允许Google oauth2登录,并且我创建了一个单独的GoogleAuth组件,用于处理所有登录/注销过程。

GoogleAuth组件通常位于header中,作为header组件的子级。但是,在某些页面(当用户未登录时显示(中,我将GoogleAuth组件放置在其他位置,并将其从标题中删除。

例如,在"登录"页面上,GoogleAuth组件将从标头中卸载,另一个实例将安装在登录页面中。

我在下面包含了GoogleAuth组件的代码。

class GoogleAuth extends React.Component {
componentDidMount() {
window.gapi.load('client:auth2', () => {
window.gapi.client.init({
clientId: 'xxxx',
scope: 'email'
}).then(() => {
this.auth = window.gapi.auth2.getAuthInstance();
const isGoogleSignedIn = this.auth.isSignedIn.get();
if(this.props.isSignedIn !== isGoogleSignedIn) {
this.onAuthChange(isGoogleSignedIn);
}
this.auth.isSignedIn.listen(this.onAuthChange);
})
});
};
componentWillUnmount() {
delete this.auth;
}
onAuthChange = (isGoogleSignedIn) => {
console.log("onAuthChange called:", this.props.place); 
//place is a label assigned to the parent component
if (!this.props.isSignedIn && isGoogleSignedIn && this.auth){
this.props.signIn(this.auth.currentUser.get().getId());
} else if (this.props.isSignedIn && !isGoogleSignedIn && this.auth) {
this.props.signOut();
}
}
handleSignIn = () => {
this.auth.signIn();
}
handleSignOut = () => {
this.auth.signOut();
}
renderAuthButton() {
if (this.props.isSignedIn === null) {
return null;
} else if (this.props.isSignedIn) {
return (
<button className="google-auth-button" onClick={this.handleSignOut}>
Sign Out
</button>
)
} else {
return (
<button className="google-auth-button" onClick={this.handleSignIn}>
Sign In With Your Google Account
</button>
)
}
}
render() {
return this.renderAuthButton()
}
}
const mapStateToProps = (state) => {
return {
isSignedIn: state.auth.isSignedIn,
userId: state.auth.userId
}
}
export default connect(
mapStateToProps,
{signIn, signOut}
)(GoogleAuth);

我面临的问题是,每次安装GoogleAuth组件时,都会调用onAuthChange方法(当用户的身份验证状态更改时触发(一次,即使从那以后已经卸载了。因此,如果我打开应用程序,然后转到登录页面(这导致GoogleAuth从Header中卸载并插入登录组件(,然后登录,我会看到以下控制台日志。

onAuthChange called: header
onAuthChange called: login

我有一些事情需要一些建议:

  1. 这是我需要解决的问题吗?如果我不采取任何措施来解决它,会发生什么
  2. 当组件卸载时,我试图通过显式删除auth对象来进行一些清理。但是,对于GoogleAuth组件的每个实例(无论是否已安装(,仍然调用onAuthChange方法
  3. 我能够通过检查this.auth是否存在来防止对操作创建者的重复调用。这种方法有什么缺点吗
  4. 是否有一段时间后,卸载的组件将自动清除
是的,因为这基本上是内存泄漏。日志告诉您,调用console.log的函数没有被GCed
  • this.auth被设置为window.gapi.auth2.getAuthInstance()delete this.auth仅从您的this中删除auth密钥。只要window.gapi.auth2.getAuthInstance()返回的对象有其他引用(例如,在库本身中,看起来很像单例(,它就不会被GCed->你的delete没有多大区别
  • 参见第1点
  • 是,当用户关闭您的选项卡时
  • 要解决此问题,您必须从componentWillUnmount中的this.auth.isSignedIn中删除侦听器,可能有一个名为this.auth.isSignedIn.removeListener(...)的函数。如果没有,您可以查看有关如何删除侦听器的文档。

    我还建议不要在组件的每个实例中加载API并初始化客户端,而是只进行一次。

    编辑:要注销侦听器,您可以看看这个问题:如何删除Google OAuth2 gap事件侦听器?但是,我建议不要为每个组件实例添加/删除侦听器。

    编辑2:示例

    class GoogleAuth extends React.Component {
    componentDidMount() {
    googleAuthPromise.then(
    googleAuth => {
    const isGoogleSignedIn = googleAuth.isSignedIn.get();
    if(this.props.isSignedIn !== isGoogleSignedIn) {
    this.onAuthChange(isGoogleSignedIn);
    }
    this.unregisterListener = listenSignIn(this.onAuthChange);
    }
    )
    };
    componentWillUnmount() {
    this.unregisterListener();
    }
    onAuthChange = async (isGoogleSignedIn) => {
    console.log("onAuthChange called:", this.props.place);
    const googleAuth = await googleAuthPromise;
    if (!this.props.isSignedIn && isGoogleSignedIn && googleAuth){
    this.props.signIn(googleAuth.currentUser.get().getId());
    } else if (this.props.isSignedIn && !isGoogleSignedIn && googleAuth) {
    this.props.signOut();
    }
    }
    handleSignIn = async () => {
    const googleAuth = await googleAuthPromise;
    return googleAuth.signIn();
    }
    handleSignOut = async () => {
    const googleAuth = await googleAuthPromise;
    return googleAuth.signOut();
    }
    renderAuthButton() {
    if (this.props.isSignedIn === null) {
    return null;
    } else if (this.props.isSignedIn) {
    return (
    <button className="google-auth-button" onClick={this.handleSignOut}>
    Sign Out
    </button>
    )
    } else {
    return (
    <button className="google-auth-button" onClick={this.handleSignIn}>
    Sign In With Your Google Account
    </button>
    )
    }
    }
    render() {
    return this.renderAuthButton()
    }
    }
    const mapStateToProps = (state) => {
    return {
    isSignedIn: state.auth.isSignedIn,
    userId: state.auth.userId
    }
    }
    export default connect(
    mapStateToProps,
    {signIn, signOut}
    )(GoogleAuth);
    // create a customer listener manager in order to be able to unregister listeners
    let listeners = []
    const invokeListeners = (...args) => {
    for (const listener of listeners) {
    listener(...args)
    }
    }
    const listenSignIn = (listener) => {
    listeners = listeners.concat(listener)
    return () => {
    listeners = listeners.filter(l => l !== listener)
    }
    }
    // make the client a promise to be able to wait for it
    const googleAuthPromise = new Promise((resolve, reject) => {
    window.gapi.load('client:auth2', () => {
    window.gapi.client.init({
    clientId: 'xxxx',
    scope: 'email'
    }).then(resolve, reject)
    })
    })
    // register our custom listener handler
    googleAuthPromise.then(googleAuth => {
    googleAuth.isSignedIn.listen(invokeListeners)
    })
    

    免责声明:这是未经测试的代码,您可能需要进行小的更正。

    最新更新