导读
vue面试中经常会问到响应式以及计算属性和watch侦测的区别等这些知识点,下面就可以从简单到难来讲一下vue的响应式是怎么工作的?本文参考Observer、Dep、Watcher 傻傻搞不清楚
易
一般回答vue响应式肯定会说在vue2.x中使用Object.defineProperty来进行数据劫持,当数据发生改变的时候,会调用setter,然后通知相依赖的watcher,最后更新视图?那这里就会有几个问题延伸,首先,Object.defineProperty是不能监测到数组的变化的?vue2.x有没有什么办法去解决?vue3.x是怎么进行数据拦截的,为什么这样可以进行拦截?watcher是什么?让你自己去实现一个观察者模式可以实现吗?是不是能问的问题很多?而这些也是我所遇见的~~
难
在vue源码中实际上在new Vue的时候会初始化vue组件中的data以及其他配置
// src/core/instance/index.js
function Vue (options) {
// ...
this._init(options)
}
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
// 合并配置
// ...
// 一系列初始化
// 比较重要
initState(vm)
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
initState(vm)
// src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = [] // _watchers
const opts = vm.$options //
if (opts.props) initProps(vm, opts.props) // 初始化 props
// ...
if (opts.data) {
// 初始化 data, 重要
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// ...
}
这里是一些初始化操作,比如初始化data,options,computed以及其他一些属性,在initData(vm) 中实际上实现了观察者模式(发布订阅),那么实际怎么做的呢?initData(vm)实际上说白了就是初始化一个dep,这个后面讲,然后利用Object.defineProperty创建setter以及getter
// src/core/observer/index.js
// ...
const dep = new Dep()
// Dep类
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ...
dep.depend()
//...
},
set: function reactiveSetter (newVal) {
// ...
dep.notify()
},
...
}
dep其实里面有一个subs用来存储依赖,所以说dep是一个依赖管理者
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {}
removeSub (sub: Watcher) {}
depend () {}
notify () {}
}
到了这里其实响应式数据和依赖管理器都已经准备好了,还需要一个什么呢?还需要一个订阅者,也就是watcher,watcher什么时候初始化呢?当执行到挂载阶段的时候初始化watcher,进行求值,求值时会调用getter,执行dep.depend(), 将相关依赖添加到dep中(这里注意 每一个Observer实例都会有一个dep) 数据发生变化,数据拦截器调用setter得通知订阅者dep.notify()
// src/core/observer/watcher.js
export default class Watcher {
// 对 getter 求值,进行依赖收集
get () {}
// 触发更新
update() {}
}
所以总结下来就是:
- 初始化data时首先创建Observer实例,为每一个实例都新建一个Dep类以及将数据属性转化成setter以及getter;
- 中间的Dep类实际上是存储相关依赖的,也就是watcher;
- watcher初始化时会进行求值,求值会触发getter;收集依赖;
但是在vue官网里,Object.defineProperty是无法监测到数组改变的,比如官网例子是这样的:
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的
在官网里是用Vue.set()以及vm.items.splice()解决的:
Vue.set(vm.items, indexOfItem, newValue) // vm.$set
vm.items.splice(indexOfItem, 1, newValue)
这两种办法可以触发响应式系统。(面试里问过)
vue3的proxy代理以及reflect反射
在vue3里,数据拦截它换成代理方法,同一时间也出现了反射方法。
function ProxyWatcher(data,queue){
return new Proxy(data,{
get:(target,key)=>{
value=target[key]
return value
},
set:(target,key,value)=>{
target[key]=value;
queue[key].notify(value)
}
})
}
调用new Proxy()可创建代替其他目标(target)对象的代理,能够拦截并改变js引擎的底层操作,拦截行为使用了一个能够响应特定操作的函数(被称为陷阱)而reflect对象所代表的反射接口,是给底层提供默认行为的方法的集合。为什么代理可以用作数据拦截呢?就是因为每个陷阱函数都允许重写js的内置行为。
后记
写一个简单的发布订阅叭(面试遇到)
function Subject(){
this.observers=[];
this.attach=function(callback){
this.observers.push(callback);
}
this.notify=function(value){
this.observers.forEach((func)=>{
func(value)
})
}
}
function Watcher(data,queue){
for(let key in data){
Object.defineProperty(data,key,{
let value=data[key];
enumerable:'',
get:()=>{},
set:(newValue)=>{
value=newValue;
queue[key].notify(value)
}
})
}
}
function Observer(queue,key,callback){
queue[key].attach(callback);
}
const Data={value:''}
const messageQueue={}
for(let key in Data){
messageQueue[key]=new Subject();
}
Observer(messageQueue,"value",(value)=>{
console.log("value",value)
})
let myData=Watcher(Data,messageQueue)
myData.value=''