保持专注

今天,我想聊聊 专注。 在现代信息社会,专注其实是一种非常难得的品质。 抖音上刷不完的短视频,公众号不停推送的文章,小红书看不完的分享。信息流无穷无尽,总有新的内容在争夺你的注意力。 很多时候,我们明明在进行重要工作,却还是忍不住分心,拿起手机刷刷朋友圈。 互联网上很多 UP 主已经从脑科学和多巴胺的角度解释过这个现象。《认知觉醒》一书中提到,专注其实是一件反人类天性的事情。因为专注意味着你的大脑在不停地思考,而思考是一件极为耗能的行为。 人作为一种生物,本能上会抵抗高耗能行为。 相反,娱乐和放空是一种低耗能行为。(别告诉我你刷抖音的时候还在思考。) 所以我们天然会倾向于那些更轻松、更即时带来快乐的事情。这并不是什么羞耻的事情,这是人的本能。 而最近很流行的 Vibe coding,某种程度上来说,其实也是一种分心。 一段时间里,我沉迷于把日常使用的工具都用 AI 重新 vibe coding 一遍。 看着一个个文件被创建,应用慢慢跑起来,界面也像模像样地出现,那种感觉其实挺爽的。仿佛在极短的时间里就做出了很多东西。 但过了一段时间之后,我开始陷入一种 AI 疲劳。 五个 project 同时运行,不停在不同项目之间切换上下文,我的大脑长期处于一种过载状态。 再回头看这些 vibe coding 的项目,我突然发现: 我其实已经看不太懂它们了。 更糟糕的是,我也不知道接下来该做什么。 理论上,我创建了五个新的、可以跑起来的应用。 但回顾起来,我几乎没有真正学到什么东西。 我没有进行太多系统性的思考。大多数时候只是机械地输入一些简单得不能再简单的需求,然后等待 AI 输出结果。 不可否认,Vibe coding 确实有它的价值。 它可以快速验证想法,快速创建小工具。 对于非技术背景的人来说,只需要提出需求,就可以开发出一些应用来自动化重复工作。 在某种意义上,AI 确实降低了软件开发的门槛。 但问题也恰恰在这里。 因为 Vibe coding 让“动手实践”这件事情变得成本极低,它第一次打破了一个原本非常重要的循环: 学习知识 → 产生思考 → 动手实践 在传统的学习路径中,实践是建立在理解和思考之上的。 而在 vibe coding 的模式下,这个过程往往被压缩成: 想法 → AI生成 中间最重要的一步 —— 思考 —— 很容易被省略。 而思考过程的缺失,其实是致命的。 在知识学习的过程中,思考往往是杠杆最高的一环。 如果没有这一环,你以为自己在不断“实践”,但实际上,大部分时间可能只是低效地生成代码而已。 ...

2026年3月15日 · 1 分钟 · hateonion

2021年回顾

工作 挣扎 2021 年,是我在西安腾讯的第二年,去年抱着满腔的热情加入公司,认识了很多不错的人,年底也收获了还不错的绩效,但是整个人却就是开心不起来。呆小瓜,包括我身边的很多朋友,都觉得,我没有之前开心了。现在想想那段时间,一方面是自己的状态确实太紧绷了,每天睡觉前都在担心 on-call,即使下班后也是盯着企业微信,担心有人找。另一方面又感觉自己没有任何成长,在不停的做重复的事情,没有时间思考,开始不断怀疑否定自己。这样的状态持续了大概三个月左右,在确定了未来业务方向自己实在不感兴趣,和呆小瓜吐露心声并获得她的支持之后,我便开始了新的工作规划。 机遇 在经历了几轮面试之后,我成功拿到了 3 个 offer。很幸运,自己之前在 toughtworks 的经验,还不错的英语底子以及爱折腾的经历给自己加了不少的分,让我保持了尚可的竞争力。综合考虑下我选择来到上海,加入 Afterpay(现在应该叫 Block 了)。 成长 Afterpay 的工作节奏和我之前在 thoughtworks 的工作节奏比较一致,这也给了我更多的时间去思考。前端的工作内容其实大同小异,并没有什么新的花样。但是和两年前相比,由于有了不同的经历,自己的视角也有所不同。过去的我更多的是从一个前端的角度去出发,把产品需求转变成技术代码。但现在自己会思考更多,不仅是 technical 上(从前到后端到端,如何规避技术风险,如何选择合适的技术高效支持业务,如何针对不同的业务场景,选择合适的模型),也会开始从产品本身,从商业模型上思考如何打造更好的产品。我想我应该还是更适合做产品吧,希望 2022 年,能够逐步构建自己真正满意的产品,不管是从业务上孵化,还是自己的 side project。 生活 Move 伴随工作地点的变换,我和呆小瓜离开了我们居住了快 3 年的温馨的西安小窝,搬到了上海的出租屋。强调是出租屋,是因为在我们有限的预算下,能够租到的房子实在屈指可数,目前这套已经是矮子里面找将军,相对不错的一套了,不过相比之前自己的房子,仍有诸多不足之处。我们也不止一次的吐槽过如同纸糊的窗户,超小的电视,以及形同虚设的隔音。虽然有很多槽点,但是生活也有了一些惊喜,比如无意间的发现的步行就可到达的菜市场,意外好吃的干米粉,热情的卤菜阿姨和卖干货的大姐,以及我们给我们提供无尽造梗素材的周边的一切。虽然和西安相比,还是有点不太习惯这边的生活,但是一切都在变好。即使是出租屋,也在一天天变得温馨。 颠倒 呆小瓜因为要陪我来上海,也换了新的工作,当她给前公司提离职时,离职原因美其名曰“随军”(源自父母爱情的一个梗)。到了上海后,呆小瓜的工作比我强度要高,我们的位置就像是 2021 的彼此倒了个个,我开始成为后勤主管,而呆小瓜开始做女强人。这样的生活和我们最初的设想其实有所偏差,毕竟我们现在连 7 点吃晚饭都做不到,更别说之前设想好的培养兴趣爱好了。我也有些自责让她为了陪我来上海就变得这么辛苦。但是呆小瓜就是这么一个小仙女,一眼就能看出我心里在想什么,明明自己每天工作的很累了但是还是装作没事人一样听见我的烂梗就哈哈大笑。新的一年,希望呆小瓜能够轻松一点,也希望我们能实现每天 7 点吃晚饭的目标。 厨艺 经过去年年夜饭的洗礼,自己今年的厨艺又精进了很多,下面是几个自己今年最满意的菜。 肉炒一切(蒜薹,香干,洋葱) 芋儿鸡(YYDS) 萝卜(扁豆)炖牛肉(没有老妈做的味道那么浓,但是意外的合我和呆小瓜的口味) 锅巴饭(偷师石泉)和砂锅(自成一派) 清蒸鱼(主要是鱼的功劳) 希望新的一年能解锁新的菜单,能够开始烹饪一些不一样的新食材。 总结 每年回顾,都会有很多遗憾,这里列几个个人觉得做得非常不好的地方 没有太多的时间陪伴父母(去年过年没回家,今年打算错峰过年)。 又是几乎没有读书的一年,知识来源过于零散化,虽然开始使用双链笔记进行整理记录,但是没有形成体系。 成长上有点停滞不前,对于新的东西学的很快,但是还是没有发现自己的核心竞争力。 博客更新几乎停滞。本来想着工作节奏慢一点就能多写点东西但是零散时间大多数被 b 站和游戏占据。 没有发展任何新的技能和爱好。 如果我能够把上面这些事情全部都做完美,不留遗憾,那我新的一年应该是个超人了。但是我并不是什么超人,遗憾也总会有,暂且实际一点,希望明年能少一点遗憾,加油!

2022年1月30日 · 1 分钟 · hateonion

浏览器跨tab通信的几种办法

什么场景下需要跨tab通信? 有一些日常中很常见的场景,如果支持跨tab通信,可能能给用户更好的体验。 博客Dark Mode切换 想象你在浏览 Dan Abramov的博客,你同时打开了好几个tab学习一系列知识。夜晚来临,你将一个页面切换成了Dark Mode。当你阅读完这一篇去阅读下一篇时,页面却不是以Dark Mode呈现的。你不得不按下F5刷新页面来寻求一致的阅读体验。 需要频繁登录的CMS系统 想象你在网店管理系统中管理你网店中的商品,你打开了好几个tab,分别管理商品类型,商品详情以及商品库存。你正在管理你的商品详情,突然你的登录session过期,你点击重新登录。但是当你切换到商品库存页面时,你发现你依旧无法操作,需要再次登录。你不得已点击了登录按钮,和你辛苦编辑了10分钟但没有保存的商品库存说了拜拜。 以上场景其实无伤大雅,你的系统依旧运转,核心功能依旧正常。但是借助跨tab通信,你能给用户提供更好更顺滑的体验。 Cookie & IndexedDB 要实现数据的一致,很容易想到的就是统一的数据存储。在有后端加入的时候,后端其实就是前端的统一数据存储。在纯前端的环境下,比较常见的数据存储包括 cookie localStorage sessionStorage IndexedDB sessionStorage 由于只存活于单个tab session中,所以不能用于跨tab通信。localStorage 有其他的应用方式,下面会深入分析。所以在这个章节我们主要关注cookie和IndexedDB。 实现思路 实现思路其实非常简单,就是通过轮询数据源,来确保每个tab的数据都是最新的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // Cookie Way // Producer Tab, 当然你也可以用js-cookie这种库更高效的操作cookie document.cookie = "blabla" // Consumer Tab setInterval(() => { const cookie = document.cookie; //Use Cookie to do something doSomething(cookie) }, 1000) // IndexedDB way // Producer Tab const transaction = db.transaction(["customers"], "readwrite"); const objectStore = transaction.objectStore("customers"); customerData.forEach(function(customer) { const request = objectStore.add(customer); request.onsuccess = function(event) { // do some callback }; }); // Consumer Tab setInterval(() => { const transaction = db.transaction(["customers"]); const objectStore = transaction.objectStore("customers"); const request = objectStore.get("444-44-4444"); request.onsuccess = function(event) { doSomething(request.result.name); }; }, 1000) 需要注意的是,cookie的读写是同步的,在cookie比较大的时候可能会阻塞主线程造成页面卡顿。而IndexedDB的API都是异步的,所以不会有cookie同步读写的性能问题。 ...

2021年10月14日 · 2 分钟 · hateonion

持续部署新法宝 - Github Actions

什么是Github Actions Github Actions 是 Github 推出的一项服务,旨在以自定义工作流的方式来简化团队开发协作中的各种需求。比如: 基于 Code Commit 的代码构建/测试/打包,也就是常说的持续集成。 应用的自动发布和部署,和上一步连起来就是所谓的持续部署。 自动管理 PR(如自动 assign viewer) 自动管理 issue(如自动给 issue 分类,自动关闭长期不更新的 issue ) Github Actions的优点 1. 灵活性 对比主要竞品 Jenkin/GoCD 以及 Circle CI/ Travis CI, 相较于 Jenkins/GoCD 只能够自建服务,CirCle CI(2.0以前) / Travis CI 只能依靠官方提供的服务,Github Actions 可以使用 Github 官方的 Runner,同时 Github Actions 也支持使用用户自建的(比如搭载某内网环境的instance)Runner。Github Actions 还自带 Linux/macOS/Windows 三种类型的 Runner,能够满足不同用户群体在不同场景的需要。 2. 复用性 对于开发者而言,编写重复的代码永远都是一种灾难。而在 Github Actions 的准则中,每一条 Action 都是一个最小的可复用单元,只需要一次编写,便可以在之后随时随地的使用。更棒的是,Action 可以通过组合的方式来使用,完成更为复杂的功能。 3. 可维护性 每一条 Action 其实就是一个 JavaScript module/Docker container,由于Action 本身就是代码,所以 Action 本身是可以很好的被版本管理的。同时由于支持使用 JavaScript 编写 Action,只要你想,你可以使用大量的测试来保证你的 Action 的质量。 ...

2020年2月22日 · 3 分钟 · hateonion

层叠上下文和层叠顺序

为什么 z-index 不工作了? 在某些场景下,我们经常会发现z-index不知为何就不工作了,比如下面的场景。(demo:stackblitz demo) 1 2 3 <div class="block-a">block-a z-index 1</div> <div class="block-b">block-b z-index 2</div> <div class="block-c">block-c z-index 3</div> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 .block-a { background: #ffb3a7; z-index: 1; position: relative; top: 60px; opacity: .8; width: 500px; height: 90px } .block-b { background: #fff143; z-index: 2; opacity: .8; height: 100px; width: 320px; position: fixed; } .block-c { background:#bddd22; opacity: .8; z-index: 3; width: 200px; padding-top: 300px; } div { color: #50616d; font-size: 24px; text-align: center; padding-top: 40px; border: #50616d 1px dashed; } block-a的 z-index 是 1, block-b 的 z-index 是 2, block-c 的 z-index 是3。 按照之前的理解,覆盖关系应该是block-c覆盖block-b覆盖block-a。但是实际结果却如下图。 ...

2019年10月11日 · 5 分钟 · hateonion

重学前端学习笔记(1)--Js类型

基础类型 Undefined Null Boolean String Number Symbol Object boolean这个类型比较简单,不单独作解释。Symbol这个类型太复杂,后面单独总结。 Undefined 和 Null undefined并不是一个关键字。我们日常访问的undefined其实是访问的global级别的undefined变量,undefined变量的值是基础类型undefined。但是在现代浏览器中,undefined都不能被覆写。 undefined代表一个变量初始化了但是没有赋值,任何变量初始化前值都是undefined。 null是JavaScript的关键字,表示的是一个空值。 所有变量定义都不要赋undefined值,这样会让代码难以理解(到底是代码没有赋值还是?) String string的最大长度是2^53-1,但是长度并不是指代字符数,而是字符串的UTF16编码的和,比如需要计算一个emoji的长度,你会发现是2。 字符串方法比如length和charAt都是基于字符串编码和的。 字符串一旦构造便不可更改,属于值类型。 Number Js中除开规定有理数,还规定了NaN,Infinity,-Infinity Js中有+0和-0,在加减法中没有问题,在乘除法中,如果是 x * -0 会得到 -0,x/-0 会得到 -Infinity。 Js中的Number类型范围是-2^53-2^53,如果不使用BigInt,只能表示这个范围之内的整数 非整数类型的Number无法使用 == 或者===来进行比较,0.2 + 0.1 == = 0.3 会返回false,如果想要得到正确的匹配结果,请使用 Math.abs(0.1 + 0.2 -0.3) <= Number.EPSILON Object Js中对象是属性的集合。属性分为数据属性和访问器属性,以key-value结构进行存储,key可以是字符串或者是个Symbol类型。 Js中的类和Java中的类有一些不同,Java中的类代表的是真正的类型,但是Js中的类只是运行时对象的一个私有属性。 Number/String/Boolean和Symbol这四个基本类型都自带对应的构造器,如果使用 new 操作符,会返回一个对象类型而不是基本类型。 Js中的基本类型和对象的边界很模糊,可以直接在基础类型上调用方法,比如"abc".charAt(0),也可以通过给原型添加方法给基础类型增加方法。 为什么基础类型可以调用对象方法?本质上是 . 运算符提供了装箱操作,这个运算符会根据基础类型构造一个临时对象。 类型转换 值转换表如下 StringToNumber String到Number的类型转换支持十进制/二进制/八进制和十六进制 30 0b111 0o13 0xFF 同时还支持科学计数法 1e3 -1e-2 和parseInt和parseFloat相比, ...

2019年8月26日 · 1 分钟 · hateonion

RxJS入门指南

Prerequisite(先导知识) 本文是RxJS的入门知识,在文章中会涉及到大量的JavaScript示例代码,想要更好的理解本文,你需要具备以下知识。 理解基本的ES6和TypeScript语法 了解过观察者模式 Observable? Observer? 在RxJSObservable和Observer。要想很好的理解两个概念,我需要借用一个生活中很常见的例子,买房。 在这个图中,我们的购房者一直在密切的关注我们的房价。房价随着时间波动,购房者可能会根据波动的房价而采取一系列的行动,比如购入或者继续观望。购房者与房价的这样一种关系其实就构成了一种观察者关系。套用到观察者模式中, 房价 – Observable 购房者 – Observer 购房者观察房价 – Subscribe(订阅) 再结合买房的例子,我们可以很学术的描述Observable和Observer的行为。 Obserable 可被观察(房价被购房者关注),并且随时间变化发出(emit)不同值(房价波动)。 Observer 观察Observable(购房者关注房价),并在Observable(房价)发出不同值(房价波动)时做出响应(买房或者观望)。 Observable和Observer之间通过订阅(Subscription)来建立观察关系。 当Observable没有Observer的时候,即使发出了值,也不会产生任何影响(无购房意愿者不会响应房价波动) RxJS中的Observable和Observer 有了基本的Observable和Observer的概念,我们再来看看RxJS中的Observable和Observer。 创建一个Observable 我们可以调用 Observable.create 方法来创建一个Observable,这个方法接受一个函数作为参数,这个函数叫做 producer 函数, 用来生成 Observable的值。该函数的参数是observer,调用observer.next()就可以生成一个有一系列值的Observable。 1 2 3 4 5 6 import { Observable } from 'rxjs'; const observable = Observable.create(observer => { observer.next('foo'); observer.next('bar'); }) 但是运行这段代码后并不会发生任何事情,我们需要一个Observer去Subscribe这个Observable,然后Observer基于Observable发出的值做出响应。 Subscribe一个Observerable 我们通过下面的这段代码去Subscribe一个Observable 1 2 3 4 5 6 7 8 import { Observable } from 'rxjs'; const observable = Observable.create(observer => { observer.next('foo'); observer.next('bar'); }) observable.subscribe(console.log) 运行代码,console中就会依次打印 foo / bar 了 ...

2019年7月17日 · 5 分钟 · hateonion

图片懒加载从简单到复杂

为什么要做图片的懒加载 假设在用户访问某个页面时就加载这个页面全部的图片(即使这些图片并不处在用户的当前的视窗中),在弱网环境或者网速较慢的环境下,这些“冗余”图片的下载会占用用户本来就非常有限的带宽,伤害用户体验(比如影响其他资源的下载)。所以对于网站的图片,理想的做法是懒加载(按需加载)。 图片懒加载的原理 在浏览器内部对于各种资源有着一套自己的优先级定义,浏览器会优先加载优先级高的资源 如果我们不去进行图片的懒加载,默认情况下,资源的priority如下。 这些优先级标记为high的图片会占用其他资源的下载带宽,可能会造成某些比较关键的资源(比如xhr call)加载缓慢,拖慢页面速度。 图片懒加载的简单实现 图片懒加载的思路一般时当页面加载时加载一个尺寸很小的占位图片(1kb以下),然后再通过js选择性的去加载真正的图片。 一个最简单的的实现如下 1 2 <!-- index.html --> <img src="placeholder.jpg" data-src="real_image.jpt" /> 1 2 3 4 5 6 7 8 9 10 // index.css img[data-src] { filter: blur(0.2em); } img { filter: blur(0em); transition: filter 0.5s; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // index.js (function lazyLoad(){ const imageToLazy = document.querySelectorAll('img[data-src]'); const loadImage = function (image) { image.setAttribute('src', image.getAttribute('data-src')); image.addEventListener('load', function() { image.removeAttribute("data-src"); }) } imageToLazy.forEach(function(image){ loadImage(image); }) })() 通过懒加载之后,资源优先级如下。 ...

2019年1月30日 · 2 分钟 · hateonion

Deep In React(五)setState中的黑魔法

以下代码全部基于React15(React16代码太复杂了看不懂哇- -)。 setState不一定是同步的 在React官方文档中有这么一句话state-updates-may-be-asynchronous。 下面这两个很经典也是新人很容易糊涂的场景就是由上面这句模棱两可的话带来的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Demo extends Component { state = { count: 1 } onClickHandler = () => { this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 1 this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 1 } render() { const { count } = this.state; return ( <button onClick={this.onClickHandler}>{count}</button> ); } } class SetTimeoutDemo extends Component { state = { count: 1 } onClickSetTimeoutHandler = () => { setTimeout(() => { this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 2 this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 3 }, 0); } render() { const { count } = this.state; return ( <button onClick={this.onClickSetTimeoutHandler}>{count}</button> ); } } stackBlitz的demo在这 Clicker 和 SetTimeoutClicker ...

2019年1月14日 · 3 分钟 · hateonion

Js中的防抖与节流

为什么需要防抖和节流? 日常与浏览器打交道比较多的前端开发者对于浏览器的各种事件想必都不会陌生,我们会针对某一个特定的事件绑定对应的响应函数,在事件被触发时让浏览器自动调用该函数。 1 2 3 4 5 function onResizeHandler() { // do something on resize } window.addEventListener('resize', onResizeHandler); 如果只是一些触发频率很低的事件,那么上面的代码并没有什么问题。但是如果像resize这样可能在短时间内被频繁触发的事件(比如click/keydown/touchStart等),我们不去做任何的处理的话,可能导致事件的响应函数在短时间内被大量的触发,造成浏览器卡顿,伤害用户体验。 而节流和防抖一个很经典的应用场景就是去控制我们的事件的触发,节省浏览器开销。 什么是防抖 JavaScript中的防抖指的是只有在x ms内没有调用该函数,才会真正调用该函数。如下图 该图上方是事件触发的次数,下方是响应函数触发的次数,可以发现在事件大量被触发时(色块密集),响应函数并没有被触发。当事件停止触发一段事件后(三个色块的时间间隔)后,响应函数才会被真正的触发。 如果你想自己试试上面的例子,可以访问这个 codePen 。 防抖的简单实现 防抖函数其实是一个高阶函数,接收一个函数和防抖事件作为参数,返回值为防抖处理的函数。 的实现其实很简单,我们需要的是是一个定时器,如果在定时器的定时结束前,响应函数被触发的话则重置这个定时器的时间。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function debounce(fn, wait, leading=false) { /** @type {number} */ let timer; /** @type {number} */ let lastCallTime; /** @type {boolean} */ let isInvoked = false; return function debounced(...args) { const context = this; const thisCallTime = Date.now(); if(leading) { if(!isInvoked) { fn.apply(context, args); isInvoked = true; } if(thisCallTime - lastCallTime >= wait) { fn.apply(context, args); } lastCallTime = Date.now(); return; } clearTimeout(timer); timer = setTimeout(() => fn.apply(context, args), wait); } } 需要注意的是,上面的函数添加了leading参数。传统的防抖函数响应函数第一次触发也需要等待x ms,使用该参数可让响应函数立即进行触发。 ...

2019年1月2日 · 2 分钟 · hateonion