Vue 2响应式原理时序图与部分源码记录如下:
图很直观!
Vue 2 响应式原理时序图
官网对响应式的说明: v2.cn.vuejs.org/v2/guide/re...
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式> 化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把"接触"过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
核心点1: Object.defineProperty劫持属性
第一步是对JavaScript对象进行监听,监听使用到的api就是 Object.defineProperty,通过它来重写对象属性的getter和setter,如何重写的,使用到的函数就是observer
主要逻辑如下:
observer.js
:
javascript
// 将一个对象转换为响应式对象
function observe(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
// 为每个对象创建一个Observer实例
return new Observer(obj);
}
// Observer类,将对象的所有属性转换为getter/setter
class Observer {
constructor(value) {
this.value = value;
if (Array.isArray(value)) {
// 数组响应式处理...
} else {
this.walk(value);
}
}
// 遍历对象所有属性并转换为getter/setter
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
}
// 核心方法:将对象的属性转换为响应式属性
function defineReactive(obj, key) {
// 为每个属性创建一个Dep实例
const dep = new Dep();
// 获取对象当前属性的值,闭包存储
let val = obj[key];
// 递归观察属性值(如果是对象)
let childOb = observe(val);
// 使用Object.defineProperty劫持属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 如果当前有正在评估的Watcher实例
if (Dep.target) {
// 收集依赖
dep.depend();
}
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
val = newVal;
// 如果新值是对象,继续观察它
childOb = observe(newVal);
dep.notify();
}
});
}
核心点2: 依赖收集
vue是什么的响应式的原理??
vue是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,在数据发生改变的时候,发布消息给订阅者,出发响应的监听回调
其中谁是发布者?谁是订阅者?
我觉得数据是发布者,使用数据的地方就是订阅者。但是单独的数据和引用数据不足以实现数据改变,订阅数据的地方监听到数据改变。这就需要维护一个数据和订阅者之间的关系列表。这个关系就是依赖收集器Dep,收集的就是订阅者Watcher
数据劫持已经由 observer.js 中的 defineReactive
完成了,接下来就是依赖收集了,其中核心类:dep.js
:
javascript
/**
* 依赖收集器类
*/
class Dep {
constructor() {
// 存储所有依赖(订阅者watcher)
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
remove(this.subs, sub);
}
// 收集依赖重点函数!!
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0; i < subs.length; i++) {
subs[i].update();
}
}
}
// 当前正在评估的watcher
// 这是一个全局唯一的变量,同一时间只能有一个watcher被评估!!JavaScript是单线程执行的!
Dep.target = null;
const targetStack = [];
// 设置当前watcher
function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}
核心点3: Watcher与数据更新
在上面的依赖收集中,每一个属性里面都定义了一个dep,记录着每一个使用到该属性的watcher实例。watcher的作用是什么?? watcher的作用就是在数据发生变化的时候,通知所有的watcher实例进行更新。watcher是一个中间层,连接着数据和视图
那么它就需要两个功能:
- 监听数据变化
- 更新视图
妙,太妙了,主逻辑如下:
watcher.js
:
javascript
/**
* Watcher类是连接Observer和View的中间层
*/
class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm;
// 存储表达式或函数
if (typeof expOrFn === 'function') {
// render函数或计算属性的getter或者watch监听函数
this.getter = expOrFn;
} else {
// 表达式解析为函数
this.getter = parsePath(expOrFn);
}
this.cb = cb;
this.deps = [];
this.depIds = new Set(); // 用于防止依赖重复收集
this.isRenderWatcher = options && options.isRenderWatcher;
// 立即执行getter,触发依赖收集,注意如果是render函数,这个value是undefined,没什么作用。重点是get()的依赖收集
this.value = this.get();
}
// 获取值并收集依赖
get() {
// 设置当前活动的Watcher
pushTarget(this);
let value;
try {
// 执行getter函数,触发响应式数据的getter
value = this.getter.call(this.vm, this.vm);
} finally {
// 清除当前Watcher
popTarget();
}
return value;
}
// 添加依赖
addDep(dep) {
// dep是Dep实例在每一个响应式属性中都有
const id = dep.id;
if (!this.depIds.has(id)) {
this.depIds.add(id);
this.deps.push(dep);
dep.addSub(this);
}
}
// 更新方法,被Dep调用
update() {
// 简单实现:直接执行run
// 实际Vue中会将watcher推入队列,进行异步更新
queueWatcher(this);
}
// 实际执行更新的方法
run() {
const oldValue = this.value;
this.value = this.get();
// 调用回调,如组件的更新函数、计算属性的setter或watch的回调
if (this.cb) {
this.cb.call(this.vm, this.value, oldValue);
}
}
}
到这儿的时候有一个强烈的疑惑:
this.value = this.get()
这行代码的作用是什么?value会是是什么东西?watcher有几个??
在文末会有额外的疑惑解决
先看到下面,然后来回滚动着看源码,马上就能看完,非常简单,非常妙啊
已经看这么多了,已经很棒了,晚上必加鸡腿🍗
核心点4: 异步更新队列(nextTick)
照着逻辑到了,数据更新的核心内容,在watcher中是如何更新的?为什么我更新完数据之后,如果直接去访问数据,拿到的还是旧值??
这就是nextTick的作用了,nextTick的作用就是将回调函数放入一个队列中,等到下一个事件循环的时候再执行这个回调函数。这样就可以避免多次更新造成的性能浪费
watcher中同样会去调用nextTick将更新推入异步更新队列,很好的解释了下面vue中常见的逻辑👇🏻:
javascript
// vue里面的一个逻辑
data() {
return {
msg: ''
}
},
methods: {
handleClick() {
this.msg = 'Hello World';
this.$nextTick(() => {
console.log(this.$el.innerHTML); // 'Hello World'
});
}
}
实现的核心逻辑如下:
javascript
/**
* 将回调函数缓存起来,一起执行
*/
function queueWatcher(watcher) {
// ...
nextTick(flushSchedulerQueue);
}
/**
* 实际批量处理队列的函数
*/
function flushSchedulerQueue() {
// ...
// 依次执行watcher的run方法
for (let i = 0; i < queue.length; i++) {
const watcher = queue[i];
watcher.run();
}
}
/**
* nextTick实现原理
* 将回调添加到任务队列,在下一个tick执行
*/
const callbacks = [];
let pending = false;
function nextTick(cb, ctx) {
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
console.error(e);
}
}
});
// 如果还没有处理队列,则启动处理
if (!pending) {
pending = true;
// 在下一个tick执行回调队列
// vue2 优先使用Promise,其次是MutationObserver,然后是setImmediate,最后是setTimeout,浏览器兼容问题
Promise.resolve().then(flushCallbacks);
}
}
/**
* 执行所有回调
*/
function flushCallbacks() {
// 重置状态,为下一次回调执行做准备
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
完整响应式流程
javascript
// 示例:Vue实例初始化并响应数据变化
function initData(vm) {
let data = vm.$options.data;
data = vm._data = data.call(vm)
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
proxy(vm, '_data', key);
}
// 响应式化数据
observe(data);
}
function proxy(target, sourceKey, key) {
Object.defineProperty(target, key, {
get() {
return target[sourceKey][key]
},
set(val) {
target[sourceKey][key] = val
}
})
}
// 组件渲染时 创建 渲染Watcher
function mountComponent(vm, el) {
// 更新组件模版内容的函数
const updateComponent = () => {
vm._update(vm._render());
};
// 创建 渲染Watcher
new Watcher(
vm,
updateComponent,
noop,
{ isRenderWatcher: true } // 标识是渲染的watcher
);
return vm;
}
// 使用示例
function egFn() {
const vm = new Vue({
data() {
return {
message: 'Hello'
}
},
render(h) {
// 在这里访问this.message会触发getter,完成依赖收集
return h('div', this.message);
}
});
// 挂载组件,创建渲染watcher并执行首次渲染
vm.$mount('#app');
// 修改数据,触发更新
vm.message = 'Updated';
// 这会触发setter -> dep.notify() -> watcher.update() -> queueWatcher -> nextTick -> flushSchedulerQueue -> watcher.run() -> 重新渲染
}
整体流程总结
- 初始化阶段
- Vue示例创建时,通过
observe
方法将data选项转换为响应式对象 - 通过
Observer
类使用Object.defineProperty 劫持每个属性,定义getter和setter - 每个属性都创建一个Dep实例用于依赖收集
- 依赖收集阶段
- 组件渲染时创建Watcher实例
- 渲染过程中访问响应式属性时触发getter
- Dep.target设为当前Watcher,并通过dep.depend()收集依赖
- 每个属性的dep记录那些Watcher依赖它
- 数据更新阶段
- 当响应式数据被修改,触发setter
- setter调用 dep.notify()通知所有依赖该数据的watcher
- watcher被添加到异步更新队列(queueWatcher)
- 通过nextTick在下一个事件循环周期统一更新Dom
ok👌,看完啦,非常nice
越学当然问题越多,下面是额外内容:
额外疑惑解决
Watcher部分代码
javascript
class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm;
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
// 如果是字符串表达式,会将其转换为函数
this.getter = parsePath(expOrFn); // 返回一个函数,用于从对象中获取属性值
}
// 立即求值并收集依赖
this.value = this.get();
}
}
this.value = this.get()
这行代码的作用是什么?value会是是什么东西?watcher有几个??
Watcher实例中的getter和value
this.value = this.get()
的作用
这行代码主要有两个时机执行:
- Watcher初始化时(构造函数中)
- 数据更新时(run方法中)
它的作用是:
- 执行get()方法,触发getter进行依赖收集
- 将获取到的值保存在this.value中,便于后续比较新旧值的变化
- 对于渲染watcher,这一步会触发组件的渲染过程
this.getter
是什么?作用是什么
this.getter
的来源是创建Watcher时传入的expOrFn
参数。根据不同类型的Watcher,它可能是:
- 渲染Watcher: 就是组件的render函数,执行的时候会触发模版中的响应式数据的读取
javascript
// 渲染Watcher的初始化
const updateComponent = () => {
vm._update(vm._render()); // 生成VNode并更新DOM
};
// 此时传给Watcher的expOrFn就是updateComponent
new Watcher(vm, updateComponent, noop, { isRenderWatcher: true });
对于渲染watcher,this.getter
就是组件的render函数,它会在组件渲染时被调用,触发getter,收集依赖。返回的值是undefined,因为render函数本身不返回值
- 计算属性Watcher:
javascript
// 计算属性的getter函数
const getter = () => {
return this.count * 2; // 例如一个简单的computed属性
};
// 创建计算属性Watcher
new Watcher(vm, getter, noop, { lazy: true });
计算属性watcher,this.getter
就是计算属性的getter函数,它会在计算属性被访问时被调用,触发getter,收集依赖,返回的值是计算属性的值
- 监听器Watcher:
javascript
watch: {
// 这会隐式创建一个Watcher
'user.name': function(newVal, oldVal) {
console.log('用户名变更为:', newVal);
}
}
// 创建Watcher
new Watcher(vm, parsePath('user.name'), callback);
监听器同理,区别在于watch会有新旧两个数据,也就是在run()函数中获取的新值和记录的旧值
结语
又理解了一遍原理,记录下来,方便后续查阅
希望对你有所帮助
祝在前端开发的道路上越走越远,技术精进,问题迎刃而解!💪✨