SwiftUI之从前端视角看SwiftUI语言
【摘要】
一、从 class 迈向 struct,从 class 迈向 function
可以将前端框架归纳为几个要素:
元件化;
响应式机制;
状态管理;
事件监听;
生命...
一、从 class 迈向 struct,从 class 迈向 function
- 可以将前端框架归纳为几个要素:
-
- 元件化;
-
- 响应式机制;
-
- 状态管理;
-
- 事件监听;
-
- 生命周期。
- 在写 SwiftUI 的时候总是想到 React 的发展史,最初 React 建立元件的方式是透过 JavaScript 的 class 语法,每个 React 的元件都是一个类别。
class MyComponent extends React.Component {
constructor() {
this.state = {
name: 'kalan'
}
}
componentDidMount() {
console.log('component is mounted')
}
render() {
return <div>my name is {this.state.name}</div>
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 透过类别定义元件虽为前端元件化带来了很大的影响,但也因为繁琐的方法定义与 this 混淆,在 React16 hooks 出现之后,逐渐提倡使用 function component 与 hooks 的方式来建立元件。
- 省去了继承与各种 OO 的花式设计模式,建构元件的心智负担变得更小了。从 SwiftUI 当中,也可以看到类似的演进,原本 ViewController 庞大的 class 以及职责,要负责 view 与 model 的互动,掌管生命周期,转为更轻量的 struct,让开发者可以更专注在 UI 互动上,减轻认知负担。
二、元件状态管理
- React 16 采取了 hooks 来做元件的逻辑复用与状态管理,例如 useState:
const MyComponent = () => {
const [name, setName] = useState({ name: 'kalan' })
useEffect(() => { console.log('component is mounted') }, [])
return <div>my name is {name}</div>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 在 SwiftUI 当中,可以透过修饰符 @State 让 View 也具有类似效果。两者都具备响应式机制,当状态变数发生改变时,React/Vue 会侦测改变并反映到画面当中。虽然不知道 SwiftUI 背后的实作,但背后应该也有类似 diff 机制的东西来达到响应式机制与最小更新的效果。
- 然而 SwiftUI 的状态管理与 React hooks 仍有差异,在 React 当中,可以将 hook 拆成独立的函数,并且在不同的元件当中使用,例如:
function useToggle(initialValue) {
const [toggle, set] = useState(initialValue)
const setToggle = useCallback(() => { set((state) => !state) }, [toggle])
useEffect(() => { console.log('toggle is set') }, [toggle])
return [toggle, setToggle]
}
const MyComponent = () => {
const [toggle, setToggle] = useToggle(false)
return <button onClick={() => setToggle()}>Click me</button>
}
const MyToggle = () => {
const [toggle, setToggle] = useToggle(true)
return <button onClick={() => setToggle()}>Toggle, but fancy one</button>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 在 React 当中,可以将 toggle 的逻辑拆出,并在不同元件之间使用,由于 useToggle 是一个纯函数,因此内部的状态也不会互相影响。
- 然而在 SwiftUI 当中 @State 只能作用在 struct 的 private var 当中,不能进一步拆出。如果想要将重复的逻辑抽出,需要另外使用 @Observable 与 @StateObject 这样的修饰符,另外建立一个类别来处理。
class ToggleUtil: ObservableObject {
@Published var toggle = false
func setToggle() {
self.toggle = !self.toggle
}
}
struct ContentView: View {
@StateObject var toggleUtil = ToggleUtil()
var body: some View {
Button("Text") {
toggleUtil.setToggle()
}
if toggleUtil.toggle {
Text("Show me!")
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 在这个例子当中把 toggle 的逻辑拆成一个 class 似乎有点小题大作了,不过仔细想想像 React 提供的 hook 功能,让轻量的逻辑共用就算单独拆成 hook 也不会觉得过于冗长,若要封装更复杂的逻辑也可以再拆分成更多 hooks,从这点来看 hook 的确是一个相当优秀的机制。后来看到了 SwiftUI-Hooks,不知道实际使用的效果如何。
- 以 React 来说,在还没有出现 hooks 之前,主要有三个方式来实作逻辑共用:
-
- HOC(Higher Order Component):将共同逻辑包装成函数后返回全新的 class,避免直接修改元件内部的实作,例如早期 react-redux 中的 connect;
-
- render props:将实际渲染的元件当作属性(props)传入,并提供必要的参数供实作端使用;
-
- children function:children 只传入必要的参数,由实作端自行决定要渲染的元件。
三、Redux 与 TCA
- 受到 Redux 的影响,在 Swift 当中也有部分开发者使用了采用了类似手法,甚至也有相对应的实作 ReSwift 的说明文。从说明文可以看到主要原因,传统的 ViewController 职责暧昧,容易变得肥大导致难以维护,透过 Reducer、Action、Store 订阅来确保单向资料流,所有的操作都是向 store dispatch 一个 action,而资料的改动(mutation)则在 reducer 处理。
- 而最近的趋势似乎从 Redux 演变成了 TCA(The Composable Architecture),跟 Redux 的中心思想类似,更容易与 SwiftUI 整合,比较不一样的地方在于以往涉及 side effect 的操作在 Redux 当中会统一由 middleware 处理,而在 TCA 的架构中 reducer 可以回传一个 Effect,代表接收 action 时所要执行的 IO 操作或是 API 呼叫。
- 既然采用了类似 redux 的手法,不知道 SwiftUI 是否会遇到与前端开发类似的问题,例如 immutability 确保更新可以被感知;透过优化 subscribe 机制确保 store 更新时只有对应的元件会更新;reducer 与 action 带来的 boilerplate 问题。
- 虽然 Redux 在前端仍然具有一定地位,也仍然有许多公司正在导入,然而在前端也越来越多弃用 Redux 的声音,主要因为 redux 对 pure function 的追求以及 reducer、action 的重复性极高,在应用没有到一定复杂程度之前很难看出带来的好处,甚至连 Redux 作者本人也开始弃坑 redux 了 4。与此同时,react-redux 仍然有在持续更新,也推出了 redux-toolkit 来试图解决导入 redux 时常见的问题。
- 取而代之的是更加轻量的状态管理机制,在前端也衍生出了几个流派:
四、全域状态管理
- 在全域状态管理上,SwiftUI 也有内建机制叫做 @EnvrionmentObject,其运作机制很像 React 的 context,让元件可以跨阶层存取变数,当 context 改变时也会更新元件:
class User: ObservableObject {
@Published var name = "kalan"
@Published var age = 20
}
struct UserInfo: View {
@EnvironmentObject var user: User
var body: some View {
Text(user.name)
Text(String(user.age))
}
}
struct ContentView: View {
var body: some View {
UserInfo()
}
}
ContentView().envrionmentObject(User())
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 从上面这个范例可以发现,不需要另外传入 user 给 UserInfo,透过 @EnvrionmentObject 可以拿到当前的 context。转换成 React 的话会像这样:
const userContext = createContext({})
const UserInfo = () => {
const { name, age } = useContext(userContext)
return <>
<p>{name}</p>
<p>{age}</p>
</>
}
const App = () => {
<userContext.Provider value={{ user: 'kalan', age: 20 }}>
<UserInfo />
</userContext.Provider>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- React 的 context 可让元件跨阶层存取变数,当 context 改变时也会更新元件。虽然有效避免了 prop drilling 的问题,然而 context 的存在会让测试比较麻烦一些,因为使用 context 时代表了某种程度的耦合。
五、响应机制
- 在 React 当中,状态或是 props 有变动时都会触发元件更新,透过框架实作的 diff 机制比较后反映到画面上。在 SwfitUI 中也可以看到类似的机制:
struct MyView: View {
var name: String
@State private var isHidden = false
var body: some View {
Toggle(isOn: $isHidden) {
Text("Hidden")
}
Text("Hello world")
if !isHidden {
Text("Show me \(name)")
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 一个典型的 SwiftUI 元件是一个 struct,透过定义 body 变数来决定 UI。跟 React 相同,它们都只是对 UI 的抽象描述,透过比对资料结构计算最小差异后,再更新到画面上。
- @State 修饰符可用来定义元件内部状态,当状态改变时会更新并反映到画面中。在 SwiftUI 当中,属性(MyView 当中的 name)可以由外部传入,跟 React 当中的属性(props)类似。
// 在其他 View 当中使用 MyView
struct ContentView: View {
var body: some View {
MyView(name: "kalan")
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 用 React 改写这个元件的话会像这样:
const MyView = ({ name }) => {
const [isHidden, setIsHidden] = useState(false)
return <div>
<button onClick={() => setIsHidden(state => !state)}>hidden</button>
<p>Hello world</p>
{isHidden ? null : `show me ${name}`}
</div>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 在撰写 SwiftUI 时会发现这跟以往用 UIKit、UIController 的开发方式不太一样。
六、列表
- SwiftUI 与 React 当中都可以渲染列表,而撰写的方式也有雷同之处。在 SwiftUI 当中可以这样写:
struct TextListView: View {
var body: some View {
List {
ForEach([
"iPhone",
"Android",
"Mac"
], id: \.self) { value in
Text(value)
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 转成 React 大概会像这样子:
const TextList = () => {
const list = ['iPhone', 'Android', 'Mac']
return list.map(item => <p key={item}>{item}</p>)
}
- 1
- 2
- 3
- 4
- 5
- 在渲染列表时为了确保效能,减少不必要的比对,React 会要求开发者提供 key,而在 SwiftUI 当中也有类似的机制,开发者必须使用叫做 Identifiable[11] 的 protocol,或是显式地传入 id。
七、Binding
- 除了将变数绑定到画面之外,也可以将互动绑定到变数之中。例如在 SwiftUI 当中我们可以这样写:
struct MyInput: View {
@State private var text = ""
var body: some View {
TextField("Please type something", text: $text)
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 在这个范例当中,就算不监听输入事件,使用 $text 也可以直接改变 text 变数,当使用 @State 时会加入 property wrapper,会自动加入一个前缀 $,型别为 Binding。
- React 并没有双向绑定机制,必须要显式监听输入事件确保单向资料流。不过像 Vue、Svelte 都有双向绑定机制,节省开发者手动监听事件的成本。
八、Combine 的出现
- 虽然我对 Combine 还不够熟悉,但从官方文件与影片看起来,很像RxJS 的 Swift 特化版,提供的 API 与操作符大幅度地简化了复杂资料流。这让我想起了以前研究 RxJS 与 redux-observable 各种花式操作的时光,真令人怀念。
九、总结
- 前文提到那么多,然而网页与手机开发仍然有相当大的差异,其中对我来说最显著的一点是静态编译与动态执行。动态执行可以说是网页最大的特色之一。
- 只要有浏览器,JavaScript、HTML、CSS,不管在任何装置上都可以成功执行,网页不需要事先下载 1xMB ~ 几百 MB 的内容,可以动态执行脚本,根据浏览的页面动态载入内容。
- 由于不需要事先编译,任何人都可以看到网页的内容与执行脚本,加上 HTML 可以 streaming 的特性,可以一边渲染一边读取内容。难能可贵的一点是,网页是去中心化的,只要有伺服器、ip 位址与网域,任何人都可以存取网站内容;而 App 如果要上架必须事先通过审查。
- 不过两者的生态圈与开发手法有很大的不同,仍然建议参考一下彼此的发展,就算平时不会碰也没关系,从不同的角度看往往可以发现不同的事情,也可以培养对技术的敏锐度。
文章来源: blog.csdn.net,作者:Serendipity·y,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/Forever_wj/article/details/122900311
【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)