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

1,595 阅读2分钟

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

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

目录大纲

  1. 测试文件:.html文件
  2. 测试动作:点击“click me”,触发 qqq函数
  3. 调度过程总结
  4. 再谈一下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底层到底干了哪些事儿,才能把最新的数据 更新到页面上去?

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

  1. 如果用户一次同步操作,改变了多个data的值,vue是触发一次update,还是多次update?
  2. 比如用户在一个click事件内,有个for循环,改变了data的某个属性N次,比如数组push N次,这个属性有一个watch监听它,那么这个watch监听函数只会执行一次还是会执行N次?怎么实现的?
  3. 用户写的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部分用到了发布订阅模式

      /**
       * 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(); 
          }
        });
      }
    
  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:举一个例子:比如用户在一个click事件内,有个for循环,改变了data的某个属性N次(这个属性有一个watch监听它),这个watch监听函数只会执行几次?

      • 只会执行一次。因为处理subs观察者队列 把watcher加入queue队列时,会有一个名叫has的去重对象(watcher会有自己id),保证watcher只会执行一次
    6. 细节5: watch: {..} 内的回调函数 如果又修改了data,那么还会触发update吗?

      • 不会有多次vm._update(vm._render())
      • 会用全局变量flushing控制,确保一次同步任务,只会有一次update 调度过程:
    /* 部分非主线代码有删减,为的是 更好的关注主线,主线弄明白了,有余力,在去了解支线 */ 
    
    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层取值渲染的时候,会去跑函数,得到返回值
            this.dirty = true; // 把对应的computed改成dirty(脏的)意味着,需要更新
        } else if (this.sync) { // 一次同步任务。 比如this.aa=xx触发aa的watch回调函数,回调函数内又非异步的修改了this.bb=xxx,此时是一次同步任务,就会走里面
            this.run();
        } else {
            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是一个去重对象,保证watcher只会执行一次
          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
    /**
    * 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里面
    function nextTick (cb, ctx) {
        var _resolve;
        callbacks.push(function () { // callbacks是全局的,存放 用了nextTick的 函数
          if (cb) {
            try {
              cb.call(ctx);
            } catch (e) {
              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++) {
          copies[i](); // 执行callbacks
        }
    }
    
  4. 触发好了用户写的watch:{ ... }的回调函数之后,最后要执行 页面渲染的必备的函数"function () { vm._update(vm._render(), hydrating); }"

调度过程总结

场景是:修改data里面的数据(假设是修改 this.aa = 123),页面上对应的位子得到update

过程总结:

  1. 触发aa的set监听函数
  2. 处理aa的订阅者队列subs(订阅者队列包含:computed,watch,关键渲染函数)
    • computed对应的 改成 this.dirty = true,下次生成vnode过程中 在model层取值的时候,就知道对应computed要重新计算了。否则会用缓存
  3. watch 里面的回调函数会放到全局对象queue队列里面去。并且由nextTick控制,同步任务期间只会一直加入进queue队列 并不会执行,同步任务结束后,才会开始run queue队列
    • 不立即执行watch的好处是:比如用户在一个click事件内,有个for循环,改变了data的某个属性N次,比如数组的push N次(这个属性有一个watch监听它),这个watch监听函数只会执行一次,不用执行N次(会有一个名叫has的去重对象(watcher会有自己id),保证watcher只会执行一次 )
    • queue队列的最后一个是 关键渲染函数 function () { vm._update(vm._render(), hydrating); }")
      • 会有全局变量flushing控制,确保一次同步任务,只会执行一次 关键渲染函数
    • nextTick原理是微任务,
  4. 后面就是执行 关键渲染函数vm._update(vm._render(), hydrating),和编译过程里面的渲染是一样的了,可以看我的另一篇这里:vue源码解析之编译过程-含2种模式(及vue-loader作用)

再谈一下vue的双向绑定v-model原理

实际是一个语法糖(语法糖的意思 可以理解为简写,下面第二行是真实的样子)

<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)。最终渲染到页面上

码字不易,点赞鼓励