定义期望的行为
如果您只需要 同步 代码,您可以做一些相对简单的事情。您只需指定边界的位置即可。
在 React 中,您可以使用 JSX 来完成此操作
<Context.Provider value={2}>
<MyComponent />
</Context.Provider>
在此示例中,Context 的值将是 2,MyComponent 但在 <Context.Provider> 的范围之外,它将是之前的值。
如果我要在原版 JS 中翻译它,我可能希望它看起来像这样:
const myFunctionWithContext = context.provider(2, myFunction)
myFunctionWithContext('an argument')
在本例中,我希望context 的值在myFunction 内为2,但在context.provider() 的范围之外,它将是之前设置的任何值。
如何解决问题
最基本的,这可以通过一个全局对象来解决
// we define a "context"
globalThis.context = 'initial value'
function a() {
// we can access the context
const currentValue = globalThis.context
console.log(`context value in a: ${currentValue}`)
// we can modify the context for the "children"
globalThis.context = 'value from a'
b()
// we undo the modification to restore the context
globalThis.context = currentValue
}
function b() {
console.log(`context value in b: ${globalThis.context}`)
}
a()
现在我们知道污染全局范围globalThis 或window 绝不是明智之举。所以我们可以改用Symbol,以确保不会有任何命名冲突:
const context = Symbol()
globalThis[context] = 'initial value'
function a() {
console.log(`context value in a: ${globalThis[context]}`)
}
a()
但是,即使此解决方案永远不会与全局范围发生冲突,它仍然不理想,并且不能很好地适应多种环境。所以让我们做一个“上下文工厂”模块:
// in createContext.js
const contextMap = new Map() // all of the declared contexts, one per `createContext` call
/* export default */ function createContext(value) {
const key = Symbol('context') // even though we name them the same, Symbols can never conflict
contextMap.set(key, value)
function provider(value, callback) {
const old = contextMap.get(key)
contextMap.set(key, value)
callback()
contextMap.set(key, old)
}
function consumer() {
return contextMap.get(key)
}
return {
provider,
consumer,
}
}
// in index.js
const contextOne = createContext('initial value')
const contextTwo = createContext('other context') // we can create multiple contexts without conflicts
function a() {
console.log(`value in a: ${contextOne.consumer()}`)
contextOne.provider('value from a', b)
console.log(`value in a: ${contextOne.consumer()}`)
}
function b() {
console.log(`value in b: ${contextOne.consumer()}`)
console.log(`value in b: ${contextTwo.consumer()}`)
}
a()
现在,只要您仅将其用于同步代码,只需在回调之前覆盖一个值并在之后(在 provider 中)重置它即可工作。
同步码的解决方案
如果您想像在 react 中那样构建您的代码,下面是几个单独的模块的样子:
// in createContext.js
const contextMap = new Map()
/* export default */ function createContext(value) {
const key = Symbol('context')
contextMap.set(key, value)
return {
provider(value, callback) {
const old = contextMap.get(key)
contextMap.set(key, value)
callback()
contextMap.set(key, old)
},
consumer() {
return contextMap.get(key)
}
}
}
// in myContext.js
/* import createContext from './createContext.js' */
const myContext = createContext('initial value')
/* export */ const provider = myContext.provider
/* export */ const consumer = myContext.consumer
// in a.js
/* import { provider, consumer } from './myContext.js' */
/* import b from './b.js' */
/* export default */ function a() {
console.log(`value in a: ${consumer()}`)
provider('value from a', b)
console.log(`value in a: ${consumer()}`)
}
// in b.js
/* import { consumer } from './myContext.js' */
/* export default */ function b() {
console.log(`value in b: ${consumer()}`)
}
// in index.js
/* import a from './a.js' */
a()
更进一步:异步代码的问题
如果b() 是异步函数,上述解决方案将不起作用,因为一旦b 返回,上下文值就会重置为其在a() 中的值(这就是provider 的工作方式)。例如:
const contextMap = new Map()
function createContext(value) {
const key = Symbol('context')
contextMap.set(key, value)
function provider(value, callback) {
const old = contextMap.get(key)
contextMap.set(key, value)
callback()
contextMap.set(key, old)
}
function consumer() {
return contextMap.get(key)
}
return {
provider,
consumer
}
}
const { provider, consumer } = createContext('initial value')
function a() {
console.log(`value in a: ${consumer()}`)
provider('value from a', b)
console.log(`value in a: ${consumer()}`)
}
async function b() {
await new Promise(resolve => setTimeout(resolve, 1000))
console.log(`value in b: ${consumer()}`) // we want this to log 'value from a', but it logs 'initial value'
}
a()
到目前为止,我还没有真正了解如何正确管理异步函数的问题,但我敢打赌,可以通过使用 Symbol、this 和 Proxy 来完成。
使用this 传递context
在为同步代码开发解决方案时,我们已经看到,只要我们使用 Symbol 键,我们就可以“负担”向“不属于我们的”对象添加属性(例如我们在第一个示例中使用了globalThis)。我们还知道,函数总是使用隐含的this 参数调用
- 全局范围 (
globalThis),
- 父作用域(调用
obj.func()时,在func内,this将是obj)
- 任意范围对象(使用
.bind、.call 或.apply 时)
- 在某些情况下,任意原始值(仅在严格模式下才有可能)
此外,javascript 允许我们将Proxy 定义为对象与使用该对象的任何脚本之间的接口。在Proxy 中,我们可以定义一组traps,每个都将处理使用我们对象的特定方式。对我们的问题感兴趣的是apply,它会捕获函数调用,并允许我们访问将调用该函数的this。
知道了这一点,我们可以使用上下文提供者context.provider(value, myFunction) 调用我们的函数的this,并使用引用我们上下文的符号:
{
apply: (target, thisArg = {}, argumentsList) => {
const scope = Object.assign({}, thisArg, {[id]: key}) // augment `this`
return Reflect.apply(target, scope, argumentsList) // call function
}
}
Reflect 将调用函数target,其中this 设置为scope,参数来自argumentsList
只要我们“存储”在this 中的内容允许我们获取范围的“当前”值(调用context.provider() 的值),那么我们应该能够从@987654376 中访问该值@ 并且我们不需要像同步解决方案那样设置/重置唯一对象。
第一个异步解决方案:浅上下文
综上所述,这是针对类似反应的上下文的异步解决方案的初步尝试。但是,与prototype chain 不同,this 在从另一个函数中调用函数时不会自动继承。因此,以下解决方案中的上下文仅在 1 级函数调用中存在:
function createContext(initial) {
const id = Symbol()
function provider(value, callback) {
return new Proxy(callback, {
apply: (target, thisArg, argumentsList) => {
const scope = Object.assign({}, thisArg, {[id]: value})
return Reflect.apply(target, scope, argumentsList)
}
})
}
function consumer(scope = {}) {
return id in scope ? scope[id] : initial
}
return {
provider,
consumer,
}
}
const myContext = createContext('initial value')
function a() {
console.log(`value in a: ${myContext.consumer(this)}`)
const bWithContext = myContext.provider('value from a', b)
bWithContext()
const cWithContext = myContext.provider('value from a', c)
cWithContext()
console.log(`value in a: ${myContext.consumer(this)}`)
}
function b() {
console.log(`value in b: ${myContext.consumer(this)}`)
}
async function c() {
await new Promise(resolve => setTimeout(resolve, 200))
console.log(`value in c: ${myContext.consumer(this)}`) // works in async!
b() // logs 'initial value', should log 'value from a' (the same as "value in c")
}
a()
第二种异步解决方案:上下文转发
上下文在另一个函数调用中的函数调用中存在的潜在解决方案可能是必须显式将上下文转发给任何函数调用(这可能很快变得很麻烦)。从上面的示例中,c() 将更改为:
async function c() {
await new Promise(resolve => setTimeout(resolve, 200))
console.log(`value in c: ${myContext.consumer(this)}`)
const bWithContext = myContext.forward(this, b)
bWithContext() // logs 'value from a'
}
其中myContext.forward 只是一个consumer 来获取值,然后直接是一个provider 来传递它:
function forward(scope, callback) {
const value = consumer(scope)
return provider(value, callback)
}
将此添加到我们之前的解决方案中:
function createContext(initial) {
const id = Symbol()
function provider(value, callback) {
return new Proxy(callback, {
apply: (target, thisArg, argumentsList) => {
const scope = Object.assign({}, thisArg, {[id]: value})
return Reflect.apply(target, scope, argumentsList)
}
})
}
function consumer(scope = {}) {
return id in scope ? scope[id] : initial
}
function forward(scope, callback) {
const value = consumer(scope)
return provider(value, callback)
}
return {
provider,
consumer,
forward,
}
}
const myContext = createContext('initial value')
function a() {
console.log(`value in a: ${myContext.consumer(this)}`)
const bWithContext = myContext.provider('value from a', b)
bWithContext()
const cWithContext = myContext.provider('value from a', c)
cWithContext()
console.log(`value in a: ${myContext.consumer(this)}`)
}
function b() {
console.log(`value in b: ${myContext.consumer(this)}`)
}
async function c() {
await new Promise(resolve => setTimeout(resolve, 200))
console.log(`value in c: ${myContext.consumer(this)}`)
const bWithContext = myContext.forward(this, b)
bWithContext()
}
a()
没有显式转发的异步函数上下文
现在我被困住了……我对想法持开放态度!