vue源码解析之调度原理(响应式原理)
可先看我的前篇会更好理解:vue源码解析之编译过程-含2种模式(及vue-loader作用)
目录大纲
- 测试文件:.html文件
- 测试动作:点击“click me”,触发 qqq函数
- 调度过程总结
- 再谈一下vue的双向绑定v-model原理
测试文件:.html文件
- CDN引入vue的未压缩版,在script标签内,直接使用vue
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
{{aa}} --- 1
<div @click="qqq">click me</div>
{{C_aa}}
</div>
<script type="module">
debugger
new Vue({
el: '#app',
data: {
aa: 123
},
watch: {
aa (nval, oval) {
console.log(nval, oval)
}
},
computed: {
C_aa () {
return this.aa + 100
}
},
methods: {
qqq () {
debugger
this.aa = this.aa + 1
}
}
})
</script>
</body>
</html>
测试动作:点击“click me”,触发 qqq函数
(说明:只截取了主线代码,并略有删减,为的是 更好的关注主线,主线弄明白了,有余力,在去了解支线。 调试方式:debugger一步步往下)
断点在qqq函数内,调试从断点开始,看看 this.aa = this.aa + 1
vue底层到底干了哪些事儿,才能把最新的数据 更新到页面上去?
有几个问题点,可以提前思考一下:
- 如果用户一次同步操作,改变了多个data的值,vue是触发一次update,还是多次update?
- 用户写的watch: {..} 内的回调函数,是在update前执行,还是update之后?
- watch: {..} 内的回调函数 如果又修改了data,那么还会触发update吗?
开始调试,执行 this.aa = this.aa + 1
第一步,拿到this.aa的值。因为是要取值,所以会触发aa的get监听函数
- 在vue中,会对data的做监听(深层对象的话会递归监听,数组会遍历监听),主要是通过Object.defineProperty监听 可以设置get和set的监听函数,取this.aa的值 会触发get函数,设置this.aa=xx 会触发set函数
- 以下get的执行步骤,请看注释 (以下dep部分用到了发布订阅模式) ```javascript /**
- A dep is an observable that can have multiple
- directives subscribing to it. / var Dep = function Dep () { this.id = uid++; this.subs = []; // 订阅者队列 subscriber }; /*
- Define a reactive property on an Object. */ function defineReactive$$1 ( obj, key, val, customSetter, shallow ) { var dep = new Dep(); // 为每个data,绑定一个dep对象(Dep构造函数结构如上)
var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return }
// cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { val = obj[key]; }
var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val; // data存在getter先执行getter
/* 为data收集依赖
(在vue中,每一个data都会绑定一个对象叫dep,会分配唯一的id。
如果有依赖内容 会放到data对应的dep内的this.subs的订阅者队列里面),
依赖内容是:比如:aa有3个依赖 1个watch、1个computed、1个页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }" */
if (Dep.target) {
dep.depend(); // 为data收集依赖
if (childOb) { // 递归处理child
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value // 拿到值
}, set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val; // data存在getter先执行getter
// 新旧值一样 没被修改,直接return停止
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) { // 只在vue初始化的时候执行
setter.call(obj, newVal);
} else {
val = newVal; // 保存一份新值
}
childOb = !shallow && observe(newVal); // 递归处理child
/* 消息推送,通知订阅者队列 this.subs。实际上会把订阅者队列在处理一遍,
放在全局queue队列里面去,最终真正执行的是queue队列,
会过滤掉computed 因为不是异步的,结果是函数的返回值。在model层取值渲染的时候,会去跑函数,得到返回值
(目前 aa 的订阅者队列this.subs内有:1个watch、1个computed、1个页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }") */
dep.notify();
} }); } ```
第二步,修改this.aa的值。会触发set监听函数
(代码在上面,详细请看注释) 执行set监听函数 最终会触发 消息推送dep.notify()
dep.notify()
调度的开始
消息推送,通知订阅者队列 this.subs。实际上会把订阅者队列在处理一遍,放在全局queue队列里面去,最终真正执行的是queue队列(目前 aa 的订阅者队列this.subs内有:1个watch、1个computed、1个页面渲染的必备的函数”function () { vm._update(vm._render(), hydrating); }”)(会过滤掉computed 因为不是异步的,结果是函数的返回值。在model层取值渲染的时候,会去跑computed对应的函数得到返回值)- 细节0:页面渲染的必备的函数”function () { vm._update(vm._render(), hydrating); }”)是什么作用,可以先看我的前篇:vue源码解析之编译过程-含2种模式(及vue-loader作用)
- 细节1:用户写的computed不是异步的,结果是函数的返回值。在model层取值渲染的时候,会去跑computed对应的函数得到返回值值(以下代码暂时没有体现)
- 调度流程只会把this.dirty = true。 把对应的computed改成dirty(脏的)意味着,需要更新。
- 细节2:异步事件(比如用户写的watch)都会放到一个全局的queue队列去,队列的最后一个是关键渲染函数vm._update(vm._render())。
- 细节3:什么时候去执行queue队列?
- 在nextTick后去执行,nextTick(flushSchedulerQueue)
- nextTick原理是一个微任务,等同步任务执行完,在执行 flushSchedulerQueue,最终去run queue队列。
- 好处:用户的一次操作,可能会改动多次或多个data的值,不用每改动一次就去更新页面,可以把一次同步任务内的所有改动,都收集起来,放到queue队列内,然后同步任务结束后 执行微任务nextTick内的回调函数, 去执行run queue队列。
- 在nextTick后去执行,nextTick(flushSchedulerQueue)
- 细节4: watch: {..} 内的回调函数 如果又修改了data,那么还会触发update吗?
- 不会有多次vm._update(vm._render())
- 会用全局变量flushing控制,确保一次同步任务,只会有一次update
调度过程: ```javascript / 部分非主线代码有删减,为的是 更好的关注主线,主线弄明白了,有余力,在去了解支线 /
Dep.prototype.notify = function notify () { var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; /**
- Subscriber interface. Will be called when a dependency changes.
/
Watcher.prototype.update = function update () {
/ istanbul ignore else */
if (this.lazy) { // 用户写的computed会进入这里,不是异步的,结果是函数的返回值。在model层取值渲染的时候,会去跑函数,得到返回值
} else if (this.sync) { // 一次同步任务。 比如this.aa=xx触发aa的watch回调函数,回调函数内又非异步的修改了this.bb=xxx,此时是一次同步任务,就会走里面this.dirty = true; // 把对应的computed改成dirty(脏的)意味着,需要更新
} else {this.run();
} };queueWatcher(this); // 用户写的 watch: { aa () {} } 往这里面走
/**
- Push a watcher into the watcher queue. 将watcher推入watcher队列。
- Jobs with duplicate IDs will be skipped unless it’s pushed when the queue is being flushed. 除非在刷新队列时推送,否则将跳过具有重复 ID 的事件。
*/
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {
} }has[id] = true;
if (!flushing) { // 全局变量flushing,确保一次同步任务,不会有多次vm._update(vm._render()),只会有一次update
queue.push(watcher); // 把watcher加入queue队列
} else { // 避免重复的
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
// nextTick原理是一个微任务,等同步任务执行完 把所有的watcher加入queue,在执行 flushSchedulerQueue,最终去run queue队列。
nextTick(flushSchedulerQueue); // 用户写的 watch: { aa () {} } 往这里面走。 是异步的
}
/**
Flush both queues and run the watchers. */ function flushSchedulerQueue () { flushing = true; var watcher, id; // queue 是一个全局的 watcher list,存放了当次同步任务内的所有用户watcher // 此处我们的watcher有2个, 一个是watch: { aa () {} },另一个 关键渲染函数 “function () { vm._update(vm._render(), hydrating); }” for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before(); // 执行vm._update(vm._render(), hydrating) 之前,beforeUpdate 在这里先执行 callHook(vm, 'beforeUpdate');
}
id = watcher.id;
has[id] = null;
watcher.run(); // watcher都在这执行,比如 1.开发者写的 watch: { aa () {} } 监听函数,在此行执行。 2. 页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }")
} } ``` watcher.run();
- watcher都在这执行,比如
- 1.开发者写的 watch: { aa () {} } 监听函数,在此行执行。
- 2.页面渲染的必备的函数”function () { vm._update(vm._render(), hydrating); }”)
- 会在确保在一次同步任务中的最后执行,因为要避免多次update ```javascript /**
- Scheduler job interface. Will be called by the scheduler.
*/
Watcher.prototype.run = function run () {
if (this.active) {
} }; /**/* 这一行很重要,有2个作用
1. 正常取data的值,比如在watch: {aa(newVal, oldVal) {}}中,newVal的值,就是从 this.get() 里拿到的
2. 页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }") 就是在此处执行。*/
var value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
if (this.user) {
var info = "callback for watcher \"" + (this.expression) + "\"";
// 开发者写的 watch: { aa () {} } 监听函数,在此行执行
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);
} else {
this.cb.call(this.vm, value, oldValue);
}
}
Evaluate the getter, and re-collect dependencies. */ Watcher.prototype.get = function get () { pushTarget(this); var value; var vm = this.vm; try {
// 页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }") 就是在此处执行。 this.getter 保存了 vm._update(vm._render(), hydrating)
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
} return value }; // 开发者写的 watch: { aa () {} } 监听函数,在此行执行 function invokeWithErrorHandling ( handler, context, args, vm, info ) { var res; try {
// 开发者写的 watch: { aa () {} } 监听函数,在此行执行
res = args ? handler.apply(context, args) : handler.call(context);
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
// avoid catch triggering multiple times when nested calls
res._handled = true;
}
} catch (e) {
handleError(e, vm, info);
} return res } ``` nextTick()
- nextTick原理是一个微任务,用了nextTick的 函数存放在全局callbacks里面
```javascript
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () { // callbacks是全局的,存放 用了nextTick的 函数
if (cb) {
try {
} catch (e) {cb.call(ctx);
} } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); // nextTick原理是一个微任务,用了nextTick的 函数存放在callbacks里面 } if (!cb && typeof Promise !== ‘undefined’) { return new Promise(function (resolve) { _resolve = resolve; }) } }handleError(e, ctx, 'nextTick');
var p = Promise.resolve(); // 微任务 timerFunc = function () { // 微任务 p.then(flushCallbacks); if (isIOS) { setTimeout(noop); } };
function flushCallbacks () { pending = false; var copies = callbacks.slice(0); // callbacks是全局的,存放 用了nextTick的 函数 callbacks.length = 0; for (var i = 0; i < copies.length; i++) { copiesi; // 执行callbacks } }
4. 触发好了用户写的watch:{ ... }的回调函数之后,最后要执行 页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }"
- 后面就是执行 关键渲染函数vm._update(vm._render(), hydrating),和编译过程里面的渲染是一样的了,可以看我的另一篇这里:[vue源码解析之编译过程-含2种模式(及vue-loader作用)](https://juejin.cn/post/6997670497723023390)
<a name="7430acbb"></a>
## 调度过程总结
场景是:修改data里面的数据(假设是修改 this.aa = 123),页面上对应的位子得到update
过程总结:
1. 触发aa的set监听函数
1. 处理aa的订阅者队列subs(订阅者队列包含:computed,watch,关键渲染函数)
- computed对应的 改成 this.dirty = true,下次model层取值的时候,就知道对应computed要重新计算了。否则会用缓存
3. watch 里面的回调函数会放到全局对象queue队列里面去。并且由nextTick控制,同步任务期间只会一直加入进queue队列,同步任务结束后,才会开始run queue队列
- queue队列的最后一个是 关键渲染函数 function () { vm._update(vm._render(), hydrating); }")
- 会有全局变量flushing控制,确保一次同步任务,只会执行一次 关键渲染函数
- nextTick原理是微任务,
4. 后面就是执行 关键渲染函数vm._update(vm._render(), hydrating),和编译过程里面的渲染是一样的了,可以看我的另一篇这里:[vue源码解析之编译过程-含2种模式(及vue-loader作用)](https://juejin.cn/post/6997670497723023390)
<a name="e559a56a"></a>
## 再谈一下vue的双向绑定v-model原理
实际是一个语法糖(语法糖的意思 可以理解为简写,下面第二行是真实的样子)
```html
<input v-model='abc' /> // 语法糖
<input :value='abc' @input='abc = $event.target.value' /> // 真实的样子
注:value是表单控件的值。以名字/值对的形式随表单一起提交
过程是: DOM Listeners -> Model -> Data Bindings -> render 到页面上
- DOM Listeners 比如 input事件,select事件(所以v-model只支持表单元素)
- Model是Model层(数据层),通过事件,修改this.abc = $event.target.value。然后会触发this.aa的set函数,然后会触发 关键渲染函数vm._update(vm._render(), hydrating)。最终渲染到页面上
码字不易,点赞鼓励