vue2和vue3响应式原理(二)

响应式流程

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

依赖收集

这里会从Observer、Dep、Watcher三个对象进行讲解,分 objectarray 两种依赖收集方式。

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

如果这里不懂,先想一个问题: 我们用 this.msg = 'xxx' 能触发 setter 派发更新,但是我们修改数组并不是用 this.arr = xxx ,而是用 this.arr.push(xxx) 等修改数组的方法。很显然,这时候并不是通过触发 arrsetter 去派发更新的。那是怎么做的呢?先带着这个问题继续往下看吧!

1.三个核心对象:Observer(蓝)、Dep(绿)、Watcher(紫)

2.依赖收集准备阶段——Observer、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) {
      // 下文派发更新里进行讲解
    }
  });
}

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

Dep.target相关讲解

  • targetStack:栈结构,用来保存Watcher
  • pushTarget:往targetStackpush当前的Watcher(排在前一个Watcher的后面),并把Dep.target赋值给当前Watcher
  • popTargettargetStack最后一个元素弹出(.pop),Dep.target赋值给最后一个Watcher(也就是还原了前一个Watcher)
  • 通过上述实现,vue保证了全局唯一的Watcher,准确赋值在Dep.target

派发更新

  • 派发更新区分对象属性、数组方法进行讲解
  • 如果想要深入了解组件的异步更新,戳这里,了解Vue组件异步更新之nextTick。本文只针对派发更新流程,不会对异步更新DOM进行展开讲解~
  • 这里可以先想一下,以下操作会发生什么?
    • 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
  });
})

 

 computed依赖收集

 

<template>
    <div id="app">
        {{ name }}
    </div>
</template>
<script>
export default {
    name: 'App',
    computed: {
      name () {
        return this.firstName + this.secondName
      }
    },
    data () {
        return {
            firstName: 'jing',
            secondName: 'boran'
        }
    }
}
</script>

  • computed中的name其实就是一个computed Watcher,这个Watcher在init阶段生成
  • 当App组件render的阶段,render函数会访问到模版中的{{ name }},则会触发computed的求值,也就是执行上面代码computedGetter()
    • 代码中判断watcher.dirty标志是什么?有什么用?
      1. 只有computed的值发生改变(也就是其依赖的数据改变),watcher.dirty才会被设为true
      2. 只有watcher.dirtytrue才会对computed进行 求值 或 重新求值
      3. 总结:也就是组件每次render,如果computed的值没改变,直接返回value值(是不需要重新计算的),这也是computed的一个特点
  • 执行watcher.evaluate()。也就是执行wathcer.get。上文依赖收集的第3点:依赖收集触发阶段有对get方法进行讲解,忘了的可以上去回顾一下
    • 首先pushTargetDep.target从App组件的渲染Watcher改为namecomputed Watcher
    • 其次执行cb:function() { return this.firstName + this.secondName }
    • 执行cb的过程中,必然会访问到firstNamesecondName,这时候就是我们熟悉的依赖收集阶段了。firstName、secondName都会把name这个computed watcher收集到自己的dep.subs[]
    • 最后popTarget把name的computed Watcher弹出栈,并恢复Dep.target为当前App组件的渲染Watcher
执行watcher.depend()
Watcher.prototype.depend = function depend () {
  var i = this.deps.length;
  while (i--) {
    // 也就是调用Dep.depend => Watcher.addDep => dep.addSub
    this.deps[i].depend(); 
  }
}

  • 遍历computed watcher的deps。其实就是firstName、secondName实例的Dep
  • dep.depend也就是调用watcher.addDep(把Dep收集进watcher.deps中),再由watcher.appDep调用dep.addSub(把Watcher收集进dep.subs中)
  • 这样一来,就完成了firstName、secondName对App组件的渲染watcher进行收集
  computed派发更新

派发相对来说比较简单了~跟响应式的派发更新基本一致,继续以案例来讲解吧!

  1. 当我们修改firstName会发生什么?this.firstName = 'change'
  2. 首先触发firstName的set,最终会调用dep.notify()。firstName的dep.subs中有2个watcher,分别执行对应watcher的notify

 

Watcher.prototype.update = function update () {      
  if (this.lazy) {
    this.dirty = true; // computed会走到这里,然后就结束了
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this); // 渲染watcher会走到这里
  }
}

  • computed watcher:将dirty属性置为true。
  • 渲染watcher会执行派发更新流程(如本文响应式流程——2.派发更新一致)
  • nextTick阶段执行flushSchedulerQueue,则会执行watcher.run()
  • watcher.run会执行watcher.get方法,也就是重新执行render、update的流程
  • 执行render又会访问到name的computed,从而又会执行computedGetter
  • 此时的watcher.dirty在本步骤3已经置为true,又会执行watcher.evaluate()进行computed的求值,执行watcher.depend()......后续的流程就是派发更新的流程了~
 user Watcher依赖收集

user Watcher的依赖收集相比computed会简单一点,这里不会赘述太多,只说核心区别,还有watch的常用配置immediatedeepsync

  1. user Watcher在init阶段会执行一次watcher.get(),在这里会访问我们watch的响应式数据,从而进行依赖收集。回顾下computed,computed在这个阶段什么也没做。
// 没错,又是这段熟悉的代码
this.value = this.lazy
  ? undefined
  : this.get(); // user Watcher和渲染 Watcher都在new Watcher阶段执行get()

 2.如果userWatcher设置的immediate: true,则会在new Watcher后主动触发一次cb的执行

Vue.prototype.$watch = function (expOrFn, cb, options) {
  ...
  var watcher = new Watcher(vm, expOrFn, cb, options);
  if (options.immediate) {
    // immediate则会执行我们传入的callback
    try {
      cb.call(vm, watcher.value);
    } catch (error) {
      
    }
  }
  return function unwatchFn () {
    watcher.teardown();
  }
};

3.deep逻辑很简单,大概讲下:深度遍历这个对象,访问到该对象的所有属性,以此来触发所有属性的getter。这样,所有属性都会把当前的user Watcher收集到自己的dep中。因此,深层的属性值修改(触发set派发更新能通知到user Watcher),watch自然就能监测到数据改变~感兴趣的同学可以自己去看看源码中traverse的实现。

4.sync。当前tick执行,以此能先于渲染Wathcer执行。不设置同步的watcher都会放到nextTick中执行。

Watcher.prototype.update = function update () {
  if (this.lazy) {
    this.dirty = true; // 计算属性
  } else if (this.sync) {
    this.run(); // 同步的user Wathcer
  } else {
    queueWatcher(this); // 普通user Watcher和渲染Watcher
  }
}

总体来说,Vue的源码其实是比较好上手的,整体代码流程非常的清晰。但是想要深入某一块逻辑,最好结合流程图debugger方式亲自上手实践。毕竟真正搞懂一门框架的源码并非易事,我也是通过不断debugger调试,一遍遍走核心流程,才能较好的学习理解vue的实现原理~

 

  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值