vue2源码解析之响应式数据

670 阅读8分钟

vue中数据的监测

vue的核心是通过数据驱动视图的更新,那么vue中是怎么知道数据变化的?又怎么知道在数据变化的时候去更新相应的视图呢?在js中提供了Object.defineProperty()方法,此方法可以重新定义一个对象身上的属性,并且读写此属性的时候可以在此方法中进行监测;
写个例子:一个obj对象,它有一个name属性,那么我们通过defineProperty来监听这个属性的读和写

// 定义obj对象和name属性
const obj = {
  name: 1
}
// 定义一个存储新值的变量
let value = 1
// 重新给obj定义name属性
Object.defineProperty(obj, 'name', {
  enumerable: true, // 可枚举
  configurable: true, // 可配置
  set(news){ // 修改的时候会执行这个方法
    console.log('修改了name属性')
    value = news // 把新值赋值给value
  },
  get(){ // 获取的时候会执行这个方法
    console.log('获取name属性')
    return value // 返回value
  }
})

image.png
vue3之前的版本就是使用了Object.defineProperty()方法来监测数据的变化;下面来解析下vue中的这段源码;

对Object类型的数据进行监测

源码预览

// 源码文件 src/core/observer/index.js
/*
实现一个Observer类,这个类的主要作用就是递归遍历对象上的每一个属性,并且
通过Object.defineProperty重新定义属性,从而达到对象上的每个属性在读和写的时候都
能被监测到
*/
export class Observer {
  constructor(value){
    this.value = value
    /*
    给value上添加_ob_属性,值为当前对象,作用标注该对象添加了监测,防止再次添加
    */
    def(value, '_ob_', this)
    // 如果是一个数组
    if (Array.isArray(value)) {
     // 后面实现
    } else { // 否则是对象
      this.walk(value)
    }
  }
  // 遍历对象,给对象身上的每个属性添加监测
  walk(obj){
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

// 添加监测的方法
export function defineReactive(obj, key, value){
    // 如果只传递了两个参数,那么这个属性的值通过key进行获取
    if (arguments.length <= 2) {
      value = obj[key]
    }
    // 重新添加对象上的属性实现属性的监测
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get(){
        console.log('读取属性'+key)
        return value
      },
      set(news){
        console.log('设置属性'+key)
        // 如果新旧的值相同则不进行任何操作
        if (news === value) {
          return
        }
        value = news
      },
    })
}
// 给对象添加属性和值
export function def (obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
        value: value,
        configurable: false,
        enumerable,
        writable: false
    })
}

Observer是一个类,构造函数内部首先把传入的值保存到value上;接着通过def函数给value上添加一个__ob__属性,这个属性的值就是当前的Observer实例(标注当前传递的value为响应式的);判断当前传递的value是数组还是对象,是数组稍后进行解析,是对象就执行Walk方法;

constructor(value){ 
    this.value = value /* 给value上添加_ob_属性,值为当前对象,作用标注该对象添加了监测,防止再次添加 */ 
    def(value, '_ob_', this) // 如果是一个数组 
    if (Array.isArray(value)) { 
    // 后面实现 
    } else { 
        // 否则是对象 
        this.walk(value) 
    } 
 }

walk函数内部遍历对象,并且使它的每个属性都去执行defineReactive方法;

// 处理对象
walk (obj: Object) {
    // 获取到对象的key
    const keys = Object.keys(obj)
    // 遍历对象 给对象的每个属性添加监听
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
}

defineReactive方法就是对对象的属性进行监听;这个函数接收三个参数:obj(目标对象),key(监听的属性),value(属性的值);如果传递了两个参数,那么就进行手动获取;通过defineProperty给obj添加key属性,get内部直接返回value;set内部首先判断新值和旧值是否相同,相同就直接返回;否则就把新值赋值给value;

// 添加监测的方法 
export function defineReactive(obj, key, value){ 
// 如果只传递了两个参数,那么这个属性的值通过key进行获取 
    if (arguments.length <= 2) { 
        value = obj[key] 
    } 
    // 重新添加对象上的属性实现属性的监测 
    Object.defineProperty(obj, key, { 
        enumerable: true, 
        configurable: true, 
        get(){ 
            console.log('读取属性'+key) 
            return value 
        }, 
        set(news){ 
            console.log('设置属性'+key) 
            // 如果新旧的值相同则不进行任何操作 
            if (news === value) { 
              return 
            } 
            value = news 
        }, 
     }) 
}

测试结果:

image.png

总结:上面Observer就是把一个普通对象转换为可监测的对象,通过def给此对象添加_ob_属性,表示该对象已经是响应式对象了,防止重复转换;通过walk给对象身上的每个属性添加监测;defineReactive中给对象下的属性添加监测;

如果对象下的属性还是一个对象呢?

image.png

可以看到data2.obj.name是没有被监测到的,只监测到了data2.obj的读取;那么这个对象下的对象下的属性是没有被监测的,那接下来在之前的基础上实现下对象下的所有属性不管层级多深都设置为可监测的;

源码预览:

// 源码文件 src/core/observer/index.js

export class Observer {
  constructor(value){
    this.value = value
    /*
    给value上添加_ob_属性,值为当前对象,作用标注该对象添加了监测,防止再次添加
    */
    def(value, '_ob_', this)
    // 如果是一个数组
    if (Array.isArray(value)) {
     // 后面实现
    } else { // 否则是对象
      this.walk(value)
    }
  }
  // 遍历对象,给对象身上的每个属性添加监测
  walk(obj){
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}
// 添加监测的方法
function defineReactive(obj, key, value){
    // 如果只传递了两个参数,那么这个属性的值通过key进行获取
    if (arguments.length <= 2) {
      value = obj[key]
    }
    // 递归给值也添加监测
    let childOb = observe(value)
    // 重新添加对象上的属性实现属性的监测
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get(){
        console.log('读取属性'+key)
        return value
      },
      set(news){
        console.log('设置属性'+key)
        // 如果新旧的值相同则不进行任何操作
        if (news === value) {
          return
        }
        value = news
      },
    })
}

// 给value添加监测
function observe (value) {
  // 如果value不是一个对象就直接返回
  if (!isObject(value)) {
    return
  }
  let ob = undefined
  // 如果已经有_ob_属性,表示它已经添加过监测,直接返回
  if (value._ob_ && value._ob_ instanceof Observer) {
    ob = value._ob_
  } else {
    ob = new Observer(value)
  }
  return ob
}

function def (obj, key, value, enumerable) {
    ...
}

function isObject (obj) {
    return typeof obj === 'object' && obj !== null
}

在之前的基础上,只给defineReactive函数内部添加了observe方法的执行,并且传递了value进去;这个value就是walk中遍历对象的值,通过observe给这个值再进行监听;

// 添加监测的方法 
function defineReactive(obj, key, value){ 
    // 如果只传递了两个参数,那么这个属性的值通过key进行获取 
    if (arguments.length <= 2) { 
        value = obj[key] 
    } 
    // 递归给值也添加监测 
    let childOb = observe(value)

observe函数内部首先判断当前的值如果不是一个对象直接返回,基本类型的值无需进行监听;接着判断当前值上如果有__ob__属性并且这个属性的值是Observer的实例,那么直接返回这个属性(表示已经是响应式数据);否则就通过Observer进行递归操作;

// 给value添加监测 
function observe (value) { 
    // 如果value不是一个对象就直接返回 
    if (!isObject(value)) { 
        return 
    } 
    let ob = undefined // 如果已经有_ob_属性,表示它已经添加过监测,直接返回 
    if (value._ob_ && value._ob_ instanceof Observer) { 
        ob = value._ob_ 
    } else { 
        ob = new Observer(value) 
    } 
    return ob 
}

测试结果:

image.png

总结: 通过递归的形式重新定义了对象下的每个属性,不管层级有多深都会被重新定义;observe用来判断当前对象是否已经被设置过,如果设置过直接返回,否则new Observer进行递归;
数据的变化已经实现了,那么数据变化之后就需要更新视图了,那么怎么知道这条数据变化之后需要更新哪些视图呢?下面进行解析;

依赖收集

数据变化之后就会更新视图,那么怎么知道更新哪些视图?vue中哪些视图使用到了这条数据就更新哪些视图,谁依赖了这条数据就更新谁,那么需要给每条数据建立一个依赖数组,用来存储这条数据的所有依赖,当数据变化的时候就执行这个数组通知所有的依赖数据变化了,这就是依赖收集和更新;

依赖收集

源码预览:

// 源码位置 src/core/observer/dep.js

// Dep类 实现一些添加依赖和删除依赖,通知依赖更新的方法
export class Dep {
  constructor(){
    // 存储依赖的数组
    this.subs = []
  }
  // 添加依赖
  addSub (sub) {
    this.subs.push(sub)
  }
  // 删除依赖
  removeSub (sub) {
    this.remove(sub)
  }
  // 添加依赖供外部使用
  depend () {
   // 依赖挂载到Dep构造器的Target属性上
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }
  // 通知依赖
  notify () {
    const subs = this.subs.slice()
    for (let i = 0; i < subs.length; i++) {
      subs[i].update()
    }
  }
  remove (sub) {
    const index = this.subs.indexOf(sub)
    if (index !== -1) {
      this.subs.splice(index, 1)
    }
  }
}

总结:Dep是一个用来管理依赖的类,内部实现了创建一个依赖数组,实现添加依赖,删除依赖和通知依赖的方法;

什么时候进行依赖收集?

在使用数据的时候(读属性的时候)就可以进行依赖收集;模板中使用到了这个数据那么就会执行它的get方法进行读取此时就会进行依赖收集;因此收集依赖是在get中完成的;
修改下defineReactive方法
源码预览:

// 源码位置 src/core/observer/index.js

import Dep from './Dep.js'
defineReactive(obj, key, value){
    // 如果只传递了两个参数,那么这个属性的值通过key进行获取
    if (arguments.length <= 2) {
      value = obj[key]
    }
    // 递归给值也添加监测
    let childOb = observe(value)
    // 创建依赖管理器
    const dep = new Dep()
    // 重新添加对象上的属性实现属性的监测
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get(){
        // 收集依赖
        if (Dep.target) {
            dep.depend()
        }
        // 把dep实例挂到window下方便一会测试
        window.dep = dep
        return value
      },
      set(news){
        // 如果新旧的值相同则不进行任何操作
        if (news === value) {
          return
        }
        value = news
      },
    })
}

defineReactive函数内部创建了Dep的实例,给对象的每个属性都创建了一个Dep依赖管理器;在Get中判断如果Dep.target有值,表示有依赖,那么就执行depeng方法进行收集;为什么判断Dep.target有值才收集依赖,因为依赖是放在Dep.target上的,因此在收集的时候也是收集Dep.target上的值;

import Dep from './Dep.js'
defineReactive(obj, key, value){
    ...
    // 创建依赖管理器
    const dep = new Dep()
     Object.defineProperty(obj, key, {
        ...
        get(){
            // 收集依赖
            if (Dep.target) {
                dep.depend()
            }
        }
        ...
     }
}

测试结果: 手动给Dep.target上设置值,表示有依赖; image.png

可以看到已经收集依赖成功了,Dep下的subs中已经有测试的依赖数据了;
总结:给对象的每个属性下创建一个依赖管理器,在读取属性的时候,并且当前依赖存在的时候就进行收集;

什么是依赖?

谁使用了这个数据,谁就是这条数据的依赖,在vue中为这个依赖创建了watcher实例,当数据变化的时候vue不是直接去修改依赖更新视图,而是通知依赖对应的watcher实例,由watcher进行更新操作;vue中的watcher监听数据也是这个类的实现;

源码预览:

// 源码位置 src/core/observer/Watcher.js
/*
Watcher类的作用 作为依赖 实现了读取属性进行收集依赖和属性变化的时候
更新依赖和执行回调的方法
*/
import Dep from './dep'
export class Watcher{
  // vm 监听的对象 path 监听的属性也可以是字符串路径 cb 变化之后执行的回调
  constructor(vm, path, cb){
    this.vm = vm
    this.cb = cb
    // 解析字符串路径,得到一个可以获取到该属性路径,对应的值,的函数
    this.getter = this.parsePath(path)
    // 收集下依赖,并且存储下当前的属性值
    this.value = this.get()
  }
  // 读取对象的属性,把当前实例作为依赖,放入Dep中
  get(){
    // 收集依赖
    pushTarget(this)
    const vm = this.vm
    // 读取属性,从而执行属性的get方法进行依赖收集
    const value = this.getter.call(vm, vm)
    // 清空依赖
    Dep.target = null
    return value
  }
  
  // 更新 执行回调, Dep中的notify方法中执行的就是update方法进行依赖更新
  update () {
    if (this.sync) { // 如果是同步直接执行
      this.run()
    } else { // 否则就是异步执行,把当前的watcher加入到队列中,通过宏任务或微任务异步执行
      queueWatcher(this)
    }
  }
  
  run () {
    if (this.active) {
      // 获取到最新的值
      const value = this.get()
      // 如果新旧不一样,或者新值是一个对象,或者深度监听
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) { // 如果是用户自己调用的watcher 执行回调并且抛出警告
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          // 把新旧值传递给回调函数并且执行
          this.cb.call(this.vm, value, oldValue)
        }
      }
  }
    
  // 更新所有的依赖
  notify () {
    const subs = this.subs.slice()
    console.log('subs', subs)
    for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update();
    }
  }

  // 解析字符串路径
  /*
  如果path等于'a.b.c' 那么回得到一个函数,这个函数就是用来获取指定对象上的a属性
  下的b属性下的c属性的值
  */
  parsePath (path) {
    const bailRE = /[^\w.$]/
    if (bailRE.test(path)) {
      return
    }
    const arr = path.splice('.')
    return function (obj) {
      // 遍历路径数组,得到最后一个属性的值
      for (let i = 0; i < arr.length; i++) {
        if (!obj) {
          return
        }
        obj = obj[arr[i]]
      }
      return obj
    }
  }
}

// 给Setter中添加通知依赖更新
function defineReactive(obj, key, value){
    ...
    const dep = new Dep()
    ...
    // 重新添加对象上的属性实现属性的监测
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get(){
        // 收集依赖
        if (Dep.target) {
            dep.depend()
        }
        console.log('读取属性'+key)
        return value
      },
      set(news){
        console.log('设置属性'+key)
        ...
        value = news
        // 在setter中通知依赖更新
        dep.notify()   
      },
    })
}

Dep.target = null
// watcher依赖栈
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

Watcher类就是作为依赖的管理器;它接收三个参数,分别为监听的对象vm,对象的属性可以是字符串路径也可以是函数,回调函数cb;构造函数内部分别保存了vm和cb;接着通过parsePath解析了字符串路径得到一个函数,这个函数能够返回该路径对应的值;把这个函数保存到getter上;接着执行get函数获取vm上的path属性,从而可以进行依赖的收集;

 constructor(vm, path, cb){
    this.vm = vm
    this.cb = cb
    // 解析字符串路径,得到一个可以获取到该属性路径,对应的值的函数
    this.getter = this.parsePath(path)
    // 收集下依赖,并且存储下当前的属性值
    this.value = this.get()
  }

parsePath函数用来解析字符串路径;比如获取下面obj对象的c属性值,parsePath就要传递一个'a.b.c'字符串路径;最后得到一个函数,再调用这个函数传递obj就可以获取C的值;

eg:
const obj = {
    a: {
        b: {
            c: 111
        }
    }
}
const getter = parsePath('a.b.c')
getter(obj) // 111

parsePath函数内部首先通过正则判断字符串路径是否是以字母或数字开头并以.结尾,如果是直接返回;否则通过.把字符串路径进行分割成数组;接着返回一个函数,函数内部遍历这个数组,通过数组的每一项获取到目标对象的值,再赋值给目标对象,直到数组遍历完毕,最后的目标对象就是最后一个属性的值;

parsePath (path) {
    const bailRE = /[^\w.$]/
    if (bailRE.test(path)) {
      return
    }
    const arr = path.splice('.')
    return function (obj) {
      // 遍历路径数组,得到最后一个属性的值
      for (let i = 0; i < arr.length; i++) {
        if (!obj) {
          return
        }
        obj = obj[arr[i]]
      }
      return obj
    }
 }

watcher构造函数最后执行了get方法;get函数首先执行了pushTarget方法进行依赖的添加;

get(){ // 收集依赖 
    pushTarget(this) 
    ...
}

pushTarget方法很简单,内部就是把当前的Watcher实例赋值给Dep.target属性上,目的就是在读取属性执行get的时候Dep.target能够有值,把它上面的值在Dep中收集起来;

Dep.target = null
// watcher依赖栈
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

把依赖添加到Dep.target上之后,接着就通过getter读取了监听的属性,从而执行了defineReactive中的这个属性的get方法,Dep.target此时有值,就执行dep.depend方法进行依赖的收集;

get(){ // 收集依赖 
    pushTarget(this) 
    const vm = this.vm 
    // 读取属性,从而执行属性的get方法进行依赖收集 
    const value = this.getter.call(vm, vm) 
    // 清空依赖 
    Dep.target = null 
    return value 
}

// 回顾 Observer中的 defineReactive方法
defineReactive {
    ...
    Object.defineProperty(obj, key, {
        get: function reactiveGetter () {
          ...
          if (Dep.target) {
            dep.depend()
          }
          return value
        },
    })
    ...
}
// 回顾 Dep类
class Dep {
    ...
    depend () {
        if (Dep.target) { 
            this.addSub(Dep.target) 
        }
    }
    ...
}

测试:监听data下的name属性的变化

image.png

小结:谁使用了这个数据,谁就是依赖,为这个依赖创建一个Watcher实例,在创建实例的时候,就去获取了这个属性,从而收集了这个依赖;

依赖怎么被收集的?

  1. 在Watcher构造函数中调用了get方法
  2. get方法中通过把当前实例赋值给Dep的target,然后通过this.getter.call(vm, vm)读取被依赖的数据,从而会触发defineReactive方法中此依赖数据的getter方法,而getter中通过Dep把Dep的target作为依赖进行收集,当执行完毕this.getter.call(vm, vm)之后,再清空Dep的target,从而可以再收集下一个依赖;
  3. Watcher构造函数中就执行了get方法的目的是首次给这个数据添加依赖,以便在修改这个数据的时候能够通知依赖更新

image.png

收集依赖完毕,什么时候进行更新依赖?

当数据变化的时候通知依赖执行update进行更新;

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      if (Dep.target) {
        dep.depend()
      }
      ...
      return value
    },
    set: function reactiveSetter (newVal) {
      ...
      dep.notify()
    }
  })
}


export default class Dep {
  ...
  notify () {
    // 遍历收集的依赖,执行依赖的update方法
    for (let i = 0, l = this.subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

class Watcher {
    ...
    update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) { // 如果是同步直接执行
          this.run()
        } else { // 否则就是异步执行,把当前的watcher加入到队列中,通过宏任务或微任务异步执行
          queueWatcher(this)
        }
  }
}

到目前为止,我们分析了vue中对象类型的数据的读和取的监测,对于数据的删除和添加是无法监测到的,后面会进行分析vue中对于数据的删除和新增是怎样监测的;

依赖的同步更新

watcher可以接收immediate参数,表示是否立即执行,如果传递了true表示立即执行及为同步执行,默认是异步执行;

export default class Watcher {
    ...
    update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) { // 如果是同步直接执行
      this.run()
    } else { // 否则就是异步执行,把当前的watcher加入到队列中,通过宏任务或微任务异步执行
      queueWatcher(this)
    }
  }
}

执行watcher的update方法的时候,会进行判断是同步还是异步,如果sync存在表示同步执行,调用run方法

export default class Watcher {
    ...
    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) ||
            this.deep
          ) {
            // set new value
            const oldValue = this.value
            this.value = value
            if (this.user) { // 如果是用户自己调用的watcher 执行回调并且抛出警告
              const info = `callback for watcher "${this.expression}"`
              invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
            } else {
              // 把新旧值传递给回调函数并且执行
              this.cb.call(this.vm, value, oldValue)
            }
          }
       }
    }

首先判断开关是否是开启状态(防止重复执行),如果开启就执行get方法,读取到当前监听属性的最新值;

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) ||
    this.deep
  ) {

  }

满足以上条件,就保存之前的值作为旧值,新值赋值给this.value;

const oldValue = this.value
this.value = value

接着判断是否是用户调用的watcher,如果是就执行invokeWithErrorHandling方法;否则就是内部调用,直接通过当前vue实例调用传递的回调cb函数;并且把新值和旧值传递进去;

if (this.user) { // 如果是用户自己调用的watcher 执行回调并且抛出警告
      const info = `callback for watcher "${this.expression}"`
      invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
  // 把新旧值传递给回调函数并且执行
  this.cb.call(this.vm, value, oldValue)
}

invokeWithErrorHandling函数;不仅执行了回调函数,并且如果值是一个promise并且不是vue就抛出了警告信息;

export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
   // 执行回调
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      // 抛出警告
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

依赖的异步更新

源码预览:

// 源码位置 src/core/observer/scheduler.js
export const MAX_UPDATE_COUNT = 100
// 存储watcher的数组
const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
// 存储已经添加了的watcher
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 如果不存在
  if (has[id] == null) {
    // 标记为存在
    has[id] = true
    // 还未执行的就放在就添加到队列中
    if (!flushing) {
      queue.push(watcher)
    } else { // 已经在执行了 那么就从队列中找到这个watcher进行追加
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // 没有执行过就执行
    if (!waiting) {
      // 标记为执行中
      waiting = true
      // 把flushSchedulerQueue放入nextTick中进行异步执行
      nextTick(flushSchedulerQueue)
    }
  }
}

函数内部首先获取到当前watcher的id,通过id判断是否已经存在has对象中,如果不存在,就给has上添加这个属性并且标记为存在(防止同一个watcher被多次添加);

const id = watcher.id
  // 如果不存在
  if (has[id] == null) {
    // 标记为存在
    has[id] = true
    ...
  }

如果还未执行flushSchedulerQueue函数,那么就把watcher放入到队列中;

// 还未执行flushSchedulerQueue函数就放在就添加到队列中
if (!flushing) {
  queue.push(watcher)
}

如果已经在执行flushSchedulerQueue函数;那么就从后往前遍历队列,找出队列中比当前watcher的id小于或等于的位置,然后当前wathcer插入到这个位置中;

else { // 已经在执行了 那么就从队列中找到比这个watcher的id小的位置进行追加
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
 }

最后判断如果没有waiting就把waiting标记为true,并且把flushSchedulerQueue函数作为参数执行nextTick中执行;

// 没有执行过就执行
if (!waiting) {
  // 标记为执行中
  waiting = true
  // 把flushSchedulerQueue放入nextTick中进行异步执行
  nextTick(flushSchedulerQueue)
}

flushSchedulerQueue函数,主要是遍历队列,挨个执行队列中的watcher的run方法;run方法在上面同步更新中已经分析过;run方法中主要执行了传递的回调函数;执行完run方法之后,就触发子组件的actived生命周期和当前组件的updated生命周期;

function flushSchedulerQueue () {
  // 获取当前时间搓的方法
  currentFlushTimestamp = getNow()
  // 标记正在执行
  flushing = true
  let watcher, id

  // 进行排序 因为组件的更新是从父级到子级,用户的watcher运行在Render watcher之前,
  queue.sort((a, b) => a.id - b.id)

  // 遍历watcher队列
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    // 清空has对应的watcher的id
    has[id] = null
    // 执行watcher的run
    watcher.run()
  }

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  // 重置相关的变量的值
  resetSchedulerState()

  // 执行子元素的actived生命周期和update生命周期
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

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

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

function callActivatedHooks (queue) {
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)
  }
}

export function activateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = false
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    callHook(vm, 'activated')
  }
}

重点是nextTick函数,nextTick是实现异步执行的关键;

源码预览

// 源码位置: src/core/util/next-tick.js

export let isUsingMicroTask = false

const callbacks = []
let pending = false

// 执行回调函数的函数
function flushCallbacks () {
  pending = false
  // 复制一份回调函数
  const copies = callbacks.slice(0)
  // 清空callbacks
  callbacks.length = 0
  // 遍历回调函数并且执行它
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc

// promise存在通过promise执行回调
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
  // 如果不是ie并且MutationObserver存在,就听过MutationObserver监听dom的变化,MutationObserver是异步的当dom变化完成之后才会执行它的回调,MutationObserver是微任务
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    // 修改dom中的内容,触发MutationObserver中的回调
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
  // 如果setImmediate存在就通过setImmediate宏任务执行flushCallbacks
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {// 如果以上都不存在,就通过setTimeout执行flushCallbacks
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 把回调放在一个函数中执行,并且把这个函数存入到callbacks数组中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 如果没有执行过,就执行
  if (!pending) {
    // 标记为执行过
    pending = true
    // 执行timerFunc
    timerFunc()
  }
  // $flow-disable-line
  // 如果没有传递回调函数,就返回一个Promise
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

首先定义了三个变量,分别是isUsingMicroTask是否使用了微任务;callbacks存储nextTick传递的回调函数;pending是否正在异步执行;

export let isUsingMicroTask = false

const callbacks = []
let pending = false

接着定义了flushCallbacks函数,这个函数是异步执行的回调函数,函数中设置pending为false,遍历callbacks回调数组,挨个执行其中的回调函数;并且清空了callbacks数组;

// 执行回调函数的函数
function flushCallbacks () {
  pending = false
  // 复制一份回调函数
  const copies = callbacks.slice(0)
  // 清空callbacks
  callbacks.length = 0
  // 遍历回调函数并且执行它
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

接着判断有没有promise,如果存在就通过promise微任务异步执行flushCallbacks;并且通过一个函数包裹复制给timerFunc;最后标记isUsingMicroTask为微任务;

let timerFunc

// promise存在通过promise执行回调
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
}

如果promise不存在,就判断MutationObserver是否存在并且不是ie浏览器,如果存在就通过MutationObserver异步执行;MutationObserver是监听dom变化完成之后异步执行它的回调函数的方法,属于微任务;

else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    // 修改dom中的内容,触发MutationObserver中的回调
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
}

创建了一个文本节点,内容为1,执行timerFunc函数的时候就修改它的内容;此时就会触发flushCallbacks回调;

如果MutationObserver也不存在,就判断setImmediate是否存在,如果存在就通过setImmediate宏任务执行回调;

 else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
}

如果以上都不存在,最后就通过setTimeout宏任务异步执行回调;

else {// 如果以上都不存在,就通过setTimeout执行flushCallbacks
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

最后就是nextTick函数;nextTick函数接收两个参数,分别是回调函数和执行回调函数的上下文;把cb回调放在一个函数中执行,并且把这个函数存在callbacks中;

export function nextTick (cb?: Function, ctx?: Object) {
    let _resolve
    // 把回调放在一个函数中执行,并且把这个函数存入到callbacks数组中
    callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
    })
    ...
}

判断有没有被执行,如果没有执行就把pending标记为true,并且执行timerFunc()函数;

// 如果没有执行过,就执行
if (!pending) {
    // 标记为执行过
    pending = true
    // 执行timerFunc
    timerFunc()
}

最后判断如果没有传递cb回调并且promise存在,就返回promise对象;

// 如果没有传递回调函数,就返回一个Promise
if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
}

问题

  • 一次修改多条数据,模板只更新一次是怎么实现的?

是通过一个waiting的开关实现的,刚开始waiting是false,就会把flushSchedulerQueue函数放在nextTick中,并且把waiting设置为true,后面多此修改数据,都不会再次把flushSchedulerQueue函数放在nextTick中;只是把watcher添加到队列数组中;等到时机一到flushSchedulerQueue函数执行,会遍历队列数组挨个执行watcher的run方法;并且会把waiting开关设置为false;给下一次数据的修改做准备;

代码如下:

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 如果不存在
  if (has[id] == null) {
    // 标记为存在
    has[id] = true
    // 还未执行flushSchedulerQueue函数就放在就添加到队列中
    if (!flushing) {
      queue.push(watcher)
    } else { // 已经在执行了 那么就从队列中找到比这个watcher的id小的位置进行追加
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // 没有执行过就执行(多次修改数据,最后只更新一次就是通过waiting开关控制)
    if (!waiting) {
      // 标记为执行中
      waiting = true
      // 把flushSchedulerQueue放入nextTick中进行异步执行
      nextTick(flushSchedulerQueue)
    }
  }
}

function flushSchedulerQueue () {
  ...
  // 遍历watcher队列
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    // 清空has对应的watcher的id
    has[id] = null
    // 执行watcher的run
    watcher.run()
  }
  ...
  // 重置相关的变量的值
  resetSchedulerState()
  ...
}

function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  waiting = flushing = false
}

小结:

  1. Observer中实现了对data数据的所有属性的监听;
  2. Dep管理器实现了对依赖的收集和删除的功能;
  3. 当使用Watcher进行数据监听的时候,并且读取了这条数据,那么会触发这条数据的getter,从而会把当前的Watcher添加到依赖中;
  4. 当数据发生变化的时候会触发setter中的依赖通知,依赖会通知所有的Watcher进行更新视图和执行回调;
  5. 如果是同步更新,并且值变化了,判断是否是用户调用,是用户调用并且返回的新值是prmise并且不是vue实例,那么就执行回调并且抛出警告;如果不是用户调用就是内部调用,直接执行回调函数;
  6. 如果是异步更新,就把watcher存在一个队列数组中,队列数组的执行是在flushSchedulerQueue函数中,flushSchedulerQueue函数的执行是在nextTick函数中,nextTick函数中依次通过判断是用promise,MutationObserver,setImmediate或setTimeout来异步执行flushSchedulerQueue函数;

image.png

对Array类型的数据进行监测

为什么要单独处理Array类型的数据

因为数组中增删属性是通过数组特有的方法,而Object.defineProperty无法监听到通过这些方法增删的数据,因此需要单独处理这些方法;

export class Observer {
 constructor (value: any) {
    ...
    // 如果是数组
    if (Array.isArray(value)) {
      // 支持原型链 通过原型的方式添加
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else { // 不支持原型链 直接给当前value上添加方法
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 遍历数组给数组每一项添加监听
      this.observeArray(value)
    } else { // 是对象
      // 遍历对象给数组每一项添加监听
      this.walk(value)
    }
  }
}

重写数组中的方法

改变数组自身的方法有7种:push、pop、shift、unshift、splice、sort和reverse; vue中通过重写这七种方法实现对数组的监听;

代码预览:

// 源码位置 src/core/observer/array.js

// def函数:给一个对象添加属性和对应的值,并且可以设置是否可枚举
export function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
  })
}

// 保存数组的原型
const arrayProto = Array.prototype
// 创建一个新的对象,并指定这个对象的原型为数组的原型
export const arrayMethods = Object.create(arrayProto)

// 要重写的七个方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]

// 重写
methodsToPatch.forEach(function(method){
  // 保留之前的方法
  const original = arrayProto[method]
  // 重新给这个对象添加这些方法
  def(arrayMethods, method, function mutator(...args){
   console.log('监听到了方法' + method)
    // 执行原有的方法
    const result = original.apply(this,args)
    // 参数
    let inserted = ''
    // 获取到新加的属性
    switch(method){
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        // 获取第二个参数之后的值,因为splice方法的第二个参数之后的参数是新添加的属性
        inserted = args.slice(2)
        break
    }
    // 如果有新增的属性,就给新的属性添加监测
    if (inserted) {
      this._ob_.observeArray(inserted)
    }
    return result
  })
})
class Observer {
    ...
    observeArray (items) {
      for (var i = 0, l = items.length; i < l; i++) {
        observe(items[i])
      }
    }
}

首先,创建一个空对象arrayMethods,对象的原型为数组的原型;接着定义了一个methodsToPatch变量,变量是一个数组,数组中存储了修改的七种方法名;

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

遍历methodsToPatch数组,获取到每项的原始方法的值为original;

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
}

通过def方法给arrayMethods对象添加这七种属性,重写其值;

def(arrayMethods, method, function mutator (...args) {
    
})

值函数内部首先执行原始的方法获取到其值,定义一个inserted变量用来存储新增的值;

const result = original.apply(this, args)
const ob = this.__ob__
let inserted

判断当前的方法是哪种方法,如果是push,unshift或splice表示会添加数据;把参数保存到inserted中;对于splice的第二个参数之后的值才是新增的值;

switch (method) {
  case 'push':
  case 'unshift':
    inserted = args
    break
  case 'splice':
    inserted = args.slice(2)
    break
}

如果inserted有值,表示有新值添加,那么调用当前实例的observeArray方法给新增的数据也设置响应;并且调用当前实例的dep的notify通知依赖更新;最后返回执行之后的结果;

if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result

重写完这七种方法之后,如果有原型,就放在原型上,如果没有原型就挨个给当前的value上添加这七种属性;

// 支持原型链 通过原型的方式添加
if (hasProto) {
    protoAugment(value, arrayMethods)
} else { // 不支持原型链 直接给当前value上添加方法
    copyAugment(value, arrayMethods, arrayKeys)
}

function protoAugment (target, src: Object) {
  target.__proto__ = src
}

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

七种方法设置完成之后,调用observeArray给当前的value设置响应;

// 处理数组
observeArray (items: Array<any>) {
    // 遍历数组 给数组的每一项添加监听
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
}

observeArray方法内部,通过遍历value,给它的每一项通过observe递归设置响应式;

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 如果不是对象,或是虚拟节点直接返回
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 如果是响应式的直接获取__ob__
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if ( // 否则不是响应式的并且是数组或对象,不是vue就通过Observer进行递归添加响应式
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

image.png

总结: 创建一个新的对象arrayMethods,对象的原型为数组的原型arrayProto,遍历这七种方法,通过Object.defineProperty给arrayMethods对象上添加这些方法,执行原来的方法original,判断哪些方法可以新增属性,获取到这些属性inserted并且给这些属性添加监测的功能,返回原来方法返回的结果;遍历value通过observe把数组的每一项设置为响应式的;

给数组收集依赖

  1. 在getter中进行收集,判断值是否是数组,是数组就通过dependArray添加依赖;
// 源码位置:src/core/observer/index.js

class Observer {
  constructor(value){
    ...
  }
  // 添加监测的方法
  defineReactive(obj, key, value){
    const dep = new Dep()
    ...
    // 重新添加对象上的属性实现属性的监测
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get(){
        if (Dep.target) {
          dep.depend()
          // 如果有子属性对象,那么也进行添加依赖
          if (childOb) {
            childOb.dep.depend()
             // 如果是数组那么给数组下的每一项添加依赖
            if (Array.isArray(value)) {
              // 通过递归添加依赖
              dependArray(value)
            }
          }
        }
        return value
      },
    })
  }
}

// 收集数组下的每一项的依赖
function dependArray (value) {
  for (let e, i = 0; i < value.length; i++) {
    e = value[i]
    e && e._ob_ && e._ob_.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

测试:监听下数组下的第一项的name属性 image.png 通过dependArray(value)给数组下的每一项都收集依赖,因为数组的某一项改变,那么它下面的所有数据的依赖都应该更新一次;

  1. 在七种方法中触发依赖更新
methodsToPatch.forEach(function(method){
  // 保留之前的方法
  const original = arrayProto[method]
  // 重新给这个对象添加这些方法
  def(arrayMethods, method, function mutator(...args){
    ...
    // 触发依赖更新
    this._ob_.dep.notify()
    return result
  })
})

通过this.ob.dep.notify()给当前数据触发依赖更新;
通过下标直接操作数组的某一项,视图是不会更新,因为Vue增加了两个全局API:Vue.setVue.delete,后续进行讲解;

小结:
vue中对于数组的监测,不仅仅要监测数组中的每一项,而且也要监测能够改变数组自身的七个方法(pop,push,shift,unshift,reverse,sort,splice);vue中通过重写这七个方法实现对它们的监测。通过遍历数组给数组的每一项实现监测;

image.png

image.png

总结:

  • 红色路线(依赖的收集):

当watcher中执行get方法的时候就会把当前的watcher实例放在Dep.target上,接着进行读取数据;就会执行definedReactive中的get方法,此方法判断Dep.target是否存在,存在就执行Dep的depend方法进行依赖的收集;depend方法中判断Dep.target存在就把它添加到依赖数组中;

  • 紫色路线(依赖的更新):

当修改数据的时候,就会执行definedReactive的set方法,set方法中会触发dep.notify方法的执行,notify方法中遍历收集的依赖数组,执行每一项依赖的update方法进行依赖的更新;update方法中执行run方法,run方法中判断了是同步更新还是异步更新;

  • 黄色路线(异步更新依赖):

首先把传递进来的watcher实例添加到queue队列中,接着判断waiting是否不存在,不存在就设置为存在,并且把flushSchedulerQueue函数放在nextTick中;nextTick通过判断promise,MutationObserver,setImmediate或setTimeout的存在来异步执行flushSchedulerQueue函数;flushSchedulerQueue函数内部遍历queue队列执行每个watcher的run方法,并且设置waiting为false;如果在执行第三步之后再次修改数据调用watcher的update方法,那么queueWatcher函数中只会把watcher添加到queue队列中,而不会再次执行第三步,因此此时的waiting为true;要等到这次异步更新完成之后才会变为false;