vue源码解析之调度原理(响应式原理)

可先看我的前篇会更好理解:vue源码解析之编译过程-含2种模式(及vue-loader作用)

目录大纲

  1. 测试文件:.html文件
  2. 测试动作:点击“click me”,触发 qqq函数
  3. 调度过程总结
  4. 再谈一下vue的双向绑定v-model原理

测试文件:.html文件

  • CDN引入vue的未压缩版,在script标签内,直接使用vue
    1. <!DOCTYPE html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>Title</title>
    6. <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
    7. </head>
    8. <body>
    9. <div id="app">
    10. {{aa}} --- 1
    11. <div @click="qqq">click me</div>
    12. {{C_aa}}
    13. </div>
    14. <script type="module">
    15. debugger
    16. new Vue({
    17. el: '#app',
    18. data: {
    19. aa: 123
    20. },
    21. watch: {
    22. aa (nval, oval) {
    23. console.log(nval, oval)
    24. }
    25. },
    26. computed: {
    27. C_aa () {
    28. return this.aa + 100
    29. }
    30. },
    31. methods: {
    32. qqq () {
    33. debugger
    34. this.aa = this.aa + 1
    35. }
    36. }
    37. })
    38. </script>
    39. </body>
    40. </html>

测试动作:点击“click me”,触发 qqq函数

(说明:只截取了主线代码,并略有删减,为的是 更好的关注主线,主线弄明白了,有余力,在去了解支线。 调试方式:debugger一步步往下)

断点在qqq函数内,调试从断点开始,看看 this.aa = this.aa + 1 vue底层到底干了哪些事儿,才能把最新的数据 更新到页面上去?

有几个问题点,可以提前思考一下:

  1. 如果用户一次同步操作,改变了多个data的值,vue是触发一次update,还是多次update?
  2. 用户写的watch: {..} 内的回调函数,是在update前执行,还是update之后?
    1. watch: {..} 内的回调函数 如果又修改了data,那么还会触发update吗?

开始调试,执行 this.aa = this.aa + 1

  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 () {

    1. var value = getter ? getter.call(obj) : val; // data存在getter先执行getter
    2. /* 为data收集依赖
    3. (在vue中,每一个data都会绑定一个对象叫dep,会分配唯一的id。
    4. 如果有依赖内容 会放到data对应的dep内的this.subs的订阅者队列里面),
    5. 依赖内容是:比如:aa有3个依赖 1个watch、1个computed、1个页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }" */
    6. if (Dep.target) {
    7. dep.depend(); // 为data收集依赖
    8. if (childOb) { // 递归处理child
    9. childOb.dep.depend();
    10. if (Array.isArray(value)) {
    11. dependArray(value);
    12. }
    13. }
    14. }
    15. return value // 拿到值

    }, set: function reactiveSetter (newVal) {

    1. var value = getter ? getter.call(obj) : val; // data存在getter先执行getter
    2. // 新旧值一样 没被修改,直接return停止
    3. if (newVal === value || (newVal !== newVal && value !== value)) {
    4. return
    5. }
    6. // #7981: for accessor properties without setter
    7. if (getter && !setter) { return }
    8. if (setter) { // 只在vue初始化的时候执行
    9. setter.call(obj, newVal);
    10. } else {
    11. val = newVal; // 保存一份新值
    12. }
    13. childOb = !shallow && observe(newVal); // 递归处理child
    14. /* 消息推送,通知订阅者队列 this.subs。实际上会把订阅者队列在处理一遍,
    15. 放在全局queue队列里面去,最终真正执行的是queue队列,
    16. 会过滤掉computed 因为不是异步的,结果是函数的返回值。在model层取值渲染的时候,会去跑函数,得到返回值
    17. (目前 aa 的订阅者队列this.subs内有:1个watch、1个computed、1个页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }") */
    18. dep.notify();

    } }); } ```

  2. 第二步,修改this.aa的值。会触发set监听函数
    (代码在上面,详细请看注释) 执行set监听函数 最终会触发 消息推送 dep.notify()

  3. dep.notify() 调度的开始
    消息推送,通知订阅者队列 this.subs。实际上会把订阅者队列在处理一遍,放在全局queue队列里面去,最终真正执行的是queue队列(目前 aa 的订阅者队列this.subs内有:1个watch、1个computed、1个页面渲染的必备的函数”function () { vm._update(vm._render(), hydrating); }”)(会过滤掉computed 因为不是异步的,结果是函数的返回值。在model层取值渲染的时候,会去跑computed对应的函数得到返回值)
    1. 细节0:页面渲染的必备的函数”function () { vm._update(vm._render(), hydrating); }”)是什么作用,可以先看我的前篇:vue源码解析之编译过程-含2种模式(及vue-loader作用)
    2. 细节1:用户写的computed不是异步的,结果是函数的返回值。在model层取值渲染的时候,会去跑computed对应的函数得到返回值值(以下代码暂时没有体现)
      • 调度流程只会把this.dirty = true。 把对应的computed改成dirty(脏的)意味着,需要更新。
    3. 细节2:异步事件(比如用户写的watch)都会放到一个全局的queue队列去,队列的最后一个是关键渲染函数vm._update(vm._render())。
    4. 细节3:什么时候去执行queue队列?
      • 在nextTick后去执行,nextTick(flushSchedulerQueue)
        • nextTick原理是一个微任务,等同步任务执行完,在执行 flushSchedulerQueue,最终去run queue队列。
        • 好处:用户的一次操作,可能会改动多次或多个data的值,不用每改动一次就去更新页面,可以把一次同步任务内的所有改动,都收集起来,放到queue队列内,然后同步任务结束后 执行微任务nextTick内的回调函数, 去执行run queue队列。
    5. 细节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层取值渲染的时候,会去跑函数,得到返回值
    1. this.dirty = true; // 把对应的computed改成dirty(脏的)意味着,需要更新
    } else if (this.sync) { // 一次同步任务。 比如this.aa=xx触发aa的watch回调函数,回调函数内又非异步的修改了this.bb=xxx,此时是一次同步任务,就会走里面
    1. this.run();
    } else {
    1. 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) {
    1. has[id] = true;
    2. if (!flushing) { // 全局变量flushing,确保一次同步任务,不会有多次vm._update(vm._render()),只会有一次update
    3. queue.push(watcher); // 把watcher加入queue队列
    4. } else { // 避免重复的
    5. // if already flushing, splice the watcher based on its id
    6. // if already past its id, it will be run next immediately.
    7. var i = queue.length - 1;
    8. while (i > index && queue[i].id > watcher.id) {
    9. i--;
    10. }
    11. queue.splice(i + 1, 0, watcher);
    12. }
    13. // queue the flush
    14. if (!waiting) {
    15. waiting = true;
    16. // nextTick原理是一个微任务,等同步任务执行完 把所有的watcher加入queue,在执行 flushSchedulerQueue,最终去run queue队列。
    17. nextTick(flushSchedulerQueue); // 用户写的 watch: { aa () {} } 往这里面走。 是异步的
    18. }
    } }

/**

  • 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++) {

    1. watcher = queue[index];
    2. if (watcher.before) {
    3. watcher.before(); // 执行vm._update(vm._render(), hydrating) 之前,beforeUpdate 在这里先执行 callHook(vm, 'beforeUpdate');
    4. }
    5. id = watcher.id;
    6. has[id] = null;
    7. 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) {
    1. /* 这一行很重要,有2个作用
    2. 1. 正常取data的值,比如在watch: {aa(newVal, oldVal) {}}中,newVal的值,就是从 this.get() 里拿到的
    3. 2. 页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }") 就是在此处执行。*/
    4. var value = this.get();
    5. if (
    6. value !== this.value ||
    7. // Deep watchers and watchers on Object/Arrays should fire even
    8. // when the value is the same, because the value may
    9. // have mutated.
    10. isObject(value) ||
    11. this.deep
    12. ) {
    13. // set new value
    14. var oldValue = this.value;
    15. this.value = value;
    16. if (this.user) {
    17. var info = "callback for watcher \"" + (this.expression) + "\"";
    18. // 开发者写的 watch: { aa () {} } 监听函数,在此行执行
    19. invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);
    20. } else {
    21. this.cb.call(this.vm, value, oldValue);
    22. }
    23. }
    } }; /**
  • Evaluate the getter, and re-collect dependencies. */ Watcher.prototype.get = function get () { pushTarget(this); var value; var vm = this.vm; try {

    1. // 页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }") 就是在此处执行。 this.getter 保存了 vm._update(vm._render(), hydrating)
    2. value = this.getter.call(vm, vm);

    } catch (e) {

    1. if (this.user) {
    2. handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    3. } else {
    4. throw e
    5. }

    } finally {

    1. // "touch" every property so they are all tracked as
    2. // dependencies for deep watching
    3. if (this.deep) {
    4. traverse(value);
    5. }
    6. popTarget();
    7. this.cleanupDeps();

    } return value }; // 开发者写的 watch: { aa () {} } 监听函数,在此行执行 function invokeWithErrorHandling ( handler, context, args, vm, info ) { var res; try {

    1. // 开发者写的 watch: { aa () {} } 监听函数,在此行执行
    2. res = args ? handler.apply(context, args) : handler.call(context);
    3. if (res && !res._isVue && isPromise(res) && !res._handled) {
    4. res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
    5. // avoid catch triggering multiple times when nested calls
    6. res._handled = true;
    7. }

    } catch (e) {

    1. 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 {
      1. cb.call(ctx);
      } catch (e) {
      1. handleError(e, ctx, 'nextTick');
      } } 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; }) } }

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 } }

  1. 4. 触发好了用户写的watch:{ ... }的回调函数之后,最后要执行 页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }"
  2. - 后面就是执行 关键渲染函数vm._update(vm._render(), hydrating),和编译过程里面的渲染是一样的了,可以看我的另一篇这里:[vue源码解析之编译过程-含2种模式(及vue-loader作用)](https://juejin.cn/post/6997670497723023390)
  3. <a name="7430acbb"></a>
  4. ## 调度过程总结
  5. 场景是:修改data里面的数据(假设是修改 this.aa = 123),页面上对应的位子得到update
  6. 过程总结:
  7. 1. 触发aaset监听函数
  8. 1. 处理aa的订阅者队列subs(订阅者队列包含:computedwatch,关键渲染函数)
  9. - computed对应的 改成 this.dirty = true,下次model层取值的时候,就知道对应computed要重新计算了。否则会用缓存
  10. 3. watch 里面的回调函数会放到全局对象queue队列里面去。并且由nextTick控制,同步任务期间只会一直加入进queue队列,同步任务结束后,才会开始run queue队列
  11. - queue队列的最后一个是 关键渲染函数 function () { vm._update(vm._render(), hydrating); }")
  12. - 会有全局变量flushing控制,确保一次同步任务,只会执行一次 关键渲染函数
  13. - nextTick原理是微任务,
  14. 4. 后面就是执行 关键渲染函数vm._update(vm._render(), hydrating),和编译过程里面的渲染是一样的了,可以看我的另一篇这里:[vue源码解析之编译过程-含2种模式(及vue-loader作用)](https://juejin.cn/post/6997670497723023390)
  15. <a name="e559a56a"></a>
  16. ## 再谈一下vue的双向绑定v-model原理
  17. 实际是一个语法糖(语法糖的意思 可以理解为简写,下面第二行是真实的样子)
  18. ```html
  19. <input v-model='abc' /> // 语法糖
  20. <input :value='abc' @input='abc = $event.target.value' /> // 真实的样子
  21. 注: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)。最终渲染到页面上

码字不易,点赞鼓励