无法在事件处理程序中访问React实例(this)

242

我正在使用ES6(使用BabelJS)编写一个简单的组件,但函数this.setState不起作用。

常见错误包括:

 

无法读取未定义的属性“setState”

或者

 

this.setState不是函数

你知道为什么吗?以下是代码:

import React from 'react'

class SomeClass extends React.Component {
  constructor(props) {
    super(props)
    this.state = {inputContent: 'startValue'}
  }

  sendContent(e) {
    console.log('sending input content '+React.findDOMNode(React.refs.someref).value)
  }

  changeContent(e) {
    this.setState({inputContent: e.target.value})
  } 

  render() {
    return (
      <div>
        <h4>The input form is here:</h4>
        Title: 
        <input type="text" ref="someref" value={this.inputContent} 
          onChange={this.changeContent} /> 
        <button onClick={this.sendContent}>Submit</button>
      </div>
    )
  }
}

export default SomeClass

这不是问题所在,但你应该避免使用refs - Brigand
@FakeRainBrigand,如果没有引用,你会如何解决这个问题? - user3696212
3
在你当前的代码中,只需将 React.findDOMNode(React.refs.someref).value 改为 this.state.inputContent,并删除 ref="someref" - Brigand
你不需要引用,因为你正在更新状态内的值。只需发送状态值即可。 - Blair Anderson
你的函数在 ES5 中需要绑定才能在函数内部访问状态或属性,但如果你使用箭头函数,则无需手动绑定,绑定会自动进行,你也可以避免作用域相关的问题。 - Hemadri Dasari
19个回答

270

在将this.changeContent作为onChange属性传递之前,需要通过this.changeContent.bind(this)将其绑定到组件实例上,否则函数体中的this变量将不会引用组件实例而是window。请参见Function::bind

在使用React.createClass而不是ES6类时,组件上定义的每个非生命周期方法都会自动绑定到组件实例上。请参见Autobinding

请注意,绑定函数会创建一个新函数。您可以直接在渲染中进行绑定,这意味着每次组件渲染时都会创建一个新函数,或者在构造函数中进行绑定,这只会触发一次。

constructor() {
  this.changeContent = this.changeContent.bind(this);
}

对比

render() {
  return <input onChange={this.changeContent.bind(this)} />;
}
组件实例上设置 Ref,而不是在 React.refs 上设置:你需要将 React.refs.someref 更改为 this.refs.someref。您还需要将 sendContent 方法绑定到组件实例,以便 this 引用它。

6
在构造函数本身上绑定功能是一个不错的事情,可以防止多次创建函数。 - Abhinav Singi
1
请原谅,但我不明白为什么需要通过 this.changeContent.bind(this)this.changeContent 绑定到组件实例。我的意思是,我们通过子类或 React.Component 编写组件,在 ES6 中,类中定义的每个方法都会自动绑定到通过子类/类本身创建的实例上。为什么这里需要手动绑定呢?这与 React 有关吗?还是我对 ES6 类方法的动态理解有误? - marco
9
在ES6中,类定义的方法不会自动绑定到实例上。这就是为什么当你需要绑定时,你需要手动进行绑定。使用BabelJS,通过使用“属性初始化语法”和箭头函数,可以直接定义会自动绑定到实例的方法。“myMethod = () => ...”代替“myMethod() { ... }”。 - Alexandre Kirszenberg
1
@AlexandreKirszenberg 请查看此示例:该方法似乎已自动绑定到实例... - marco
2
@marco 这里有一个不同的例子。当你使用object.method()调用一个方法时,method函数体内的this变量将会指向object对象。但是如果你将object.method传递给另一个函数,只有函数本身的值会被传递,它将失去object上下文。这就是为什么在React中,有时需要在将事件处理程序传递给组件之前手动绑定它,以避免丢失当前组件实例的上下文。 - Alexandre Kirszenberg
显示剩余2条评论

105

Morhaus是正确的,但这可以在不使用bind的情况下解决。

您可以使用箭头函数类属性提案一起使用:

class SomeClass extends React.Component {
  changeContent = (e) => {
    this.setState({inputContent: e.target.value})
  } 

  render() {
    return <input type="text" onChange={this.changeContent} />;
  }
}

由于箭头函数在构造函数的作用域中声明,并且由于箭头函数从其声明作用域维护this,因此所有这些都起作用。这里的缺点是这些不会成为原型上的函数,它们将在每个组件中重新创建。然而,由于bind会产生相同的结果,因此这并不是什么大问题。


1
这在 TypeScript 中也完美运行(通常不必担心 TypeScript 中的绑定,但我想这是不同的)。 - mejdev
这个不行。我得到了“属性声明只能在 .ts 文件中使用”的错误。 - BHouwens
@BHouwens 在babel REPL中,这是它的代码。我不知道你在做什么,但你做错了些什么。 - Kyeotic
可能是我设置了构造函数,但除此之外,我的示例代码与原来相同,无法编译。不过 bind 方法可以正常工作。 - BHouwens
3
一个构造函数不会导致这段代码出错,你可能有其他问题。也许你没有正确的插件?这不是2015预设的一部分,它被称为 babel-plugin-transform-class-properties。如果你展示给我你的代码,我可以告诉你出了什么问题。 Babel REPL会给你一个方便分享的链接。 - Kyeotic
这篇文章为整个事情提供了一些启示:http://reactkungfu.com/2015/07/why-and-how-to-bind-methods-in-your-react-component-classes/ - Erwin Mayer

57

当我们从React.createClass()组件定义语法转换为ES6类的方式来扩展React.Component时,此问题是我们大多数人都会遇到的首要问题。

它是由于在React.createClass()extends React.Componentthis上下文差异引起的

使用React.createClass()将自动正确绑定this上下文(值),但是使用ES6类时情况并非如此。通过扩展React.Component的方式进行操作时,默认情况下this上下文为null。类的属性不会自动绑定到React类(组件)实例。


解决此问题的方法

我知道一共有4个通用方法。

  1. 在类构造函数中绑定函数。被许多人认为是最佳实践方法,它避免了直接触及JSX,也不会在每次组件重新渲染时创建新的函数。

class SomeClass extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    console.log(this); // the React Component instance
  }
  render() {
    return (
      <button onClick={this.handleClick}></button>
    );
  }
}
  • 将函数内联。您仍然可以在某些教程/文章等中找到此方法的使用,因此重要的是您意识到它。它与#1相同的概念,但请注意,绑定函数会为每个重新渲染创建一个新函数。

  • class SomeClass extends React.Component {
      handleClick() {
        console.log(this); // the React Component instance
      }
      render() {
        return (
          <button onClick={this.handleClick.bind(this)}></button>
        );
      }
    }
    
  • 使用箭头函数。在箭头函数出现之前,每个新定义的函数都会创建自己的this值。但是,箭头函数不会创建自己的this上下文,因此this具有React组件实例的原始含义。因此,我们可以:

    class SomeClass extends React.Component {
      handleClick() {
        console.log(this); // the React Component instance
      }
      render() {
        return (
          <button onClick={ () => this.handleClick() }></button>
        );
      }
    }
    
    或者
    class SomeClass extends React.Component {
      handleClick = () => {
        console.log(this); // the React Component instance
      }
      render() {
        return (
          <button onClick={this.handleClick}></button>
        );
      }
    }
    
  • 使用实用函数库来自动绑定您的函数。有一些实用工具库可以自动执行此操作。以下是其中一些流行的实用工具库,仅列举几个:

    • Autobind Decorator 是一个 NPM 包,将类的方法绑定到 this 的正确实例,即使这些方法被分离。此包在方法前使用 @autobind 来绑定组件上下文的正确引用到 this

      import autobind from 'autobind-decorator';
      
      class SomeClass extends React.Component {
        @autobind
        handleClick() {
          console.log(this); // the React Component instance
        }
        render() {
          return (
            <button onClick={this.handleClick}></button>
          );
        }
      }
      

      自动绑定装饰器足够智能,可以像方法#1一样一次性绑定组件类中的所有方法。

    • Class Autobind 是另一个广泛使用的NPM包,用于解决这个绑定问题。与Autobind Decorator不同,它并不使用装饰器模式,而是在构造函数中使用一个函数来自动绑定组件的方法到正确的this引用。

    • import autobind from 'class-autobind';
      
      class SomeClass extends React.Component {
        constructor() {
          autobind(this);
          // or if you want to bind only only select functions:
          // autobind(this, 'handleClick');
        }
        handleClick() {
          console.log(this); // the React Component instance
        }
        render() {
          return (
            <button onClick={this.handleClick}></button>
          );
        }
      }
      

      PS:另一个非常相似的库是React Autobind


      建议

      如果我是你,我会坚持使用第一种方法。但是,一旦你在类构造函数中有了大量绑定,我建议你探索一下第四种方法中提到的辅助库。


      其他

      这与你遇到的问题无关,但是你不应过度使用refs

        

      你的第一反应可能是使用refs在你的应用程序中"使事情发生"。如果是这种情况,请花点时间更加批判地思考状态在组件层次结构中应该由谁拥有。

      为了实现类似于您所需的功能,就像受控组件一样,是首选的方法。建议您考虑使用您的组件状态state。因此,您可以通过this.state.inputContent轻松访问该值。


  • 5
    这篇回答比被采纳的答案更加完整实用。 - cortexlock
    这里缺少了另一个答案中的方法 https://dev59.com/2V0b5IYBdhLWcg3wIeEw#34050078 - Kyeotic
    @Tyrsius,它在那里。请查看我的答案中的第三种方法,一个箭头函数+类属性提案。 - Kaloyan Kosev
    @KaloyanKosev 如果不是点击操作,而只是简单的方法调用呢? - Rushi trivedi

    9
    虽然之前的答案已经提供了解决方案的基本概述(即绑定、箭头函数、为您完成此操作的装饰器),但我仍然没有看到一个解释为什么这是必要的,而我认为这是混淆的根源,导致不必要的步骤,如不必要的重新绑定和盲目地跟随他人的做法。

    this是动态的

    为了理解这个特定的情况,需要简要介绍一下this的工作原理。关键在于,this是运行时绑定,取决于当前执行上下文。因此,它通常被称为“上下文”,提供有关当前执行上下文的信息,而您需要绑定的原因是因为您会失去“上下文”。但让我用一段代码片段来说明这个问题:

    const foobar = {
      bar: function () {
        return this.foo;
      },
      foo: 3,
    };
    console.log(foobar.bar()); // 3, all is good!
    

    在这个例子中,我们得到了预期的 3。但是看看下面这个例子:
    const barFunc = foobar.bar;
    console.log(barFunc()); // Uh oh, undefined!
    

    也许让人意想不到的是,它记录了undefined——数字3去哪了呢?答案在于“上下文”,或者说你如何执行一个函数。比较一下我们调用这些函数的方式:

    // Example 1
    foobar.bar();
    // Example 2
    const barFunc = foobar.bar;
    barFunc();
    

    注意区别。在第一个示例中,我们明确指定了bar方法1所在的位置——在foobar对象上:
    foobar.bar();
    ^^^^^^
    

    但是在第二种情况中,我们将方法存储到一个新变量中,并使用该变量调用方法,而不明确说明方法实际存在的位置,因此失去了上下文:

    barFunc(); // Which object is this function coming from?
    

    在这里出现了问题,当你将一个方法存储在变量中时,关于该方法所在位置的原始信息(方法执行的上下文)就丢失了。没有这些信息,在运行时,JavaScript解释器无法绑定正确的this - 没有特定的上下文,this不能按预期工作2

    与React相关

    以下是一个React组件的示例(为简洁起见进行了缩短),它遭受了this问题的困扰:
    handleClick() {
      this.setState(({ clicks }) => ({ // setState is async, use callback to access previous state
        clicks: clicks + 1, // increase by 1
      }));
    }
    
    render() {
      return (
        <button onClick={this.handleClick}>{this.state.clicks}</button>
      );
    }
    

    但是为什么,以及前一节如何与此相关呢?这是因为它们遭受了同样问题的抽象。如果您看一下React 如何处理事件处理程序
    // Edited to fit answer, React performs other checks internally
    // props is the current React component's props, registrationName is the name of the event handle prop, i.e "onClick"
    let listener = props[registrationName];
    // Later, listener is called
    

    因此,当你执行onClick={this.handleClick}时,方法this.handleClick最终被分配给变量listener3。但现在你看到问题出现了——由于我们将this.handleClick分配给了listener,我们不再明确指定handleClick来自哪里!从React的角度来看,listener只是一些函数,没有附加到任何对象(或在这种情况下,React组件实例)。我们失去了上下文,因此解释器无法推断要在handleClick内使用的this值。

    为什么绑定起作用

    你可能会想,如果解释器在运行时决定this值,为什么我可以绑定处理程序以使其起作用?这是因为你可以使用Function#bind在运行时保证this值。这是通过在函数上设置一个内部this绑定属性来完成的,允许它不推断this

    this.handleClick = this.handleClick.bind(this);
    

    当这行代码被执行时,可能在构造函数中,当前的this(即React组件实例)被捕获并设置为一个全新函数的内部this绑定,该函数是从Function#bind返回的。这确保了在运行时计算this时,解释器不会尝试推断任何内容,而是使用您提供的this值。

    为什么箭头函数属性有效

    基于转译,箭头函数类属性目前通过Babel工作:

    handleClick = () => { /* Can use this just fine here */ }
    

    变成:

    constructor() {
      super();
      this.handleClick = () => {}
    }
    

    这是因为箭头函数不会绑定它们自己的this,而是使用它们所在的封闭作用域中的this。在这种情况下,它使用构造函数的this,该this指向React组件实例,从而给出了正确的this。

    1 我使用“方法”一词来指代应该绑定到对象的函数,“函数”则指其他类型的函数。

    2 在第二个片段中,打印出的是 undefined 而不是 3,因为当无法通过特定上下文确定时,this 默认为全局执行环境(非严格模式下为 window,否则为 undefined)。在这个例子中,window.foo 不存在,因此返回 undefined。

    3 如果你深入研究事件队列中的事件如何执行,invokeGuardedCallback 将被调用以处理监听器。

    4 实际上,这个问题更加复杂。React在内部尝试对其自身使用Function#apply来监听事件,但箭头函数无法绑定this,因此不起作用。这意味着,当箭头函数内的this被实际评估时,this会在当前模块的每个执行环境的每个词法环境中向上解析。最终解析到具有this绑定的执行环境构造函数,该构造函数具有指向当前React组件实例的this,从而使其正常工作。


    2
    您可以通过以下三种方式来解决这个问题:
    1.在构造函数中绑定事件函数,如下所示:
    import React from 'react'
    
    class SomeClass extends React.Component {
      constructor(props) {
        super(props)
        this.state = {inputContent: 'startValue'}
        this.changeContent = this.changeContent.bind(this);
      }
    
      sendContent(e) {
        console.log('sending input content '+React.findDOMNode(React.refs.someref).value)
      }
    
      changeContent(e) {
        this.setState({inputContent: e.target.value})
      } 
    
      render() {
        return (
          <div>
            <h4>The input form is here:</h4>
            Title: 
            <input type="text" ref="someref" value={this.inputContent} 
              onChange={this.changeContent} /> 
            <button onClick={this.sendContent}>Submit</button>
          </div>
        )
      }
    }
    
    export default SomeClass
    

    2.调用时绑定

    import React from 'react'
    
    class SomeClass extends React.Component {
      constructor(props) {
        super(props)
        this.state = {inputContent: 'startValue'}
      }
    
      sendContent(e) {
        console.log('sending input content '+React.findDOMNode(React.refs.someref).value)
      }
    
      changeContent(e) {
        this.setState({inputContent: e.target.value})
      } 
    
      render() {
        return (
          <div>
            <h4>The input form is here:</h4>
            Title: 
            <input type="text" ref="someref" value={this.inputContent} 
              onChange={this.changeContent} /> 
            <button onClick={this.sendContent.bind(this)}>Submit</button>
          </div>
        )
      }
    }
    
    export default SomeClass
    

    3. 通过使用箭头函数

    import React from 'react'
    
    class SomeClass extends React.Component {
      constructor(props) {
        super(props)
        this.state = {inputContent: 'startValue'}
      }
    
      sendContent(e) {
        console.log('sending input content '+React.findDOMNode(React.refs.someref).value)
      }
    
      changeContent(e) {
        this.setState({inputContent: e.target.value})
      } 
    
      render() {
        return (
          <div>
            <h4>The input form is here:</h4>
            Title: 
            <input type="text" ref="someref" value={this.inputContent} 
              onChange={this.changeContent} /> 
            <button onClick={()=>this.sendContent()}>Submit</button>
          </div>
        )
      }
    }
    
    export default SomeClass
    

    1

    我的建议是将箭头函数用作属性。

    class SomeClass extends React.Component {
      handleClick = () => {
        console.log(this); // the React Component instance
      }
      render() {
        return (
          <button onClick={this.handleClick}></button>
        );
      }
    }
    

    不要使用箭头函数。
    class SomeClass extends React.Component {
          handleClick(){
            console.log(this); // the React Component instance
          }
          render() {
            return (
              <button onClick={()=>{this.handleClick}}></button>
            );
          }
        }
    

    因为第二种方法每次渲染都会生成新的函数,实际上这意味着新的指针和新版本的props。如果您稍后关心性能,则可以使用React.PureComponent或在React.Component中覆盖shouldComponentUpdate(nextProps, nextState)并浅层检查props何时到达。

    1
    你可以按照以下步骤解决这个问题:

    将 sendContent 函数更改为

     sendContent(e) {
        console.log('sending input content '+this.refs.someref.value)
      }
    

    将渲染函数更改为:
    <input type="text" ref="someref" value={this.state.inputContent} 
              onChange={(event)=>this.changeContent(event)} /> 
       <button onClick={(event)=>this.sendContent(event)}>Submit</button>
    

    1
    我们需要在构造函数中将事件函数与组件绑定,如下所示,
    import React from 'react'
    
    class SomeClass extends React.Component {
      constructor(props) {
        super(props)
        this.state = {inputContent: 'startValue'}
        this.changeContent = this.changeContent.bind(this);
      }
    
      sendContent(e) {
        console.log('sending input content '+React.findDOMNode(React.refs.someref).value)
      }
    
      changeContent(e) {
        this.setState({inputContent: e.target.value})
      } 
    
      render() {
        return (
          <div>
            <h4>The input form is here:</h4>
            Title: 
            <input type="text" ref="someref" value={this.inputContent} 
              onChange={this.changeContent} /> 
            <button onClick={this.sendContent}>Submit</button>
          </div>
        )
      }
    }
    
    export default SomeClass
    

    谢谢


    1
    我们必须使用bind将函数与this绑定,以便在类中获取该函数的实例。就像在示例中一样。
    <button onClick={this.sendContent.bind(this)}>Submit</button>
    

    这样,this.state 就会成为一个有效的对象。

    1

    如果有人需要查看这个答案,这里有一个将所有函数绑定在一起而不需要手动绑定的方法。

    在构造函数中:

    for (let member of Object.getOwnPropertyNames(Object.getPrototypeOf(this))) {
        this[member] = this[member].bind(this)
    }
    

    或者在 global.jsx 文件中创建此函数。
    export function bindAllFunctions({ bindTo: dis }) {
    for (let member of Object.getOwnPropertyNames(Object.getPrototypeOf(dis))) {
        dis[member] = dis[member].bind(dis)
        }
    }
    

    在你的constructor()中这样调用它:

    bindAllFunctions({ bindTo: this })
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接