vue-Observe、Dep、Watcher

问题:如代码所示,我们在多次更新data属性,会不会触发多次dom diff计算和渲染,在更新data属性到dom渲染过程发生了什么事?

<template>
    <div>
        <div>{{title}}</div>
        <div @click="refresh">更新</div>
    </div>
</template>
 
<script>
    export default {
        data() {
            return {
                title: ''
            };
        },
        methods: {
            refresh() {
                this.title = '测试数据1';
                this.title = '测试数据2';
                this.title = '测试数据3';
                this.title = '测试数据4';
                this.title = '测试数据5';
                this.title = '测试数据6';
            }
        }
    };
</script>

一、了解Observer、Dep、Watcher

1、vue组件初始化

vue组件初始化的时候会执行_init函数,从而做了一些初始化处理,在处理不同阶段中调用不同的vue生命周期函数,例:breforeCreate、created等

Vue.prototype._init = function (options) {
    var vm = this;
    initEvents(vm);
    // 将render函数转化为vnode
    initRender(vm);
    // 调用beforeCreate生命周期函数
    callHook(vm, 'beforeCreate');
    initInjections(vm);
    // 初始化处理props、methods、data、computed、watch
    initState(vm);
    initProvide(vm);
    // 调用created生命周期函数
    callHook(vm, 'created');
     
    ....
     
    // 挂载组件到DOM树
    if (vm.$options.el) {
        vm.$mount(vm.$options.el);
    }
}

2、Observer

initState → initData → observe → new Observer(data) → Observer.walk(data) → defineReactive$$1(data, key) → Object.defineProperty(data, key, {get, set})
通过调用链可以发现在initData开始,由Observer开始对data每个key值进行set、get的拦截监听(利用Object.defineProperty),同时可以发现在defineReactive$$1中对每个key值的拦截监听都会创建一个Dep对象,在get的时候调用dep.depend(),在set的时候调用dep.notify()

function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
    var dep = new Dep();
 
 
    // 对data的key进行set、get拦截
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            dep.depend();
        },
        set: function reactiveSetter (newVal) {
            dep.notify();
        }
    }
}

3、Dep

Dep对象主要将watcher对象维护在自己的属性中,同时也将自己加入到watcher中,在notify时候会调用watcher的update方法

var Dep = function Dep () {
  this.id = uid++;
  this.subs = [];
};
 
 
// 将watcher加入到dep的subs数组中
Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};
 
Dep.prototype.removeSub = function removeSub (sub) {
  remove(this.subs, sub);
};
 
 
// 将dep加入到watcher的deps数组中、同时
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};
 
 
// 遍历dep中watcher、调用他们update方法
Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
   
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

4、Watcher

vm.$mount → mountComponent → new Watcher(vm, updateComponent)
由调用链在创建Watcher对象后,会将自己压入栈顶,等待触发data 属性getter时候,将dep和watcher对象互相绑定

// 挂载组件
function mountComponent (
  vm,
  el,
  hydrating
) {
 
 
    updateComponent = function () {
        // vm.render()执行render函数生成新节点
        vm._update(vm._render(), hydrating);
    };
     
    // 创建Watcher对象
    new Watcher(vm, updateComponent, noop, {
        before: function before () {
            if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate');
            }
        }
    }, true);
}
 
 
/************* Watcher函数 **********************/
var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
    // 存储的dep数组
    this.deps = [];
    // 存储的dep id
    this.depIds = new _Set();
    // 存储调用者传过来的updateComponent
    this.expression = expOrFn.toString();
 
 
    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
    }
     
    this.value = this.lazy ? undefined : this.get();
}
 
 
// watcher的get方法
Watcher.prototype.get = function get () {
    // 将当前watcher压栈
    pushTarget(this);
 
 
    value = this.getter.call(vm, vm);
    // 将当前watcher出栈
    popTarget();
}
 
 
// 添加dep到Watcher对象属性中
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)) {
      // 将watcher对象添加到dep对象中
      dep.addSub(this);
    }
  }
};
 
 
// Watcher对象update方法
Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    // 懒加载
    this.dirty = true;
  } else if (this.sync) {
    // 同步组件
    this.run();
  } else {
    // watcher队列
    queueWatcher(this);
  }
};
 
 
// Watcher对象run方法
Watcher.prototype.run = function run () {
  if (this.active) {
    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) {
        try {
          this.cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
        }
      } else {
        this.cb.call(this.vm, value, oldValue);
      }
    }
  }
};

二、vue组件中数据更新会发生什么事情?

由于上面我们同步了一些关键信息,通过断点结合,可以看到整个调用过程
1、数据更新,我们首先会触发Data的set拦截,同时调用dep的notify方法。
2、遍历dep对象中的watcher数组,执行watcher的update方法。
3、在watcher的update函数中调用queueWatcher方法,并将自己作为入参。
4、在queueWatcher函数中,判断has map是否有当前watcher id,没有则将watcher存入queue队列,记录watcher id到has map中,调用nextTick方法;有则跳过这一步,等待异步刷新队列执行(异步刷新队列:flushSchedulerQueue)。
5、在nextTick中,将flushSchedulerQueue push进callbacks数组,执行timerFunc方法。
6、timerFunc中等待浏览器的微任务/宏任务回调时候遍历执行callbacks数组中的异步刷新队列方法flushSchedulerQueue。
7、在flushSchedulerQueue中,调用watcher的run方法。
8、在watcher的run中,调用watcher的get方法。
9、在watcher的get中,调用watcher的getter属性方法。
10、在Watcher对象初始化时候,getter就是mountComponent时候传入updateComponent方法。
11、执行updateComponent方法,会调用组件_update方法,传入当前组件_render函数返回值。
12、在_render函数中,获取当前组件data中的值(当前最新的值)。
13、在_update中,调用patch方法,传入新旧Vnode。
14、在patch中,调用patchVnode方法,开始diff比较虚拟dom树。


堆栈调用.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。