外观
Vue 3 的响应式怎么实现?
Vue 3 用 Proxy 拦截对象的读取和写入。读取时把当前正在运行的效果记录下来,写入时把这些效果重新执行。组件渲染就是一个效果,所以用到的数据一变,组件会再渲染。
原理
reactive(obj) 返回深层代理。效果函数运行期间,get 调用 track(target, key) 收集依赖;set 调用 trigger(target, key) 通知它们。没被读过的属性,后来变了也不会通知任何人。
js
const state = reactive({ count: 0 })
// 渲染时读了 state.count,这次渲染就订阅了 countref 用一个带 value 的对象装原始值,因为 Proxy 只能代理对象。模板里的 ref 会自动解包,<script> 里要写 .value。把 ref 放进 reactive 对象再读取时,也会自动解包。
computed 懒计算:依赖变了只把结果标成脏,下次读取才重算,并把自己注册成依赖的订阅者。watch 追踪指定数据源并跑副作用;watchEffect 立刻执行一次,自动收集里面读到的数据。
Vue 2 用 Object.defineProperty 在已有的键上拦截。后加的属性、按下标改数组,默认观察不到,要走 Vue.set 或数组的变更方法。Proxy 可以拦截新增、删除和更多数组操作。
Vue 3.5 起,模板编译会保持对解构后的 props 的响应式。更早的版本里,把 props 解构到局部变量会失去响应。
追问
为什么改一个没在模板里用到的属性,组件不更新?
渲染时没有读它,track 没有记录这个键。这是精确更新,不是 bug。
reactive 换掉整个对象会怎样?
替换绑定本身要靠 ref。let state = reactive(...) 再 state = reactive(...) 不会通知旧对象的订阅者,因为订阅在旧对象上。
和 React 的差别?
Vue 在读取时收集依赖,组件默认只跟用到的数据走。React 是状态变了就重新执行组件函数,再靠 memo 和编译器减少子树渲染。见 状态管理。
易错点
- 解构
reactive对象会把当时的值拷出来,之后不再追踪。要保持联系可以用toRefs。 - 已经是代理的对象不要重复包一层去比较引用。
- 响应式只存在于被读过的访问路径上。绕过代理去改原始对象(
toRaw之后写入)不会触发更新。