Vue响应式原理 vue源码(十一)

前言

看过很多讲响应式的文章,大多都是告诉你们,有Observer,Dep,Wathcer类,Object.definePorperty,先会触发get中的dep.depend收集依赖,然后数据改变时,会触发set中的dep.notify去通知Wathcer执行它的update方法,这样响应式就完成了

这只能说是一个粗略的总结,如果我告诉你,Watcher的update方法其实只是一个调用其他函数的方法而已,它还会进行非常多的操作,其中还会涉及到异步更新的原由!这只是其中的一些小细节,Vue2.0x内部的响应式实现是包括整个observer文件夹的,其中大多细节上面都是没有提及的。

这次文章可能会非常的长,我是有分开写的打算的,但是想了又想,这种断点式分析感觉不能中断,所以我将用这一篇博客带大家理清楚Vue2.0x的响应式到底是怎么样的,包括其实现的内部细节!

Vue响应式

探索响应式之前,要先了解Object.defineProperty和发布订阅模式。Vue2.0x都是使用Object.defineProperty进行数据劫持并通过发布订阅完成的双向数据绑定(响应式)。Vue3将Object.defineProperty替换成了Proxy,这里还是只讨论Vue2.0x的响应式原理!

开始前的准备

思考

试想一下,如果自己用js实现响应式,该怎么做?

此时,有个需求:给两个变量a和b,要求b永远是a的10倍。

直接上代码,给了a,b两个变量,让b = a * 10,之后改变a的值,b并不会去改变

let a = 3
let b = a * 10
console.log(b) // 30
a = 4
console.log(b) // 30
// 当改变a时,b并没有变化,

但是,再执行一次b = a * 10后,就可以使b又是a的10倍了

let a = 3
let b = a * 10
console.log(b) // 30
a = 4
console.log(b) // 30
// 当改变a时,b并没有变化,
// 除非再执行一次 b = a * 10
b = a * 10
console.log(b) // 40

那么,将b = a * 10放进一个函数function alwaysTen中

function alwaysTen(a) {
    return a * 10
}

然后将上面代码转换一下

let a = 3
let b = a * 10
console.log(b) // 30
a = 4
b = alawaysTen(a)
console.log(b) // 40

function alwaysTen(a) {
  return a * 10
}

看,当改变a的值时,去主动调用alwaysTen这个函数,b就会永远是a的10倍了!

但还是有个问题,怎么知道a的值被改变了呢?上面的代码中是我们人为的知道它改变了,但是程序中,它的值会是经常变化的,总不可能在每一次变化后都手动执行alwaysTen函数吧。

因此,最主要的问题,就集中在怎么知道a的值改变了,并要在改变的时候执行相应的alwaysTen函数。
总结起来就是:

  1. 要能知道a的值何时改变
  2. 并且在a的值改变时调用alwaysTen函数

好,现在引入Object.defineProperty来解决这个问题

Object.defineProperty

Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性

Object.defineProperty(obj, prop, desc)
  1. obj 需要定义属性的当前对象
  2. prop 当前需要定义的属性名
  3. desc 属性描述符

属性描述符如下图

在这里插入图片描述

这里面,要注意的其实是get与set存取描述符,是由一对 getter、setter 函数功能来描述的属性

  • get:一个给属性提供getter的方法,如果没有getter则为undefined。该方法返回值被用作属性值。默认为undefined。
  • set:一个给属性提供setter的方法,如果没有setter则为undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认值为undefined。
<div id="app">
  <p>你好,<span id="name"></span></p>
</div>
let obj = {}
// 数据拦截
Object.defineProperty(obj, 'name', {
  get () {
    console.log('有人想获取name属性')
    return document.getElementById('name').innerHTML
  },
  set (newValue) {
    console.log('有人想要修改name属性')
    document.getElementById('name').innerHTML = newValue
  }
})
obj.name = 'jerry'
console.log(obj.name)

输出:
在这里插入图片描述

可以看到,当修改值时,会触发属性描述符内部的set函数,因此会打印’有人想要修改name属性’,然后读取obj.name时会触发属性描述符内部的get函数,因此会打印’有人想获取name属性’。

好!重点来了,修改值时,触发set,并可以在set函数内部添加任意操作!!

这不就成了吗?

将之前上面的代码拿下来

a = 3
b = a * 10
let temp = null
Object.defineProperty(window, 'a', {
  get() {
    console.log('有人想获取a的值')
    return temp
  },
  set(newValue) {
    console.log('有人改变了a的值')
    b = alwaysTen(newValue)
    temp = newValue
  }
})
console.log(b) // 30
a = 4
console.log(b) // 40
    
function alwaysTen(a) {
  return a * 10
}

输出:
在这里插入图片描述

好的!这不就完成了吗,在set中调用了alwaysTen函数,也就实现了在a的值被修改时,监听到这个修改,然后去执行相应的操作。

其实这个操作就叫做数据劫持/数据拦截

Vue2.0x内部就是通过Object.defineProperty来实现数据劫持的。但远远不够,Vue中还使用到了发布订阅模式,因为Vue涉及的不止是这样改变一个值的10倍这么简单了。他还要考虑编译解析渲染监听等。

所以,还需要了解什么是发布订阅模式

发布订阅模式

关于发布订阅,直接上图吧,好理解

图片4

而Vue要实现发布订阅,当然也需要三个主体:Observer,Dep,Watcher

Observer在这里就是Publisher,Dep是中间的Publish channel,而Wathcer则是Subscriber,当然现在还没说到,可以在看过后面之后再返回来品

响应式入口

在了解完Object.defineProperty和发布订阅模式后,终于可以开始深入了

现在就来揭开响应式的面纱!

首先要找到响应式是在源码中哪个文件夹里实现的

看过之前博客的应该都知道在initData中,最后一步会执行observe(data, true)将data响应化

所以这次,就通过initData这里作为入口来分析整个响应式!

// from src\core\instance\state.js initData
observe(data, true /* asRootData */)

ctrl+鼠标左键点击observe,进入到src\core\observer\index.js中

可以看到,现在已经离开了instance文件夹,来到了observer文件夹,这就是Vue2.0x响应式实现的源码所在地

响应式开始

observe

从initData中跳进来,就是这个observe函数了,先分析这个函数!

observe函数会返回一个Observer实例,可能从缓存中直接返回,或者直接new一个返回

不过在初始化时,是不会有缓存的Observer实例的,所以都会执行ob=new Observer()

// from src\core\observer\index.js
// 是否可以添加到观察者模式
export function toggleObserving (value: boolean) {
  shouldObserve = value
}

/**
 * from src\core\observer\index.js 
 *
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 * 尝试给一个value对象创建一个observer实例,
 * 如果观察成功,返回一个新的observer实例
 * 或者返回一个已经存在的observer 如果这个value对象早已拥有
 */
// observe作用就是为了拿到Observe实例并返回,从缓存中或者new一个
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 判断是否为对象 判断是否为VNode
  if (!isObject(value) || value instanceof VNode) {
    // 如果不是对象 或者 是实例化的Vnode 也就是vdom
    return
  }
  // 观察者 创建一个ob
  let ob: Observer | void
  // 检测是否有缓存ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // 直接将缓存的ob拿到
    ob = value.__ob__
  } else if (
    // 如果没有缓存的ob
    shouldObserve && // 当前状态是否能添加观察者
    !isServerRendering() && // 不是ssr
    (Array.isArray(value) || isPlainObject(value)) && // 是对象或数组
    Object.isExtensible(value) && // 是否可以在它上面添加新的属性
    !value._isVue  // 是否是Vue实例
  ) {
    // new 一个Observer实例 复制给ob
    ob = new Observer(value)
  }
  // 如果作为根data 并且当前ob已有值
  if (asRootData && ob) {
    // ++
    ob.vmCount++
  }
  // 最后返回ob,也就是一个Obesrver实例 有这个实例就有__ob__,然后其对象和数组都进行了数据劫持
  return ob
}

observe会返回一个Observer实例,那么就要来看看class Observer了

Observer constructor

/**
 * from src\core\observer\index.js class Observer
 *
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 * 附加到每个被观察者的观察者类对象。一旦链接,观察者就会转换目标对象的属性键放入g
 */
export class Observer {
  value: any;
  // Dep类
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data  将此对象

  constructor (value: any) {
    this.value = value
    // 这里会new一个Dep实例
    this.dep = new Dep()
    this.vmCount = 0
    // def添加__ob__属性,value必须是对象
    def(value, '__ob__', this)
    // 判断当前value是不是数组
    if (Array.isArray(value)) {
      // 如果是数组
      // 检测当前浏览器中有没有Array.prototype
      // 当能使用__proto__时
      // 这里完成了对数组方法的数据劫持,不使用这7个方法都不会触发响应式
      if (hasProto) {
        // 有原型时  将arrayMethods覆盖value.__proto__,也就是把增加了副作用的7个
        protoAugment(value, arrayMethods)
      } else {
        // 复制增加了副作用的7个数组方法
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 遍历将数组所有元素进行observe进行数据劫持
      this.observeArray(value)
    } else {
      // 不是数组是对象,执行这里
      // walk就是给对象的所有key进行数据劫持
      this.walk(value)
    }
  }
}

功能:

  1. 首先new一个dep实例
  2. 通过def(内部也就是Object.defineProperty)添加__ob__属性,此属性是是否已添加数据劫持的一个标记
  3. 判断传入的value是否是数组:
  4. 如果是数组,将数组7个方法进行覆盖或重写,以实现数组方法的响应式,之后会通过observeArray遍历数组进行数据劫持式
  5. 如果不是数组,是对象,使用walk方法进行数据劫持
  6. 最后数组和对象都会被数据劫持

数组响应式分析

在Vue官方文档中,给出了7种可以响应式更改数组的方法:

  1. push
  2. pop
  3. shift
  4. unshift
  5. splice
  6. sort
  7. reverse

Vue 不能检测以下数组的变动:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

但为什么是这样的呢?此处就开始分析数组的响应式方法了!

可以看到,在Observer构造函数内部,进行了数组的判断,如果是数组的话,又要进行hasProto判断,分别执行protoAugment和copyAugment,之后再执行observeArray。

// from Observer constructor

// 判断当前value是不是数组
if (Array.isArray(value)) {
  // 如果是数组
  // 检测当前浏览器中有没有Array.prototype
  // 当能使用__proto__时
  // 这里完成了数组的方法重写,不使用这7个方法都不会触发响应式
  if (hasProto) {
    // 有原型时  将arrayMethods覆盖value.__proto__,也就是把增加了副作用的7个数组方法放了进来
    protoAugment(value, arrayMethods)
  } else {
    // 复制增加了副作用的7个数组方法
    copyAugment(value, arrayMethods, arrayKeys)
  }
  // 遍历将数组所有元素进行observe数据劫持
  this.observeArray(value)
} else {
  // 不是数组是对象,执行这里
  // walk就是给对象的所有key进行数据劫持
  this.walk(value)
}

hasProto

// from src\core\util\env.js
// can we use __proto__?
export const hasProto = '__proto__' in {}

功能: 1. 判断是否能使用__proto__,返回一个布尔值,由此决定使用protoAugment或是copyAugment

arrayMethods

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */
// 导入def 也就是 Object.defineProperty
import { def } from '../util/index'
// 复制一份 Array.prototype到arrayMethods
const arrayProto = Array.prototype
// arrarMethods是Array.proto的复制
export const arrayMethods = Object.create(arrayProto)
// 获取这7个数组方法,通过def拦截这7个方法,给它们增加副作用
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
/**
 * Intercept mutating methods and emit events
 * 拦截转换方法并发出事件
 */
// 将这7个方法遍历
methodsToPatch.forEach(function (method) {
  // cache original method
  // 从原型中把原始方法拿出,在后面会调用一次原始方法,
  // 并在原始方法的上增加副作用
  const original = arrayProto[method]
  // 额外通知更新 def相当于Object.defineProperty
  // 给arrayMehods的method方法定义一个函数mutator
  // 就是在执行push pop等方法的基础上干一些额外的事
  // 也就是下面的ob.dep.notify()通知改变
  def(arrayMethods, method, function mutator (...args) {
    // 执行数组方法原本应该做的事情
    const result = original.apply(this, args)
    // 获取到这个数组的__ob__实例
    const ob = this.__ob__
    
    let inserted
    // 这三个方法特殊,因为会对数组进行增加操作,之前数组所有元素都是已经
    // 做过响应式了,所以要对新增加的元素再进行响应式处理
    // 所以要通过inserted是否有值,对新增值的三个数组方法进行再次遍历响应式
    switch (method) {
      case 'push':
      case 'unshift': 
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 如果有新增的值,也就是使用了push unshift splice三个方法
    // 调用ob.observeArray,也就是遍历将数组所有元素进行observe
    // 也就是说增加和删除元素,都还是会响应式
    if (inserted) ob.observeArray(inserted)
    // notify change
    // 通知更新
    ob.dep.notify()
    // 最后返回数组方法原本操作的结果
    return result
  })
})

protoAugment和copyAugment的调用都传入了arrayMethods这个参数,那就先来分析这个参数是什么,ctrl+左键点击arrayMethods,来到src\core\observer\array.js,其实这个文件就是对数组方法进行响应式的源码

// 复制一份 Array.prototype到arrayMethods
const arrayProto = Array.prototype
// arrarMethods可以通过原型链访问到 arrayProto也就是复制的Array.prototype
export const arrayMethods = Object.create(arrayProto)

将7个数组方法放入methodsToPatch数组

// 获取这7个数组方法,通过def拦截这7个方法,给它们增加副作用
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

拷贝一份Array.prototype给arrayMethods,可以通过其原型链访问到拷贝的Array.prototype的方法。所以不会对Array.prototype产生影响

/**
 * Intercept mutating methods and emit events
 * 拦截转换方法并发出事件
 */
// 将这7个方法遍历
methodsToPatch.forEach(function (method) {
  // cache original method
  // 从原型中把原始方法拿出,在后面会调用一次原始方法,
  // 并在原始方法的上增加副作用
  const original = arrayProto[method]
  // 额外通知更新 def相当于Object.defineProperty
  // 给arrayMehods的method方法定义一个函数mutator
  // 就是在执行push pop等方法的基础上干一些额外的事
  // 也就是下面的ob.dep.notify()通知改变
  def(arrayMethods, method, function mutator (...args) {
    // 执行数组方法原本应该做的事情
    const result = original.apply(this, args)
    // 获取到这个数组的__ob__实例
    const ob = this.__ob__
    
    let inserted
    // 这三个方法特殊,因为会对数组进行增加操作,之前数组所有元素都是已经
    // 做过响应式了,所以要对新增加的元素再进行响应式处理
    // 所以要通过inserted是否有值,对新增值的三个数组方法进行再次遍历响应式
    switch (method) {
      case 'push':
      case 'unshift': 
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 如果有新增的值,也就是使用了push unshift splice三个方法
    // 调用ob.observeArray,也就是遍历将数组所有元素进行observe
    // 也就是说增加和删除元素,都还是会响应式
    if (inserted) ob.observeArray(inserted)
    // notify change
    // 通知更新
    ob.dep.notify()
    // 最后返回数组方法原本操作的结果
    return result
  })

遍历数组

现在分析这个遍历过程

// cache original method
// 从原型中把原始方法拿出保存,在后面会调用一次原始方法,
// 并在原始方法上增加副作用
const original = arrayProto[method]

首先保存一份原始数组方法

// 执行数组方法原本应该做的事情
const result = original.apply(this, args)
// 获取到这个数组的__ob__实例
const ob = this.__ob__
    
let inserted
// 这三个方法特殊,因为会对数组进行增加操作,之前数组所有元素都是已经
// 做过响应式了,所以要对新增加的元素再进行响应式处理
// 所以要通过inserted是否有值,对新增值的三个数组方法进行再次遍历响应式
switch (method) {
  case 'push':
  case 'unshift': 
    inserted = args
    break
  case 'splice':
    inserted = args.slice(2)
    break
}
// 如果有新增的值,也就是使用了push unshift splice三个方法
// 调用ob.observeArray,也就是遍历将数组所有元素进行observe
// 也就是说增加和删除元素,都还是会响应式
if (inserted) ob.observeArray(inserted)
// notify change
// 通知更新
ob.dep.notify()
// 最后返回数组方法原本操作的结果
return result

通过def给arrayMethods的method(methodsToPatch中的7个方法)设置副作用

总结:

  • 首先执行之前保存的原始数组方法,用result保存执行结果
  • 对push和unshift和splice三个可以给数组增加元素的方法进行额外判断和操作,如果是新增值的数组方法,insert变量不为空,所以会执行ob.observeArray 对数组元素遍历进行数据劫持
  • 之后ob.dep.notify()通知更新,也就代表着这7个方法执行后,都会通知更新,也就成了响应式,方法的返回结果还是之前保存的原始方法执行的结果,所以只是单纯的添加了个响应式的副作用而已
  • 所以通过下标或长度去改变数组值是不会触发响应式的,因为vue内部根本没有这么写,只是对这7个数组方法进行响应式而已

observeArray

/**
 * Observe a list of Array items.
 */
// 遍历将数组所有元素进行observe
observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

遍历将数组所有元素进行observe

protoAugment

/**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 * 通过拦截来扩充目标对象或数组原型链使用__proto__
 */
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  // 这里直接用劫持的7个数组覆盖
  target.__proto__ = src
  /* eslint-enable no-proto */
}

当能使用原型时,调用该方法
也就是protoAugment(value, arrayMethods),value是传入的数组

value.__proto__ = arrayMethods,将更改完的数组方法覆盖给value

copyAugment


// 方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作
// 为名称的属性)组成的数组,只包括实例化的属性和方法,不包括原型上的。
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

/**
 * Augment a target Object or Array by defining
 * hidden properties.
 * 通过定义隐藏属性。
 */
/* istanbul ignore next */
// target: value数组 src arrayMethods  keys arrayKeys
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    // 给target设置key属性 内容为src[key] 也就是arrayMethods的值
    def(target, key, src[key])
  }
}

当不能使用原型时,调用该方法

copyAugment(value, arrayMethods, arrayKeys)

复制arrarMethods上的方法,包括那7个重写的数组方法

对象响应式分析

现在分析完了数组方法的响应式,现在返回到对象

else {
// 不是数组是对象,执行这里
// walk就是给对象的所有key进行响应化
this.walk(value)
}

walk

/**
 * Walk through all properties and convert them into
 * getter/setters. This method should only be called when
 * value type is Object.
 * 遍历所有属性,将其转换为getter/setters。这个方法只应该在value的类型为对象时调用
 */
// walk就是给对象的所有key进行响应化
walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    // 遍历对象的每个key,通过defineReactive进行响应化
    defineReactive(obj, keys[i])
  }
}

如果不是数组而是对象,调用walk方法

遍历对象所有key值,通过defineReactive进行数据劫持(设置getter,setter)

defineReactive

来到了一个至关重要的函数了!之前一直说通过defineReactive进行数据劫持,那么就来看看这个函数都做了些什么吧!

/**
 * Define a reactive property on an Object.
 * 在对象上定义一个响应式属性
 */
export function defineReactive (
  obj: Object,  // 对象
  key: string,  // 对象的key
  val: any, // 监听的数据
  customSetter?: ?Function, //日志函数
  shallow?: boolean // 是否要添加__ob__属性
) {
  // 实例化一个Dep对象, 其中有空的观察者列表
  const dep = new Dep()
  
  // 获取obj的key的描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 检测key中是否有描述符 如果是不可配置 直接返回
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 满足预定义的getter/setters
  // 获取key中的get
  const getter = property && property.get
  // 获取key中的set
  const setter = property && property.set
  // 如果getter不存在或setter存在 并且参数长度为2
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 递归响应式处理 给每一层属性附加一个Obeserver实例
  // shallow不存在时代表没有__ob__属性 将val进行observe返回一个ob实例赋值给childO
  // 如果是对象继续调用 observe(val) 函数观测该对象从而深度观测数据对象
  // walk 函数中调用 defineReactive 函数时没有传递 shallow 参数,所以该参数是 und
  // 默认就是深度观测
  let childOb = !shallow && observe(val)
  // 数据拦截
  // 通过Object.defineProperty对obj的key进行数据拦截
  Object.defineProperty(obj, key, {
    // 枚举描述符
    enumerable: true,
    // 描述符
    configurable: true,
    get: function reactiveGetter () {
      // 获取值
      const value = getter ? getter.call(obj) : val
      // 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
      if (Dep.target) {
        // 加入到dep去管理watcher 
        dep.depend()
        // 如果存在子对象
        if (childOb) {
          // 也加进去管理
          childOb.dep.depend()
          // 如果值是数组,要特殊处理
          if (Array.isArray(value)) {
            // 循环添加watcher
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 获取value值 触发依赖收集
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        // 新旧值比较 如果是一样则不执行了
        return
      }
      /* eslint-enable no-self-compare 不是生产环境的情况下*/
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      // 对于没有setter的访问器属性 返回
      if (getter && !setter) return
      // 如果setter存在
      if (setter) {
        // 设置新值
        setter.call(obj, newVal)
      } else {
        // 如果没有setter ,直接给新值
        val = newVal
      }
      // 递归,对新来的值 对新值进行observe 返回ob实例
      childOb = !shallow && observe(newVal)
      // 当set时触发通知
      dep.notify()
    }
  })
}
// 实例化一个Dep对象, 其中有空的观察者列表
// 这个dep常量所引用的Dep实例对象其实就是一个闭包,可以引用着属于自己的dep常量
// 每次调用defineReactive定义访问器属性时,该属性的 setter/getter 都闭包引用了一个属于自己的dep常量
const dep = new Dep()

首先,会实例化一个Dep对象,这个dep其实是一个闭包引用,保持对其内部subs观察者数组的引用

// 获取obj的key的描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
// 检测key中是否有描述符 如果是不可配置 直接返回
if (property && property.configurable === false) {
  return
}

获取obj的key的属性描述符,并判断其configurable属性,如果是不可配置的,直接返回

// 保存了来自 property 对象的 get 和 set 
// 避免原有的 set 和 get 方法被覆盖
const getter = property && property.get
// 获取key中的set
const setter = property && property.set

保存来自property的get和set方法,以免被覆盖

深度观察属性
// 递归响应式处理 给每一层属性附加一个Obeserver实例
// shallow不存在时代表没有__ob__属性 将val进行observe返回一个ob实例赋值给childOb
// 如果是对象继续调用 observe(val) 函数观测该对象从而深度观测数据对象
// walk 函数中调用 defineReactive 函数时没有传递 shallow 参数,所以该参数是undefined
// 所以默认就是深度观测
let childOb = !shallow && observe(val)

深度观察属性,递归响应式处理,给每一层属性附加一个Obeserver实例

data: {
    a: {
        b : {
            c: 1
        }
    }
}

深度观察属性是什么意思呢?举个栗子:

data: {
  // 属性 a 通过 setter/getter 通过闭包引用着 dep 和 childOb
  a: {
    // 属性 b 通过 setter/getter 通过闭包引用着 dep 和 childOb
    b: {
        c: 1
        __ob__: {b, dep, vmCount}
    }
    __ob__: {a, dep, vmCount}
  }
  __ob__: {data, dep, vmCount}
}

当遇到这样嵌套属性的data时,深度观察会去递归每一层,给每一层属性都添加一个Observer实例__ob__,并且也会实例化一个属于那一层的dep实例

这样就可以实现,无论多复杂的obj对象,都可以访问其所有属性,即使它嵌套的很深。

接下来就要通过设置getter和setter实现数据劫持了!

// 数据拦截
// 通过Object.defineProperty对obj的key进行数据拦截
Object.defineProperty(obj, key, {
  // 枚举描述符
  enumerable: true,
  // 描述符
  configurable: true,
  get: function reactiveGetter () {
    // 获取值
    const value = getter ? getter.call(obj) : val
    // 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
    if (Dep.target) {
      // 加入到dep去管理watcher 
      dep.depend()
      // 如果存在子对象
      if (childOb) {
        // 也加进去管理
        childOb.dep.depend()
        // 如果值是数组,要特殊处理
        if (Array.isArray(value)) {
          // 循环添加watcher
          dependArray(value)
        }
      }
    }
    return value
  },
  set: function reactiveSetter (newVal) {
    // 获取value值 触发依赖收集
    const value = getter ? getter.call(obj) : val
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
      // 新旧值比较 如果是一样则不执行了
      return
    }
    /* eslint-enable no-self-compare 不是生产环境的情况下*/
    if (process.env.NODE_ENV !== 'production' && customSetter) {
      customSetter()
    }
    // #7981: for accessor properties without setter
    // 对于没有setter的访问器属性 返回
    if (getter && !setter) return
    // 如果setter存在
    if (setter) {
      // 设置新值
      setter.call(obj, newVal)
    } else {
      // 如果没有setter ,直接给新值
      val = newVal
    }
    // 递归,对新来的值 对新值进行observe 返回ob实例
    childOb = !shallow && observe(newVal)
    // 当set时触发通知
    dep.notify()
  }
})

可以看到,defineReactive内部使用了Object.defineProperty对所有的key进行设置了getter和setter,就是在此时完成了数据劫持,并添加相应的副作用。

接下来看看getter内部添加了什么副作用呢?

getter

get: function reactiveGetter () {
  // 获取值
  const value = getter ? getter.call(obj) : val
  // 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
  if (Dep.target) {
    // 加入到dep去管理watcher 
    dep.depend()
    // 如果存在子对象
    if (childOb) {
      // 也加进去管理
      childOb.dep.depend()
      // 如果值是数组,要特殊处理
      if (Array.isArray(value)) {
        // 循环添加watcher
        dependArray(value)
      }
    }
  }
  return value
},

首先保存原始get返回的value值,因为只添加副作用,不改变值的获取

// 获取值
const value = getter ? getter.call(obj) : val

判断Dep.target,存在就执行dep.depend,再判断childOb,存在就执行childOb.dep.depend(),再判断是否是数组,是数组则执行dependArray

// 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
if (Dep.target) {
  // 加入到dep去管理watcher 
  dep.depend()
  // 如果存在子对象
  if (childOb) {
    // 也加进去管理
    childOb.dep.depend()
    // 如果值是数组,要特殊处理
    if (Array.isArray(value)) {
      // 循环添加watcher
      dependArray(value)
    }
  }

所以Dep.target,dep.depend和dependArray分别是什么呢?

现在来到src\core\observer\dep.js

Dep.target
Dep.target = null
const targetStack = []
// 压栈
export function pushTarget (target: ?Watcher) {
  // 压栈
  targetStack.push(target)
  // target就是watcher dep是Dep对象
  Dep.target = target
}

export function popTarget () {
  // 出栈
  targetStack.pop()
  // 成为最后一个元素
  Dep.target = targetStack[targetStack.length - 1]
}

在这里面找到Dep.target,可以看到此变量为全局变量,并且结合pushTarget的参数target:? Wathcer,所以Dep.target就是个Watcher实例

dep.depend
// 添加watcher 
// 为Watcher.newDeps.push(dep) 一个dep对象
depend () {
  // target就是Watcher dep就是dep对象,dep中是否有watcher对象
  if (Dep.target) {
    // 用当前的watcher调用addDep
    // 为了多对多关系,得分析addDep
    Dep.target.addDep(this)
  }
}

dep.depend方法,判断Dep.target然后执行Dep.target.addDep。这里的Dep.target就是一个Watcher实例,其addDep方法,先留在这,之后依赖收集时一起收了它。所以dep.depend方法其实就是完成依赖收集

dependArray
/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 * 在接触数组时收集对数组元素的依赖关系,因为我们不能像属性getter那样拦截数组元素访问。
 */
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    // 判断是否存在__ob__实例,并且每个都调用depend添加wathcer管理
    e && e.__ob__ && e.__ob__.dep.depend()
    // 递归完数组所有内容,直到不是数组,跳出递归
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

接着回到src\core\observer\index.js,找到dependArray函数,其实就是遍历数组,对数组每个元素触发dep.depend收集依赖,因为不能直接对数组进行收集依赖。

分析完这三个方法,就可以返回去分析get内部的操作了

// 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
if (Dep.target) {
  // 加入到dep去管理watcher 
  dep.depend()
  // 如果存在子对象
  if (childOb) {
    // 也加进去管理
    childOb.dep.depend()
    // 如果值是数组,要特殊处理
    if (Array.isArray(value)) {
      // 循环添加watcher
      dependArray(value)
    }
  }
}

当Dep.target也就是Watcher实例存在时,执行dep.depend收集依赖,接着判断是否childOb是否存在,如果存在就执行childOb.dep.depend

额外为Vue.set和Vue.delete收集的依赖

而这个childOb.dep.depend就是我们需要分析的,首先要知道childOb是啥,之前分析过,当data被observe后,如下

data: {
    a: {
        b : {
            c: 1
        }
    }
}
// 经过observe处理后,会添加__ob__属性
data: {
  // 属性 a 通过 setter/getter 通过闭包引用着dep和childOb也就是内部的__ob__
  a: {
    // 属性 b 通过 setter/getter 通过闭包引用着dep和childOb也就是内部的__ob__
    b: {
        c: 1
        __ob__: {b, dep, vmCount}
    }
    __ob__: {a, dep, vmCount}
  }
  __ob__: {data, dep, vmCount}
}

对于属性a来说,其通过getter/setter保持了对dep.subs观察者数组队列的引用,此数组用来收集依赖

另外属性a的setter/getter 还通过闭包引用着childOb,且childOb === data.a.ob 所以 childOb.dep === data.a.ob.dep。

也就是说 childOb.dep.depend() 这句话的执行说明除了要将依赖收集到属性a的dep.subs中,还要将同样的依赖收集到 data.a.ob.dep.subs中

为什么要将同样的依赖分别收集到这两个不同的subs数组中呢?其实答案就在于这两个数组收集的依赖的触发时机是不同的,即作用不同

两个数组如下:

  1. 第一个数组是dep.subs
  2. 第二个数组是childOb.subs 也就是data.a.ob.subs

第一个dep.subs中的收集的依赖是在值被修改时触发的,通过set方法内部的dep.notify

第二个childOb.subs中收集的依赖则是在Vue.set给数据对象添加新属性时触发

好,这里肯定都会发出疑问:怎么突然就是给Vue.set时触发了?

首先我们知道,在Vue2.0x中,使用的Object.definePorperty进行响应式,initData只会对事先写在data中的属性进行响应化

如果后面再给data添加属性,vue2是不能将新添加的属性进行响应化的(vue3可以,proxy)

所以vue提供了一个Vue.set方法,向响应式对象中添加一个property,并确保这个新property触发依赖收集,触发视图更新。

那么这个触发依赖收集怎么做到的呢?直接看看set方法吧!

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 * 给对象设置一个属性,添加新属性和添加触发更改通知(dep.notify),如果这个属性不是早已存在
 * Vue.set
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    // 判断数据 是否是undefined或者null
    // 判断数据类型是否是string,number,symbol,boolean
    (isUndef(target) || isPrimitive(target))
  ) {
    // target必须是对象或者数组,否则发出警告
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 如果是数组 并且检查key是否是有效的数组索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 设置数组长度
    target.length = Math.max(target.length, key)
    // 像数组尾部添加一个新数据,相当于push
    target.splice(key, 1, val)
    // 返回val
    return val
  }
  // 如果key在target上 并且不是通过原型链查找的 
  if (key in target && !(key in Object.prototype)) {
    // 赋值
    target[key] = val
    return val
  }
  // 声明一个对象ob 值为该target对象中的原型上面的所有方法和属性,表明该数据加入过观察者中
  const ob = (target: any).__ob__
  // 如果是vue 或者  检测vue被实例化的次数 vmCount
  if (target._isVue || (ob && ob.vmCount)) {
    // 如果不是生产环境,发出警告 
    // 避免添加响应式属性给vue实例或者根$data
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 如果ob不存在,证明没有添加观察者,不是相应,直接赋值返回
  if (!ob) {
    target[key] = val
    return val
  }
  // 通过defineReactive将ob.value加入的观察者
  defineReactive(ob.value, key, val)
  // 触发通知更新,通知订阅者obj.value更新数据
  ob.dep.notify()
  return val
}

为了容易理解,把set简化一下,并且直接使用Vue.set给一个对象添加属性

Vue.set = function set(target, key, val) {
    // 声明一个对象ob 值为该target对象中的原型上面的所有方法和属性,表明该数据加入过观察者中
    const ob = target.__ob__
    // 通过defineReactive将ob.value加入的观察者
    defineReactive(ob.value, key, val)
    // 触发通知更新,通知订阅者obj.value更新数据
    ob.dep.notify()
    return val
}

Vue.set(data.a, 'lu', 1)

此时ob.dep.notify()其实是data.a.__ob__.dep.notify

而第二个数组是data.a.ob.sub,这是不是就是触发了第二个数组的收集的依赖?

所以ob属性以及ob.dep的主要作用是为了添加、删除属性时有能力触发依赖,而这就是Vue.set和Vue.delete的原理

执行完这些后,来到判断数组,执行dependArray,对数组每个元素进行收集依赖,因为数组不能直接进行收集。

// 如果值是数组,要特殊处理
if (Array.isArray(value)) {
  // 循环添加watcher
  dependArray(value)
}

好了,getter内部添加的副作用分析完毕了,总结如下

  • 如果Dep.target存在,收集依赖
  • 如果有childOb,给childOb也收集相同依赖用于Vue.set和Vue.del
  • 如果是数组,执行dependArray,遍历数组元素收集依赖

所以这里分析的依赖收集都是要建立在触发了getter函数并且Dep.target存在的情况下!

以上已经差不多将Observer和Dep的关系讲解的很透彻了,每一个经过observe处理的属性都对应一个dep实例,dep内有一个收集依赖(也就是添加观察者watcher)的数组队列subs。

那么Watcher是怎么被添加的呢,也可以说依赖是怎么被收集的呢?

收集依赖是怎么触发的?

怎么收集依赖?上文说到的收集依赖其实就是将Watcher观察者实例加入到对应的dep.subs观察者数组中,所以就来看看Watcher

先忽略掉构造函数内部一大段代码,这涉及到一些属性,之后会带着updateComponent来讲,现在只需要注意最后一段代码

// from src\core\observer\watcher.js class Watcher
constructor (
  vm: Component,  // dom
  expOrFn: string | Function, //获取值的函数,或是更新视图的函数
  cb: Function, //回调函数
  options?: ?Object, //参数
  isRenderWatcher?: boolean //是否是渲染过的watcher
) {
  // 上面先忽略
  // 
  this.value = this.lazy
    ? undefined // 当lazy为真时
    : this.get() // lazy不在时 计算getter,并重新收集依赖项。
}

这里通过this.lazy来判断,不过提一句,通常lazy都是不存在的(后面会说),所以都会执行this.get()方法

watcher.get

/**
 * Evaluate the getter, and re-collect dependencies.
 * 计算getter,并重新收集依赖项。
 */
get () {
  // 添加dep.target dep.target = this
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 这时dep.target = this ,然后执行this.getter.call也就触发get方法,判断dep.target是否存在,存在则dep.depend()
    // 获取值 触发get 也就触发Object.definePorperty的get中的dep.depend(),依赖收集
    // 每个watcher第一次实例化的时候,都会作为订阅者订阅其相应的Dep。
    value = this.getter.call(vm, vm)
  } catch (e) {
    // 如果报错
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    // “触摸”每个属性,以便它们都被跟踪为深度监视的依赖项
    if (this.deep) {
      // 为 seenObjects 深度收集val 中的key
      traverse(value)
    }
    // 出栈一个dep.target
    popTarget()
    // 清理依赖项集合
    this.cleanupDeps()
  }
  // 返回值
  return value
}

pushTarget

好,get方法一进来,就执行了pushTarget,然后执行value = this.getter.call(vm, vm)

export function pushTarget (target: ?Watcher) {
  // 压栈
  targetStack.push(target)
  // target就是watcher dep是Dep对象
  Dep.target = target
}

pushTarget的作用其实就是将Dep.target赋值,而pushTarget(this)里的this指向当前实例化的Watcher对象,所以执行了pushTarget后,Dep.target此时保存的就是当前实例化的Wathcer对象,也就是要被收集的依赖。

watcher.getter(updateComponent)

之后继续执行this.getter

value = this.getter.call(vm, vm)

此时的getter其实是UpdateComponent,其内部会调用_update和_render函数,先不去考虑这些方法,只要记住这个函数的执行就意味着对被观察目标的求值,并将得到的值赋值给 value 变量,最后返回了value

而Watcher实例对象的value属性会保存这个返回值,也就是说Watcher实例对象保存着被观察的值

this.value = this.lazy
  ? undefined // 当lazy为真时
  : this.get() // lazy不在时 计算getter,并重新收集依赖项。

触发属性的getter

当对观察目标进行求值的时候,肯定会触发数据的getter函数

get: function reactiveGetter () {
  // 获取值
  const value = getter ? getter.call(obj) : val
  // 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
  if (Dep.target) {
    // 加入到dep去管理watcher 
    dep.depend()
    // 如果存在子对象
    if (childOb) {
      // 也加进去管理
      childOb.dep.depend()
      // 如果值是数组,要特殊处理
      if (Array.isArray(value)) {
        // 循环添加watcher
        dependArray(value)
      }
    }
  }
  return value
},

进入get后,判断Dep.target是否存在,此时Dep.target是存在的,因为我们执行了this.get方法内部的pushTarget,此时Dep.target就是一个Watcher实例,因此会执行dep.depend()

// 添加watcher 
// 为Watcher.newDeps.push(dep) 一个dep对象
depend () {
  // target就是Watcher dep就是dep对象,dep中是否有watcher对象
  if (Dep.target) {
    // 用当前的watcher调用addDep
    // :todo 为了多对多关系,得分析addDep
    Dep.target.addDep(this)
  }
}

watcher.addDep(避免重复收集依赖)

dep.depend一来也判断了Dep.target,同样的,也是存在的,所以执行Dep.target.addDep(this),其实就是执行Watcher.addDep(this),此时的this是dep这个实例对象因此来看addDep

/**
 * Add a dependency to this directive.
 * 向该指令添加依赖项
 */
addDep (dep: Dep) {
  // dep.id 陆续自+
  const id = dep.id 
  // 如果id不存在
  if (!this.newDepIds.has(id)) {
    // :todo
    // 你保存我的引用
    // 我也要保存你的引用
    // newDepIds添加一个id
    this.newDepIds.add(id)
    // newDeps添加一个dep
    this.newDeps.push(dep)
    // 如果depIds中id不存在
    if (!this.depIds.has(id)) {
      // 给subs数组添加一个Watcher对象
      dep.addSub(this)
    }
  }
}

addDep接收一个dep实例,也就是接收到了属性通过闭包保持引用dep实例对象

首先保存dep实例的唯一id

接下来这波操作啊,很复杂,是当遇到重复的依赖时,只会收集一个依赖

如果不进行避免重复收集依赖操作

当然,先给大家看看为什么要进行这波避免重复收集依赖的操作吧

假设不进行避免重复收集依赖的操作,也就是直接执行addSub

addDep (dep: Dep) {
    dep.addSub(this)
}

而addSub其实也只是将Watcher实例添加进了subs数组

addSub (sub: Watcher) {
  // 给subs数组添加一个Watcher对象
  this.subs.push(sub)
}

现在给出这样式的模板

<div id="app">
    <p>{{name}}</p>
    <p>{{name}} + {{age}}</p>
</div>

这里,渲染函数会将其解析为如下样式(调试可得)

with (this) {
    return _c('div',{attrs:{"id":"app"}},
        [
            _c('p',[_v(_s(name))]),
            _v(" "),
            _c('p',[_v(_s(name)+" + "+_s(age))])
        ]
    )
  }

可以看到,渲染函数会读取两次name进行求值,也就相应的触发了两次getter,接着触发两次addDep和两次addSub

因此这样会出现同一个Watcher实例被收集多次的问题,所以会在addDep中进行避免重复收集依赖操作

如何避免重复收集依赖

addDep (dep: Dep) {
  const id = dep.id 
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

首先判断newDepIds中是否有相同的dep实例的id,如果有,代表存在重复的,就不会进行下面操作,如果没有,就将id添加进newDepIds数组中,并且把对应的dep实例添加进newDeps数组中。

所以此时无论一个属性重复出现多少次,都只会收集一个Wathcer实例(依赖),并且当其改变时,会通知所有相同的属性都改变(这里涉及到通知更新)

这样已经完成了避免重复收集依赖操作,但是代码还没完

一次求值和多次求值避免重复收集依赖

既然已经完成了避免处理,那这段代码又是做了什么呢

if (!this.depIds.has(id)) {
    dep.addSub(this)
}

好,带着这个问题,继续回去分析watch的get方法

// from class watch 
// get方法
finally {
  // "touch" every property so they are all tracked as
  // dependencies for deep watching
  // “触摸”每个属性,以便它们都被跟踪为深度监视的依赖项
  if (this.deep) {
    // 为 seenObjects 深度收集val 中的key
    traverse(value)
  }
  // 出栈一个dep.target
  popTarget()
  // 清理依赖项集合
  this.cleanupDeps()
}

在get的finally中,会执行cleanupDeps

cleanupDeps

来分析cleanupDeps

/**
 * Clean up for dependency collection.
 * 清理依赖项集合。
 */
cleanupDeps () {
  // 获取deps长度
  let i = this.deps.length
  // 遍历
  while (i--) {
    const dep = this.deps[i]
    // 如果在newDepIds中不存在dep的id
    if (!this.newDepIds.has(dep.id)) {
      // 清楚依赖项
      dep.removeSub(this)
    }
  }
  // 互换
  let tmp = this.depIds 
  this.depIds = this.newDepIds 
  this.newDepIds = tmp 
  // 换完后
  this.newDepIds.clear() // 清空对象
  // 互换
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  // 换完后
  this.newDeps.length = 0 // 清空数组
}

while内部进行移除废弃的观察者,得先看后面的关键再来理解这个while

看,这里最后面,两个经典的互换和清空,将newDepIds内容全部给了depIds,并清空newDepIds,将newDeps内容全部给了deps,并清空newDeps数组。

这也就代表着,每一次执行watcher.get方法时,最后都会将newDepids和newDeps用depIds和deps保存,并且清除掉newDepids和newDeps。

所以上一次求值的id和deps都会保存在depIds和deps中。

那么回到addDep中

addDep (dep: Dep) {
  const id = dep.id 
  // 如果id在newDepids中不存在,直接不做任何操作
  if (!this.newDepIds.has(id)) {
    // newDepIds添加一个id
    this.newDepIds.add(id)
    // newDeps添加一个dep
    this.newDeps.push(dep)
    // 如果depIds中id不存在,也就是总共的depIds中没有这个id时,才添加
    if (!this.depIds.has(id)) {
      // 给subs数组添加一个Watcher对象
      dep.addSub(this)
    }
  }
}

首先,判断newDepids中是否有相同id,如果没有,就给newDepIds和newDeps分别加入id和dep,然后判断这个id是否在总共的depIds中存在,如果不存在,才会进行addSub添加

一次求值和多次求值避免重复收集依赖总结

所以可以总结出:

  1. newDepIds是用来避免在一次求值中收集重复的依赖,因为每次执行完后,都会将其赋给depIds后清空
  2. depIds是用来避免在多次求值中收集重复的依赖
移除废弃依赖

这里再回去分析内部的while移除废弃观察者

// from cleanupDeps
// 获取deps长度
let i = this.deps.length
// 遍历
while (i--) {
    const dep = this.deps[i]
    // 如果在newDepIds中不存在dep的id
    if (!this.newDepIds.has(dep.id)) {
      // 清楚依赖项
      dep.removeSub(this)
    }
}

可以看到,while遍历的其实是上一次保存的deps,也就是上一次收集到的依赖数组,然后进行判断,上一次收集的依赖是否会在这一次收集的依赖中,如果不存在,代表这个依赖已经被废弃,所以调用dep.removeSub进行删除

removeSub (sub: Watcher) {
  // 删除watcher对象
  remove(this.subs, sub)
}

removeSub就是将Watcher观察者从当前subs数组中删除

收集依赖总结

到这里,依赖收集已经分析完毕了

总结一下:

  1. 首先会执行watcher.get方法,触发pustTarget
  2. pustTarget给Dep.target赋值当前Wathcer实例然后调用this.getter
  3. this.getter(updateComponent内部有_update和_render函数),render函数会对属性求值,也就触发了属性的get操作
  4. 属性的get操作会判断Dep.target是否存在
  5. 此时Dep.target是存在的,然后执行dep.depend
  6. dep.depend执行Wathcer.addDep
  7. Wathcer.addDep内部进行了避免重复收集依赖的操作,并且收集依赖
  8. 执行完addDep后,此次依赖收集就完成了

但是我们分析的依赖收集都是要建立在new Wathcer时,因为只有new Watcher时才会执行构造函数内部的get方法,才会进行那些依赖收集,那么在哪里new了Watcher呢?

跟着mountComponet来分析Watcher

通过在文件夹中寻找new Watcher

在这里插入图片描述

可以看到src\core\instance\lifecycle.js和src\core\instance\state.js中都有new Watcher

不过经过调试可得知:state.js中的watcher只有在有computed属性时才会执行到。

而lifecycle.js中new Watcher是在mountComponent中的,这是必定会发生的,因为无论是手动挂载或者是vue自动挂载,都会执行到mountComponent

因此我们通过这个mountComponet内部的new Wathcer来分析Watcher

// mountComponent :安装组件
export function mountComponent(
  vm: Component, //vnode
  el: ? Element, //dom
  hydrating ? : boolean //ssr相关
): Component {
  // 忽略
  
  // 执行生命周期 beforeMount 钩子函数
  callHook(vm, 'beforeMount')
  // 更新组件 
  let updateComponent
  /* istanbul ignore if */
  // 忽略 如果开发环境
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // 忽略 看性能用的
    }
  } else {
    // updateComponet函数 直接更新view视图
    updateComponent = () => {
      
      vm._update(
        /*
          render 是  虚拟dom,需要执行的编译函数 类似于这样的函数
          (function anonymous( ) {
          with(this){return _c('div',{attrs:{"id":"app"}},[_c('input',{directives:[{name:"info",rawName:"v-info"},{name:"data",rawName:"v-data"}],attrs:{"type":"text"}}),_v(" "),_m(0)])}
          })
        */
        vm._render(), 
        // ssr相关
        hydrating
        )
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  // 我们在观察者的构造函数中设置vm._watcher
  // 因为观察者的初始patch可能调用$foreceUpdate(例如 inside child 组件的挂载钩子)
  // 它依赖于已经定义的vm._watcher
  new Watcher(
    vm, //vnode
    updateComponent, //上面的更新视图函数
    noop, //回调函数
    {
      before() {
        // 如果已经挂载并且没有被销毁
        if (vm._isMounted && !vm._isDestroyed) {
          // 触发生命周期 beforeUpdate 钩子函数
          callHook(vm, 'beforeUpdate')
        }
      }
    }, 
    true /* isRenderWatcher */ )
    
  //忽略
}

我已经将mountComponent简化了,其他部分都忽略掉,只关注其updateComponent(作为new Wathcer时的第二个参数)和new Watcher

upadateComponent

所以先看updateComponent,if中的updateComponent可以省略,所以直接看else里

// updateComponet函数 直接更新view视图
updateComponent = () => {
  vm._update(
    /*
      render 是  虚拟dom,需要执行的编译函数 类似于这样的函数
      (function anonymous( ) {
      with(this){return _c('div',{attrs:{"id":"app"}},[_c('input',{directives:[{name:"info",rawName:"v-info"},{name:"data",rawName:"v-data"}],attrs:{"type":"text"}}),_v(" "),_m(0)])}
      })
    */
    vm._render(), 
    // ssr相关
    hydrating
    )
}

updateComponent内部_update函数,_update函数将vue.prototype._render函数的返回值作为参数

upadateComponent内的_render和_update

因为这里涉及到render,所以直接给出其功能:

  • _render函数返回一个虚拟DOM,其经过compiler的parse,optimize,generate之后返回一个虚拟DOM
  • _update函数就是将这个虚拟DOM通过patch转换成真实DOM

所以updateComponent的功能就是将虚拟DOM转为真实DOM

new Watcher传参分析

然后结合Watcher构造函数的参数来分析new Watcher的传参

new Watcher(
  vm, //vnode
  updateComponent, //上面的更新视图函数
  noop, //回调函数
  {
    before() {
      // 如果已经挂载并且没有被销毁
      if (vm._isMounted && !vm._isDestroyed) {
        // 触发生命周期 beforeUpdate 钩子函数
        callHook(vm, 'beforeUpdate')
      }
    }
  }, 
  true /* isRenderWatcher */ )

// 结合watcher constructor 来自watcher.js class Watcher
constructor (
    vm: Component,  // dom
    expOrFn: string | Function, //获取值的函数,或是更新视图的函数
    cb: Function, //回调函数
    options?: ?Object, //参数
    isRenderWatcher?: boolean //是否是渲染过的watcher
  ) 

  1. 第一个参数:vm 传入vm 也就是当前vm实例
  2. 第二个参数:expOrFN 传入updateComponent函数
  3. 第三个参数:cb 传入noop 空函数
  4. 第四个参数:options 传入一个包含before函数的对象
  5. 第五个参数: isRenderWatcher 传入true

new Watcher传参结合Watcher constructor分析

传完参数之后,就带着这些参数来分析Watcher constructor吧

watcher.vm

// 获取到vm
this.vm = vm

获取组件实例,vm属性代表观察者所属哪个组件

watcher._watcher

// 如果是渲染函数的watcher 
if (isRenderWatcher) {
  // 把当前Watcher对象给_wathcer
  vm._watcher = this
}

如果是渲染函数的watcher,把Wathcer实例赋给当前组件实例的_watcher属性

// 把观察者添加到_watchers数组中
vm._watchers.push(this)

将观察者添加到组件实例的_watchers数组中,也就是说所有这个组件中的观察者都会加入到这个数组中

watcher属性(deep,user,lazy,sync,before)声明

// 如果有options
if (options) {
  // 获取参数
  this.deep = !!options.deep // 是否深度观察
  this.user = !!options.user // 
  this.lazy = !!options.lazy // 是否懒惰观察,也就是不观察
  this.sync = !!options.sync // 是否同步求值
  this.before = options.before // before 算是回调钩子函数 组件更新前触发
} else {
  // 否则都为false
  this.deep = this.user = this.lazy = this.sync = false
}

给vm设置deep,user,lazy,sync属性,因为传入的options是一个只包含before函数的对象,所以这四个属性都为false,before则为before函数

watcher属性(cb,active,dirty)

this.cb = cb // 回调函数
this.active = true // 激活 涉及到后面组件更新
this.dirty = this.lazy // for lazy watchers  用于懒惰的观察者

给组件实例添加cb属性为传入的noop空函数,添加active属性为true,添加dirty属性

watcher属性(id,deps,newDeps,depIds,newDepIds,避免重复收集依赖)

this.id = ++uid // uid for batching uid用于批处理
this.deps = [] // 观察者队列
this.newDeps = [] // 同样是观察者队列  但每一次求值最后都会赋给deps,并清空数组
this.depIds = new Set() // depId 不可重复
this.newDepIds = new Set() // 每一次求值最后都会赋给depIds 并clear set 不可重复

这里就是之前提到的避免重复收集依赖的数组和set了

newDeps和newDepIds在每一次求值后都会执行cleanupDeps,赋值给deps和depIds,并清空数组和clear set

watcher.getter(此时是updateComponent)

if (typeof expOrFn === 'function') {
  // 获取值函数
  this.getter = expOrFn
} else {
  // 调用parsePath返回值 返回一个遍历所有属性的函数,也是触发get
  this.getter = parsePath(expOrFn) //updateComponent
  if (!this.getter) {
    // 如果不存在
    // 给一个noop空函数
    this.getter = noop
    process.env.NODE_ENV !== 'production' && warn(
      `Failed watching path: "${expOrFn}" ` +
      'Watcher only accepts simple dot-delimited paths. ' +
      'For full control, use a function instead.',
      vm
    )
  }
}

expOrFn就是传入的updateComponent函数

首先进行判断,如果是函数,就直接把updateComponent赋给this.getter,这也印证了上面我说的this.getter就是updateComponent

如果不是函数,将调用parsePath,这个函数返回一个遍历路径分割成数组后每个元素的函数,也相当于触发get操作

如果getter不是函数,并且getter不存在,给getter一个noop空函数

/**
 * Parse simple path.
 * 解析简单路径
 */
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
  // 匹配不是 数字字母下划线 $符号   开头的为true
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      //将对象中的一个key值 赋值给该对象 相当于 obj = obj[segments[segments.length-1]];
      obj = obj[segments[i]]
    }
    return obj
  }
}

首先判断,如果是数字字母下划线$符号开头的,直接返回

然后将路径通过.分割成数组,返回一个遍历这些数组元素的函数,也就触发了get操作,这个函数主要用于data.xiaolu.name这种的调用

执行watcher.get方法

// 
this.value = this.lazy
  ? undefined // 当lazy为真时
  : this.get() // lazy不在时 计算getter,并重新收集依赖项。

最后,判断lazy属性,可以从前面看出,lazy为false,所以会执行this.get方法,并将返回值赋给value属性

跟随着mountComponent传参分析watcher总结

好,调用get方法后,又回到了之前的收集依赖的开始过程,进行依赖收集了!
这样跟随着mountComponent传入的参数,我们把Watcher也分析的差不多了!

触发通知更新

现在就要开始分析当改变值的时候触发的set操作了

首先要知道,触发通知更新的前提是依赖收集完毕

<div id="data">
    {{name}}
</div>

现在,给一个这样的模板,按照之前的说法

在mountComponent中,首先会new Watcher实例,因此触发watcher.get方法,相应执行updateComponent,也就是执行render函数,而我们知道template模板会通过compiler编译成render函数,而render函数会对这个name属性进行求值,因此会触发name属性的getter,从而收集依赖。此时依赖已经收集完成

setter

这样当我们修改name属性时,也就会触发name属性的setter,这时候就会调用dep.notify去通知更新了

set: function reactiveSetter (newVal) {
  // 获取旧value值
  const value = getter ? getter.call(obj) : val
  /* eslint-disable no-self-compare */
  if (newVal === value || (newVal !== newVal && value !== value)) {
    // 新旧值比较 如果是一样则不执行了
    return
  }
  // 忽略
  // 当set时触发通知
  dep.notify()
}

新旧值对比

首先会通过getter获取一波旧值,然后比较新旧值,如果一样,就直接返回了

// 获取value值
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
  // 新旧值比较 如果是一样则不执行了
  return
}

dep.notify

然后进行一些相应的更改值的操作,这里忽略掉,直接看dep.notify

// 通知所有watcher对象更新视图,也就是执行update
notify () {
  // stabilize the subscriber list first
  // 浅拷贝一份subs数组,也就是Watchers列表
  const subs = this.subs.slice()
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    // subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    // 如果不运行async,则不会在调度程序中对sub进行排序
    // 我们现在需要对它们进行分类以确保它们发射正确秩序
    subs.sort((a, b) => a.id - b.id)
  }
  // 所有subs中的wathcers执行update函数,也就是更新
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

浅拷贝dep.subs观察者数组

dep.notify()调用,因此this指向dep实例,所以刚开始,浅拷贝一份这个dep实例的观察者数组subs

const subs = this.subs.slice()

if语句块中的是处理同步调用的,现在不需要看

遍历执行wathcer.update

直接看最后的for循环,将浅拷贝的观察者数组subs进行遍历,取到里面的观察者Watcher实例,并让其执行update方法

// subs中的所有wathcers执行update函数,也就是更新
for (let i = 0, l = subs.length; i < l; i++) {
  subs[i].update()
}

update

因此,我们又要跳到wathcer的update方法来分析了

update () {
  /* istanbul ignore else */
  // 如果是懒惰的lazy
  if (this.lazy) {
    // 
    this.dirty = true
  } else if (this.sync) { //如果是同步
    // 
    this.run()
  } else {
    // 异步队列
    // 数据并不会立即更新,而是异步,批量排队执行
    queueWatcher(this)
  }
}

首先判断lazy属性,这是懒惰的观察者,关于计算属性的,在之前传参里我们可以知道这个lazy为false,因此不执行

再判断sync属性,此时sync属性也是false,因此跳到下面的else(这几个属性的声明,我留了目录)

执行queueWatcher方法

这时候执行queueWathcer,内部涉及太多函数和属性了,这些都是在src\core\observer\scheduler.js文件中声明的,所以要先来分析这个文件

scheduler.js分析

声明变量

export const MAX_UPDATE_COUNT = 100

const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {} 
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0 

首先,声明变量:

  • MAX_UPDATE_COUNT: 最大循环次数,超过此次数,报错
  • queue: 记录观察者队列的数组
  • activatedChildren 记录活跃的子组件数组
  • has: 记录观察者的id的对象
  • circular: 持续循环更新的次数,如果超过100次 则判断已经进入了死循环,则会报错
  • waiting: 观察者在更新数据时候 等待的标志
  • flushing: 进入flushSchedulerQueue 函数等待标志
  • index: queue观察者队列的索引

resetSchedulerState重置状态

/**
 * Reset the scheduler's state.
 * 重置计划程序的状态
 * 也就是清空观察者watcher队列中所有数据
 */
function resetSchedulerState () {
  // 观察队列长度和活跃子组件长度都变为0
  index = queue.length = activatedChildren.length = 0
  // 观察者记录的id
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  // 两个等待标志设为false
  waiting = flushing = false
}

resetSchedulerState的声明
功能:清空观察者watcher队列中所有数据

  • 将所有数组清空,对象重置为空对象,标志都设置为false

getNow(Date.now和performance.now的选择)

export let currentFlushTimestamp = 0

let getNow: () => number = Date.now

if (
  inBrowser && //如果是浏览器
  window.performance && //如果performance存在
  typeof performance.now === 'function' && // 如果performance.now是函数
  document.createEvent('Event').timeStamp <= performance.now()  //如果时间戳小于现在
) {
  getNow = () => performance.now()
}

这一段则是对getNow函数的声明

首先getNow其实就是Date.now,但是为了更精确,会去判断performace.now是否可用,如果可用就将getNow设置为performance.now

因为performance.now()是当前时间与performance.timing.navigationStart的时间差,
以微秒(百万分之一秒)为单位的时间,与 Date.now()-performance.timing.navigationStart
的区别是不受系统程序执行阻塞的影响,因此更加精准。

所以getNow的功能就是:获取当前时间戳,可能使用Date.now或者Performance.now

callUpdatedHooks

// 触发 updated生命周期钩子函数
function callUpdatedHooks (queue) {
  // 获取观察者队列长度
  let i = queue.length
  // 遍历
  while (i--) {
    const watcher = queue[i]
    // 获取到虚拟dom
    const vm = watcher.vm
    // 如果有watcher 并且 已经mounted并且没被Destroyed
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      // 触发updated生命周期钩子函数
      callHook(vm, 'updated')
    }
  }
}

遍历queue观察者队列,获取到每个观察者上的vm属性,这是在new Watcher时记录的,记录当前的组件

然后判断vm._watcher,这在之前wathcer中也提到过,_watcher是代表渲染函数的watcher,因此这个判断是:如果是渲染函数的观察者,并且挂载了但没被销毁

在满足这个条件时,就会调用callHook(vm, ‘updated’),这个组件就会触发updated生命周期钩子函数

queueActivatedComponent

// 添加活跃的组件函数,把活跃的vm添加到activatedChildren中
export function queueActivatedComponent (vm: Component) {
  vm._inactive = false
  activatedChildren.push(vm)

将组件的_inactive设为false,然后加入到activatedChildren记录活跃子组件队列中

callActivatedHooks

// 调用组件激活的钩子
function callActivatedHooks (queue) {
  // 遍历观察者队列
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)
  }
}

遍历queue观察者队列,将queue中所有的watcher的_inactive属性设为true,然后调用activateChildComponent函数,此函数来自于src\core\instance\lifecycle.js,功能是判断是否有不活跃的组件 禁用他 如果有活跃组件则触发钩子函数activated

queueWatcher

// 将观察者watcher推进到观察者队列中,过滤重复id,除非是刷新队列时推送
export function queueWatcher (watcher: Watcher) {
  // 获取id
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    // 如果进入flushSchedulerQueue 函数等待标志 为false
    if (!flushing) {
      // 把观察者添加到队列中
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      // 如果已经刷新,则根据其id拼接观察程序
      // 如果已经超过了它的id,它将立即运行。
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      //根据id大小拼接插入在数组的哪个位置
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    // 观察者在更新数据时候 等待的标志 为false
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        // 刷新两个队列并运行观察程序
        // 更新观察者,运行watcher.run(),并且调用组件更新和激活的钩子
        flushSchedulerQueue()
        return
      }
      // 更新观察者 运行观察者watcher.run() 函数 并且调用组件更新和激活的钩子
      // 异步清空回调函数队列
      nextTick(flushSchedulerQueue)
    }
  }
}
获取id(避免重复的观察者入列)

这里开始一步步分析了

// 获取id
const id = watcher.id
if (has[id] == null) {
  has[id] = true
  // 忽略
}

先获取到watcher的id属性,这是唯一的,然后通过has对象是否有id这个属性来避免重复的观察者入列的操作

因为入队列后的观察者,会将has[id]置为true,下一次重复id观察者再想进来时,就会被这个has[id]==null的判断给挡下

watcher添加进queue
// 如果进入flushSchedulerQueue 函数等待标志 为false
if (!flushing) {
  // 把观察者添加到队列中
  queue.push(watcher)
} else {
  // if already flushing, splice the watcher based on its id
  // if already past its id, it will be run next immediately.
  // 如果已经在更新了,则根据其id拼接观察程序
  // 如果已经超过了它的id,它将立即运行。
  let i = queue.length - 1
  while (i > index && queue[i].id > watcher.id) {
    i--
  }
  //根据id大小拼接插入在数组的哪个位置
  queue.splice(i + 1, 0, watcher)
}

判断flushing属性,当进入flushSchedulerQueue这个函数后,flushing会被设置为true,代表此时正在执行更新

所以这段代码意思其实就是:当我没在执行更新时,你可以把观察者直接添加入queue队列,如果是在更新时,则根据id拼接进queue,如果执行顺序id已经超过了其id,它将在下一个立即运行。(根据id排序执行顺序,flushSchedulerQueue这里面的操作)

执行nextTick(flushSchedulerQueue)
// queue the flush
// 观察者在更新数据时候 等待的标志 为false
if (!waiting) {
  waiting = true
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    flushSchedulerQueue()
    return
  }
  nextTick(flushSchedulerQueue)
}

判断waiting属性,在开头设置的waiting为false,所以会进入,并将waiting设为true,这意味着无论调用多少次 queueWatcher 函数,该 if 语句块的代码只会执行一次

然后if语句中if,这是我们不需要管的,因此直接看最后

nextTick(flushSchedulerQueue)

这里涉及到两个函数,我们先分析nextTick这个函数,而要分析清楚nextTick,也要把相应的文件分析清楚,src\core\util\next-tick.js

next-tick.js分析

export let isUsingMicroTask = false

// 回调函数队列
const callbacks = []
// pending状态
let pending = false

同样,声明变量:

  • isUsingMicroTask: 是否使用了微任务
  • callbacks: 回调函数队列
  • pending: pending状态

flushCallbacks

// 执行所有Callback队列中的所有函数
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

将pending置为false,浅拷贝一份callbacks回调函数队列,将callbacks数组清空,然后遍历将拷贝的回调函数全部执行

功能:执行callbacks回调函数队列中所有函数,并将callbacks数组清空

timerFunc 异步更新的原由

// timerFunc函数
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:

// nextTick行为利用可以访问的微任务队列
// 通过自带的Promise.then。或是MutationObserver。
// MutationObserver有更广泛的支持,但是它被严重地窃听进来了
// 当触发in-touch事件处理程序时,iOS>=9.3.3中的UIWebView。它
// 触发几次后完全停止工作。。。所以,如果是有自带的
// Promise可用,我们将使用它:

// 默认使用Promise解决方法 关于宏任务微任务 优先度 Promise是微任务
/* istanbul ignore next, $flow-disable-line */
// 如果Promise存在并且native
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 使用Promise
  const p = Promise.resolve()
  timerFunc = () => {
    // 通过Promise微任务清空回调队列
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.

    // 在有问题的uiwebview中,Promise.then不会完全崩溃,但是
    // 它可能会陷入一种奇怪的状态,即回调被推送到
    // 微任务队列,但队列不会被刷新,直到浏览器
    // 需要做一些其他的工作,例如处理计时器。所以我们可以
    // 通过添加空计时器“强制”刷新微任务队列。

    // 添加空计时器 强制刷新微任务队列
    if (isIOS) setTimeout(noop)
  }
  // 使用微任务 为true
  isUsingMicroTask = true
  // 如果Promise不能用,用MutationObserver
  // 如果不是IE 并且MutationObserver存在并native
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)

  // 如果本地的Promise不可用,请使用MutationObserver,
  // 例如PhantomJS,iOS7,Android 4.4
  //(#6466 MutationObserver在IE11中不可靠)它会在指定的DOM发生变化时被调用。

  // 通过MutationObserver的方式执行
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  // MutationObserver还是微任务
  isUsingMicroTask = true
  // 如果MutationObserver还不能用,判断setImmediate是否存在并native,用setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.

  // 回退到setImmediate。
  // 在技术上,它利用(宏)任务队列
  // 但它仍然是比setTimeout更好的选择。
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  // 最后回退到使用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

timerFunc就比较有意思了,这就是vue异步处理任务的关键了

首先timerFunc会依次判断Promise,MutationObserver,setImmediate,setTimeout这四个异步调用,但是是有区别的,Promise和MutationObserver是MicaroTask微任务,而setImmediate和setTimeout是MacaroTask宏任务,关于微任务,宏任务,这里涉及到EventLoops了,不过记住微任务肯定是好的了,不然vue内部也不会这样按顺序去依次判断

所以timerFunc 最后都是通过异步调用flushCallbacks,也就是异步的执行清空回调函数队列,也就是异步执行回调函数

接下来看next-tick

next-tick

// 为callbacks 收集队列cb函数 并且根据 pending 状态是否要触发callbacks 队列函数
// 异步清空回调函数队列
export function nextTick (
  cb?: Function,  // 回调函数
  ctx?: Object //this指向
  ) {
  let _resolve
  // 向callbacks回调函数队列添加一个函数
  callbacks.push(() => {
    // 如果cb存在
    if (cb) {
      try {
        // 指向cb这个函数
        cb.call(ctx)
      } catch (e) {
        // 如果不是函数,报错
        handleError(e, ctx, 'nextTick')
      }
      // 如果_resolve存在
    } else if (_resolve) {
      // 执行_resolve
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 通过异步 清空回调任务队列
    timerFunc()
  }
  // $flow-disable-line
  // 如果cb不存在 并且Promise存在
  if (!cb && typeof Promise !== 'undefined') {
    // 返回一个Promise
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

一步一步分析

let _resolve
// 向callbacks回调函数队列添加一个函数
callbacks.push(() => {
  // 如果cb存在
  if (cb) {
    try {
      // 指向cb这个函数
      cb.call(ctx)
    } catch (e) {
      // 如果不是函数,报错
      handleError(e, ctx, 'nextTick')
    }
    // 如果_resolve存在
  } else if (_resolve) {
    // 执行_resolve
    _resolve(ctx)
  }
})

会声明一个_resolve变量,然后像callbacks回调函数队列添加一个函数

函数内部会判断cb,cb是nextTick的参数,之前我们是通过这样调用nextTick(flushSchedulerQueue),因此cb就是flushSchedulerQueue

所以cb存在,因此callbacks回调函数队列其实添加的就是一个包含flushSchedulerQueue执行的函数,也就是当清空回调函数队列时,调用回调函数里的函数,也就是相当于执行了flushSchedulerQueue

if (!pending) {
  pending = true
  // 通过异步 清空回调任务队列
  timerFunc()
}

然后判断pending,此时pending是false,所以执行,pending置为true,然后执行timerFunc(),异步清空回调任务队列

而此时回调任务队列中,是flushSchedulerQueue函数,所以我们最后回到src\core\observer\scheduler.js文件中分析这最后的flushSchedulerQueue函数

flushSchedulerQueue

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)

  // 遍历观察者数组
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 执行run
    watcher.run()
    // in dev build, check and stop circular updates.
    // 在dev build中,检查并停止循环更新。
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

一步一步分析

获取时间戳和排序

currentFlushTimestamp = getNow()

flushing = true

let watcher, id

queue.sort((a, b) => a.id - b.id)
  • 首先通过getNow获取当前时间戳,可能以Date.now或者performance.now获取
  • 接着将flushing置为true,这在之前添加进queue观察者队列时提到过,为true代表正在执行更新
  • 声明wathcer和id两个变量
  • 刷新前对队列排序,这样可以确保:
    1. 组件从父级更新到子级。(因为父对象总是在子对象之前创建)
    1. 组件的用户观察程序在其渲染观察程序之前运行(因为用户观察程序是在渲染观察程序之前创建的)
    1. 如果某个组件在父组件的观察程序运行期间被破坏,则可以跳过它的观察程序。

执行watcher.run

// do not cache length because more watchers might be pushed
// as we run existing watchers
// 当我们运行现有的观察者时,不要缓存长度,因为可能会推送更多观察者
// 遍历观察者数组
for (index = 0; index < queue.length; index++) {
  // 获取单个观察者
  watcher = queue[index]
  // 如果存在before
  if (watcher.before) {
    watcher.before()
  }
  // 获取id
  id = watcher.id
  has[id] = null
  // 运行观察者
  watcher.run()
}
  • 首先遍历观察者数组,这里的数组是会动态增加的,之前提到过,在执行更新时,如果有观察者Watcher想入列,会根据id对应插入,如果id是在之前已经被执行过id的里面,它将在下一个立即运行。
  • 获取每个观察者,看是否有before这个方法,我们这之前分析过,在mountComponent是new Wathcer时传入的options中就有before这个方法,并且保存在了watcher.before中,因此这会调用此方法,会判断当前组件是否已经挂载并且还没被销毁,满足这个条件,就会触发beforeUpdate生命周期钩子函数
before() {
  // 如果已经挂载并且没有被销毁
  if (vm._isMounted && !vm._isDestroyed) {
    // 触发生命周期 beforeUpdate 钩子函数
    callHook(vm, 'beforeUpdate')
  }
}
  • 获取id,并将has中id置为null
  • 执行watcher.run() !!!这就是更新的关键了!!!
watcher.run

此时,先来分析run,这就是每次更新数据的关键了,看懂这个,以后就不用再说通知watcher执行update函数更新了

run () {
  // 如果是活跃
  if (this.active) {
    // 获取值
    const 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) ||
      // 获取deep 如果为true
      this.deep
    ) {
      // set new value
      // 设置新值
      const oldValue = this.value
      // 赋值新值
      this.value = value
      // 如果是user
      if (this.user) {
        try {
          // 更新回调函数
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          // 如果出错
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        // 如果不是user,更新回调函数 获取到新的值 和旧的值
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}
  • 首先判断这个wathcer实例的active属性,在wathcer的构造函数初始化时,active属性默认就是true,所以会执行if语句内部代码块
  • 进入if后,执行了const value = this.get(),就是这里了,至关重要的一次执行。之前也说过,渲染函数的观察者执行get方法,会执行它的getter方法,这时getter方法就是updateComponent(内部_update和_render),之前也说了updateComponent作用就是将render渲染的虚拟DOM转为真实DOM,所以这里其实是完成了一次重新渲染
  • 而在后面的if判断,渲染函数的观察者是不会执行这后面的,因为this.get()的返回值其实也就是updateComponent的返回值,这个返回值是undefined
  • 而后面这个if判断其实是给非渲染函数的观察者值变更时使用的,更改新的值:比如计算属性computed或watch监听属性
// 
{
// set new value
// 设置新值
const oldValue = this.value
// 赋值新值
this.value = value
// 如果是user
if (this.user) {
  try {
    // 更新回调函数
    this.cb.call(this.vm, value, oldValue)
  } catch (e) {
    // 如果出错
    handleError(e, this.vm, `callback for watcher "${this.expression}"`)
  }
} else {
  // 如果不是user,更新回调函数 获取到新的值 和旧的值
  this.cb.call(this.vm, value, oldValue)
}

对于执行到这里的,已经不是渲染函数的观察者了,所以会将老值保存,新值赋值,再通过是否是user执行回调,是user放在try/catch里是因为watch是用户自己写的监听回调函数,各种形式都有,所以要放在try/catch中,而不是用户写的就直接执行回调

执行完watcher.run后还需执行的代码

// flushSchedulerQueue内部执行完watcher.run方法后的还需执行的代码
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()

resetSchedulerState()
// call component updated and activated hooks
// 调用组件更新并激活钩子函数
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
// 触发父层flush事件钩子函数
if (devtools && config.devtools) {
  devtools.emit('flush')
}

在执行完watcher.run后,还要继续执行这些

  • 浅拷贝activatedChildren和queue一份给activatedQueue和updatedQueue
  • 执行resetSchedulerState,将activatedChildren和queue的数组都清空,一些状态都置为false,has对象也被清空
  • callActivatedHooks queue中所有watcher的_inactive属性置为true,并判断是否有不活跃的组件 禁用他 如果有活跃组件则触发钩子函数activated
  • callUpdatedHooks,触发updated生命周期钩子函数

当触发玩updated生命周期钩子函数时,这次触发通知更新,已经全部完成了!

触发通知更新总结

总结:

  1. 数据发生更改,触发setter
  2. set会执行dep.notify
  3. dep.notify会去遍历dep.subs观察者数组,遍历执行watcher.update方法
  4. watcher.update执行了queueWatcher
  5. queueWatcher执行了nextTick(flushSchedulerQueue)
  6. nextTick(flushSchedulerQueue)先把flushSchedulerQueue添加进callbacks回调函数数组
  7. 其次nextTick执行timerFunc
  8. timerFunc是异步的(通过Promise,MutationObserver,setImmediate,setTimeout这四种异步方法)执行flushCallbacks
  9. flushCallbacks是异步执行callbacks中所有回调函数,也就是异步执行添加进去的flushSchedulerQueue
  10. flushSchedulerQueue会获取时间戳,对id进行排序,最主要是遍历执行watcher.run方法
  11. watcher.run执行watcher.getter
  12. watcher.getter也就是执行updateComponent
  13. updateComponent将虚拟DOM转换成真实DOM,这里就完成了数据更改后的重新渲染
  14. 之后返回flushSchedulerQueue中,继续执行后续函数
  15. 先resetSchedulerState重置状态
  16. callActivatedHooks调用组件更新并激活钩子函数
  17. callUpdatedHooks触发updated生命周期钩子函数
  18. 至此!一个触发通知更新完成

响应式原理总结

这一切都是从initData和render函数的watcher开始分析的

  1. 从initData开始,initData函数最后会执行observe(data, true)
  2. observe会判断是否拥有__ob__属性,因为是第一次初始化,所以肯定是没有的,因此会执行new Observer
  3. new Observer一个实例时会在内部也new一个dep实例,并且添加__ob__属性,再对数组的元素和对象的属性添加数据劫持(defineReactive)
  4. 当挂载时,也就是执行vue.$mount这个函数时,会执行mountComponet
  5. mountComponet会声明updateComponet并将updateComponet作为第二个参数去执行new Watcher
  6. new Wathcer构造函数内部会声明一系列属性,最后会执行watcher.get方法
  7. 执行watcher.get方法,会触发pustTarget
  8. pustTarget给Dep.target赋值为当前Wathcer实例然后调用watcher.getter
  9. watcher.getter(updateComponent内部有_update和_render函数),_render函数会对属性求值,也就触发了属性的get操作
  10. 属性的get操作会判断Dep.target是否存在
  11. 此时Dep.target是存在的,然后执行dep.depend
  12. dep.depend执行Wathcer.addDep
  13. Wathcer.addDep内部进行了避免重复收集依赖的操作,并且收集依赖
  14. 执行完addDep后,此次依赖收集就完成了
  15. 数据发生更改,触发setter
  16. set会执行dep.notify
  17. dep.notify会去遍历dep.subs观察者数组,遍历执行watcher.update方法
  18. watcher.update执行了queueWatcher
  19. queueWatcher执行了nextTick(flushSchedulerQueue)
  20. nextTick(flushSchedulerQueue)先把flushSchedulerQueue添加进callbacks回调函数数组
  21. 其次nextTick执行timerFunc
  22. timerFunc是异步的(通过Promise,MutationObserver,setImmediate,setTimeout这四种异步方法)执行flushCallbacks
  23. flushCallbacks是异步执行callbacks中所有回调函数,也就是异步执行添加进去的flushSchedulerQueue
  24. flushSchedulerQueue会获取时间戳,对id进行排序,最主要是遍历执行watcher.run方法
  25. atcher.run执行watcher.getter
  26. watcher.getter也就是执行updateComponent
  27. updateComponent将虚拟DOM转换成真实DOM,这里就完成了数据更改后的重新渲染
  28. 之后返回flushSchedulerQueue中,继续执行后续函数
  29. 先resetSchedulerState重置状态
  30. callActivatedHooks调用组件更新并激活钩子函数
  31. callUpdatedHooks触发updated生命周期钩子函数
  32. 至此!一个响应式从头到尾正式完成!

彩蛋:traverse.js 深度监听

最后observer文件夹中我们只剩下了一个文件还没分析:traverse.js
其traverse的调用是在wathcer.get方法中

get () {
  // 添加dep.target dep.target = this
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 这时dep.target = this ,然后执行this.getter.call也就触发get方法,判断dep.target是否存在,存在则dep.depend()
    // 获取值 触发get 也就触发Object.definePorperty的get中的dep.depend(),依赖收集
    // 每个watcher第一次实例化的时候,都会作为订阅者订阅其相应的Dep。
    value = this.getter.call(vm, vm)
  } catch (e) {
    // 如果报错
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    // “触摸”每个属性,以便它们都被跟踪为深度监视的依赖项
    if (this.deep) {
      // 为 seenObjects 深度收集val 中的key
      traverse(value)
    }
    // 出栈一个dep.target
    popTarget()
    // 清理依赖项集合
    this.cleanupDeps()
  }
  // 返回值
  return value
}

会根据其deep属性是否存在,去执行traverse,我们假设deep存在,去分析这个函数,来到rc\core\observer\traverse.js


const seenObjects = new Set()

export function traverse (val: any) {
  // 为seenObjects深度收集val中的key
  _traverse(val, seenObjects)
  seenObjects.clear()
}

可以看到traverse其实是调用_traverse

// 为seenObjects深度收集val中的key
function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  // 是否是数组
  const isA = Array.isArray(val)
  // 如果不是数组并且不是对象或被冻结 或是Vnode实例
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    // 返回
    return
  }
  // 如果val存在__ob__属性
  if (val.__ob__) {
    // 获取__ob__的dep.id
    const depId = val.__ob__.dep.id
    // 如果seenObjects中有这个depId
    if (seen.has(depId)) {
      // 返回
      return
    }
    // 如果没有这个depId,给seenObjects这个set添加一个depId
    seen.add(depId)
  }
  // 如果是数组
  if (isA) {
    i = val.length
    // 遍历所有值,进行递归检查添加
    while (i--) _traverse(val[i], seen)
  } else {
    // 如果不是数组,获取所有key
    keys = Object.keys(val)
    i = keys.length
    // 遍历对象的所有key进行循环递归检查添加
    while (i--) _traverse(val[keys[i]], seen)
  }
}

  • 如果不是数组并且不是对象或被冻结 或是Vnode实例,返回
  • 如果val存在__ob__属性,获取其depid,这波操作也是避免重复收集相同依赖,如果没有相同id,就添加进seenObjects
  • 如果是数组,遍历数组,递归执行_traverse,每次都会获取值,也就触发getter进行深度依赖收集
  • 如果是对象,遍历对象,递归执行_traverse,每次都会获取值,也就触发getter进行深度依赖收集

至此,深度依赖收集也完成了

很多人会问deep属性哪来的,其实是用户给的,在watch监听属性时,想进行深度监听,就要传deep:true这个options

想说的话

能坚持一步一步看到这里的人,我愿称你为最强(也给我点个赞吧!)

我也是刚看20天左右Vue源码的小白,如果发现我写的有什么问题,可以在评论区指出,大家可以一起探讨,毕竟这只是我对响应式的理解和分析

不过还是希望看完这篇能对你们理解响应式有所帮助吧!

猜你喜欢

转载自blog.csdn.net/qq_46299172/article/details/108007042