javascript - 在React组件中有关.this绑定(bind)的问题

标签 javascript reactjs this

目标:在React组件内的.this()绑定(bind)过程中,我想完全了解正在制作的引用以及计算机执行的精确步骤。

说明:我有一些代码(在下面列出的定向),在代码内,计算机通过handleChange行绑定(bind)了this.handleChange = this.handleChange.bind(this);输入处理程序。有一个父组件MyApp,它有一个子组件GetInput和另一个子组件RenderInput

问题:

问题1.我的困惑主要来自于认为.this()自动引用最接近的“父”对象,并且通过.this()进行绑定(bind)将因此将其重定向到.bind()写入位置的最接近的父对象。在以下情况下,它似乎重定向到MyApp组件。但是,MyApp类是一个函数console.log(typeof MyApp) //expected: function。因此,为什么.this()在下面的代码中未引用全局对象?

问题2.调用handleChange处理程序时,计算机将执行哪些逐步过程?是否是以下内容:

  • RenderInput组件内的初始调用:<p>{this.props.input}</p>
  • 引用RenderInput父元素,它是GetInput组件:<input value={this.props.input} onChange={this.props.handleChange}/></div>
  • 计算机读取onChange={this.props.handleChange}
  • 转到GetInput组件的父级(即MyApp组件),并显示:handleChange={this.handleChange}(这是我最不确定的步骤)
  • 查找.this()绑定(bind)到的位置:this.handleChange = this.handleChange.bind(this);
  • 引用MyApp作为this的绑定(bind)值
  • 执行handleChange处理程序:handleChange(event) {this.setState({inputValue: event.target.value });}
  • class MyApp extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          inputValue: ""
        };
        this.handleChange = this.handleChange.bind(this);
      }
      handleChange(event) {
        this.setState({
          inputValue: event.target.value
        });
      }
      render() {
        return (
          <div>
            {
              <GetInput
                input={this.state.inputValue}
                handleChange={this.handleChange}
              />
            }
            {
              <RenderInput input={this.state.inputValue} />
            }
          </div>
        );
      }
    }
    
    class GetInput extends React.Component {
      constructor(props) {
        super(props);
      }
      render() {
        return (
          <div>
            <h3>Get Input:</h3>
            <input
              value={this.props.input}
              onChange={this.props.handleChange}/>
          </div>
        );
      }
    };
    
    class RenderInput extends React.Component {
      constructor(props) {
        super(props);
      }
      render() {
        return (
          <div>
            <h3>Input Render:</h3>
            <p>{this.props.input}</p>
          </div>
        );
      }
    };
    
    

    最佳答案

    让我们从this.bind的行为都不是React特定的事实开始。因此,为简单起见,让我们暂时忘记React,只看一些原始的JS代码(不用担心!稍后我们将返回React)。

    现在让我们从头开始,这是一个对象:

    {
      username: "KamiFightingSpirit"
    }
    

    看起来很简单,但是对象的值可以是任意值(数组,其他对象,函数等)。我们还添加一个函数:

    {
      username: "KamiFightingSpirit",
      logUsername: function () {
        console.log( this.username );
      }
    }
    
    this到底是什么? this指向执行上下文,您可能还听说过:

    this/execution context is anything before the dot that precedes function call.



    考虑到this与作用域不同,让我们快速检查一下。它是在执行期间计算的。

    const soUser = {
      username: "KamiFightingSpirit",
      logUsername: function () {
        console.log(this.username);
      }
    };
    
    soUser.logUsername();
    // -> KamiFightingSpirit
    

    好的,在执行期间this等于soUser

    // Let's just borrow the method from soUser
    const userNameLogger = soUser.logUsername;
    
    const nestedObjects = {
      username: "This property will not be logged", // neither "KamiFightingSpirit" will be logged
      sampleUser: {
        username: "Igor Bykov",
        logUsername: userNameLogger
      }
    };
    
    nestedObjects.sampleUser.logUsername();
    // -> Igor Bykov
    

    很好,又可以了。 this等于函数调用之前的点之前的对象。在这种情况下,对象是nestedObjects.sampleUser的值。

    再次注意,执行上下文不能像范围那样工作。如果在点前的对象中缺少使用的属性,则不会检查父对象中是否存在。这是相同的示例,但是缺少username:

    const nestedObjects = {
      username: "undefined will be logged",
      sampleUser: {
        logUsername: userNameLogger
      }
    };
    
    nestedObjects.sampleUser.logUsername();
    // -> undefined
    

    我们到了一半。现在,我们如何以编程方式创建大量用户?

    // this is called constructor function
    function User(name) {
      // const this = {}; <- implicitly when used with "new" keyword
      this.name = name;
      // return this; <- implicitly when used with "new" keyword
    }
    
    console.log( new User("LonelyKnight") );
    // -> {name: "LonelyKnight"}
    

    在这里,new强制创建新对象(并因此创建执行内容)。

    但是,以这种方式创建对象非常危险。如果您在不使用new的情况下调用相同的函数,它将执行但不会创建新对象,并且this将被评估为window对象。这样,我们将有效地将name分配给window

    由于这个原因和其他一些原因,在较新的JavaScript版本中引入了class。类与构造函数完全一样(实际上,它们是更聪明,更漂亮的构造函数)。

    因此,以下示例非常类似于上一个:

    class User {
      constructor(name) {
        this.name = name;   
      }
    }
    

    我们快要到了!现在说,我们还希望能够更改用户名。

    class User {
    
      constructor(name) {
        this.name = name;   
      }
    
      changeName(newName) {
        this.name = newName;
      }
    
    }
    
    let batman = new User("Bat");
    console.log(batman.name); // -> Bat
    batman.changeName("Batman!");
    console.log(batman.name); // -> Batman!
    

    很酷,它有效!注意,我们没有使用.bind。在这种情况下没有必要,因为我们在类的实例上执行所有操作。

    现在,让我们回到React。在React中,我们倾向于将函数(而不是实例)从 parent 传递给 child 。如前所述,类非常类似于智能构造函数。因此,首先让我们看看如果对每个组件使用构造函数而不是类的话,代码会是什么样。

    如果我们抛弃React添加的所有JSX和合成糖,执行的操作将类似于以下内容:

    function Child(f) {
      // Random property
      this.rand = "A";
      f(); // -> Window
    }
    
    function User(name) {
      this.name = name;
      this.logThis = function(){
        console.log(this);
      }
      this.render = function(){
        return new Child(this.logThis);
      }
    }
    
    // Somewhere in React internals (in overly-simplified constructor-based universe)
    const U = new User(``);
    U.render();
    

    请注意,由于我们只调用f(),所以前面没有点,因此没有上下文可在其中执行f()。在这种情况下(除非设置了严格模式),this会被评估为全局对象,该对象在浏览器中就是Window

    现在,让我们回到类并编写类似的内容:

    // Child component
    class Child {
    
      constructor(f) {
        setTimeout(
          () => f("Superman"), // -> throws "Cannot set "name" of undefined"
          100
        );
      }
    
    }
    
    // Parent component
    class User {
    
      constructor(name) {
        this.name = name;
      }
    
      changeName(newName) {
        this.name = newName;
      }
    
      render() {
       return new Child(this.changeName);
      }
    
    }
    
    // Somewhere in React internals (in overly-simplified universe)
    const batman = new User("batman");
    batman.render();
    

    由于类use strict mode by default(上面的示例在f()之前看不到任何内容)将this评估为undefined,尝试将新属性分配给undefined,并引发错误,失败了。

    因此,为避免这种情况,我们需要使用.bind或类似的函数,以确保始终在正确的上下文中执行它。
    .bind到底做什么? Some internal black magic。为了完全理解它,您可能需要深入研究JS编译器代码(通常以C / C++编写)。

    但是,有一个更简单的选择。 MDN(是一个很棒的网站)offers you ready-to-use polyfills基本上显示了如何在 Vanilla JS中重写.bind。如果仔细观察,您会注意到两个polyfill都只包装了对.apply.call的调用。因此,有趣的部分实际上并未“公开”。

    我猜这是因为我们无法使用内部机制,所以内部JS可能无法忠实地复制内部C++ / C魔术。

    但是,如果我们至少要严重地复制.bind功能,我们会发现.bind并不那么复杂(至少在基本级别上),它的主要功能只是确保执行上下文始终保持不变。

    这是最简单的.customBind的致命实现:
    Function.prototype.customBind = function(obj, ...bindedArgs) {
      // Symbol ensures our key is unique and doesn't re-write anything
      const fnKey = Symbol();
      // Inserts function directly into the object
      obj[fnKey] = this;
      // Return a wrapper that just calls the function
      // from within specified object each time it's called.
      return (...args) => obj[fnKey](...bindedArgs, ...args);
    };
    

    虽然有效,但缺点是我们实际上将函数插入到对象中。尽管我们可以使用Object.defineProperty更好地隐藏它,但它仍然存在。

    这是一种更复杂的方法,无论如何都可以改变原始对象,但是仅是您期望的那样(尽管此实现并不比前一个更好,这只是一个假设的示例):

    // Please, never use this code for anything practical
    // unless you REALLY understand what you are doing.
    
    // Implements customBind
    Function.prototype.customBind = function(context, ...bindedArgs) {
      // context => intended execution context
      // bindedArgs => original .bind also accept those
      // Saves function that should be binded into a variable
      const fn = this;
      // Returns a new function. Original .bind also does.
      return (...args) => {
    
        // Symbol is used to ensure that
        // fn's key will not unintentionally
        // re-writte something in the original
        // object.
        const fnSymbol = Symbol();
    
        // Since we can't directly manipulate
        // execution context (not doable in JS),
        // neither we can just call "context.fn()" since
        // .fn is not in context's prototype chain,
        // the best thing we can do is to dinamically
        // mock execution context, so, we'll be able to
        // run our binded function, inside the mocked
        // context.
        const contextClone = {
            ...context,
            // adds binded function into a
            // clone of its intended execution
            // context.
            [fnSymbol]: fn,
        };
      
        // Executes binded function inside the exact clone
        // of its intended execution context & saves returned
        // value. We will return it to the callee
        // later on.
        const output = contextClone[fnSymbol](...bindedArgs, ...args);
        // Deletes property, so, it'll not leak into
        // the original object on update that we're
        // going to perform.
        delete contextClone[fnSymbol];
        // The function that we've run on our clone, might
        // possibly change something inside the object it
        // operated upon. However, since the object it
        // operated upon is just a mock that we've created,
        // the original object will stay unchanged. In order
        // to avoid such a situation, let's merge our possibly
        // changed clone into the original object.
        context = Object.assign(context, contextClone);
        // Finally, let's return to the callee,
        // the result returned by binded function.
        return output;
      };
    };
    
    // Let's test it works!
    const soUser = {
        name: `Kami`,
        logName: function() {
            console.log(`My name is ${this.name}`);
        },
        changeName: function(newName) {
            this.name = newName;
        },
    };
    
    // Let's just borrow these methods from soUser
    const soUserOwnedLogger = soUser.logName.customBind(soUser);
    const soUserNameChanger = soUser.changeName.customBind(
      soUser,
      "KamiFightingSpirit"
    );
    
    // Let's use borrowed methods into another object.
    const outterSystemUser = {
        name: `UU-B235`,
        soUserLogger: soUserOwnedLogger,
        soUserChange: soUserNameChanger,
    };
    
    
    soUserOwnedLogger();
    outterSystemUser.soUserChange();
    soUserOwnedLogger();
    console.log(`"name" in soUuser: ${soUser.name}`);
    console.log(`"name" in outterSystemUser: ${outterSystemUser.name}`);


    希望能帮助到你!

    关于javascript - 在React组件中有关.this绑定(bind)的问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59310338/

    相关文章:

    javascript - ReactJS 如何在 React 中切换页面?

    javascript - 如何在 React 中将 id 传递给 componentdidmount

    jquery - 我可以计算表 "$("这个 tbody tr").length 的行数吗?

    javascript - 难以理解对象中的 JS 函数

    javascript - Bootstrap 中的嵌套选项卡

    javascript - testcafe requestLogger 仅记录 fixture 中的第一个测试

    javascript - 模拟不同选择器的点击事件

    Java:继承、实例变量和 this

    javascript - 使用 innerHTML 创建 div 后无法收听悬停(等)事件

    javascript - 通过状态栏选择并删除与下一个元素合并