为什么顺序调用对 React Hooks 很重要?

栏目: IOS · Android · 发布时间: 5年前

内容简介:在 React Conf 2018 上,React团队提出了Hooks提案。如果你想知道什么是 Hooks,及它能解决什么问题,查看我们的讲座(介绍),理解React Hooks(常见的误解)。最初你可能会不喜欢 Hooks:

在 React Conf 2018 上,React团队提出了Hooks提案。

如果你想知道什么是 Hooks,及它能解决什么问题,查看我们的讲座(介绍),理解React Hooks(常见的误解)。

最初你可能会不喜欢 Hooks:

为什么顺序调用对 React Hooks 很重要?

它们就像一段音乐,只有经过几次用心聆听才会慢慢爱上:

为什么顺序调用对 React Hooks 很重要?

当你阅读文档时,不要错过关于最重要的部分——创造属于你自己的Hooks!太多的人纠结于反对我们的观点(class学习成本高等)以至于错过了Hooks更重要的一面,Hooks像 functional mixins ,可以让你创造和搭建属于自己的Hook。

Hooks受启发于一些现有技术,但在 Sebastian 和团队分享他的想法之后,我才知道这些。不幸的是,这些API和现在在用的之间的关联很容易被忽略,通过这篇文章,我希望可以帮助更多的人理解 Hooks提案中争议较大的点。

接下来的部分需要你知道 Hook API 的 useState 和如何写自定义Hook。如果你还不懂,可以看看早先的链接。

(免责说明:文章的观点仅代表个人想法,与React团队无关。话题大且复杂,其中可能有错误的观点。)

一开始当你学习时你可能会震惊,Hooks 重渲染时是依赖于固定顺序调用的,这里有说明。

这个决定显然是有争议的,这也是为什么会有人反对我们的提案。我们会在恰当的时机发布这个提案,当我们觉得文档和讲座可以足够好的描绘它时。

如果你在关注 Hooks API 的某些点,我建议你阅读下 Sebastian对 1000+ 评论RFC的 全部回复 足够透澈但内容非常多,我可能会将评论中的每一段都变成自己的博客文章。(事实上,我已经做过一次!)

我今天要关注一个具体部分。你可能还记得,每个 Hook 可以在组件里被多次使用,例如,我们可以用 useState 声明多个state:

function Form() {
  const [name, setName] = useState('Mary');              // State variable 1
  const [surname, setSurname] = useState('Poppins');     // State variable 2
  const [width, setWidth] = useState(window.innerWidth); // State variable 3

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  });

  function handleNameChange(e) {
    setName(e.target.value);
  }

  function handleSurnameChange(e) {
    setSurname(e.target.value);
  }

  return (
    <>
      <input value={name} onChange={handleNameChange} />
      <input value={surname} onChange={handleSurnameChange} />
      <p>Hello, {name} {surname}</p>
      <p>Window width: {width}</p>
    </>
  );
}
复制代码

注意我们用数组解构语法来命名 useState() 返回的 state 变量,但这些变量不会连接到React组件上。相反,这个例子中, React将 name 视为“第一个state变量”, surname 视为“第二个state变量”,以此类推 。它们在重新渲染时用 顺序调用 来保证被正确识别。这篇文章详细的解释了原因。

表面上看,依赖于顺序调用只是 感觉有问题 ,直觉是一个有用的信号,但它有时会误导我们 —— 特别是当我们还没有完全消化困惑的问题。 这篇文章,我会提到几个经常有人提出修改Hooks的方案,及它们存在的问题

这篇文章不会详尽无遗,如你所见,我们已经看过十几种至数百种不同的替代方案,我们一直在 考虑 替换组件API。

诸如此类的博客很棘手,因为即使你涉及了一百种替代方案,也有人强行提出一个来:“哈哈,你没有想到 这个 ”!

在实践中,不同替代方案提到的问题会有很多重复,我不会列举 所有 建议的API(这需要花费数月时间),而是通过几个具体示例展示最常见的问题,更多的问题就考验读者举一反三的能力了。

这不是说 Hooks 就是完美的 ,但是一旦你了解其他解决方案的缺陷,你可能会发现 Hooks 的设计是有道理的。

缺陷 #1:无法提取 custom hook

出乎意料的是,大多数替代方案完全没有提到custom hooks。可能是因为我们在“motivation”文档中没有足够强调 custom hooks,不过在弄懂 Hooks 基本原理之前,这是很难做到的。就像鸡和蛋问题,但很大程度上 custom hooks 是提案的重点。

例如:有个替代方案是限制一个组件调用多次 useState() ,你可以把 state 放在一个对象里,这样还可以兼容class不是更好吗?

function Form() {
  const [state, setState] = useState({
    name: 'Mary',
    surname: 'Poppins',
    width: window.innerWidth,
  });
  // ...
}
复制代码

要清楚,Hooks 是允许这种风格写的,你不必将state拆分成一堆state变量(请参阅参见问题解答中的建议)。

但支持多次调用 useState() 的关键在于,你可以从组件中提取出部分有状态逻辑(state + effect)到 custom hooks 中,同时可以单独使用本地 state 和 effects:

function Form() {
  // 在组件内直接定义一些state变量
  const [name, setName] = useState('Mary');
  const [surname, setSurname] = useState('Poppins');

  // 我们将部分state和effects移至custom hook
  const width = useWindowWidth();
  // ...
}

function useWindowWidth() {
  // 在custom hook内定义一些state变量
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    // ...
  });
  return width;
}
复制代码

如果你只允许每个组件调用一次 useState() ,你将失去用 custom hook 引入 state 能力,这就是 custom hooks 的关键。

缺陷 #2: 命名冲突

一个常见的建议是让组件内 useState() 接收一个唯一标识key参数(string等)区分 state 变量。

和这主意有些出入,但看起来大致像这样:

// :warning: This is NOT the React Hooks API
function Form() {
  // 我们传几种state key给useState()
  const [name, setName] = useState('name');
  const [surname, setSurname] = useState('surname');
  const [width, setWidth] = useState('width');
  // ...
复制代码

这试图摆脱依赖顺序调用(显示key),但引入了另外一个问题 —— 命名冲突。

当然除了错误之外,你可能无法在同一个组件调用两次 useState('name') ,这种偶然发生的可以归结于其他任意bug,但是,当你使用一个 custom hook 时,你总会遇到想添加或移除state变量和effects的情况。

这个提议中,每当你在 custom hook 里添加一个新的 state 变量时,就有可能破坏使用它的任何组件(直接或者间接),因为 可能已经有同名的变量 位于组件内。

这是一个没有针对变化而优化的API,当前代码可能看起来总是“优雅的”,但应对需求变化时十分脆弱,我们应该从错误中吸取教训。

实际中 Hooks 提案通过依赖顺序调用来解决这个问题:即使两个 Hooks 都用 name state变量,它们也会彼此隔离,每次调用 useState() 都会获得独立的 “内存单元”。

我们还有其他一些方法可以解决这个缺陷,但这些方法也有自身的缺陷。让我们加深探索这个问题。

缺陷 #3:同一个 Hook 无法调用两次

useState “加key”的另一种衍生提案是使用像Symbol这样的东西,这样就不冲突了对吧?

// :warning: This is NOT the React Hooks API
const nameKey = Symbol();
const surnameKey = Symbol();
const widthKey = Symbol();

function Form() {
  // 我们传几种state key给useState()
  const [name, setName] = useState(nameKey);
  const [surname, setSurname] = useState(surnameKey);
  const [width, setWidth] = useState(widthKey);
  // ...
复制代码

这个提案看上去对提取出来的 useWindowWidth Hook 有效:

// :warning: This is NOT the React Hooks API
function Form() {
  // ...
  const width = useWindowWidth();
  // ...
}

/*********************
 * useWindowWidth.js *
 ********************/
const widthKey = Symbol();
 
function useWindowWidth() {
  const [width, setWidth] = useState(widthKey);
  // ...
  return width;
}
复制代码

但如果尝试提取出来的 input handling,它会失败:

// :warning: This is NOT the React Hooks API
function Form() {
  // ...
  const name = useFormInput();
  const surname = useFormInput();
  // ...
  return (
    <>
      <input {...name} />
      <input {...surname} />
      {/* ... */}
    </>    
  )
}

/*******************
 * useFormInput.js *
 ******************/
const valueKey = Symbol();
 
function useFormInput() {
  const [value, setValue] = useState(valueKey);
  return {
    value,
    onChange(e) {
      setValue(e.target.value);
    },
  };
}
复制代码

(我承认 useFormInput() Hook 不是特别好用,但你可以想象下它处理诸如验证和 dirty state 标志之类,如 Formik 。)

你能发现这个bug吗?

我们调用 useFormInput() 两次,但 useFormInput() 总是用同一个 key 调用 useState() ,就像这样:

const [name, setName] = useState(valueKey);
  const [surname, setSurname] = useState(valueKey);
复制代码

我们再次发生了冲突。

实际中 Hooks 提案没有这种问题,因为 每次 调用 useState() 会获得单独的state 。依赖于固定顺序调用使我们免于担心命名冲突。

缺陷 #4:钻石问题(多层继承问题)

从技术上来说这个和上一个缺陷相同,但它的臭名值得说说,甚至维基百科都有介绍。(有些时候还被称为“致命的死亡钻石” —— cool!)

我们自己的 mixin 系统就受到了伤害。

比如 useWindowWidth()useNetworkStatus() 这两个 custom hooks 可能要用像 useSubscription() 这样的 custom hook,如下:

function StatusMessage() {
  const width = useWindowWidth();
  const isOnline = useNetworkStatus();
  return (
    <>
      <p>Window width is {width}</p>
      <p>You are {isOnline ? 'online' : 'offline'}</p>
    </>
  );
}

function useSubscription(subscribe, unsubscribe, getValue) {
  const [state, setState] = useState(getValue());
  useEffect(() => {
    const handleChange = () => setState(getValue());
    subscribe(handleChange);
    return () => unsubscribe(handleChange);
  });
  return state;
}

function useWindowWidth() {
  const width = useSubscription(
    handler => window.addEventListener('resize', handler),
    handler => window.removeEventListener('resize', handler),
    () => window.innerWidth
  );
  return width;
}

function useNetworkStatus() {
  const isOnline = useSubscription(
    handler => {
      window.addEventListener('online', handler);
      window.addEventListener('offline', handler);
    },
    handler => {
      window.removeEventListener('online', handler);
      window.removeEventListener('offline', handler);
    },
    () => navigator.onLine
  );
  return isOnline;
}
复制代码

这是一个真实可运行的示例。 custom hook 作者准备或停止使用另一个 custom hook 应该是要安全的,而不必担心它是否已在链中某处“被用过了”

(作为反例,遗留的 React createClass() 的 mixins 不允许你这样做,有时你会有两个 mixins,它们都是你想要的,但由于扩展了同一个 “base” mixin,因此互不兼容。)

这是我们的 “钻石”::gem:

/ useWindowWidth()   \                   / useState()  :red_circle: Clash
Status                        useSubscription() 
       \ useNetworkStatus() /                   \ useEffect() :red_circle: Clash
复制代码

依赖于固定的顺序调用很自然的解决了它:

/ useState()  :white_check_mark: #1. State
       / useWindowWidth()   -> useSubscription()                    
      /                                          \ useEffect() :white_check_mark: #2. Effect
Status                         
      \                                          / useState()  :white_check_mark: #3. State
       \ useNetworkStatus() -> useSubscription()
                                                 \ useEffect() :white_check_mark: #4. Effect
复制代码

函数调用不会有“钻石”问题,因为它们会形成树状结构。:christmas_tree:

缺陷 #5:复制粘贴的主意被打乱

或许我们可以通过引入某种命名空间来挽救给 state 加“key”提议,有几种不同的方法可以做到这一点。

一种方法是使用闭包隔离state的key,这需要你在 “实例化” custom hooks时给每个 hook 裹上一层 function:

/*******************
 * useFormInput.js *
 ******************/
function createUseFormInput() {
  // 每次实例化都唯一
  const valueKey = Symbol();  

  return function useFormInput() {
    const [value, setValue] = useState(valueKey);
    return {
      value,
      onChange(e) {
        setValue(e.target.value);
      },
    };
  }
}
复制代码

这种作法非常繁琐,Hooks 的设计目标之一就是避免使用高阶组件和render props的深层嵌套函数。在这里,我们不得不在使用 任何 custom hook 时进行“实例化” —— 而且在组件主体中只能单次使用生产的函数,这比直接调用 Hooks 麻烦好多。

另外,你不得不操作两次才能使组件用上 custom hook。一次在最顶层(或在编写 custom hook 时的函数里头),还有一次是最终的调用。这意味着即使一个很小的改动,你也得在顶层声明和render函数间来回跳转:

// :warning: This is NOT the React Hooks API
const useNameFormInput = createUseFormInput();
const useSurnameFormInput = createUseFormInput();

function Form() {
  // ...
  const name = useNameFormInput();
  const surname = useNameFormInput();
  // ...
}
复制代码

你还需要非常精确的命名,总是需要考虑“两层”命名 —— 像 createUseFormInput 这样的工厂函数和 useNameFormInputuseSurnameFormInput 这样的实例 Hooks。

如果你同时调用两次相同的 custom hook “实例”,你会发生state冲突。事实上,上面的代码就是这种错误 —— 发现了吗? 它应该为:

const name = useNameFormInput();
  const surname = useSurnameFormInput(); // Not useNameFormInput!
复制代码

这些问题并非不可克服,但我认为它们会比遵守Hooks规则 的阻力大些。

重要的是,它们打破了复制粘贴的小算盘。在没有封装外层的情况下这种 custom hook 仍然可以使用,但它们只可以被调用一次(这在使用时会产生问题)。不幸的是,当一个API看起来可以正常运行,一旦你意识到在链的某个地方出现了冲突时,就不得不把所有定义好的东西包起来了。

缺陷 #6:我们仍然需要一个代码检查工具

还有另外一种使用密钥state来避免冲突的方法,如果你知道,可能会真的很生气,因为我不看好它,抱歉。

这个主意就是每次写 custom hook 时 组合 一个密钥,就像这样:

// :warning: This is NOT the React Hooks API
function Form() {
  // ...
  const name = useFormInput('name');
  const surname = useFormInput('surname');
  // ...
  return (
    <>
      <input {...name} />
      <input {...surname} />
      {/* ... */}
    </>    
  )
}

function useFormInput(formInputKey) {
  const [value, setValue] = useState('useFormInput(' + formInputKey + ').value');
  return {
    value,
    onChange(e) {
      setValue(e.target.value);
    },
  };
}
复制代码

和其他替代提议比,我最不喜欢这个,我觉得它没有什么价值。

一个 Hook 经过多次调用或者与其他 Hook 冲突之后,代码可能 意外产出 非唯一或合成无效密钥进行传递。更糟糕的是,如果它是在某些条件下发生的(我们会试图 “修复” 它对吧?),可能在一段时间后才发生冲突。

我们想提醒大家,记住所有通过密钥来标记的 custom hooks 都很脆弱,它们不仅增加了运行时的工作量(别忘了它们要转成 密钥 ),而且会渐渐增大 bundle 大小。 但如果说我们非要提醒一个问题,是哪个问题呢

如果非要在条件判断里声明 state 和 effects,这种方法可能是有作用的,但按过去经验来说,我发现它令人困惑。事实上,我不记得有人会在条件判断里定义 this.state 或者 componentMount 的。

这段代码到底意味着什么?

// :warning: This is NOT the React Hooks API
function Counter(props) {
  if (props.isActive) {
    const [count, setCount] = useState('count');
    return (
      <p onClick={() => setCount(count + 1)}>
        {count}
      </p>;
    );
  }
  return null;
}
复制代码

props.isActivefalsecount 是否被保留?或者由于 useState('count') 没有被调用而重置 count

如果条件为保留 state,effect 又会发生什么?

// :warning: This is NOT the React Hooks API
function Counter(props) {
  if (props.isActive) {
    const [count, setCount] = useState('count');
    useEffect(() => {
      const id = setInterval(() => setCount(c => c + 1), 1000);
      return () => clearInterval(id);
    }, []);
    return (
      <p onClick={() => setCount(count + 1)}>
        {count}
      </p>;
    );
  }
  return null;
}
复制代码

无疑它不会在 props.isActive 第一次是 true 之前 运行,但一旦变成 true ,它会停止运行吗?当 props.isActive 转变为 false 时 interval 会重置吗?如果是这样,effect 与 state(我们说不重置时) 的行为不同令人困惑。如果 effect 继续运行,那么 effect 外层的 if 不再控制 effect,这也令人感到困惑,我们不是说我们想要基于条件控制的 effects 吗?

如果在渲染期间我们没有“使用” state 但 它却被重置,如果有多个 if 分支包含 useState('count') 但只有其中一个会在给定时间里运行,会发生什么?这是有效的代码吗?如果我们的核心思想是 “以密钥分布”,那为什么要 “丢弃” 它?开发人员是否希望在这之后从组件中提前 return 以重置所有state呢? 其实如果我们真的需要重置state,我们可以通过提取组件使其明确:

function Counter(props) {
  if (props.isActive) {
    // Clearly has its own state
    return <TickingCounter />;
  }
  return null;
}
复制代码

无论如何这可能成为是解决这些困惑问题的“最佳实践”,所以不管你选择哪种方式去解释,我觉得条件里 声明 state 和 effect的语义怎样都很怪异,你可能会不知不觉的感受到。

如果还要提醒的是 —— 正确地组合密钥的需求会变成“负担”,它并没有给我们带来任何想要的。但是,放弃这个需求(并回到最初的提案)确实给我们带来了一些东西,它使组件代码能够安全地复制粘贴到一个 custom hook 中,且不需要命名空间,减小bundle大小及轻微的效率提升(不需要Map查找)。

慢慢理解。

缺陷 #7:Hooks 之间无法传值

Hooks 有个最好的功能就是可以在它们之间传值。

以下是一个选择信息收件人的模拟示例,它显示了当前选择的好友是否在线:

const friendList = [
  { id: 1, name: 'Phoebe' },
  { id: 2, name: 'Rachel' },
  { id: 3, name: 'Ross' },
];

function ChatRecipientPicker() {
  const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);

  return (
    <>
      <Circle color={isRecipientOnline ? 'green' : 'red'} />
      <select
        value={recipientID}
        onChange={e => setRecipientID(Number(e.target.value))}
      >
        {friendList.map(friend => (
          <option key={friend.id} value={friend.id}>
            {friend.name}
          </option>
        ))}
      </select>
    </>
  );
}

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  const handleStatusChange = (status) => setIsOnline(status.isOnline);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });
  return isOnline;
}
复制代码

当改变收件人时, useFriendStatus Hook 就会退订上一个好友的状态,订阅接下来的这个。

这是可行的,因为我们可以将 useState() Hook 返回的值传给 useFriendStatus() Hook:

const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);
复制代码

Hooks之间传值非常有用。例如:React Spring可以创建一个尾随动画,其中多个值彼此“跟随”:

const [{ pos1 }, set] = useSpring({ pos1: [0, 0], config: fast });
  const [{ pos2 }] = useSpring({ pos2: pos1, config: slow });
  const [{ pos3 }] = useSpring({ pos3: pos2, config: slow });
复制代码

(这是demo。)

在Hook初始化时添加默认参数或者将Hook写在装饰器表单中的提议,很难实现这种情况的逻辑。

如果不在函数体内调用 Hooks,就不可以轻松地在它们之间传值了。你可以改变这些值结构,让它们不需要在多层组件之间传递,也可以用 useMemo 来存储计算结果。但你也无法在 effects 中引用这些值,因为它们无法在闭包中被获取到。有些方法可以通过某些约定来解决这些问题,但它们需要你在心里“核算”输入和输出,这违背了 React 直接了当的风格。

在 Hooks 之间传值是我们提案的核心,Render props 模式在没有 Hooks 时是你最先能想到的,但像Component Component 这样的库,是无法适用于你遇到的所有场景的,它由于“错误的层次结构”存在大量的语法干扰。Hooks 用扁平化层次结构来实现传值 —— 且函数调用是最简单的传值方式。

缺陷 #8:步骤繁琐

有许多提议处于这种范畴里。他们尽可能的想让React摆脱对 Hooks 的依赖感,大多数方法是这么做的:让 this 拥有内置 Hooks,使它们变成额外的参数在React中无处不在,等等等。

我觉得 Sebastian的回答 比我的描述,更能说服这种方式,我建议你去了解下“注入模型”。

我只想说这和 程序员 倾向于用 try / catch 捕获方法中的错误代码是一样的道理,同样对比 AMD由我们自己传入 require 的“显示”声明,我们更喜欢 import (或者 CommonJS require ) 的 ES模块。

// 有谁想念 AMD?
define(['require', 'dependency1', 'dependency2'], function (require) {
  var dependency1 = require('dependency1'),
  var dependency2 = require('dependency2');
  return function () {};
});
复制代码

是的,AMD 可能更“诚实” 的陈述了在浏览器环境中模块不是同步加载的,但当你知道了这个后,写 define 三明治 就变成做无用功了。

try / catchrequire 和 React Context API都是我们更喜欢“环境”式体验,多于直接声明使用的真实例子(即使通常我们更喜欢直爽风格),我觉得 Hooks 也属于这种。

这类似于当我们声明组件时,就像从 React 抓个 Component 过来。如果我们用工厂的方式导出每个组件,可能我们的代码会更解耦:

function createModal(React) {
  return class Modal extends React.Component {
    // ...
  };
}
复制代码

但在实际中,这最后会变得多此一举而令人厌烦。当我们真的想以某种方式抓React时,我们应该在模块系统层面上实现。

这同样适用于 Hooks。尽管如此,正如 Sebastian的回答 中提到的,在 技术上 可以做到从 react 中“直接”导入不同实现的 Hooks。(我以前的文章有提到过。)

另一种强行复杂化想法是把Hooksmonadic(单子化) 或者添加像 React.createHook() 这样的class理念。除了runtime之外,其他任何添加嵌套的方案都会失去普通函数的优点: 便于调试

在调试过程中,普通函数中不会夹杂任何类库代码,且可以清晰的知道组件内部值的流向,间接性很难做到这点。像启发于高阶组件(“装饰器” Hooks)或者 render props( adopt 提案 或 generators的 yield 等)类似的方案,都存在这样的问题。间接性也使静态类型变得复杂。

如我之前提到的,这篇文章不会详尽无遗,在其他提案中有许多有趣的问题,其中有一些更加晦涩(例如于并发和高级编译相关),这可能是在未来另一篇文章的主题。

Hooks并非完美无瑕,但这是我们可以找到解决这些问题的最佳权衡。还有一些我们 仍然需要修复 的东西,这些问题在 Hooks 中比在 class 中更加别扭,这也会写在别的文章里头。

无论我是否覆盖掉你喜欢的替换方案,我希望这篇文章有助于阐述我们的思考过程及我们在选择API时考虑的标准。如你所见,很多(例如确保复制粘贴、移动代码、按希望的方式进行增删依赖包)不得不针对变化而优化。我希望React开发者们会看好我们所做的这些决定。

翻译原文 Why Do React Hooks Rely on Call Order? (2018-12-13)


以上所述就是小编给大家介绍的《为什么顺序调用对 React Hooks 很重要?》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

埃隆·马斯克传

埃隆·马斯克传

陆西 / 重庆出版社 / 2014-7 / 38.00元

埃隆·马斯克(Elon Musk)1971年出生于南非,毕业于美国宾夕法尼亚大学,工程师、企业家、亿万富翁。最成功的全球网上支付平台Paypal创始人之一,现任美国商业航天企业空间探索技术公司(SpaceX)CEO,电动车生产企业特斯拉汽车(Tesla Motors)CEO,光伏发电服务供应企业SolarCity董事长。如今他被视为经济危机后美国创新与科技实力的新象征。 今天PayPal依然......一起来看看 《埃隆·马斯克传》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具