SwiftUI @EnvironmentObject更新时重绘父视图而非当前视图问题
问题背景
我在探究ObservableObject内属性变更时,@EnvironmentObject对视图重绘的影响机制。根据苹果官方EnvironmentObject文档说明:可观察对象发生变更时,环境对象会将当前视图标记为失效。
如果该规则成立,点击ContentView中的按钮时,下方示例代码应当会使ChildView失效并重新初始化,预期控制台输出如下:
initializing parent initializing child // after pressing the button initializing child
但实际运行结果与预期相悖,实际控制台输出为:
initializing parent initializing child // after pressing the button initializing parent
核心疑问:该现象的产生原因是什么?ParentView并未依赖Library实例,为何会被重新创建?
示例代码
class Library: ObservableObject { @Published var item: Int = 0 } struct ContentView: View { @StateObject var library: Library = Library() var body: some View { VStack { ParentView() .environmentObject(library) Button { library.item += 1 } label: { Text("increment") } } } } struct ParentView: View { init() { print("initializing parent") } var body: some View { VStack { Text("Parent view") ChildView() } } } struct ChildView: View { @EnvironmentObject var library: Library init() { print("initializing child") } var body: some View { Text("Child view") } }
原因解析
这个现象的核心误区是把SwiftUI视图结构体的初始化和**视图实际重绘(body重新计算)**两个行为混为一谈了,具体逻辑如下:
- SwiftUI里的
View是用来描述界面结构的轻量值类型,不是真正对应屏幕上渲染节点的持久化对象,系统在做视图树diff、状态变更校验的过程中,会频繁创建、销毁这些结构体值,这个过程性能开销极低,和大家认知里「视图销毁重建」完全不是一个概念。 - 你在
init方法里打印的日志,只能证明对应的View结构体被创建了新的值,不能证明这个视图被标记为失效、触发了重绘。 - 当
Library的@Published属性变更时,SwiftUI会从.environmentObject(library)的注入位置(也就是ParentView的调用层)开始向下遍历整个子视图树,做状态依赖校验,这个遍历过程中会生成子树节点对应的新View结构体值,这就是你看到ParentView初始化日志的直接原因。 - 遍历校验过程中,系统只会给明确订阅了该环境对象的视图(也就是这里的
ChildView)打上需要重绘的标记,触发它的body属性重新计算;ParentView本身没有声明@EnvironmentObject依赖Library,所以即使它的结构体被重新创建,它的body不会被调用,根本没有发生重绘,完全符合苹果官方文档描述的规则。 - 你可以在
ParentView和ChildView的body属性里加入打印日志验证:点击按钮后只会出现ChildView的body调用日志,不会出现ParentView的body调用日志。
内容的提问来源于stack exchange,提问作者slow
相关产品推荐
相关产品推荐

