7.9 data

7.9.1 问题思考

在开始分析data之前,我们先抛出几个问题让读者思考,而答案都包含在接下来内容分析中。

  • 前面已经知道,Dep是作为管理依赖的容器,那么这个容器在什么时候产生?也就是实例化Dep发生在什么时候?

  • Dep收集了什么类型的依赖?即watcher作为依赖的分类有哪些,分别是什么场景,以及区别在哪里?

  • Observer这个类具体对getter,setter方法做了哪些事情?
  • 手写的watcher和页面数据渲染监听的watch如果同时监听到数据的变化,优先级怎么排?
  • 有了依赖的收集是不是还有依赖的解除,依赖解除的意义在哪里?

带着这几个问题,我们开始对data的响应式细节展开分析。

7.9.2 依赖收集

data在初始化阶段会实例化一个Observer类,这个类的定义如下(忽略数组类型的data):

  1. // initData
  2. function initData(data){
  3. ···
  4. observe(data,true)
  5. }
  6. // observe
  7. function observe(value, asRootData){
  8. ···
  9. ob =newObserver(value);
  10. return ob
  11. }
  12. // 观察者类,对象只要设置成拥有观察属性,则对象下的所有属性都会重写getter和setter方法,而getter,setting方法会进行依赖的收集和派发更新
  13. varObserver=functionObserver(value){
  14. ···
  15. // 将__ob__属性设置成不可枚举属性。外部无法通过遍历获取。
  16. def(value,'__ob__',this);
  17. // 数组处理
  18. if(Array.isArray(value)){
  19. ···
  20. }else{
  21. // 对象处理
  22. this.walk(value);
  23. }
  24. };
  25. function def (obj, key, val, enumerable){
  26. Object.defineProperty(obj, key,{
  27. value: val,
  28. enumerable:!!enumerable,// 是否可枚举
  29. writable:true,
  30. configurable:true
  31. });
  32. }

Observer会为data添加一个__ob__属性, __ob__属性是作为响应式对象的标志,同时def方法确保了该属性是不可枚举属性,即外界无法通过遍历获取该属性值。除了标志响应式对象外,Observer类还调用了原型上的walk方法,遍历对象上每个属性进行getter,setter的改写。

  1. Observer.prototype.walk =function walk (obj){
  2. // 获取对象所有属性,遍历调用defineReactive###1进行改写
  3. var keys =Object.keys(obj);
  4. for(var i =0; i < keys.length; i++){
  5. defineReactive###1(obj, keys[i]);
  6. }
  7. };

defineReactive###1是响应式构建的核心,它会先实例化一个Dep类,即为每个数据都创建一个依赖的管理,之后利用Object.defineProperty重写getter,setter方法。这里我们只分析依赖收集的代码。

  1. function defineReactive###1(obj,key,val,customSetter,shallow){
  2. // 每个数据实例化一个Dep类,创建一个依赖的管理
  3. var dep =newDep();
  4. var property =Object.getOwnPropertyDescriptor(obj, key);
  5. // 属性必须满足可配置
  6. if(property && property.configurable ===false){
  7. return
  8. }
  9. // cater for pre-defined getter/setters
  10. var getter = property && property.get;
  11. var setter = property && property.set;
  12. // 这一部分的逻辑是针对深层次的对象,如果对象的属性是一个对象,则会递归调用实例化Observe类,让其属性值也转换为响应式对象
  13. var childOb =!shallow && observe(val);
  14. Object.defineProperty(obj, key,{
  15. enumerable:true,
  16. configurable:true,s
  17. get:function reactiveGetter (){
  18. var value = getter ? getter.call(obj): val;
  19. if(Dep.target){
  20. // 为当前watcher添加dep数据
  21. dep.depend();
  22. if(childOb){
  23. childOb.dep.depend();
  24. if(Array.isArray(value)){
  25. dependArray(value);
  26. }
  27. }
  28. }
  29. return value
  30. },
  31. set:function reactiveSetter (newVal){}
  32. });
  33. }

主要看getter的逻辑,我们知道当data中属性值被访问时,会被getter函数拦截,根据我们旧有的知识体系可以知道,实例挂载前会创建一个渲染watcher

  1. newWatcher(vm, updateComponent, noop,{
  2. before:function before (){
  3. if(vm._isMounted &&!vm._isDestroyed){
  4. callHook(vm,'beforeUpdate');
  5. }
  6. }
  7. },true/* isRenderWatcher */);

与此同时,updateComponent的逻辑会执行实例的挂载,在这个过程中,模板会被优先解析为render函数,而render函数转换成Vnode时,会访问到定义的data数据,这个时候会触发gettter进行依赖收集。而此时数据收集的依赖就是这个渲染watcher本身。

代码中依赖收集阶段会做下面几件事:

  1. 为当前的watcher(该场景下是渲染watcher)添加拥有的数据
  2. 为当前的数据收集需要监听的依赖

如何理解这两点?我们先看代码中的实现。getter阶段会执行dep.depend(),这是Dep这个类定义在原型上的方法。

  1. dep.depend();
  2. Dep.prototype.depend =function depend (){
  3. if(Dep.target){
  4. Dep.target.addDep(this);
  5. }
  6. };

Dep.target为当前执行的watcher,在渲染阶段,Dep.target为组件挂载时实例化的渲染watcher,因此depend方法又会调用当前watcheraddDep方法为watcher添加依赖的数据。

  1. Watcher.prototype.addDep =function addDep (dep){
  2. var id = dep.id;
  3. if(!this.newDepIds.has(id)){
  4. // newDepIds和newDeps记录watcher拥有的数据
  5. this.newDepIds.add(id);
  6. this.newDeps.push(dep);
  7. // 避免重复添加同一个data收集器
  8. if(!this.depIds.has(id)){
  9. dep.addSub(this);
  10. }
  11. }
  12. };

其中newDepIds是具有唯一成员是Set数据结构,newDeps是数组,他们用来记录当前watcher所拥有的数据,这一过程会进行逻辑判断,避免同一数据添加多次。

addSub为每个数据依赖收集器添加需要被监听的watcher

  1. Dep.prototype.addSub =function addSub (sub){
  2. //将当前watcher添加到数据依赖收集器中
  3. this.subs.push(sub);
  4. };
  1. getter如果遇到属性值为对象时,会为该对象的每个值收集依赖

这句话也很好理解,如果我们将一个值为基本类型的响应式数据改变成一个对象,此时新增对象里的属性,也需要设置成响应式数据。

  1. 遇到属性值为数组时,进行特殊处理,这点放到后面讲。

通俗的总结一下依赖收集的过程,每个数据就是一个依赖管理器,而每个使用数据的地方就是一个依赖。当访问到数据时,会将当前访问的场景作为一个依赖收集到依赖管理器中,同时也会为这个场景的依赖收集拥有的数据。

7.9.3 派发更新

在分析依赖收集的过程中,可能会有不少困惑,为什么要维护这么多的关系?在数据更新时,这些关系会起到什么作用?带着疑惑,我们来看看派发更新的过程。在数据发生改变时,会执行定义好的setter方法,我们先看源码。

  1. Object.defineProperty(obj,key,{
  2. ···
  3. set:function reactiveSetter (newVal){
  4. var value = getter ? getter.call(obj): val;
  5. // 新值和旧值相等时,跳出操作
  6. if(newVal === value ||(newVal !== newVal && value !== value)){
  7. return
  8. }
  9. ···
  10. // 新值为对象时,会为新对象进行依赖收集过程
  11. childOb =!shallow && observe(newVal);
  12. dep.notify();
  13. }
  14. })

派发更新阶段会做以下几件事:

  • 判断数据更改前后是否一致,如果数据相等则不进行任何派发更新操作
  • 新值为对象时,会对该值的属性进行依赖收集过程
  • 通知该数据收集的watcher依赖,遍历每个watcher进行数据更新,这个阶段是调用该数据依赖收集器的dep.notify方法进行更新的派发。
    1. Dep.prototype.notify =function notify (){
    2. var subs =this.subs.slice();
    3. if(!config.async){
    4. // 根据依赖的id进行排序
    5. subs.sort(function(a, b){return a.id - b.id;});
    6. }
    7. for(var i =0, l = subs.length; i < l; i++){
    8. // 遍历每个依赖,进行更新数据操作。
    9. subs[i].update();
    10. }
    11. };
  • 更新时会将每个watcher推到队列中,等待下一个tick到来时取出每个watcher进行run操作
    1. Watcher.prototype.update =function update (){
    2. ···
    3. queueWatcher(this);
    4. };
    queueWatcher方法的调用,会将数据所收集的依赖依次推到queue数组中,数组会在下一个事件循环'tick'中根据缓冲结果进行视图更新。而在执行视图更新过程中,难免会因为数据的改变而在渲染模板上添加新的依赖,这样又会执行queueWatcher的过程。所以需要有一个标志位来记录是否处于异步更新过程的队列中。这个标志位为flushing,当处于异步更新过程时,新增的watcher会插入到queue中。
    1. function queueWatcher (watcher){
    2. var id = watcher.id;
    3. // 保证同一个watcher只执行一次
    4. if(has[id]==null){
    5. has[id]=true;
    6. if(!flushing){
    7. queue.push(watcher);
    8. }else{
    9. var i = queue.length -1;
    10. while(i > index && queue[i].id > watcher.id){
    11. i--;
    12. }
    13. queue.splice(i +1,0, watcher);
    14. }
    15. ···
    16. nextTick(flushSchedulerQueue);
    17. }
    18. }
    nextTick的原理和实现先不讲,概括来说,nextTick会缓冲多个数据处理过程,等到下一个事件循环tick中再去执行DOM操作,它的原理,本质是利用事件循环的微任务队列实现异步更新

当下一个tick到来时,会执行flushSchedulerQueue方法,它会拿到收集的queue数组(这是一个watcher的集合),并对数组依赖进行排序。为什么进行排序呢?源码中解释了三点:

  • 组件创建是先父后子,所以组件的更新也是先父后子,因此需要保证父的渲染watcher优先于子的渲染watcher更新。
  • 用户自定义的watcher,称为user watcheruser watcherrender watcher执行也有先后,由于user watchersrender watcher要先创建,所以user watcher要优先执行
  • 如果一个组件在父组件的 watcher 执行阶段被销毁,那么它对应的 watcher 执行都可以被跳过。
  1. function flushSchedulerQueue (){
  2. currentFlushTimestamp = getNow();
  3. flushing =true;
  4. var watcher, id;
  5. // 对queue的watcher进行排序
  6. queue.sort(function(a, b){return a.id - b.id;});
  7. // 循环执行queue.length,为了确保由于渲染时添加新的依赖导致queue的长度不断改变。
  8. for(index =0; index < queue.length; index++){
  9. watcher = queue[index];
  10. // 如果watcher定义了before的配置,则优先执行before方法
  11. if(watcher.before){
  12. watcher.before();
  13. }
  14. id = watcher.id;
  15. has[id]=null;
  16. watcher.run();
  17. // in dev build, check and stop circular updates.
  18. if(has[id]!=null){
  19. circular[id]=(circular[id]||0)+1;
  20. if(circular[id]> MAX_UPDATE_COUNT){
  21. warn(
  22. 'You may have an infinite update loop '+(
  23. watcher.user
  24. ?("in watcher with expression \""+(watcher.expression)+"\"")
  25. :"in a component render function."
  26. ),
  27. watcher.vm
  28. );
  29. break
  30. }
  31. }
  32. }
  33. // keep copies of post queues before resetting state
  34. var activatedQueue = activatedChildren.slice();
  35. var updatedQueue = queue.slice();
  36. // 重置恢复状态,清空队列
  37. resetSchedulerState();
  38. // 视图改变后,调用其他钩子
  39. callActivatedHooks(activatedQueue);
  40. callUpdatedHooks(updatedQueue);
  41. // devtool hook
  42. /* istanbul ignore if */
  43. if(devtools && config.devtools){
  44. devtools.emit('flush');
  45. }
  46. }

flushSchedulerQueue阶段,重要的过程可以总结为四点:

  • queue中的watcher进行排序,原因上面已经总结。
  • 遍历watcher,如果当前watcherbefore配置,则执行before方法,对应前面的渲染watcher:在渲染watcher实例化时,我们传递了before函数,即在下个tick更新视图前,会调用beforeUpdate生命周期钩子。
  • 执行watcher.run进行修改的操作。
  • 重置恢复状态,这个阶段会将一些流程控制的状态变量恢复为初始值,并清空记录watcher的队列。
  1. newWatcher(vm, updateComponent, noop,{
  2. before:function before (){
  3. if(vm._isMounted &&!vm._isDestroyed){
  4. callHook(vm,'beforeUpdate');
  5. }
  6. }
  7. },true/* isRenderWatcher */);

重点看看watcher.run()的操作。

  1. Watcher.prototype.run =function run (){
  2. if(this.active){
  3. var value =this.get();
  4. if( value !==this.value || isObject(value)||this.deep ){
  5. // 设置新值
  6. var oldValue =this.value;
  7. this.value = value;
  8. // 针对user watcher,暂时不分析
  9. if(this.user){
  10. try{
  11. this.cb.call(this.vm, value, oldValue);
  12. }catch(e){
  13. handleError(e,this.vm,("callback for watcher \""+(this.expression)+"\""));
  14. }
  15. }else{
  16. this.cb.call(this.vm, value, oldValue);
  17. }
  18. }
  19. }
  20. };

首先会执行watcher.prototype.get的方法,得到数据变化后的当前值,之后会对新值做判断,如果判断满足条件,则执行cb,cb为实例化watcher时传入的回调。

在分析get方法前,回头看看watcher构造函数的几个属性定义

  1. var watcher =functionWatcher(
  2. vm,// 组件实例
  3. expOrFn,// 执行函数
  4. cb,// 回调
  5. options,// 配置
  6. isRenderWatcher // 是否为渲染watcher
  7. ){
  8. this.vm = vm;
  9. if(isRenderWatcher){
  10. vm._watcher =this;
  11. }
  12. vm._watchers.push(this);
  13. // options
  14. if(options){
  15. this.deep =!!options.deep;
  16. this.user =!!options.user;
  17. this.lazy =!!options.lazy;
  18. this.sync =!!options.sync;
  19. this.before = options.before;
  20. }else{
  21. this.deep =this.user =this.lazy =this.sync =false;
  22. }
  23. this.cb = cb;
  24. this.id =++uid$2;// uid for batching
  25. this.active =true;
  26. this.dirty =this.lazy;// for lazy watchers
  27. this.deps =[];
  28. this.newDeps =[];
  29. this.depIds =new_Set();
  30. this.newDepIds =new_Set();
  31. this.expression = expOrFn.toString();
  32. // parse expression for getter
  33. if(typeof expOrFn ==='function'){
  34. this.getter = expOrFn;
  35. }else{
  36. this.getter = parsePath(expOrFn);
  37. if(!this.getter){
  38. this.getter = noop;
  39. warn(
  40. "Failed watching path: \""+ expOrFn +"\" "+
  41. 'Watcher only accepts simple dot-delimited paths. '+
  42. 'For full control, use a function instead.',
  43. vm
  44. );
  45. }
  46. }
  47. // lazy为计算属性标志,当watcher为计算watcher时,不会理解执行get方法进行求值
  48. this.value =this.lazy
  49. ?undefined
  50. :this.get();
  51. }

方法get的定义如下:

  1. Watcher.prototype.get=functionget(){
  2. pushTarget(this);
  3. var value;
  4. var vm =this.vm;
  5. try{
  6. value =this.getter.call(vm, vm);
  7. }catch(e){
  8. ···
  9. } finally {
  10. ···
  11. // 把Dep.target恢复到上一个状态,依赖收集过程完成
  12. popTarget();
  13. this.cleanupDeps();
  14. }
  15. return value
  16. };

get方法会执行this.getter进行求值,在当前渲染watcher的条件下,getter会执行视图更新的操作。这一阶段会重新渲染页面组件

  1. newWatcher(vm, updateComponent, noop,{ before:()=>{}},true);
  2. updateComponent =function(){
  3. vm._update(vm._render(), hydrating);
  4. };

执行完getter方法后,最后一步会进行依赖的清除,也就是cleanupDeps的过程。

关于依赖清除的作用,我们列举一个场景: 我们经常会使用v-if来进行模板的切换,切换过程中会执行不同的模板渲染,如果A模板监听a数据,B模板监听b数据,当渲染模板B时,如果不进行旧依赖的清除,在B模板的场景下,a数据的变化同样会引起依赖的重新渲染更新,这会造成性能的浪费。因此旧依赖的清除在优化阶段是有必要。

  1. // 依赖清除的过程
  2. Watcher.prototype.cleanupDeps =function cleanupDeps (){
  3. var i =this.deps.length;
  4. while(i--){
  5. var dep =this.deps[i];
  6. if(!this.newDepIds.has(dep.id)){
  7. dep.removeSub(this);
  8. }
  9. }
  10. var tmp =this.depIds;
  11. this.depIds =this.newDepIds;
  12. this.newDepIds = tmp;
  13. this.newDepIds.clear();
  14. tmp =this.deps;
  15. this.deps =this.newDeps;
  16. this.newDeps = tmp;
  17. this.newDeps.length =0;
  18. };

把上面分析的总结成依赖派发更新的最后两个点

  • 执行run操作会执行getter方法,也就是重新计算新值,针对渲染watcher而言,会重新执行updateComponent进行视图更新
  • 重新计算getter后,会进行依赖的清除