ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Enzyme ReactWrapper `.update()` API 深度解析:让组件树快照与真实 DOM 保持同步

Enzyme ReactWrapper `.update()` API 深度解析:让组件树快照与真实 DOM 保持同步 Enzyme ReactWrapper.update()API 深度解析让组件树快照与真实 DOM 保持同步【免费下载链接】enzymeJavaScript Testing utilities for React项目地址: https://gitcode.com/gh_mirrors/en/enzyme在 React 集成测试中Enzyme 的mount()返回的ReactWrapper本质上持有一份组件树的快照。当组件状态被绕过 Enzyme 的 API 改变异步setState、外部回调、setImmediate触发的事件等时这份快照就会与真实 DOM 脱节。本文围绕官方 API 文档 .update() 展开完整讲解.update()的用途、三条关键注意事项、官方示例并结合 packages/enzyme/src/ReactWrapper.js 的源码剖析快照同步在底层是如何实现的、为什么它永远不会强制重渲染以及在 Hooks 测试场景中何时必须手动调用它。什么是.update()根据 API 文档.update() Self的官方定义是Syncs the enzyme component tree snapshot with the react component tree. Useful to run before checking the render output if something external may be updating the state of the component somewhere.也就是说.update()做的事情非常单一把 Enzyme 内部缓存的组件树快照与 React 实际渲染出的组件树重新对齐。它的典型使用场景是在你断言渲染输出之前有任何外部因素可能已经修改了组件状态此时先调用.update()再执行find()/text()/html()等查询得到的才是当前真实输出。返回值为ReactWrapper本身返回this因此支持链式调用。官方文档给出了三条必须牢记的注意事项NOTE无论你在哪个 wrapper 实例上调用它永远更新的是 root它只更新 Enzyme 对已渲染树的表示不触碰 React 本身它不触发重渲染。若你确实想强制重渲染应使用wrapper.setProps({})见 setProps。官方示例完整可运行以下示例完整继承自官方文档演示了通过外部调用instance()方法改变状态后必须先update()才能断言新输出这一核心用法class UpdateEnzyme extends React.Component { constructor(props) { super(props); this.state { count: 0, }; this.increment this.increment.bind(this); } increment() { const { count } this.state; this.setState({ count: count 1 }); } render() { const { count } this.state; return button typebutton classNameincrement onClick{this.increment}{count}/button; } }const wrapper mount(UpdateEnzyme /); expect(wrapper.find(button.increment).text()).to.equal(0); wrapper.instance().increment(); // Update Enzymes view of output wrapper.update(); expect(wrapper.find(button.increment).text()).to.equal(1);示例中的关键点在于wrapper.instance().increment()是绕过 Enzyme 直接调用组件实例方法的React 内部确实执行了setState并重渲染了但 Enzyme 的快照不会自动感知这次变化。若跳过中间的wrapper.update()第二个断言拿到的仍是旧的0。这正是.update()存在的意义。源码剖析update()是如何同步快照的update()的实现在 ReactWrapper.js核心逻辑只有四步update() { const root this[ROOT]; if (this ! root) { return root.update(); } privateSetNodes(this, this[RENDERER].getNode()); this[LINKED_ROOTS].forEach((linkedRoot) { if (linkedRoot ! this[UPDATED_BY]) { linkedRoot[UPDATED_BY] this; try { linkedRoot.update(); } finally { linkedRoot[UPDATED_BY] null; } } }); return this; }逐段对应文档中的三条 NOTE1. 永远更新 root非根实例委托给根方法开头先取this[ROOT]如果当前实例不是根直接return root.update()委托出去。这与setProps()、setState()、unmount()等方法的守卫模式一致均抛出can only be called on the root错误区别在于update()选择的是静默委托而非报错。2. 只更新 Enzyme 的表示privateSetNodes真正干活的是一行privateSetNodes(this, this[RENDERER].getNode());this[RENDERER].getNode()是从适配器adapter的渲染器上重新取一次当前已渲染的组件树然后 privateSetNodes 把它写入 wrapper 私有的NODE/NODES字段并同步length。整个过程中没有任何render()调用——这就是不强制重渲染的源码证据它只做读取 覆盖缓存React 侧一个字节都不会变。3. 关联根同步wrappingComponent场景当通过mount(..., { wrappingComponent })挂载时构造函数会创建一个特殊的WrappingComponentWrapperReactWrapper.js并与主根互相推入对方的LINKED_ROOTS。update()遍历这些关联根并递归同步保证主 wrapper 和 wrapping wrapper 两侧的快照一致测试用例可见 getWrappingComponent.jsx。其中UPDATED_BY哨兵配合try/finally还原是一个精妙的递归保护两个根互为关联根时A 更新 B、B 又更新 A 会形成无限循环。源码注释明确写道——Only update a linked it root if it is not the originator of our update(). This is needed to prevent infinite recursion when there is a bi-directional link between two roots.与其他 API 的分工谁会自己更新谁需要你手动调用从源码看Enzyme 内部所有会真正触发 React 重渲染的方法都会在渲染回调里自动调用this.update()setProps()this[RENDERER].render(this[UNRENDERED], null, () { this.update(); ... })L342-L358setState()在instance.setState的回调中this.update()L373-L393setContext()渲染回调中() this.update()L404-L413mount()/unmount()同样在渲染/卸载后自动update()L302-L326因此只要你走的是 Enzyme 自己的 API 改状态通常不需要再手动update()。需要手动调用的正是文档所说something external may be updating the state的情形仓库测试套件里有大量真实用例可参考异步setState场景ReactWrapper-spec.jsxit(has updated output after an asynchronous setState, () { const wrapper mount(Test /); wrapper.find(.async-btn).simulate(click); return new Promise((resolve) { setImmediate(() { wrapper.update(); // 异步状态落地后手动对齐快照 resolve(); }); }).then(() { expect(wrapper.find(.show-me)).to.have.lengthOf(1); }); });子组件通过 props 回调改父状态ReactWrapper-spec.jsxit(has updated output after child prop callback invokes setState, () { const wrapper mount(Test /); wrapper.find(Child).props().callback(); wrapper.update(); expect(wrapper.find(.show-me)).to.have.lengthOf(1); });Hooks 场景在useState/useEffect/useReducer等 Hook 测试如 useState.jsx中事件模拟后触发 Hook 状态更新时同样依赖wrapper.update()刷新快照后再断言。与 ShallowWrapper 的update()的差异shallow()渲染的ShallowWrapper也有update()ShallowWrapper.js但约束更严格if (this[ROOT] ! this) { throw new Error(ShallowWrapper::update() can only be called on the root); } if (this[NODES].length ! 1) { throw new Error(ShallowWrapper::update() can only be called when wrapping one node); }即 ShallowWrapper 的update()必须在根上调用且只能包装单个节点而 ReactWrapper 版本对子实例采取静默委托到根的宽松策略。这是两者在错误处理哲学上的一个显著区别。实战要点小结判断是否需要update()的标准状态变化是否绕过了 Enzyme 的 API异步 setState、外部回调、instance()方法调用、setImmediate/Promise 中的更新update()是纯快照操作成本很低在不确定时先 update 再断言是安全习惯需要真正强制重渲染时请用wrapper.setProps({})不要用update()替代在wrappingComponent挂载时update()会自动同步主根与 wrapping wrapper 两侧的快照无需分别调用。【免费下载链接】enzymeJavaScript Testing utilities for React项目地址: https://gitcode.com/gh_mirrors/en/enzyme创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表