簡體   English   中英

關於 React 組件中 .this 綁定的問題

[英]Question on .this binding within a React Component

目標:在 React 組件中的.this()綁定期間,我想完全了解正在創建的引用以及計算機采取的精確分步過程。

描述:我有一些代碼(在下面列出),在代碼中,計算機通過this.handleChange = this.handleChange.bind(this);行綁定handleChange輸入處理程序this.handleChange = this.handleChange.bind(this); . 有一個父組件MyApp ,它有一個子組件GetInput和另一個子組件RenderInput

問題:

問題 1. 我的困惑主要源於認為.this()自動引用最近的“父”對象,並且通過.this()綁定會因此將其重定向到編寫.bind()的最近的父對象。 在下面的情況下,它似乎重定向到MyApp組件。 然而, MyApp類是一個函數console.log(typeof MyApp) //expected: function 因此,為什么.this()在下面的代碼中沒有引用全局對象?

問題 2. 當 handleChange 處理程序被調用時,計算機進行的分步處理是什么? 是不是如下:

  1. RenderInput組件中的初始調用: <p>{this.props.input}</p>
  2. 引用RenderInput父級,即GetInput組件: <input value={this.props.input} onChange={this.props.handleChange}/></div>
  3. 計算機讀取onChange={this.props.handleChange}
  4. 轉到GetInput組件的父級,即MyApp組件並讀取: handleChange={this.handleChange} (這是我最不確定的步驟)
  5. 查找.this()綁定到的位置: this.handleChange = this.handleChange.bind(this);
  6. 引用MyApp作為this的綁定值
  7. 執行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 上下文是函數調用之前的點之前的任何內容。

讓我們快速檢查一下,記住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的值。

再次注意,執行上下文不像作用域那樣工作。 如果對象中點之前的對象中缺少 used 屬性,則不會執行父對象中是否存在的檢查。 這是相同的示例,但缺少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對象。 這樣我們就可以有效地為window分配name

由於這個原因以及更多的原因,在較新版本的 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!

很酷,它有效! 請注意,我們沒有使用 no .bind 在這種情況下沒有必要,因為我們在類的實例上執行所有操作。

現在,讓我們回到 React。 在 React 中,我們傾向於將函數而不是實例)從父級傳遞給子級。 正如我之前所說,類非常像智能構造函數。 所以,讓我們首先看看如果我們為每個組件使用構造函數而不是類,我們的代碼會是什么樣子。

如果我們扔掉 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();

由於類默認使用嚴格的模式,上述前看到什么的例子f()評估this為undefined,嘗試分配新屬性未定義並引發錯誤,無法做到這一點。

所以,為了避免這種情況,我們需要使用.bind或類似的函數來確保它總是在正確的上下文中執行。

.bind到底是做什么的? 一些內部黑魔法 要完全理解它,您可能需要深入研究 JS 編譯器代碼(通常用 C/C++ 編寫)。

但是,還有一個更簡單的選擇。 MDN(它是一個很棒的站點) 為您提供了現成的 polyfills ,它們基本上展示了.bind是如何用 vanilla JS 重寫的。 如果你仔細看,你會發現,這兩個polyfills只是換到電話.apply.call 所以,有趣的部分實際上並沒有“公開”。

我猜這是因為我們無法訪問內部機制,因此內部的 C++/C 魔法可能無法用 JS 忠實地再現。

但是,如果我們至少要糟糕地重現.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}`);

希望能幫助到你!

下面例子中的關鍵字this

  handleChange(event) {
    this.setState({
      inputValue: event.target.value
    });
  }

指的是父函數handleChangehandleChange沒有setState方法。 當我們擴展class MyApp extends React.Component時,組件是做什么的。 它是從React.Component '繼承' setState ......這就是為什么我們必須手動將它綁定到那個類(這只是語法糖,正如你指出的那樣,它是一個函數......)

更深入:當你像這樣創建一個構造函數時:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

然后使用 new 關鍵字調用該函數,如下所示:

const personOne = new Person('Bob', 26)

在幕后發生的是關鍵字new創建一個空對象並將this設置為它的引用,這就是為什么在函數體本身中我們有this.name = name等...

你可以把它想象成這樣:

const this = {}

this.name = 'Bob'
this.age = 26

this現在將是一個像{ name: 'Bob', age: 26 }

附帶說明:在許多示例中,您只會看到如下所示的箭頭函數:

  handleChange = (event) => {
    this.setState({
      inputValue: event.target.value
    });
  }

那是因為箭頭函數沒有自己的this上下文......它會自動冒泡到父級,不需要綁定......

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM