Vue的依赖收集、更新

拈花ヽ惹草 提交于 2020-03-12 12:21:00

我们都知道,只要在 Vue 实例中声明过的数据,那么这个数据就是响应式的。
什么是响应式,也即是说,数据发生改变的时候,视图会重新渲染,匹配更新为最新的值。

  1. 那么Vue如何监听数据变化?
  2. 数据变化后,如何通知视图更新?
  3. 数据变化后,视图怎么知道何时更新
    思考以上3个问题
    先介绍下Object.defineProperty(),因为这个方法是Vue实现响应式系统的重中之重

Object.defineProperty()

MDN上的定义:
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
语法:

Object.defineProperty(obj, prop, desc)

使用:

const obj = {name: 'Bob'}
Object.defineProperty(obj, 'name', {
  get(){
    console.log('get 被触发')
  },
  set(val){
    console.log('set 被触发')
  }
})
obj.name //get 被触发
obj.name = 'Lily' //set 被触发
Object.defineProperty可以为对象属性设置get、set函数,

get:属性被访问时触发
set:属性被赋值时触发
其实Object.defineProperty()可以为属性设置很多特性,例如configurable,enumerable,但是现在不过多解释
到这里,就可以回答第一个问题:Vue如何监听数据变化?其实Vue是在属性的set方法中做了处理,这样,只要数据改变,触发set方法,就能知道数据变化了

一、如何进行依赖收集

data 中声明的每个属性,都有一个依赖收集器 subs,保存着依赖了它的页面(或者watch、computed)

new Vue({    
    data(){        
        return {            
            name:"111"        
        }    
    }
})
//页面 A 引用了name
<div>{{name}}</div>

此时name属性结构如下

{
  Dep:{
    subs: [watcher]
  }
}

可以看到,name 属性,使用了 一个 dep 保存了 页面A 这个依赖,而保存的实际上是 页面A的 Watcher。

Tip

简单说一下,watcher 是什么,每个 Vue 实例都会拥有一个专属的 watcher,可用于实例更新
那数据是如何进行依赖收集的呢?
依赖收集又分为两个流程

1、数据初始化流程

首先,在实例初始化的时候,需要对数据进行响应式处理,也就是给每个属性都使用 Object.defineProperty 处理
先看下源码的初始化流程

1、实例初始化中,调用 initState 处理部分选项数据

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}


Vue.prototype._init = function (options) {
  const vm: Component = this
  ...
  initState(vm)
  ...
}
  

function initState (vm) {
  const opts = vm.$options
  ... props,computed,watch 等选项处理
  if (opts.data) {
    initData(vm)
  } 
}

2、initData 遍历 data,definedReactive 处理每个属性

function initData(vm) {  
    var data = vm.$options.data;
    data = typeof data === 'function' ? 
           data.call(vm, vm) : data || {};

    // ... 遍历 data 数据对象的key,重名检测,合规检测等代码
    new Observer(data);
}

function Observer(value) {    

    var keys = Object.keys(value);

    // ...被省略的代码
    for (var i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i]);
    }
};

3、definedReactive 给对象的属性 通过 Object.defineProperty 设置响应式

function defineReactive(obj, key) {    

    // dep 用于收集所有 依赖我的 东西
    var dep = new Dep();    
    var val  = obj[key]    

    Object.defineProperty(obj, key, {        
        enumerable: true,        
        configurable: true,

        get() { ...依赖收集 },
        set() { ....依赖更新 }
    });
}

2、依赖收集流程

  1. 一个模版渲染时,首先执行渲染函数
with(this){  
    return _c('div',{},[name])
}
  1. 渲染函数读取实例上的data
  2. 读取data触发Object.defineProperty.get方法,开始收集 watcher
function defineReactive(obj, key) {    

    var dep = new Dep();    
    var val  = obj[key]    

    Object.defineProperty(obj, key, {

        get() {            
            if (Dep.target) {                
                // 收集依赖
                dep.addSub(Dep.target)
            }            
            return val
        }
    });
}

可以看到上面代码中有Dep、Dep.target、dep.addSub,解释下

Dep是一个构造函数,用于创建实例,并带有很多方法
实例会包含一个属性 subs 数组,用于存储不同数据 【收集的依赖】

var Dep = function Dep () {
    this.id = uid++;
    this.subs = [];//保存watcher的数组
  };

Dep.target指向当前watcher,watcher 在 依赖收集中只起到被收集的作用,具体怎么用于依赖更新下面再讲

dep.addSub原型上的方法,作用是往 dep.subs 存储器中 中直接添加 watcher

Dep.prototype.addSub = function(sub) {    
    this.subs.push(sub);
};

所以,上面有个例子打印出data中name属性的结构就是这么来的

{
  Dep:{
    subs: [watcher]
  }
}

以上便可以回答了开篇的第二个问题 数据变化后,如何通知视图更新?
恩,通知那些存在 依赖收集器中的 watcher
PS
以上的源码解析的都是当data内属性是基本类型时的流程,是对象或者数组时,递归处理为每个子项都添加dep.subs
另外一个重要的不同点:数据是对象或者数组时,会另外在每级子项存储一个__ob__

{
  __ob__:{
    dep:{
      subs:[wacher]
    }
  }
}

可以看到__ob__下面还是我们熟悉的存储依赖的结构,为什么这么做呢?

上面依赖收集是通过Object.definePropertyget、set实现的,他的缺陷是对于本身不存在对象中的属性是监控不到的,新增或删除一个子项就需要另外处理了

vue内部为对象实现的$set、$del读取__ob__内容,实现通知视图更新
vue内部重写数组的push、shift等也是读取__ob__内容,实现通知视图更新

二、 如何进行依赖更新

经过上面的讲解,我们都知道,每个属性都会保存有一个 依赖收集器 subs保存着各种watcher
数据变化时,触发Object.defineProperty - set,遍历subs,逐个通知watcher,然后调用watcher.update完成更新

function defineReactive(obj, key, val) {   

    var dep = new Dep(); 
    var childOb = observe(val);   

    Object.defineProperty(obj, key, {

       get(){   
            ... 属性被读取,完成依赖收集            
            return val
       },

       set(newVal) {    
       
            // 值没有变化
           if (newVal ===val) return
           
           // 附新值
           val = newVal;     
         
            // 触发更新
           dep.notify();    
       }    
    }); 
}

1、dep.notify()

重点在dep.notify(),会遍历subs,为每一项都执行update,由于subs里保存的是watcher,所以实际上调用的是watcher.update

var Dep = function Dep() {    

    this.subs = []; // 依赖存储器

};

// 遍历 subs ,逐个通知依赖,就是逐个调用 watcher.update
Dep.prototype.notify = function() {    

    var subs = this.subs.slice();    

    for (var i = 0, l = subs.length; i < l; i++) {

        subs[i].update();
    }
};

2、watcher.update

那么watcher.update是如何做更新的呢

function Watcher(vm, expOrFn) {    

    this.vm = vm;    

    // 保存传入的更新函数    
    this.getter = expOrFn;

    // 新建 watcher 的时候,立即执行更新函数
    this.get();
};



Watcher.prototype.get = function() {  

    // 执行更新函数
    this.getter.call(this.vm,this.vm);  

};

Watcher.prototype.update = function() {    
    this.get()
}

看到上面的源码

  1. Watcher 新建实例的时候,会保存传入的函数(这个函数会作为更新用)
  2. watcher 实例有 update 方法,作用是执行上一步保存的更新函数

3、new Watcher

那么 watcher 是什么时候开始创建的呢?

function Vue(options) {    

    this._init(options);

}

Vue.prototype._init = function(options) {
    // ...处理组件选项等
    this.$mount()
}

Vue.prototype.$mount = function() {

    // ...解析template成redner函数保存

    /** 每个实例新建一个watcher,

           并且利用watcher 保存更新函数 **/

    new Watcher(this,        

        // 这个函数是更新函数,传入watcher保存下来,用于后面页面初始化或者页面更新

        function() {
             /** ...调用保存的渲染函数生成VNode,

                          并生成DOM插入页面中**/
        }
    );
};

这便可以回答了开篇的第三个问题 数据变化后,视图怎么知道何时更新

恩,在数据变化触发 set 函数时,通知视图,执行watcher.update,触发页面渲染函数生成VNode,插入页面,完成更新

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!