avatar
前端开发 / 动画 / 日语初心者

七天之后#00・想要做得更好

2025/6/13
11 mins

「上手くなりたい」伴随着这句话的是哭着跑过宇治桥的久美子。我至今对这一幕、第一次面对这一幕时内心的触动记忆犹新。久美子的情感爆发让那时的我控制不住自己的泪水,但说到底,我迄今为止都无法真正理解久美子当时的不甘心——就像初中时久美子也无法理解高坂丽奈的不甘心一样。

或许是从大学毕业开始,也或许更早,我就已经和久美子有着一样所谓「恶劣的性格」——逃避着关于自己的一切,得过且过地活着。也正因如此,我至今没能完成积攒了许久的愿望清单,也没能在6月之前找到一份新工作。而现在,距离我离开上一份工作的日子,已经过去了十个月。

过往的学习与工作,我都没有做到能够称得上「好」的程度,但即便如此我仍旧选择了「想要做得更好」来作为驱动自己前进的核心动力之一,相比于毫无作为的自责,更想要至少能够体会努力后的「不甘」。于是决定付出行动,我会在周记系列文章中记录一周或近几周的见闻和想法(可能不具备什么价值),尝试用输出促进输入。

小知识点:消除异步的传染性

function findUser() {
  return fetch(`/api/user`)
}

async function action1() {
  const user = await findUser()
  // ...
  return result 
}

async function aciton2() {
  const result1 = await action1()
  // ...
  return result
}

上面这段代码是在前端开发工作中十分常见的一段请求处理代码。在请求完成后,我们可能会有许多处理请求结果的函数,这些函数中的操作很大概率都是同步操作,却因为最开始的请求是异步的,导致后续链路上所有的函数都变成了异步函数。

不过在有 async/await 语法糖的现在,异步函数再多也不会让代码整体变得过于难读,最大的影响大概就是对于并行逻辑的处理增加了心智负担,以及过多 async/await 增加的性能损耗。

要解决这个问题,核心是想办法让 fetch 或者说 promise 返回同步值,但这明显是做不到的,所以只能另辟蹊径。或许我们可以用一个容器函数包裹这个 promise ,容器函数负责执行 promise 并记录它的结果,这样我们只需要在 promise 执行完成后再次调用容器函数,取出缓存的结果就好了,取出结果的这个过程就是同步的。

// 伪代码
const cache = {
  status: 'pending',
  value: null
}
function syncify() {
  if (cache.status === 'fulfilled') {
    return cache.value
  }
  
  promise.then(data => setFulfilled(data)).catch(err => setRejcted(err))
}

剩下的问题就是,如何在 promise 执行完后,立刻执行第二次获取结果。这种场景很容易想到用 EventEmitter 实现,但这样又变回异步了,并且无法中断后续链路的执行。

想要中断整个链路,并且不侵入性修改后续函数的逻辑,很容易就想到用 throw 实现,用 throw 抛出 promise 本身,然后在 promise 执行完成后再次调用容器函数获取缓存的结果,这样完成了异步函数的同步化。

下面是针对于 fetch 的简单改造:

function fetchSyncify(fn) {
  const originFetch = globalThis.fetch

  const cache = {
    status: 'pending',
    value: null
  }

  function newFetch(...args) {
    if (cache.status === 'fulfilled') {
      return cache.value
    }

    if (cache.status === 'rejected') {
      throw cache.value
    }

    const promise = originFetch(...args)
      .then(res => res.json())
      .then(data => {
        cache.status = 'fulfilled'
        cache.value = data
      })
      .catch(err => {
        cache.status = 'rejected'
        cache.value = err
      })

    throw promise
  }

  globalThis.fetch = newFetch

  try {
    fn() // 第一次执行 newFetch ,抛出 Promise ,中断流程
  } catch (error) {
    if (error instanceof Promise) {
      error.finally(() => {
        globalThis.fetch = newFetch
        fn() // 第二次执行 newFetch ,读取缓存的结果
        globalThis.fetch = originFetch
      })
    }
  }

  globalThis.fetch = originFetch
}

使用时,我们就可以将后续处理函数的 async/await 全部去掉了:

function findUser() {
  return fetch(`/api/user`)
}

function action1() {
  const user = findUser()
  // ...
  return result 
}

function aciton2() {
  const result1 = action1()
  // ...
  return result
}

fetchSyncify(action2)

以上是比较简单的处理方式,更完备的代码可以参考 React 的 Suspense 组件的实现。

沟通的快乐

在过去的很长一段时间里,我都认为自己很讨厌与别人面对面交流,因为担心说错话、因为内心戏太多把自己看得太重、因为不想与别人发生冲突……简而言之就是缩在自己的保护壳内逃避人与人之间的关系性。

改变了我这种认知、或者说让我走出舒适区的事,是去年7月底的旅途。独自一个人前往日本旅行,旅行途中我不得不自己去和各种各样的人交流,餐馆的店员、神社的巫女、途中遇到的同好……即便语言不通也能通过肢体语言和翻译器表达出自己意思。

回国之后,正好被公司发了大礼包,赋闲在家,也趁着这段时间参加了许多线下活动。越是和大家一起参加活动、一起聊天,就越是发现自己似乎并不排斥与别人交流,反而很享受一群人谈论自己所热爱的东西时的氛围。

Live 后 Off 会

《安达与岛村》穿透文字的细腻

「两位主角之间那种小心翼翼的感情和互动,在日常的故事中刻画得淋漓尽致。这种小心翼翼让故事充满了一股忧郁的气息,却又同时能在这忧郁中感受到甜蜜。」我在读完第一卷的故事后留下了这样的感想。

故事讲述了安达与岛村从第一次相见到互相产生兴趣的过程,作者采用的是第一人称视角、两个人分别的第一人称视角来讲述这段故事。虽然故事中存在大量的心理活动描写,但是这些心理描写并不会让人觉得故事很拖沓,反而会有种跟随视角的主角逐渐发掘其内心的感觉。

安达与岛村阅读记录

时隔四年,再一次打开《FGO》

起因是日服的剧情推进到玛修升到五星了,作为看着玛修长大的单推人,心情必然是十分激动(也没有十分),于是我重新下回国服继续推之前搁置了很久的主线了。

即便过了这么多年,我依旧觉得《FGO》的剧情,是手游行业的天花板(部分章节除外)。2.5.1 英灵们的以身铺路以及俄里翁舍生击坠神明的一击;2.6 两代乐园妖精延续千年的悲惨巡礼,现在回想起来也令我十分动容。

不过 2.6 的剧情实在是让我又爱又恨,总之下面是推完 2.6 时写下的无厘头感想:

没有春之记忆的阿尔托莉雅,被排挤的霍普;惨死在玉座之下的魔女摩根,坠下大洞的芭万希;几千年的巡礼只能换来这样的结局。 满嘴谎言的奥伯龙,直到最后都在述说着自己的厌恶,无法分辨真假。 一个讲述爱与希望的故事,也是一个令人愤怒的故事,第一次觉得这是一个不该存在的异闻带。

未归还的圣剑,形成了1.6的特异点; 未锻造的圣剑,形成了2.6的异闻带。 FSN时,阿尔托莉雅作为从者守护士郎; 妖精国时,村正为守护阿尔托莉雅以身铸剑。 蘑菇笔下的宿命感?毕竟呆毛才是Fate系列核心

一但给菌类写舒服了他就会开始整活,看看我们空洞骑士玛修小姐,真想请高松灯为她唱一首成为人类之歌。

FGO2.6通关结算