平平无奇古哥哥
2023/05/23阅读:22主题:山吹
对线面试官:Vue响应式原理

面试官:
讲一下什么是响应式,Vue是怎么实现响应式的?
小明:
所谓响应式,指数据变更 -> 视图自动更新。
Vue2是通过Object.defineProperty进行数据侦听,并且在数据getter时做依赖收集,在setter时通知页面更新视图来实现响应式的。
面试官:
你刚刚提到依赖收集,那你讲讲什么是依赖收集,为什么要做依赖收集?
小明:
收集依赖是为了知道哪些视图用到了data里的状态。
举个例子,在模板里我们使用了两个状态name和sex:

但在data里面,我们声明了3个状态,name、sex和age:

当我们通过this.age = xxx来改变age时,页面其实并不需要更新。
可是Vue是怎么知道哪个状态变了需要更新,哪个变了不需要更新呢,这就需要一种机制来保存哪些状态被视图引用了,而这个机制,就叫依赖收集。
(注:其实引用状态的并不只有视图,watch和computed里面也引用了data里的属性,也会做依赖收集,但此处为了便于理解,我们统称为视图。)
面试官:
那你知道依赖收集的实现原理吗?
小明:
依赖收集的实现主要分两步,依靠Dep和Watch两个类来实现:
-
首先是当Vue实例化时,会遍历数据对象中的每个属性,并为每个属性创建一个依赖收集器(Dep)对象,用于存储依赖于该属性的Watcher对象。 -
然后当组件渲染时,会解析模板变量,触发属性的getter,同时会创建Watcher对象,用来将组件实例、属性和callback方法关联起来,并且存到依赖收集器对象里。
这样当状态变更时,Vue就知道是哪个组件需要更新了,只需调用与该组件对应的callback方法去完成更新即可。
面试官:
嗯,不错不错(假装听懂),那你知道什么是细粒度更新吗,为什么Vue要采取中等粒度的更新方式?
小明:
我们刚刚讲到,当状态变更时,Vue会通知到需要更新的组件,然后会在组件内部做虚拟DOM的diff,通过diff算法来实现组件级别的更新。简单来说就是,Vue的更新是组件级别的。
之所以说Vue2的这种组件级别的更新机制是中等粒度的,其实是相对于Vue1和React来说的:
-
在Vue1中并没有虚拟DOM,Vue1的更新机制完全是依靠响应式来实现的,状态变更时直接通知到需要变更的DOM,属于DOM级别的更新。但这样做的缺陷是要创建的Watcher对象太多,在大型项目中会产生性能问题。 -
React也使用了虚拟DOM,但React的更新是应用级别的。React每次更新,是将一整颗新的虚拟DOM树和旧的进行比对,这样更新的性能会相对较差,有很多不必要的更新。
Vue2采取组件级别的更新,相当于是取了个折中的方案,这也是一种平衡的设计理念。
面试官:
那你知道Vue2的响应式有哪些缺陷吗?
小明:
由于Object.defineProperty这个API的限制,导致Vue2的响应式存在两大缺陷:
-
无法监听到对象新增属性或者删除属性的变更。 -
无法直接对数组进行监听,Vue2是通过重写了数组的push、pop、shift、unshift、sort、reverse方法来实现对数组的响应式的,我们通过这些方法来改变数组是可以被Vue监听到的。但是对于直接更改数组长度、向指定位置添加元素以及修改指定位置元素,是无法被监听到的。
对于这些问题,在开发中可以使用$set这个API去解决。举个例子:

另外,在Vue3中使用了Proxy去实现响应式,这两类问题也就不存在了。
面试官:
嗯嗯,你小子讲得还可以。
下周来参加二面吧,下次我们来聊聊Vue3的响应式实现,好好准备下,能不能过就看下次了。
小明:
好的,收到!
未完待续。。
作者介绍