[英]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 處理程序被調用時,計算機進行的分步處理是什么? 是不是如下:
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()
綁定到的位置: this.handleChange = this.handleChange.bind(this);
MyApp
作為this
的綁定值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
});
}
指的是父函數handleChange
和handleChange
沒有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.