vue响应式原理---图文解释


响应式原理的基础

响应式原理是基于object.defineProperty(object,props,descriptor)descriptor里面可以定义

get和set方法,可以在获取属性值时触发get方法(k可以收集依赖),设置属性值时触发set方法(更新依赖)


提示:以下是本篇文章正文内容,下面案例可供参考

一、响应式流程

案例

// 案例
export default {
    name: 'App',
    data () {
        return {
            msg: 'hello world',
            arr = [1, 2, 3]
        }
    }
}

1.依赖收集

这里会从observe、dep、watcher 三个对象进行讲解,分object、array两种依赖收集方式。

一定要注意!!!数组的依赖收集跟对象的属性是不一样的。对象的属性经过深度遍历后,最终就是以一个基本类型的数据为单位收集依赖,但数组仍然是一个引用类型。

1> 三个核心对象:observe(蓝色)、dep(绿色)、watcher(紫色)

2>依赖收集准备阶段----observe、dep的实例化

对象、数组的不同处理方式。核心代码 + 图 进行讲解

// 以下是initData调用的方法讲解,排列遵循调用顺序
function observe (value, asRootData) {
  if (!isObject(value)) return // 非对象则不处理
  // 实例化Observer对象
  var ob;
  ob = new Observer(value);
  return ob
}

function Observer (value) {
  this.value = value; // 保存当前的data
  this.dep = new Dep(); // 实例化dep,数组进行依赖收集的dep(对应案例中的arr)
  def(value, '__ob__', this);    
  if (Array.isArray(value)) {
    if (hasProto) {
      // 这里会改写数组原型。__proto__指向重写数组方法的对象
      protoAugment(value, arrayMethods); 
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value); 
  }
}
// 遍历数组元素,执行对每一项调用observe,也就是说数组中有对象会转成响应式对象
Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
}
// 遍历对象的全部属性,调用defineReactive
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  // 如案例代码,这里的 keys = ['msg', 'arr']
  for (var i = 0; i < keys.length; i++) {        
    defineReactive(obj, keys[i]);
  }
}

     

接下来核心分析 defineReactive 做了什么。注意 childOb ,这是数组进行依赖收集的地方(也就是为什么我们 this.arr.push(4) 能找到 Watcher 进行派发更新)

function defineReactive (obj, key, val) {
  // 产生一个闭包dep
  var dep = new Dep();
  // 如果val是object类型,递归调用observe,案例代码中的arr会走这个逻辑
  var childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {    
    get: function reactiveGetter () { 
      // 求value的值
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) { // Dep.target就是当前的Watcher
        // 这里是闭包dep
        dep.depend();
        if (childOb) {
          // 案例代码中arr会走到这个逻辑
          childOb.dep.depend(); // 这里是Observer里的dep,数组arr在此依赖收集
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 下文派发更新里进行讲解
    }
  });
}

           

依赖收集触发阶段——Wather实例化、访问数据、触发依赖收集

  • Dep.target相关讲解
    • targetStack:栈结构,用来保存Watcher
    • pushTarget:往targetStackpush当前的Watcher(排在前一个Watcher的后面),并把Dep.target赋值给当前Watcher
    • popTargettargetStack最后一个元素弹出(.pop),Dep.target赋值给最后一个Watcher(也就是还原了前一个Watcher)
    • 通过上述实现,vue保证了全局唯一的Watcher,准确赋值在Dep.target
// new Wathcer核心
function Watcher (vm, expOrFn, cb, options, isRenderWatcher) {
  if (typeof expOrFn === 'function') {
  // 渲染watcher中,这里传入的expOrFn是updateComponent = vm.update(vm.render())
  // this.getter等价于vm.update(vm.render())
    this.getter = expOrFn; 
  } else {
    ...
  }
  // 这里进行判断,lazy为true时(计算属性)则什么都不执行,否则执行get
  this.value = this.lazy
    ? undefined
    : this.get(); // 本次为渲染Watcher,执行get,继续往下看~
}

// Watcher的get方法
Watcher.prototype.get = function get () {
  // 这里很关键,pushTarget就是把当前的Wather赋值给“Dep.target”
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    // 1. 这里调用getter,也就是执行vm.update(vm.render())
    // 2. 执行vm.render函数就会访问到响应式数据,触发get进行依赖收集
    // 3. 此时的Dep.target为当前的渲染Watcher,数据就可以理所应当的把Watcher加入自己的subs中
    // 4. 所以此时,Watcher就能监测到数据变化,实现响应式
    value = this.getter.call(vm, vm);
  } catch (e) {
    ...
  } finally {
    popTarget();
    /*
    * cleanupDeps是个优化操作,会移除Watcher对本次render没被使用的数据的观测
    * 效果:处于v-if为false中的响应式数据改变不会触发Watcher的update
    * 感兴趣的可以自己去debugger调试,这里就不展开了
    */
    this.cleanupDeps(); 
  }
  return value
}


整体流程

2. 派发更新

  • 派发更新区分对象属性、数组方法进行讲解
  • 这里可以想一下,以下操作会发生什么?
    • this.msg = 'new val'
    • this.arr.push(4)
  • 毫无疑问都会先触发他们之中的get,再触发什么呢?我们接下来看

1>对象属性修改触发set,派发更新。this.msg = 'new val'

...
Object.defineProperty (obj, key, {
    get () {...},
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      // 判断新值相比旧值是否已经改变
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      // 如果新值是引用类型,则将其转化为响应式
      childOb = !shallow && observe(newVal);
      // 这里通知dep的所有watcher进行更新
      dep.notify();
    }
}        
...

 

2>数组调用方法。this.arr.push(4) 

   这里可以联合数组的依赖收集再看一遍,你就恍然大悟了。为什么 对象的属性 、数组 的依赖收集方式不一样      

// 数组方法改写是在 Observer 方法中
function Observer () {
    if (hasProto) { 
        // 用案例讲解,也就是this.arr.__proto__ = arrayMethods
        protoAugment(value, arrayMethods); 
    }
}   

// 以下是数组方法重写的实现
var arrayProto = Array.prototype; // 保存真实数组的原型
var arrayMethods = Object.create(arrayProto); // 以真数组为原型创建对象
// 可以看成:arrayMethods.__proto__ = Array.prototype
var methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];

// 一个装饰器模型,重写7个数组方法
methodsToPatch.forEach(function (method) {
  // 保存原生的数组方法
  var original = arrayProto[method];
  // 劫持arrayMethods对象中的数组方法
  def(arrayMethods, method, function mutator () {
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];

    var result = original.apply(this, args);
    var ob = this.__ob__; // 当我门调用this.arr.push(),这里就能到数组对象的ob实例
    var inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    if (inserted) { ob.observeArray(inserted); }
    // 由于数组对象在new Observer中实例化了一个dep,并通过childOb逻辑收集了依赖,这里就能在ob实例中拿到dep属性
    ob.dep.notify();
    return result
  });
})

二、小总结

1.核心对象:Dep与Watcher

dep: vue在data里申明的每一个属性都会生成一个Dep的实例对象,Dep.subs存储着当该属性变化时需要去更新的Watcher;

watcher:有三种情况会生成Watcher的实例对象

  1. 定义在computed里的计算属性;
  2. 在watch里写的监听函数;
  3. 组件的渲染Watcher;

2.收集依赖与跟新依赖

收集依赖

将Watcher的实例对象w分发到它所依赖的属性的Dep中,过程如下:

1.将Dep.target = 当前的Watcer 的实例对象w;

2.w执行定义的函数(即在computed/watch写的函数);

3.执行函数的过程如果使用data里定义的属性,则会触发属性的get方法,get方法中Dep实例对象dep会将Dep.target中存储的w放入到dep.subs数组中,完成依赖收集。

更新依赖

当修改我们申明的某个属性时,会触发属性的set方法,set方法会将dep.subs数组中收集的Watcher实例对象进行更新,即触发我们定义在computed和watch里面的函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值