Vue源码分析(9)--实例分析响应式设计

前言

本文是vue2.x源码分析的第九篇,主要看响应式设计的处理过程!

实例代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue</title>
  <script src="./vue9.js" type="text/javascript" charset="utf-8" ></script>
</head>
<body>
  <div id="app">
    {{message}}
  </div>
  <script>
    var vm=new Vue({
      el:'#app',
      name:'app',
      data:{
        message:'message',
      }
    });
    debugger;
    setTimeout(()=>vm.message='messages',0)    
  </script>
</body>
</html>

1、关键断点

initData(vm)
proxy(vm,"_data",'message')
observe(data,true/asRootdata/)

vm.$mount(vm.options.el)
mount.call(this,el,hydrating)
mountComponent(this,el,hydrating)
vm._watcher=new Watcher(vm,updateComponent,noop)

2、详细分析

从Watcher开始分析

    Watcher = function Watcher (vm,expOrFn,cb,options) {
      this.vm = vm;
      vm._watchers.push(this);
      this.deep = this.user = this.lazy = this.sync = false;
      this.cb = cb;
      this.id = ++uid$2; // uid for batching
      this.active = true;
      this.dirty = this.lazy; // for lazy watchers
      this.deps = [];
      this.newDeps = [];
      this.depIds = new _Set();
      this.newDepIds = new _Set();
      this.expression = expOrFn.toString();
      if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
      } else {
        ...
      }
      this.value = this.lazy
        ? undefined
        : this.get();
    };
    Watcher.prototype.get = function get () {
      pushTarget(this); //这个this即是刚创建的watcher实例,称之为render watcher,本质上执行Dep.target = this;
      var value;
      var vm = this.vm;
      if (this.user) {
        ...
      } else {
        value = this.getter.call(vm, vm); //this.getter得到执行,即是updateComponent函数被执行
      }
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
      return value
    };

接下来执行updateComponent

    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };

先执行vm._render

  Vue.prototype._render = function () {
    var vm = this;
    var ref = vm.$options;
    var render = ref.render;
    var staticRenderFns = ref.staticRenderFns;
    var _parentVnode = ref._parentVnode;
    ...
    var vnode;
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement);
    } catch (e) {
      ...
    }
    vnode.parent = _parentVnode;
    return vnode
  };

以上主要执行的是render函数,该函数是通过AST得到的匿名函数

    function() {
        with(this){ //this是vm._renderProxy,而不是vm
            return _c('div',{attrs:{"id":"app"}},[_v(_s(message))])
        }
    }

注意_s(message),它在执行时会执行vm.message取值操作,从而触发vm.message的get函数,进而触发vm._data.message的get函数,看下该函数:

 get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {  // Dep.target在上面被设为了render watcher
        dep.depend();  //data的每个属性都有一个dep实例对应,让render watcher收集该属性的dep
        ...
      }
      return value
    },
    Dep.prototype.depend = function depend () {
      if (Dep.target) {
        Dep.target.addDep(this);
      }
    };
    //addDep如下:
    Watcher.prototype.addDep = function addDep (dep) {
      var id = dep.id;
      if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id);
        this.newDeps.push(dep);
        if (!this.depIds.has(id)) {
          dep.addSub(this); //dep的subs收集该watcher
        }
      }
    };
    //addSub如下:
    Dep.prototype.addSub = function addSub (sub) {
      this.subs.push(sub);//dep的subs收集该watcher
    };
    //render watcher收集了所有dep,同时每个dep又都收集了render watcher,这时this.\_render执行完毕,返回了Vnode

接下来执行vm._update函数

 Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate');
    }
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;
    var prevActiveInstance = activeInstance;
    activeInstance = vm;
    vm._vnode = vnode;
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
    } else {
     ...
    }
    ...
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  };

主要执行vm.__patch__函数,该函数内部主要执行在上节提到的createElm函数,该函数的四步执行完后就能创建真实DOM结构了,于是页面首次渲染就完成了。

下面分析当vm.message的值发生变化,vue是如何追踪到变化并更新页面的:
首先message的值发生变化会触发vm.message的set函数,进而触发vm._data.message的set函数,看下该函数:

    set: function reactiveSetter (newVal) {
          var value = getter ? getter.call(obj) : val;
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          if ("development" !== 'production' && customSetter) {
            customSetter();
          }
          if (setter) {
            setter.call(obj, newVal);
          } else {
            val = newVal;  //旧值被新值替换
          }
          childOb = observe(newVal);//观测新值
          dep.notify(); //这个dep与get函数中的是同一个,在get中dep.subs中已经订阅了render watcher
    }

看下notify函数:

Dep.prototype.notify = function notify () {
  // stabilize(使稳固) the subscriber list first
  var subs = this.subs.slice(); //只有一个render watcher
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update(); //执行render watcher的update函数
  }
};

看下update函数:

    Watcher.prototype.update = function update () {
      /* istanbul ignore else */
      if (this.lazy) {
        this.dirty = true;
      } else if (this.sync) {
        this.run();
      } else {
        queueWatcher(this);//执行该函数
      }
    };

看下queueWatcher函数:

    function queueWatcher (watcher) {
      var id = watcher.id;
      if (has[id] == null) { //has是个对象,存放watcher的id
        has[id] = true;
        if (!flushing) { //flushing可看成全局变量,默认false
          queue.push(watcher); //render 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 >= 0 && queue[i].id > watcher.id) {
            i--;
          }
          queue.splice(Math.max(i, index) + 1, 0, watcher);
        }
        // queue the flush
        if (!waiting) {//waiting可看成全局变量,默认false
          waiting = true;
          nextTick(flushSchedulerQueue);
        }
      }
    }

看下flushSchedulerQueue函数:

    function flushSchedulerQueue () {
      debugger;
      flushing = true;
      var watcher, id, vm;
      queue.sort(function (a, b) { return a.id - b.id; });
      // do not cache length because more watchers might be pushed
      // as we run existing watchers
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        id = watcher.id;
        has[id] = null;
        watcher.run();
        ...
      }
      // reset scheduler before updated hook called
      var oldQueue = queue.slice();
      resetSchedulerState();
      // call updated hooks
      index = oldQueue.length;
      while (index--) {
        watcher = oldQueue[index];
        vm = watcher.vm;
        if (vm._watcher === watcher && vm._isMounted) {
          callHook(vm, 'updated');
        }
      }
      ...
    }

看下watcher.run函数:

    Watcher.prototype.run = function run () {
      debugger;
      if (this.active) {
        var value = this.get();
        ...
      }
    };

这个this.get就是初次渲染时调用过一次的this.get

    Watcher.prototype.get = function get () {
      pushTarget(this); //这个this即是刚创建的watcher实例,称之为render watcher,本质上执行Dep.target = this;
      var value;
      var vm = this.vm;
      if (this.user) {
        ...
      } else {
        value = this.getter.call(vm, vm); //this.getter得到执行,即是updateComponent函数被执行
      }
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
      return value
    };

然后updateComponent函数又一次被执行,从而this._render,this._update都得到执行,DOM结构得以创建,唯一不同的是message值已经被修改成新值了,从而页面实现了更新。

3、总结:

第一次渲染:

  • observe(data) 为下次页面更新做准备
  • updateComponent 只要该函数得到执行,就会生成真实DOM
  • new Watcher(vm,updateComponent,noop),在内部updateComponent得到执行

第二次渲染(更新):

  • 触发set函数
  • dep.notify
  • watcher.update
  • queueWatcher
  • flushSchedulerQueue
  • watcher.run
  • updateComponent再次执行,生成新DOM
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容