关于 React 组件中 .this 绑定的问题
Question on .this binding within a React Component
目标: 在 React 组件中的 .this()
绑定过程中,我想完全理解正在制作的引用和确切的分步过程电脑需要。
描述: 我有一些代码(在下面列出),在代码中计算机通过 this.handleChange = this.handleChange.bind(this);
行绑定 handleChange
输入处理程序.有一个父组件 MyApp
,它有一个子组件 GetInput
和另一个子组件 RenderInput
。
问题:
问题 1. 我的困惑主要源于认为 .this()
自动引用最近的 "parent" 对象,并且通过 .this()
的绑定因此会将其重定向到最近的父对象.bind()
已写入。在下面的例子中,它似乎重定向到 MyApp
组件。然而,MyApp
class 是一个函数 console.log(typeof MyApp) //expected: function
。因此,为什么 .this()
没有在下面的代码中引用全局对象?
问题2. 调用handleChange handler 时计算机进行的步骤是什么?是不是以下:
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
:
handleChange(event) {
this.setState({
inputValue: event.target.value
});
}
指的是父函数 handleChange
而 handleChange
没有 setState
方法。当我们扩展 class MyApp extends React.Component
时,组件的作用是什么。它是 React.Component
中的 'inheriting' setState
... 这就是为什么我们必须手动将它绑定到那个 class (这只是语法糖,就像你指出的那样它是一个函数...)
更深入:当您像这样创建构造函数时:
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
上下文...它会自动冒泡到父级,无需绑定...
让我们从 this
和 .bind
的行为都不是特定于 React 的事实开始。所以,为了简单起见,让我们暂时忘记 React,只看一些普通的 JS 代码(不用担心!我们稍后会回到 React)。
现在让我们从头开始,这是一个object:
{
username: "KamiFightingSpirit"
}
看起来很简单,但 object 的值可能是任何值(数组、其他 object、函数等)。我们还添加一个函数:
{
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
等于函数调用之前的点之前的 object。在这种情况下,object 是 nestedObjects.sampleUser
的值。
再次注意,执行上下文不像作用域那样工作。如果使用的 属性 在点之前的 object 中丢失,则不会执行 parent 中是否存在的检查。这是相同的示例,但缺少 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
强制创建一个新的 object(因此,一个执行内容)。
然而,以这种方式创建 objects 是相当危险的。如果您在没有 new
的情况下调用相同的函数,它将执行但不会创建新的 object,并且 this
将被评估为 window
object。这样我们就可以有效地将 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!
太棒了,有用!请注意,我们没有使用 no .bind
。在这种情况下没有必要,因为我们在 class.
的实例上执行所有操作
现在,让我们回到 React。在 React 中,我们倾向于将 functions(not 实例)从 parents 传递到 children。正如我之前所说,classes 非常像智能构造函数。因此,让我们首先看看如果我们对每个组件使用构造函数而不是 classes,我们的代码会是什么样子。
如果我们扔掉 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
被评估为全局 object,在浏览器中为 Window
。
现在,让我们回到 classes 并写一些非常相似的东西:
// 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();
因为 class 是 use strict mode by default,上面的例子在 f()
之前什么都看不到,将 this
计算为未定义,尝试将新的 属性 分配给未定义并抛出错误不这样做。
因此,为了避免这种情况,我们需要使用 .bind
或类似的函数以确保它始终在正确的上下文中执行。
.bind
到底是做什么的? Some internal black magic。要完全理解它,您可能需要深入研究 JS 编译器代码(通常用 C/C++ 编写)。
不过,还有一个更简单的选择。 MDN(它是一个很棒的网站)offers you ready-to-use polyfills 基本上展示了 .bind
在 vanilla JS 中可能是如何 re-written。如果你仔细观察,你会注意到这两个 polyfill 都只是包装了对 .apply
或 .call
的调用。所以,有趣的部分其实不是"disclosed".
我猜这是因为内部 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 中。虽然我们可以用 Object.defineProperty
更好地隐藏它,但它仍然会在那里。
这里有一个更复杂的方法,无论如何都可以改变原始的 object,但只能按照你期望的方式改变(尽管这个实现并不比前一个好。这只是一个假设的例子):
// 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}`);
希望对您有所帮助!
目标: 在 React 组件中的 .this()
绑定过程中,我想完全理解正在制作的引用和确切的分步过程电脑需要。
描述: 我有一些代码(在下面列出),在代码中计算机通过 this.handleChange = this.handleChange.bind(this);
行绑定 handleChange
输入处理程序.有一个父组件 MyApp
,它有一个子组件 GetInput
和另一个子组件 RenderInput
。
问题:
问题 1. 我的困惑主要源于认为 .this()
自动引用最近的 "parent" 对象,并且通过 .this()
的绑定因此会将其重定向到最近的父对象.bind()
已写入。在下面的例子中,它似乎重定向到 MyApp
组件。然而,MyApp
class 是一个函数 console.log(typeof MyApp) //expected: function
。因此,为什么 .this()
没有在下面的代码中引用全局对象?
问题2. 调用handleChange handler 时计算机进行的步骤是什么?是不是以下:
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
:
handleChange(event) {
this.setState({
inputValue: event.target.value
});
}
指的是父函数 handleChange
而 handleChange
没有 setState
方法。当我们扩展 class MyApp extends React.Component
时,组件的作用是什么。它是 React.Component
中的 'inheriting' setState
... 这就是为什么我们必须手动将它绑定到那个 class (这只是语法糖,就像你指出的那样它是一个函数...)
更深入:当您像这样创建构造函数时:
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
上下文...它会自动冒泡到父级,无需绑定...
让我们从 this
和 .bind
的行为都不是特定于 React 的事实开始。所以,为了简单起见,让我们暂时忘记 React,只看一些普通的 JS 代码(不用担心!我们稍后会回到 React)。
现在让我们从头开始,这是一个object:
{
username: "KamiFightingSpirit"
}
看起来很简单,但 object 的值可能是任何值(数组、其他 object、函数等)。我们还添加一个函数:
{
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
等于函数调用之前的点之前的 object。在这种情况下,object 是 nestedObjects.sampleUser
的值。
再次注意,执行上下文不像作用域那样工作。如果使用的 属性 在点之前的 object 中丢失,则不会执行 parent 中是否存在的检查。这是相同的示例,但缺少 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
强制创建一个新的 object(因此,一个执行内容)。
然而,以这种方式创建 objects 是相当危险的。如果您在没有 new
的情况下调用相同的函数,它将执行但不会创建新的 object,并且 this
将被评估为 window
object。这样我们就可以有效地将 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!
太棒了,有用!请注意,我们没有使用 no .bind
。在这种情况下没有必要,因为我们在 class.
现在,让我们回到 React。在 React 中,我们倾向于将 functions(not 实例)从 parents 传递到 children。正如我之前所说,classes 非常像智能构造函数。因此,让我们首先看看如果我们对每个组件使用构造函数而不是 classes,我们的代码会是什么样子。
如果我们扔掉 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
被评估为全局 object,在浏览器中为 Window
。
现在,让我们回到 classes 并写一些非常相似的东西:
// 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();
因为 class 是 use strict mode by default,上面的例子在 f()
之前什么都看不到,将 this
计算为未定义,尝试将新的 属性 分配给未定义并抛出错误不这样做。
因此,为了避免这种情况,我们需要使用 .bind
或类似的函数以确保它始终在正确的上下文中执行。
.bind
到底是做什么的? Some internal black magic。要完全理解它,您可能需要深入研究 JS 编译器代码(通常用 C/C++ 编写)。
不过,还有一个更简单的选择。 MDN(它是一个很棒的网站)offers you ready-to-use polyfills 基本上展示了 .bind
在 vanilla JS 中可能是如何 re-written。如果你仔细观察,你会注意到这两个 polyfill 都只是包装了对 .apply
或 .call
的调用。所以,有趣的部分其实不是"disclosed".
我猜这是因为内部 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 中。虽然我们可以用 Object.defineProperty
更好地隐藏它,但它仍然会在那里。
这里有一个更复杂的方法,无论如何都可以改变原始的 object,但只能按照你期望的方式改变(尽管这个实现并不比前一个好。这只是一个假设的例子):
// 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}`);
希望对您有所帮助!