Nic*_*owr 6 javascript node.js reactjs react-context
我想在 Nodejs 中复制 React Context 的行为,但我正在努力解决它。
在反应,通过创建只有一个背景下,我可以提供,在我的组件占用不同的值,取决于value给予<Provider/>。所以以下工作:
const MyContext = React.createContext(0);
const MyConsumer = () => {
return (
<MyContext.Consumer>
{value => {
return <div>{value}</div>
}}
</MyContext.Consumer>
)
}
const App = () =>
<React.Fragment>
<MyContext.Provider value={1}>
<MyConsumer/>
</MyContext.Provider>
<MyContext.Provider value={2}>
<MyConsumer/>
</MyContext.Provider>
</React.Fragment>;
ReactDOM.render(
<App/>,
document.getElementById("react")
);Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="react"></div>Run Code Online (Sandbox Code Playgroud)
但是我不知道如何在 Nodejs 中实现它。我已经查看了 React Context 的源代码,但它没有多大帮助......这是我到目前为止所得到的:
// context.js
export const createContext = (defaultValue: number) => {
const context = {
value: defaultValue,
withContext: null,
useContext: null,
};
function withContext(value: number, callback: (...args: any[]) => any) {
context.value = value;
return callback;
}
function useContext() {
return context;
}
context.withContext = withContext;
context.useContext = useContext;
return context;
};
// functions.js
import { context } from "./index";
export function a() {
const result = context.useContext();
console.log(result);
}
export function b() {
const result = context.useContext();
console.log(result);
}
// index.js
import { createContext } from "./context";
import { a, b } from "./functions";
export const context = createContext(0);
const foo = context.withContext(1, a);
const bar = context.withContext(2, b);
console.log("foo", foo());
console.log("bar", bar());
Run Code Online (Sandbox Code Playgroud)
显然,value被覆盖并被2记录两次。
任何帮助都感激不尽!
She*_*aff 11
NodeJS 提出了一个新的内置函数来实现这一点:异步上下文跟踪。
感谢@Emmanuel Meric de Bellefon 指出了这一点。
如果您只需要它用于同步代码,您可以做一些相对简单的事情。您所需要的只是指定边界在哪里。
在 React 中,您可以使用 JSX 来完成此操作
<Context.Provider value={2}>
<MyComponent />
</Context.Provider>
Run Code Online (Sandbox Code Playgroud)
在这个例子中, 的值Context将是2forMyComponent但在它的范围之外<Context.Provider>将是之前的值。
如果我用普通 JS 翻译它,我可能希望它看起来像这样:
<Context.Provider value={2}>
<MyComponent />
</Context.Provider>
Run Code Online (Sandbox Code Playgroud)
在此示例中,我希望 的值context在其2范围内myFunction但在其范围之外context.provider()将是之前设置的任何值。
最基本的是,这可以通过全局对象来解决
const myFunctionWithContext = context.provider(2, myFunction)
myFunctionWithContext('an argument')
Run Code Online (Sandbox Code Playgroud)
现在我们知道污染全局范围globalThis或window. 因此我们可以使用Symbol来代替,以确保不会出现任何命名冲突:
// 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()Run Code Online (Sandbox Code Playgroud)
然而,即使这个解决方案永远不会与全局范围产生冲突,它仍然不理想,并且不能很好地适应多个上下文。那么让我们创建一个“上下文工厂”模块:
const context = Symbol()
globalThis[context] = 'initial value'
function a() {
console.log(`context value in a: ${globalThis[context]}`)
}
a()Run Code Online (Sandbox Code Playgroud)
现在,只要您仅将其用于同步代码,只需在回调之前覆盖一个值并在(in provider)之后重置它即可。
如果您想像 React 一样构建代码,那么使用几个单独的模块会如下所示:
// 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()Run Code Online (Sandbox Code Playgroud)
如果是异步函数,上面提出的解决方案将不起作用b(),因为一旦b返回,上下文值就会重置为其值a()(这就是provider工作原理)。例如:
// 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()Run Code Online (Sandbox Code Playgroud)
到目前为止,我还没有真正了解如何正确管理异步函数的问题,但我打赌可以通过使用Symbol,this和来完成Proxy。
this通过context在开发同步代码解决方案时,我们发现我们可以“负担得起”向“不属于我们的”对象添加属性,只要我们使用Symbol键来执行此操作(就像我们globalThis在第一个示例中所做的那样)例子)。我们还知道函数总是使用隐式this参数调用,该参数是
globalThis),obj.func(), inside func, thiswill be 时obj).bind,.call或 时.apply)此外,javascript 允许我们将代理定义为对象与使用该对象的任何脚本之间的接口。在 a 中Proxy,我们可以定义一组陷阱,每个陷阱将处理使用我们的对象的特定方式。对于我们的问题来说,有趣的是apply,它捕获函数调用并让我们访问this该函数将被调用的对象。
知道了这一点,我们可以“增强”this使用上下文提供者调用的函数context.provider(value, myFunction),并使用引用上下文的符号:
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()Run Code Online (Sandbox Code Playgroud)
Reflect将调用设置target为且参数来自的函数thisscopeargumentsList
只要我们“存储”的内容this允许我们获取范围的“当前”值(context.provider()调用的值),那么我们应该能够从内部访问该值myFunction,并且不需要设置/重置就像我们为同步解决方案所做的那样的唯一对象。
将所有这些放在一起,这是对类似反应上下文的异步解决方案的初步尝试。然而,与原型链不同的是,this当从另一个函数中调用一个函数时,它不会自动继承。因此,以下解决方案中的上下文只能在 1 级函数调用中幸存:
{
apply: (target, thisArg = {}, argumentsList) => {
const scope = Object.assign({}, thisArg, {[id]: key}) // augment `this`
return Reflect.apply(target, scope, argumentsList) // call function
}
}
Run Code Online (Sandbox Code Playgroud)
要使上下文在另一个函数调用中的函数调用中幸存下来,一个潜在的解决方案可能是必须将上下文显式转发到任何函数调用(这可能很快就会变得很麻烦)。从上面的例子,c()将更改为:
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()Run Code Online (Sandbox Code Playgroud)
其中myContext.forwarda 只是consumer获取值,然后直接 aprovider将其传递:
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'
}
Run Code Online (Sandbox Code Playgroud)
将其添加到我们之前的解决方案中:
function forward(scope, callback) {
const value = consumer(scope)
return provider(value, callback)
}
Run Code Online (Sandbox Code Playgroud)
现在我陷入困境......我愿意接受想法!
你的“在 NodeJS 中复制 React 的 Context”的目标有点模糊。来自 React 文档:
Context 提供了一种通过组件树传递数据的方法,而无需在每个级别手动向下传递 props。
NodeJS 中没有组件树。我能想到的最接近的类比(也基于您的示例)是调用堆栈。此外,如果值发生变化,React 的 Context 还会导致树的重新渲染。我不知道这在 NodeJS 中意味着什么,所以我很乐意忽略这个方面。
因此,我假设您本质上是在寻找一种方法,使值可以在调用堆栈中的任何位置访问,而不必将其作为参数传递到堆栈中在函数之间传递到堆栈中。
我建议你使用所谓的连续本地存储之一来实现这一目标。他们使用的模式与您尝试做的略有不同,但可能没问题。
我最喜欢的是CLS Hooked(无隶属关系)。它利用 NodeJS 的 async_hooks 系统来保留提供的上下文,即使堆栈中存在异步调用也是如此。上次发布是 4 年前,它仍然按预期工作。
我使用 CLS Hooked 重写了您的示例,尽管我认为这不是最好/最直观的使用方式。我还添加了一个额外的函数调用来演示可以覆盖值(即创建某种子上下文)。最后,有一个明显的区别 - 上下文现在必须有一个 ID。如果你想坚持使用这种 React Contexty 模式,你可能不得不接受它。
// context.js
import cls from "cls-hooked";
export const createContext = (contextID, defaultValue) => {
const ns = cls.createNamespace(contextID);
return {
provide(value, callback) {
return () =>
ns.run(() => {
ns.set("value", value);
callback();
});
},
useContext() {
return ns.active ? ns.get("value") : defaultValue;
}
};
};
Run Code Online (Sandbox Code Playgroud)
// my-context.js
// your example had a circular dependency problem
// the context has to be created in a separate file
import { createContext } from "./context";
export const context = createContext("my-context", 0);
Run Code Online (Sandbox Code Playgroud)
// zz.js
import { context } from "./my-context";
export const zz = function () {
console.log("zz", context.useContext());
};
Run Code Online (Sandbox Code Playgroud)
// functions.js
import { context } from "./my-context";
import { zz } from "./zz";
export const a = function () {
const zzz = context.provide("AAA", zz);
zzz();
const result = context.useContext();
console.log("a", result);
};
export const b = function () {
const zzz = context.provide("BBB", zz);
zzz();
const result = context.useContext();
console.log("b", result);
};
Run Code Online (Sandbox Code Playgroud)
// index.js
import { context } from "./c";
import { a, b } from "./functions";
const foo = context.provide(1, a);
const bar = context.provide(2, b);
console.log("default value", context.useContext());
foo();
bar();
Run Code Online (Sandbox Code Playgroud)
运行node index日志:
default value 0
zz AAA
a 1
zz BBB
b 2
Run Code Online (Sandbox Code Playgroud)
如果堆栈中发生各种异步调用,这也将起作用。
我的方法有点不同。我并没有尝试复制 React 的 Context,它也有一个限制,因为它总是绑定到单个值。
// cls.ts
import cls from "cls-hooked";
export class CLS {
constructor(private readonly NS_ID: string) {}
run<T>(op: () => T): T {
return (cls.getNamespace(this.NS_ID) || cls.createNamespace(this.NS_ID)).runAndReturn(op);
}
set<T>(key: string, value: T): T {
const ns = cls.getNamespace(this.NS_ID);
if (ns && ns.active) {
return ns.set(key, value);
}
}
get(key: string): any {
const ns = cls.getNamespace(this.NS_ID);
if (ns && ns.active) {
return ns.get(key);
}
}
}
Run Code Online (Sandbox Code Playgroud)
// operations-cls.ts
import { CLS } from "./cls";
export const operationsCLS = new CLS("operations");
Run Code Online (Sandbox Code Playgroud)
// consumer.ts
import { operationsCLS } from "./operations-cls";
export const consumer = () => {
console.log(operationsCLS.get("some-value")); // logs 123
};
Run Code Online (Sandbox Code Playgroud)
// app.ts
import { operationsCLS } from "./operations-cls";
import { consumer } from "./consumer";
cls.run(async () => {
cls.set("some-value", 123);
consumer();
});
Run Code Online (Sandbox Code Playgroud)
我更喜欢将 CLS 视为魔法,因为在没有我干预的情况下它总是运行良好,所以不能在这里发表太多评论,抱歉:]
| 归档时间: |
|
| 查看次数: |
205 次 |
| 最近记录: |