今天的练习
昨天太忙,三轮都没答。今天这三组题是把昨天那几格重出的,不给新难度,也不往回退。三轮下来十八个小问全过。
题目一:四个函数式接口,形状各不一样
Function、Consumer、Supplier、Predicate 这四个的形状不一样:哪个不接参数、哪个只进不出、哪个进出都是一个值、哪个固定吐 boolean?Predicate 除了 test,还能把条件拼起来,两个都要满足用哪个,取反用哪个?
我答的是:Function 进出都是一个值;Consumer 只进不出,接收一个参数、没有返回值;Supplier 不进只出;Predicate 固定吐布尔值、只接收一个参数。组合那边,两个都要满足用 and,取反用 negate,or 表示满足任意一个都行。
助手给了一串好记的锚:Consumer 是消费者,只吃不吐;Supplier 是供应商,只供货不收订单;Function 是加工;Predicate 是判断题。往上还有变体,BiFunction 和 BiConsumer 是两个参数进去,UnaryOperator 是进出同一个类型。
同一轮还问了 collect 的另外两个收集器。把一批 name 拼成一整条字符串用 joining,分隔符就写在 joining 的参数最前面,我写的是 collect(Collectors.joining(","))。助手补了三参数版本,joining(", ", "[", "]") 能直接拼出前后缀。想按「年龄是否大于 18」把列表分成两队,用 partitioningBy,返回的 Map 的 key 是 Boolean。它和 groupingBy 的根本区别也在这儿:partitioningBy 只分两组、key 只能是布尔;groupingBy 的 key 可以是 String、枚举、任意类型,组数也不限。
题目二:orElse 和 orElseGet 差在哪
orElse(默认值) 和 orElseGet(() -> 默认值) 功能看着一样,差别在哪?给一个能真正看出区别的场景。Optional 里装着值,想先按一个条件过滤一下、不满足就当成空的往下走,调哪个方法?
我答的是差别在执行时机:orElse(loadFromDatabase()) 这种写法,不管 Optional 里有没有值都会先算一次,白浪费。orElse 的默认值是"先算好再给",orElseGet 是"需要才算"。第二问是 filter,答对了。
助手补了生产里更常见的坑:默认值来自查库、或者带副作用的时候,比如顺手记一条日志、改个状态,orElse 会白跑一次甚至把状态改了。所以遇到这种默认值,一律用 orElseGet。
题目三:改 width 和改 color,浏览器的活不一样多
改一个元素的 width,和只改它的 color,浏览器要干的活一样多吗?这两件事各叫什么名字?循环里交替「读 offsetHeight」和「写 style」,跑起来会明显变慢,读一下为什么也慢?
我答的是:改大小位置会引起重排,只改颜色这些属性引起重绘;重排一定会引起重绘,重绘不一定导致重排。第二问我答的是读 offsetHeight 会强制浏览器立刻结算前面攒着的样式改动,进去先写、一读就马上重排,重排完又写、一读又强制重排,这样循环下去就叫布局抖动。
助手给了两个正式名字,单次触发叫强制同步布局,连续触发叫布局抖动(layout thrashing)。工程上的解法是把读和写分批处理,先集中把要读的读完,再集中写,别交叉着来,或者扔进 requestAnimationFrame。
题目四:ref 什么时候才自动解包
script setup 里写了 const count = ref(0)。模板里写 {{ count }} 能直接用,为什么不用写成 {{ count.value }}?在 script 的 JS 代码里想改它,直接 count = 1 行不行,该怎么写?如果 ref 塞进数组里,模板写 {{ list[0] }} 还能自动解包吗?
我答的是模板里 Vue 解析时会自己加上 .value,但在 JS 代码里改必须写 count.value = 1;塞进数组那种不行,list[0] 拿到的实际是一个 ref 对象,要写成 {{ list[0].value }}。
助手补了规则,把"为什么"钉死:自动解包只发生在 ref 作为响应式对象的属性被访问的时候,加上模板顶层的属性。数组下标、Map 和 Set 里装的 ref 都不解包,这就是 list[0] 必须写 .value 的原因。
题目五:z-index 999 为什么被 1 压在下面
一个盒子写了 position: absolute; z-index: 999,结果被旁边一个 z-index: 1 的盒子压在下面。这个比较范围叫什么?它被什么撑出来?哪些常见写法会顺手创建一个这样的范围?
我答的是层叠上下文,页面元素不是在同一个范围内比大小,而是分属不同的层叠上下文,它由祖先元素创建。祖先里只要有一个元素创建了它,我和那个 z-index: 1 就分属两个范围,写 999 也没用。第二问我答了两个:相对定位或者绝对定位时给了层级,还有粘性定位和固定定位。
助手补的是最容易踩的那几个,因为页面上压根没出现 position:opacity 小于 1、transform 不是 none、filter 不是 none、will-change 指定了这些属性、isolation: isolate,还有 flex 或者 grid 的子元素配上非 auto 的 z-index。写了个 transform 结果 z-index 突然失灵,基本都是踩在这上面。
题目六:深分页慢在哪
一张一千万行的订单表,主键 id 连续。LIMIT 10 很快,翻到很后面写成 LIMIT 10000000, 10 就慢得离谱,慢在哪一步?记住上一页末尾那条 id、下一页写成 WHERE id > ? ORDER BY id LIMIT 10 这种套路叫什么?另一种叫延迟关联的改法,思路是什么?
我答的是慢在执行时机:limit 是先 select 出结果之后再分页,中间要查出几千万行,再从那一位往后取十行。第二种套路叫游标分页,直接靠索引定位到 id 大于某个值的位置往后取,不用扫描前面的行。延迟关联的思路是先查出要取的那十行的 id,再按 id 直接去取数据。
助手把慢的那一步说细了:MySQL 会先取 offset 加 limit 行(这里就是一千万零十行)再丢掉前面一千万行,再加上 SELECT * 要逐行回表取完整数据,代价就是这么堆起来的。延迟关联之所以快,是因为先走的那个索引只带 id,属于覆盖索引,不用回表。他还补了游标分页的限制:只能顺着翻,不能跳页,排序字段得唯一,不然会漏行或者重复。
题目七:乐观锁和悲观锁
更新时带上旧版本号比对:UPDATE orders SET status = 1, version = version + 1 WHERE id = ? AND version = ?。这条 SQL 影响行数为 0 说明什么?这套做法叫什么锁?与之相对的、让数据库先把这行锁住、后到的事务排队等的那种,又叫什么锁?两种各适合什么场景?想先把这行查出来看最新数据、并且不许别的事务改它,SELECT 该怎么写?
我答的是影响行数为 0 说明 version 对不上,这行记录已经被别的事务改过了。这套叫乐观锁,顾名思义是假设冲突很少,只用版本号做对比;对应的叫悲观锁。场景我说的是乐观锁适合读多写少、冲突少、重试成本低,悲观锁适合冲突多、竞争激烈、需要强一致、不想频繁重试。
第二问我写的是 SELECT * FROM orders WHERE id = ? FOR UPDATE,加排他锁,别的事务想改这行、或者也想 FOR UPDATE 都会被阻塞。助手补了两句:RR 下普通 SELECT 读的是快照(MVCC),FOR UPDATE 是当前读,读的是最新已提交版本;只做普通 SELECT 的事务不会被它阻塞,读和读之间不冲突。
题目八:跨域看的哪三样
前端页面在 http://localhost:5173,后端接口在 http://localhost:8080,一发请求就被拦。浏览器判断同源看哪三样?这两个地址是哪一样不一样?把两个端口都改成 8080 还跨不跨?一个写 localhost、一个写 127.0.0.1 呢?浏览器拦这一下想防什么,后端常见怎么解决?
我答的是同源看协议、主机、端口三样,这两个地址是端口不一样才算跨域,都改成 8080 就不跨了;一个写 localhost 一个写 127.0.0.1 还是跨域,浏览器会认为主机不同。防护那块我答的是同源策略限制跨源响应读取,防的是恶意网站随便读你在其他网站的数据。
助手在这里纠正了我一处:同源策略管的是"响应读不读得到",请求本身是发出去的,所以顺带提到的 CSRF 钻的正是这个空子,得靠 SameSite 和 CSRF Token 去防,不能指望同源策略。后端解决方式我答了 CORS 响应头放行来源、开发时用代理绕过,助手补了复杂请求会先发 OPTIONS 预检,还有要带 cookie 的时候 Allow-Credentials 得是 true,并且 Allow-Origin 不能写成星号。