无论如何,是否有一个变量设置为等于每次访问该变量时都会调用的函数

时间:2019-07-13 21:31:42

标签: javascript

我试图通过React重新创建'useState'钩子,这是一个愚蠢的有趣的个人练习,但是从我的'global state'访问变量时遇到麻烦。我知道这在react之外没有实际用途,但是我只是认为无论如何都要尝试一下。

目前,我在下面有以下实现,但是由于解构变量仅是第一次设置,并且在使用相应的setter时不会更新,因此它将始终返回相同的变量。我完全理解为什么会这样,但是我不确定是否有办法使它完全起作用,或者这是一个失败的原因。重构后的设置器确实会更新全局状态,但是正如前面提到的那样,变量当然不会再访问全局状态,因为它只是设置了初始时间。

let PROP_ID = 0;
let GLOBAL_STATE = {};

const useState = prop => {
  const id = PROP_ID++;
  GLOBAL_STATE[id] = prop;

  return [
    (() => {
      return GLOBAL_STATE[id];
    })(),
    function(nv) {
      GLOBAL_STATE[id] = nv;
    }
  ];
};

const [userName, setUserName] = useState("Chris");
const [favCol, setFavCol] = useState("red");

console.log(GLOBAL_STATE);
console.log(userName);

setUserName("Bob");

console.dir(GLOBAL_STATE);
console.log(userName);

我想知道是否有一种方法可以将解构后的引用变量设置为等于某种函数,当引用该变量时,该函数将始终被调用以从全局状态获取新变量。

4 个答案:

答案 0 :(得分:1)

这是一个非常有趣的问题。

简短的答案是:不,那不可能*

长答案

长的答案是JavaScript如何处理基元和对象。在赋值期间复制原始值(userName在这里:const [userName, setUserName] = useState("Chris");是字符串),而在对象的情况下将复制引用。

为了使用它,我带来了类似的东西(提醒您,这不是解决您挑战的方法,而是对我的回答的解释):

let PROP_ID = 0;
let GLOBAL_STATE = {};

const useState = prop => {
  const id = PROP_ID++;
  GLOBAL_STATE[id] = {
      _value: prop,
      [Symbol.toPrimitive]() { return this._value },
  }

  const tuple = [
    GLOBAL_STATE[id],
    (nv) => GLOBAL_STATE[id]._value = nv,
  ];

  return tuple;
};

const [userName, setUserName] = useState("Chris");
const [favCol, setFavCol] = useState("red");

console.log(GLOBAL_STATE);
console.log(userName);

console.log('set user name to:', setUserName("Bob"));

console.dir(GLOBAL_STATE);
console.log('' + userName);

GLOBAL_STATE条目现在是对象,因此在调用useState之后对它进行重构时,仅会更改引用。然后更新该对象内的更改数据,但我们首先分配的内容仍然存在。

我添加了Symbo.toPrimitive属性,该属性将对象强制为原始值,但遗憾的是,这无法单独使用。仅当以'' + userName运行时。这意味着它的行为与您预期的不同。此时,我停止了实验。

反应

我去了Facebook的Github,试图追踪他们在做什么,但由于进口而放弃了进口。因此,我将基于Hooks API行为在此处进行有根据的猜测。我认为您的实现非常忠实于原始版本。我们在函数中使用useState,并且该值在此处不变。仅当状态更改后,组件才用新值重新渲染,该值又被分配并且不会更改。


*我将很高兴欢迎任何人证明这一观点错误。

答案 1 :(得分:1)

我认为您在这里错过了一个难题。

反应挂钩取决于其调用在给定功能组件中的位置。如果没有封装功能,则将删除钩子提供的状态的有用性,因为在您的示例中它们仅被调用一次,因此解构语法中的引用永远不会像您观察到的那样得到更新。

让它们在函数的上下文中工作。

const { component, useState } = (function () {
  const functions = new WeakMap()
  const stack = []
  let hooks
  let index

  function component (fn) {
    return function (...args) {
      try {
        stack.push({ hooks, index })
        hooks = functions.get(fn)
        index = 0

        if (!hooks) {
          functions.set(fn, hooks = [])
        }

        return fn.apply(this, args)
      } finally {
        ({ hooks, index } = stack.pop())
      }
    }
  }

  function useState (initialValue) {
    const hook = index++

    if (hook === hooks.length) {
      hooks.push(initialValue)
    }

    return [
      hooks[hook],
      function setState (action) {
        if (typeof action === 'function') {
          hooks[hook] = action(hooks[hook])
        } else {
          hooks[hook] = action
        }
      }
    ]
  }

  return { component, useState }
})()

const fibonacci = component(function () {
  const [a, setA] = useState(1)
  const [b, setB] = useState(1)

  setA(b)
  setB(a + b)

  return a
})

const sequence = component(function () {
  const [text, setText] = useState('')

  setText(
    text.length === 0
    ? fibonacci().toString()
    : [text, fibonacci()].join()
  )

  return text
})

for (let i = 0; i < 20; i++) {
  console.log(sequence())
}

此处的stack变量使我们可以嵌套有状态函数调用,而hooks变量按{{的当前执行的component中的位置跟踪现有的钩子状态。 1}}。

该实现似乎过于复杂,但是stackcomponent()的重点是部分模仿React框架如何处理功能组件。这仍然比React的工作方式简单得多,因为我们将相同功能的所有调用都视为功能组件的相同实例。

另一方面,在React中,特定功能可以用于几个不同的实例,这些实例可以基于多种因素(例如虚拟DOM层次结构中的位置,stack)相互区分和key道具等,因此比这要复杂得多。


在我看来,您只是想让您的示例正常工作。为此,您需要做的就是将变量更改为getter函数:

ref

比您拥有的要简单得多,不需要任何全局变量即可工作。

如果手动吸气剂似乎不太方便,那么您就无法进行结构分解,但是您可以实现一种几乎易于使用的方法:

const useState = state => [
  () => state,
  value => { state = value }
];

const [getUserName, setUserName] = useState('Chris');
const [getFavCol, setFavCol] = useState('red');

console.log(getUserName());
setUserName('Bob');
console.log(getUserName());

答案 2 :(得分:0)

接下来的事情怎么样...

let PROP_ID = 0;
let GLOBAL_STATE = {};

const useState = (varName, prop) => {
  const id = PROP_ID++;
  GLOBAL_STATE[id] = prop;

  Object.defineProperty(window, varName, {
    get: function(){
      return GLOBAL_STATE[id];
    }
  });

  return ((nv) => {
      GLOBAL_STATE[id] = nv;
    });
};

const setUserName = useState("userName", "Chris");
const setFavCol = useState("favCol", "red");

console.log(GLOBAL_STATE);
console.log(userName);

setUserName("Bob");

console.dir(GLOBAL_STATE);
console.log(userName);

请注意,我对您的界面做了一些更改,以便您必须将变量的名称传递给useState函数。似乎有点麻烦,但在这种情况下,可以按照您的示例在全局范围(即“窗口”)上配置吸气剂,这可能不是最佳实践。

答案 3 :(得分:0)

有一个解决方案,但前提是您可以使用肮脏的hack方法。

以下方法使用with语句和包含自定义Proxyget handler,并且需要对象分解语法,以便从setter函数的属性键确定变量名。 :

// initialize useState() hook with independent scope
function createHook (scope = Object.create(null)) {
  const setter = /^set([A-Z][^\W_]*)$/;

  function useState (initialValue) {
    // return proxy from useState() so that object destructuring syntax
    // can be used to get variable name and initialize setter function
    return new Proxy(scope, {
      get (target, propertyKey) {
        if (!setter.test(propertyKey)) {
          throw new TypeError(`Invalid setter name '${propertyKey}'`);
        }

        // get variable name from property key of setter function
        const [, name] = propertyKey.match(setter);
        const key = name[0].toLowerCase() + name.slice(1);

        // support updater callback
        const setState = value => {
          target[key] = (
            typeof value === 'function'
            ? value(target[key])
            : value
          );
        };

        // initialize state
        setState(initialValue);

        // return setter as property value
        return setState;
      }
    });
  }

  return { scope, useState };
}

// example usage with a little magic
{
  const { scope, useState } = createHook();
  const { setFoo } = useState('bar');

  console.log(scope.foo);

  setFoo(42);

  console.log(scope.foo);
}

// example use with more magic
const { scope, useState } = createHook();

with (scope) {
  const { setUserName } = useState('Chris');
  const { setFavCol } = useState('red');

  console.log(userName, favCol);

  setUserName('Bob');
  setFavCol(color => `dark${color}`);

  console.log(userName, favCol);
}

通过滥用隐式全局变量,以下用法最终与Jon Trent's answer非常相似:

function createHook(e=Object.create(null)){var t=/^set([A-Z][^\W_]*)$/;return{scope:e,useState:n=>new Proxy(e,{get(e,r){if(!t.test(r))throw new TypeError(`Invalid setter name '${r}'`);var[,o]=r.match(t),c=o[0].toLowerCase()+o.slice(1),s=t=>{e[c]="function"==typeof t?t(e[c]):t};return s(n),s}})}}

const { useState } = createHook(window);
const { setUserName } = useState('Chris');
const { setFavCol } = useState('red');

console.log(userName, favCol);

setUserName('Bob');
setFavCol(color => `dark${color}`);

console.log(userName, favCol);