SWR
由 Next.js
(React SSR
框架)背后的同一团队创建。号称最牛逼的React
数据请求库
SWR
:是stale-while-revalidate
的缩写 ,源自 HTTP Cache-Control
协议中的 stale-while-revalidate 指令规范
。也算是HTTP
缓存策略的一种,这种策略首先消费缓存中旧(stale
)的数据,同时发起新的请求(revalidate
),当返回数据的时候用最新的数据替换运行的数据。数据的请求和替换的过程都是异步的,对于用户来说无需等待新请求返回时才能看到数据。
SWR
的缓存策略:
接受一个缓存key
,同一个 key
在缓存有效期内发起的请求,会走 SWR
策略
在一定时间内,同一个key
发起多个请求,SWR
库会做节流,只会有一个请求真正发出去
举个官网的简单列子
import useSWR from 'swr'
function Profile() {
const { data, error, isValidating, mutate } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
这个例子是前端较为基础的请求,通过使用useSWR
实现了简单明了的请求,当然它还有很多更强大的功能。
const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)
useSWR
接受三个参数:一个 key
、一个异步请求函数 fetch
和一个 config
配置 。
key
: 请求的唯一key string
(或者是 function
/ array
/ null
) 是数据的唯一标识符,标识数据请求,通常是 API URL
,并且 fetch
接受 key
作为其参数。
key
为函数function
或者null
:可以用来有条件地请求数据实现按需请求,当函数跑出错误或者falsy
值时,SWR
将不会发起请求。
// 有条件的请求
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
// ...或返回一个 falsy 值
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
// ... 或在 user.id 未定义时抛出错误
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)
依赖请求场景:当需要一段动态数据才能进行下一次数据请求时,它可以确保最大程度的并行性(avoiding waterfalls
)以及串行请求。
function MyProjects () {
const { data: user } = useSWR('/api/user')
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
// 传递函数时,SWR 会用返回值作为 `key`。
// 如果函数抛出错误或返回 falsy 值,SWR 会知道某些依赖还没准备好。
// 这种情况下,当 `user`未加载时,`user.id` 抛出错误
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}
fetcher(args)
: 返回数据的异步函数,接受 key
做参数并返回数据,你可以使用原生的 fetch
或 Axios
之类的工具。
config
data
: 通过 fetcher
用给定的 key
获取的数据(如未完全加载,返回 undefined
,这时可以用来做一些loading
态)
error
: fetcher
抛出的错误(或者是undefined
)
isValidating
: 是否有请求或重新验证加载
mutate(data?, shouldRevalidate?)
: 更改缓存数据的函数,可以在数据更改发起数据重新验证的场景
可以使用 useSWRConfig()
所返回的 mutate
函数,来广播重新验证的消息给其他的 SWR hook(*)
。使用同一个 key
调用 mutate(key)
即可。以下示例显示了当用户点击 “注销” 按钮时如何自动重新请求登录信息
import useSWR, { useSWRConfig } from 'swr'
function App () {
const { mutate } = useSWRConfig()
return (
<div>
<Profile />
<button onClick={() => {
// 将 cookie 设置为过期
document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
// 告诉所有具有该 key 的 SWR 重新验证
mutate('/api/user')
}}>
Logout
</button>
</div>
)
}
通常情况下 mutate
会广播给同一个 cache provider
下面的 SWR hooks
。如果没有设置 cache provider
,即会广播给所有的 SWR hooks
。
通过 SWR
官网介绍,SWR
有以下比较亮点特性
极速、轻量、可重用的 数据请求
内置 缓存 和重复请求去除
间隔轮询
聚焦时、网络恢复时重新验证
本地缓存更新
智能错误重试
分页和滚动位置恢复
虽然 SWR
特性很多,功能很强大,能极大提升用户体验,但是 SWR
却使用很简洁的思路完成上述所有的功能,并通过一个hook useSWR
即可拥有几乎全部功能。即功能强大,api
使用却十分简单,开发体验十分喜人。
上面说的 SWR
更多是功能特性,体验优化。那么站在技术的角度,SWR
又是扮演什么角色?
纯纯的 hook
请求库
全局状态管理
数据缓存
自动化重新数据验证(轮询、断网重连、页面聚焦等)
下面我们拆解他的功能,按照 SWR
一样的思路代码实现相同
useSWR
是一个 react hook
,通过这个 hook
你可以获取数据,并在数据获取后,触发页面重新渲染,这是 hook
基本特性,平平无奇,react``useState
+ promise
就能轻松实现。
function useSwr(key, fetcher) {
const [state, setState] = useState({})
const revalidate = useCallback(async () => {
try {
const result = await fetcher(key)
setState({
error: undefined,
data: result
})
} catch (error) {
setState({
...state,
error
})
}
})
useEffect(() => {
revalidate()
}, [key])
return { data, error }
}
有点简单,也的确没亮点,虽然 SWR
里面允许 key
是 string
、array
甚至 function
。但觉得这些都不是亮点。但是.... SWR
有一个小操作,有点按需更新的意思
function App() {
// const { data, error, isValidating } = useSWR('/api/user', fetcher)
const { data } = useSWR('/api/user', fetcher)
return <div>hello {data.name}!</div>
}
useSWR
会返回 data, error, isValidating
,只要有一个变化页面就会重新渲染。可页面只用到 data
, 是否可以 仅仅 data
更改时候才触发重新渲染呢?
SWR
做了一个操作,有点 vue mvvm
模型的意思(setter
,getter
在脑海里琅琅上口)。React
的setState
会触发更新,直接使用肯定不行,SWR
就封装了一下,SWR
是在 state.js
里面实现该逻辑
function useStateWithDeps(state) {
const stateRef = useRef(state)
//用于存储哪些属性被订阅
const stateDependenciesRef = useRef({
data: false,
error: false,
isValidating: false
})
const rerender = useState({})[1]
const setState = useCallback((payload) => {
let shouldRerender = false
const currentState = stateRef.current
for (const k in payload) {
// 是否有变化
if (currentState[k] !== payload[k]) {
currentState[k] = payload[k]
// 是否有被使用
if (stateDependenciesRef.current[k]) {
shouldRerender = true
}
}
}
if (shouldRerender && !unmountedRef.current) {
rerender({})
}
})
useEffect(() => {
stateRef.current = state
})
return [stateRef, stateDependenciesRef.current, setState]
}
// 如果单纯设计 stateDependenciesRef,可以把setter、getter 写在 useStateWithDeps 里面。但use 并没有直接暴露 stateDependenciesRef,而是暴露 useSwr。所以把数据劫持放在 useSwr
function useSwr(key, fetcher) {
//......
const [stateRef, stateDependencies, setState] = useStateWithDeps({
data,
error,
isValidating
})
return {
get data() {
stateDependencies.data = true
return data
},
get error() {
stateDependencies.error = true
return error
},
get isValidating() {
stateDependencies.isValidating = true
return isValidating
}
}
}
SWR
可不是简单管理一个组件的状态,而是组件之间相同 key
直接的数据是可以保持同步刷新,牵一发而动全身。React
的 useState
使用就是只会触发使用组件的重新渲染,即谁用我,我就更新谁。那么如何做到组件之间,一个地方修改,所有地方都能触发重新渲染。
下面演示,精简版的 React
全局状态库数据管理的实现。SWR
底层逻辑与之不谋而合
import { useState, useEffect } from 'react'
//全局数据存储
let data = {}
//发布订阅机制
const listeners = []
function broadcastState(state) {
data = {
...data,
...state,
}
listeners.forEach((listener) => listener(data))
}
const useData = () => {
const [state, setState] = useState(data)
function handleChange(payload) {
setState({
...state,
...payload,
})
broadcastState(payload)
}
useEffect(() => {
listeners.push(handleChange)
return () => {
listeners.splice(listeners.indexOf(handleChange), 1)
}
}, [])
return [state, handleChange]
}
export default useData
在上面讲到全局状态时候,我们定义了一个 data
存储了数据,在 SWR
底层,则是采用一个 weakMap
存储数据,道理相似。
SWR
是一个请求库,对于数据存储,并不是直接存储 Data
, 而是存储 Promise<Data>
充分利用promise
状态一旦更改就不会变的特性,也十分适合异步数据请求
//区分 key,可以理解为安置 key 管理
const globalState = new Map({
//更新数据事件,即上文中的 listeners
STATE_UPDATERS: {}, //[key:callbacks]
//重新获取数据事件
EVENT_REVALIDATORS: {}, //[key:callbacks]
// 异步数据请求缓存,缓存的是 promise
FETCH: {}, //[key:callbacks]
})
function useSwr(key, fetcher) {
//...
const [stateRef, stateDependencies, setState] = useStateWithDeps(cacheInfo)
//获取数据函数
const revalidate = async () => {
if (cache) {
} else {
// 没有 await
const fetch=fetcher(...args)
setCache(key,fetch)
}
}
}
即间隔固定时间,重新发送请求,更新数据
function useSwr(key,fetcher) {
//...
// Polling
useEffect(() => {
let timer
function next() {
timer = setTimeout(execute, interval)
}
function execute() {
revalidate().then(next)
}
next()
return () => {
if (timer) {
clearTimeout(timer)
timer = -1
}
}
}, [interval])
}
也是如同上文的的全局状态管理,在使用 useSwr
时候把重新获取数据的函数(事件)推送到全局的数据存储里面,然后订阅浏览器事件,并从全局数据存储里面读取事件执行
//subscribe-key.js
function subscribeCallback(events, callback) {
events.push(callback)
return () => {
const index = events.indexOf(callback)
// 释放事件
if (index >= 0) {
// O(1): faster than splice
events[index] = events[events.length - 1]
events.pop()
}
}
}
// useSwr.js
function useSwr(key, fetcher) {
//...
//获取数据函数
const revalidate = async () => {
//...fetcher()
}
useEffect(() => {
// 更新数据,推入队列,确保其他组件更新数据,能通过 broadcastState 触发当前组件更新
const onStateUpdate = () => {
//... setState()
}
// 重新刷新数据,在一些网络恢复、聚焦时候执行
const onRevalidate = () => {
//... revalidate()
}
const unsubUpdate = subscribeCallback(key, STATE_UPDATERS, onStateUpdate)
const unsubEvents = subscribeCallback(key, EVENT_REVALIDATORS, onRevalidate)
return () => {
unsubUpdate()
unsubEvents()
}
}, [key, revalidate])
//...
}
浏览器订阅事件如下
在 useEffect
统一监听浏览器事件即可
// web-preset.js
const onWindowEvent =window.addEventListener
const onDocumentEvent = document.addEventListener.bind(document)
const offWindowEvent =window.removeEventListener.bind(window)
const offDocumentEvent =document.removeEventListener.bind(document)
const initFocus = (callback) => {
// 页面重新聚焦 重新获取数据
onDocumentEvent('visibilitychange', callback)
onWindowEvent('focus', callback)
return () => {
offDocumentEvent('visibilitychange', callback)
offWindowEvent('focus', callback)
}
}
const initReconnect = (callback) => {
// 网络恢复,重新获取数据
const onOnline = () => {
online = true
callback()
}
// nothing to revalidate, just update the status
const onOffline = () => {
online = false
}
onWindowEvent('online', onOnline)
onWindowEvent('offline', onOffline)
return () => {
offWindowEvent('online', onOnline)
offWindowEvent('offline', onOffline)
}
}
useSWR(key, fetcher, options)
中options
支持需要配置属性,那么如果期望在某个范围内,所有的hook
,共用一套配置如何实现呢。SWR
提供一个组件叫 SwrConfig
import useSWR, { SWRConfig } from 'swr'
function App () {
return (
<SWRConfig
value={{
refreshInterval: 3000,
fetcher: (resource, init) => fetch(resource, init).then(res => res.json())
}}
>
<Dashboard />
</SWRConfig>
)
}
Dashboard
下所有的 useSwr
共用 value
作为配置。
组件提供全局配置的 provider
,子组件都共用这个配置,是一种很常见组件的设计思路。主要思路就是利用 react.createContext
提供 Provider
、Consumer
能力,不过现在使用 useContext
,使用上会比 Consumer
好太多了。
const SWRConfigContext = createContext({})
const ConfigProvider = (props) => {
// mergeConfigs 会处理中间件 merge逻辑
// 必须继承上一个 provider SWRConfig 的配置 进行 merge
const extendedConfig = mergeConfigs(useContext(SWRConfigContext), value)
return createElement(
SWRConfigContext.Provider,
mergeObjects(props, {
value: extendedConfig, // swr 一些运算处理的配置
})
)
}
export const useSWRConfig = () => {
return mergeConfigs(defaultConfig, useContext(SWRConfigContext))
}
export const SWRConfig = OBJECT.defineProperty(ConfigProvider, 'default', {
value: defaultConfig,
})
然后在使用中就可以使用全局配置
const fallbackConfig = useSWRConfig()
// 格式化用户入参
const [key, fn, _config] = normalize(args)
const config = mergeConfigs(fallbackConfig, _config)
SWR
也支持中间件,让你能够在 SWR hook
之前和之后执行代码。
useSWR(key, fetcher, { use: [a, b, c] })
中间件执行的顺序是 a → b → c
,如下所示:
enter a
enter b
enter c
useSWR()
exit c
exit b
exit a
那么 swr 是如何实现洋葱模型的呢?代码简单只有10行不到的代码。就是实现一个 compose
逻辑,然后通过函数执行栈一层层嵌套即可,这里有个注意点就是,从最后一个开始嵌套,然后从第一个开始执行。逐层释放执行栈,则刚好是完美洋葱模型的执行顺序。
一个中间件格式如下:
接受上一个 useSwr
这个hook
,返回一个新的 hook
。 很符合 compose
函数的思想呀
// Apply middleware
let next = hook //原始的中间件
const { use } = config //中间件列表
if (use) {
for (let i = use.length; i-- > 0; ) {
next = use[i](next)
}
}
return next(key, fn || config.fetcher, config)
这个其实逻辑很简单,但却很关键,所以也在这说明一下
假设我们对一个 key
,发了2个请求req1
、req2
。发出的顺序和数据返回数据如下
// req1------------------>res1 (current one)
// req2---------------->res2
因为 req2 发出的事件比较晚,那么我们页面展示的数据应该
因为 req2
发出的事件比较晚,那么我们页面展示的数据应该是以 res2
。即始终只更新最晚一次请求的返回值,即 req2
的返回值(这里就算 res2
返回更早也是展示 res2
,取决于请求事件)
function useSwr(key, fetcher) {
const revalidate = async () => {
FETCH[key] = [currentFetcher(...fnArgs), getTimestamp()]
;[newData, startAt] = FETCH[key]
newData = await newData
//...
// 当请求数据返回时候,发现staryAt 不一致,说明有其他同 key 请求已经 发出去
if (!FETCH[key] || FETCH[key][1] !== startAt) {
//!(FETCH[key] && FETCH[key][1] == startAt)
if (shouldStartNewRequest) {
if (isCurrentKeyMounted()) {
getConfig().onDiscarded(key)
}
}
return false
}
}
}
这里有一个容易疑惑的点就是为何只是判断 startAt
不相等就放弃当前数据更改呢?这是因为 FETCH
是全局缓存,是用 map
存储,实时更新。且 FETCH[key]
始终只存一个请求,一旦不等就说明在此之后有相同的 key
请求被发出。
startAt
这个变量是存储在当前组件的作用域里面,而 FETCH
全局缓存,所有组件共享的数据
SWR
里面还有需要工具函数可以学习
hash
与深比较SWR
中的hash.js
用于哈希 key
、data
,形成一个字符串,并在深比较函数 compare
通过哈希后字符串判断数据是否有变化,是否需要重新请求、重新渲染
SWR
的key
格式可以是 function / array / null
,也是在统一的 normalize.js
里做处理,如果是 falsy
值,则表示不发请求
SWR
还有许多 options
配置和功能,比如上轮询间隔、是否启用缓存、是否开重复请求去除、错误重试、超时重试、支持 ssr
等。这些都不影响主流逻辑,下面我们按照上面拆解的核心功能,查看 SWR
源码。
SWR
对把逻辑拆分到一个个文件,通过文件名以及我们上面的分析,很容易猜出文件中的逻辑
├── constants
│ └── revalidate-events.ts
├── index.ts
├── types.ts
├── use-swr.ts
└── utils
├── broadcast-state.ts // 组件状态修改通知其他组件渲染
├── cache.ts // 缓存,缓存事件:如重新请求、网络恢复等事件
├── config-context.ts //全局配置 react context
├── config.ts
├── env.ts
├── global-state.ts //缓存,搭配 cache 使用
├── hash.ts // 对数据hash,形成字符串,用于深比较
├── helper.ts
├── merge-config.ts
├── mutate.ts // 更改缓存
├── normalize-args.ts // 格式化入参
├── resolve-args.ts //初始化操作,是一个 hoc 逻辑,
├── serialize.ts // hash
├── state.ts // 属性按需触发重新渲染
├── subscribe-key.ts // 添加事件订阅
├── timestamp.ts
├── use-swr-config.ts
├── web-preset.ts //浏览器事件:聚焦、网络状态变更
└── with-middleware.ts //中间件
核心流程图
src/use-swr.ts
function useSwr(args) {
//...
const fallbackConfig = useSWRConfig()
// 格式化用户入参
const [key, fn, _config] = normalize(args)
const config = mergeConfigs(fallbackConfig, _config)
// 读取全局缓存,如数据缓存(promise)、事件缓存
const [EVENT_REVALIDATORS, STATE_UPDATERS, MUTATION, FETCH] =
SWRGlobalState.get(cache)
//当前 key 读取存储,有缓存优先使用缓存数据
const cached = cache.get(key)
const data = isUndefined(cached) ? fallback : cached
const info = cache.get(keyInfo) || {}
const error = info.error
//按需更新
const [stateRef, stateDependencies, setState] = useStateWithDeps({
data,
error,
isValidating,
})
//获取数据函数
const revalidate = async () => {
const shouldStartNewRequest = !FETCH[key] || !opts.dedupe
if (!shouldStartNewRequest) {
} else {
// 没有 await
FETCH[key] = [currentFetcher(...fnArgs), getTimestamp()]
}
//...
;[newData, startAt] = FETCH[key]
newData = await newData
//...
finishRequestAndUpdateState()
//...
broadcastState()
}
useEffect(() => {
// 更新数据,推入队列,确保其他组件更新数据,能通过 broadcastState 触发当前组件更新
const onStateUpdate = () => {
//... setState()
}
// 重新刷新数据,在一些网络恢复、聚焦时候执行
const onRevalidate = () => {
//... revalidate()
}
const unsubUpdate = subscribeCallback(key, STATE_UPDATERS, onStateUpdate)
const unsubEvents = subscribeCallback(key, EVENT_REVALIDATORS, onRevalidate)
return () => {
unsubUpdate()
unsubEvents()
}
}, [key, revalidate])
return {
get data() {
stateDependencies.data = true
return data
},
get error() {
stateDependencies.error = true
return error
},
get isValidating() {
stateDependencies.isValidating = true
return isValidating
},
}
}
SWR
是 一个很轻的 hook
请求库,能在提升用户体验的前提下,也保证很好的开发体验和很低的开发成本。设计理念也很 React
,核心功能的实现逻辑也很简单。通过分析SWR
源码,学习核心功能的实现方式,能有效提升代码逻辑思维。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ljFVnDzq4vVSxaUztswCiA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。