Vue源码,你真的看懂了吗(二)

517 阅读10分钟

响应式原理

阅读之前需了解的知识

  • MVVM模式
  • 观察者模式和发布者-订阅者模式
  • Object.defineProperty与ES6中的Proxy
  • 数据劫持
  • 原型对象和原型链

变化侦测

变化侦测就是侦测数据的变化。从Vue2.0开始,引入了虚拟DOM,将更新粒度调整为中等程度,也就是一个状态所绑定的依赖不再是具体的dom节点,而是一个组件。当状态变化之后,会通知到组件,组件内部再使用虚拟dom进行比对。这样可以大大降低依赖的数量,从而降低依赖追踪所消耗的内存

图:每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

下面将具体介绍vue中是如何侦测数据的变化的,从而实现数据驱动视图变化。

核心实现类

  • Observer

    利用 Object.defineProperty 给对象的属性添加getter和setter,用于依赖收集和派发更新。使数据的变化可以被观察到!

  • Dep

    收集当前响应式对象的依赖关系,每个响应式对象包括其子对象都拥有一个Dep实例(Dep.subs数组是watcher实例数组),当数据发生变更的时候,通过dep.notify()通知数组里面的所有watcher让其触发更新。

  • Watcher

    观察者对象,就是依赖。实例分为render(渲染) watcher、computed(计算) watcher、user(侦测器) watcher。

    依赖中记录了所有数据属性以及一些对响应式数据的操作的包装,可以响应数据的变化。

依赖收集

  • initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集。
  • initState 时,对侦听属性初始化时,触发 user watcher 依赖收集。
  • render()的过程,触发 render watcher 依赖收集。
  • re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcher 的订阅,重新对dep.subs赋值,进行新一轮依赖的收集。

派发更新

  • 组件中响应的数据发生变化,触发 setter 的逻辑。
  • 调用 dep.notify()通知 subs数组中所有的Watcher 实例进行更新操作。
  • 每一个 watcher 调用 update 方法触发视图更新或用户的某个回调函数。

实现原理

  • Data通过Observer转换成了getter/setter的形式(响应式数据)来追踪变化。

  • 当外界通过watcher读取数据的时候,会触发getter从而将watcher添加到依赖中。

  • 数据发生变化后,会触发setter,从而向Dep中的依赖(wathcer)发送通知。

  • wathcer接收到通知之后,会向外界发送通知,之后可能会触发视图更新,也可能会触发用户的某个回调函数等。

总结

vue.js采用数据劫持结合发布-订阅模式,通过Object.defineproperty来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调。

注意事项——当前实现存在的不足

由于js的限制,vue不能检测数组和对象的变化。

  • 对象

Vue 无法检测 property 的添加或移除。

由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

解决方法:

使用 Vue.set(object, propertyName, value) 方法 / vm.$set向嵌套对象添加响应式 property。

Vue.set(vm.someObject, 'b', 2)

this.$set(this.someObject,'b',2)
  • 数组

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

1、当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

2、当你修改数组的长度时,例如:vm.items.length = newLength

解决方法:

用Vue.set / vm.$set / Array.prototype.splice 实现和 vm.items[indexOfItem] = newValue 相同的效果。

Vue.set(vm.items, indexOfItem, newValue)

vm.$set(vm.items, indexOfItem, newValue)

vm.items.splice(indexOfItem, 1, newValue)

用 Array.prototype.splice 实现和 vm.items.length = newLength 相同的效果。

vm.items.splice(newLength)

代码实现

Observer类的实现

/*
 * 主文件 index.js
 */
 
 // 方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作
// 为名称的属性)组成的数组,只包括实例化的属性和方法,不包括原型上的。
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
 
/**
 * Observer类
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // 根$数据的vm数

  constructor (value: any) {
    this.value = value
    
    // dep实例
    this.dep = new Dep()
    
    this.vmCount = 0
    
    // dep中添加__ob__属性,指向自身,循环引用
    def(value, '__ob__', this)
    
    // 如果是数组,先判断浏览器是否支持__proto__属性,最后遍历数组的所有元素进行响应式处理
    // 如果支持,就使用protoAugment将arrayMethods覆盖原型
    // 如果不支持,就直接在数组上挂载一些方法,当用户使用这些方法时,不是使用浏览器原生提供的Array.prototype上的方法,而是拦截器提供的方法。
    if (Array.isArray(value)) {
      if (hasProto) {
      // 覆盖响应式数据的原型
        protoAugment(value, arrayMethods)
      } else {
      // 在数据上挂载方法,不去原型上找方法
        copyAugment(value, arrayMethods, arrayKeys)
      }
      
      // 遍历数组的所有元素进行响应式处理
      this.observeArray(value)
      
    } else {
    // 如果是对象,执行walk将值进行响应式处理
      this.walk(value)
    }
  }

  /**
   * 遍历所有的属性,将其转换为getter/setters。
   * 给对象的所有key进行响应化,也就是逐一调用defineReactive
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * 遍历数组中的所有元素进行observe,实现深度响应
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
/**
 * index.js中一些具体方法的实现
 */


/**
 * 通过拦截来扩充目标对象或数组原型链使用__proto__
 */
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

/**
 * 将arrayMethods中的值(7个处理过的方法)直接赋在数组的同名属性上
 * 当响应式数据使用这些方法时直接在数组上找,而不是去原型上找
 */
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])
  }
}


/**
 * 导入Object.defineProperty ,定义一个属性
 */
function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}


/**
 * 给对象创建observer实例
 * 返回一个新的Observer实例或者是已经存在的Observer实例
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果不是对象或者是实例化的vnode,就直接返回
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  
  // 创建一个Observer
  let ob: Observer | void
  
  // 如果有缓存ob,就直接拿来用
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&	// 当前状态可以添加观察者
    !isServerRendering() &&	// 不是ssr
    (Array.isArray(value) || isPlainObject(value)) &&	// 是对象或者是数组
    Object.isExtensible(value) &&	// 可以在它上面添加新的属性
    !value._isVue	// 不是vue实例
  ) {
  // 没有缓存ob,把value响应化并返回ob实例
    ob = new Observer(value)
  }
  
  // 如果是根data,并且当前ob有值,根$数据的vm数加一
  if (asRootData && ob) {
    ob.vmCount++
  }
  
  // 最后把observer实例返回
  // 返回的实例添加了__ob__属性,并且其对象和数组都进行了响应化
  return ob
}

/**
 * 在对象上定义一个响应式属性
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,		// 监听的数据,通过闭包来保存值
  customSetter?: ?Function,		// 日志函数
  shallow?: boolean		// 是否要添加__ob__属性
) {
// 实例化一个Dep对象
  const dep = new Dep()

// Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果key中没有描述符或者是不可配置,直接返回
  if (property && property.configurable === false) {
    return
  }

  // 满足预定义的getter/setters
  const getter = property && property.get
  const setter = property && property.set
  
  // 如果对象不可获取值或者可以设置值,并且只传递了两个参数
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  
  // 递归响应式处理,给每一层的属性附加一个Observer实例
  // shallow不存在代表没有__ob__属性,这时候对val进行observe返回一个ob实例
  // walk函数中调用defineReactive时没有传递shallow参数,所以此时该参数为undefined,也就是默认是深度观测
  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中有Watcher实例化对象(触发了watcher的get),就将watcher添加到dep中
      if (Dep.target) {
        dep.depend()
        
        // 如果存在子对象,也将子对象的dep中添加watcher
        if (childOb) {
          childOb.dep.depend()
          
          // 如果值是数组,处理方式有所差异,要对数组中每一项都做depend操作
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      
      // 触发get最终都要把值返回出去
      return value
    },
    
    set: function reactiveSetter (newVal) {
      // 获取值,触发依赖收集
      const value = getter ? getter.call(obj) : val
      
      /* eslint-disable no-self-compare */
      // 如果新旧值相同就没必要执行操作,直接返回
      //  newVal !== newVal && value !== value 属于这种情况:NaN === NaN 
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      
      /* eslint-enable no-self-compare */
      // 不再生产环境下
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter() // 执行日志函数
      }
      
      // 对于没有setter的访问器属性,直接返回
      if (getter && !setter) return
      
      // 有setter就设置新值,没有就直接给新值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      
      // 对新来的值也进行observe响应式处理,返回ob实例
      childOb = !shallow && observe(newVal)
      
      // 触发set说明该响应式数据发生了变化,这时候应该通知依赖进行派发更新
      dep.notify()
    }
  })
}

/**
 * 在接触到数组时收集对数组每个元素的依赖关系
 * 因为我们不能像属性getter那样拦截数组元素访问
 */
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    // 判断当前元素是否存在__ob__实例,调用depend添加watcher
    e && e.__ob__ && e.__ob__.dep.depend()
    // 递归调用直到不是数组
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

数组的变化侦测方法

/*
 * 数组变化侦测 array.js
 */

// 复制一份原型方法
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

// 需要拦截的7个方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * 拦截转换方法并发出事件
 */
methodsToPatch.forEach(function (method) {
  // 取出原始方法
  const original = arrayProto[method]
  
  // def相当于Object.defineProperty,给arrayMehods的method方法定义一个函数mutator
  def(arrayMethods, method, function mutator (...args) {
  
  // 执行数组原本应该执行的方法
    const result = original.apply(this, args)
    // 获取数组的Object实例
    const ob = this.__ob__
    
    // 用于记录是否有增加数据,判断需要对哪些数据进行响应式处理
    let inserted
    // 下面三种方法比较特殊,因为会对数组进行增加数据
    // 之前的数据已经做过响应式了,但是新增加的没有,所以还要对新增加的进行响应式处理
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    
    // 有新值的话,遍历数组进行深度响应式处理
    if (inserted) ob.observeArray(inserted)
    // 派发更新
    ob.dep.notify()
    
    // 返回数组原本操作的结果
    return result
  })
})


/**
 * Observe a list of Array items.
 * 数组中新操作的对象进行响应式处理
 */
Observer.prototype.observeArray = function observeArray(items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

dep类的实现

/*
 * Dep类 dep.js
 */
 
 let uid = 0
 
/**
 * dep可以被观测到,也可以有多个订阅它的指令
 */
export default class Dep {
// 全局唯一watcher,静态属性
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    // Dep实例id
    this.id = uid++
    // 存放watcher的数组
    this.subs = []
  }

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

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

  // 如果dep中存在watcher,就添加当前watcher
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  
  // 派发更新,通知所有依赖watcher执行update操作
  notify () {
    // 浅拷贝一份subs数组
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 如果不运行async,就不会在调度程序中对sub进行排序
      // 需要对watcher进行排序以确保watcher执行update顺序正确
      subs.sort((a, b) => a.id - b.id)
    }
    // 通知subs中所有依赖watcher执行update操作
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

//全局正在被评估的watcher观察者,当前目标观察程序
Dep.target = null

// Dep.target用targetStack栈结构来管理
const targetStack = []

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

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

watcher类的实现

/*
 * Watcher类 watcher.js
 */
 
let uid = 0

/**
 * 观察者分析表达式,收集依赖关系,并在表达式值更改时触发回调
 * 同时用于$watch() api和指令
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;	// 用来告诉当前观察者实例对象是否是深度观测
  user: boolean;	// 用来标识当前观察者实例对象是 开发者定义的 还是 内部定义的
  lazy: boolean;
  sync: boolean;	// 用来告诉观察者当数据变化时是否同步求值并执行回调

  dirty: boolean;	// 只有计算属性的观察者实例对象的dirty值为true,因为计算属性是惰性求值,用来缓存计算属性的值
  active: boolean;	// 当前实例对象是否是激活状态
  
  // 用来实现避免收集重复依赖,且移除无用依赖
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  
  before: ?Function;	// 可以理解为 Watcher 实例的钩子,当数据变化之后,触发更新之前,调用在创建渲染函数的观察者实例对象时传递的 before 选项。
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,	// 回调,获取值或者是更新视图的函数
    cb: Function,	// 回调函数
    options?: ?Object,	// 参数
    isRenderWatcher?: boolean	// 是否是渲染过的watcher
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    
    // 有参数的话先获取参数
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
    // 没有参数的话默认赋值为false
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // expOrFn是getter的解析表达式,如果是函数,直接赋给getter,作为表达式的get拦截器函数
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
    // 如果是keepAlive组件会走这里
    // parsePath:解析路径获取值
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
      // 没有值的话就执行空函数
        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
        )
      }
    }
    // 如果惰性求值的话值为undefined,如果lazy不存在,计算getter,重新收集依赖
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * 计算getter,并重新收集依赖项,返回观察目标的值
   */
  get () {
  // 将当前watcher添加到dep.target
    pushTarget(this)
    
    let value
    const vm = this.vm
    try {
    // this.getter.call就是触发当前watcher的get方法,判断dep.target是否存在,存在就收集依赖并且获取值返回
    // 每个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 {
      // 遍历每一个属性,以便它们都被跟踪为深度侦测的依赖项
      if (this.deep) {
        traverse(value)
      }
      
      // 当前dep.target出栈
      popTarget()
      // 每次求值完毕后清空 newDepIds 和 newDeps 这两个属性的值,并且在被清空之前把值分别赋给了 depIds 属性和 deps 属性
      this.cleanupDeps()
    }
    
    // 返回值
    return value
  }

  /**
   * 添加依赖项,新旧deps同步,并且dep和watcher互相保存各自的引用
   * 这是真正执行收集依赖进subs数组的操作
   */
  addDep (dep: Dep) {
    const id = dep.id
    // id不存在的时候再进行收集,防止重复收集新的依赖项
    if (!this.newDepIds.has(id)) {
    // 当前watcher的新deps数组中添加当前依赖项dep
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      // 如果旧的deps中不存在该依赖,再往旧的subs中添加当前watcher,避免重复求值时收集重复的观察者
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  /**
   * 清空依赖集合
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      // 移除废弃的观察者
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    
    // 将旧id高新的depid,最后清空新的depid
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    
    // 新旧deps数组进行互换,清空新deps数组
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * 观察者接口,响应式数据发生变化的时候触发
   */
  update () {
    // lazy为true,
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
    // 同步的话直接运行更新
      this.run()
    } else {
    // 异步的话数据不会立即更新,会将需要重新求值并执行回调的观察者放到一个异步队列中,当所有数据变化结束之后统一求值并执行回调
      queueWatcher(this)
    }
  }

  /**
   * 调度程序作业接口,实质上是执行更新操作
   */
  run () {
  // 如果是活跃的
    if (this.active) {
    // 获取值
    // 对于渲染函数的观察者来讲,重新求值等价于重新执行渲染函数,最终结果就是重新生成了虚拟DOM并更新真实DOM,此时value返回值为undefined
      const value = this.get()
      
      // if语句内的代码是为非渲染函数类型的观察者准备的
      // 对比新值旧值是否相等,如果是对象,需要用isObject函数来判断,因为对象虽然引用地址不变,但是值可能改变了
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // 保存旧值,将新值赋给value
        const oldValue = this.value
        this.value = value
        
        if (this.user) {
        // 如果是开发者定义的观察者,需要放进try catch,因为开发者的回调函数在执行过程中行为不可预知
          try {
          // 更新value的值
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
        // 更新value的值
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * 评估观察者的值,用于懒惰观察者
   */
  evaluate () {
    this.value = this.get()
    // 取过一次值之后,dirty重置为false
    this.dirty = false
  }

  /**
   * 当newDeps数据被清空后重新收集依赖,当属性是计算属性的时候才会执行这个函数
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * 取消观察数据,从所有的依赖项订阅服务中删除自身watcher实例
   */
  teardown () {
  // 如果是激活状态才执行
    if (this.active) {
      // 如果组件实例还没有被销毁,就删除当前实例watcher数组中的当前观察者
      if (!this.vm._isBeingDestroyed) {
      // 由于这个参数的性能开销比较大,所以仅在组件没有被销毁的情况下才会执行该操作
        remove(this.vm._watchers, this)
      }
      // 遍历从所有订阅服务dep中删除自身
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      // 变为不活跃
      this.active = false
    }
  }
}

深层遍历转换成响应式

/*
 * 深层遍历转换成响应式 traverse.js
 * 递归遍历对象以唤起所有已转换的getter
 * 使对象内的每个嵌套属性作为深层依赖关系收集
 */
 
const seenObjects = new Set()

export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

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
  }
  
  // !!这一步解决了循环引用导致死循环的问题,如果该对象被遍历过就跳过
  // set结构的作用也在此体现了
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  
  // 如果是数组就递归遍历它的每一项
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
  // 如果不是数组,是对象,就递归遍历它的每个键值
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

相关API实现原理

vm.$watch

deep

watch: {
  a () {
    console.log('a 改变了')
  }
}

上面的代码使用 watch 选项观测了数据对象的a属性,此时会创建Watcher实例对象会读取a的值从而触发属性a的get拦截器函数,最终将依赖收集。但是当属性a的值是一个对象,如下所示,修改a.b的值是触发不了响应的。

data () {
  return {
    a: {
      b: 1
    }
  }
},
watch: {
  a () {
    console.log('a 改变了')
  }
}

深度观测就是用来解决这个问题的,使用深度观测时要把deep选项参数设置为true。

原理: 调用traverse()递归地读取观察属性的所有子属性的值,这样被观察属性的所有子属性都会收集到观察者,从而达到深度观测。

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {

      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      
      
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

unwatch

实际上是执行了watcher.teardown()来观察数据,其本质是把watcher实例从正在被观察的状态的所有依赖列表中移除。

具体代码解析可参考上面代码实现中的teardown()函数实现。

vm.$set

/**
 * Vue.set方法
 * 
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
// 检查target的值,如果是undefinded,null或者原始类型值,在非生产环境下打印警告信息
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 如果是数组以及数组索引有效
  if (Array.isArray(target) && isValidArrayIndex(key)) {
  // 将数组长度设置为两者较大值,否则当要设置的元素索引大于数组的长度时splice无效
    target.length = Math.max(target.length, key)
    // 将指定位置元素的值转换为新值,本身splice方法能触发响应
    target.splice(key, 1, val)
    return val
  }
  
  // 如果不是数组,那就是纯对象
  // key在target中并且不在Object的原型上
  if (key in target && !(key in Object.prototype)) {
  // 设置对象的值
    target[key] = val
    return val
  }
  // 引用observer对象
  const ob = (target: any).__ob__
  
  // 第一个条件成立时,说明你正在使用 Vue.set/$set 函数为 Vue 实例对象添加属性,为了避免属性覆盖的情况出现,在非生产环境下会打印警告信息。
  // 第二个条件成立时,说明当前正在根数据添加属性,这也是不允许的,因为根数据对象ob实例收集不到依赖
  if (target._isVue || (ob && ob.vmCount)) {
    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, key, val)
  // 同时触发响应,派发更新
  ob.dep.notify()
  // 返回值
  return val
}

/**
 * Vue.delete方法
 */
export function del (target: Array<any> | Object, key: any) { 
// 检测 target 是否是 undefined 或 null 或者是原始类型值,如果是的话那么在非生产环境下会打印警告信息。
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  
  // 用splice方法移除数组,触发响应
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  
  const ob = (target: any).__ob__
  
    // 第一个条件成立时,说明你正在为 Vue 实例对象删除属性,为了避免删除vue中定义的属性情况出现,出于安全因素考虑,在非生产环境下会打印警告信息。
  // 第二个条件成立时,说明当前正在根数据删除属性,这也是不允许的,因为根数据对象ob实例收集不到依赖
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  
  // 如果将要删除的属性原本就不在对象上,直接返回
  if (!hasOwn(target, key)) {
    return
  }
  // 删除属性
  delete target[key]
  
  // 如果不是响应式数据,直接返回
  if (!ob) {
    return
  }
  // 是响应式数据就派发更新
  ob.dep.notify()
}

思考

Watcher 和 Dep 的关系

watcher中(响应式数据被读取时第一次触发getter时当前watcher也会记录自己被收集进当前的dep)实例化了dep,并向dep.subs中添加了订阅者,dep通过notify遍历了dep.subs通知每个watcher进行更新。

vue中是如何检测数组变化的?

函数劫持。

Vue通过原型拦截的方式重写了数据的7个方法,首先获取数组的Observer对象,如果有加入新的值,就调用observeArray对新的值进行响应式处理,然后手动调用notify,派发更新,渲染页面。

为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。

Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

Vue为什么不把所有的数据都放到data?

data用来存放绑定的数据。data中的数据都会增加getter、setter,会收集对应的watcher。

如果data里的数据是属于纯展示的数据,根本不需要对这个数据进行监听,特别是一些复杂的列表/对象,放进data中会浪费性能

可以选择放进computed,因为如果computed是直接返回一个没有引用其他实例属性的值,即没有任何访问响应式数据(如data/props上的属性/其他依赖过的computed等)的操作,根据Vue的依赖收集机制,只有在computed中引用了实例属性,触发了属性的getter,getter会把依赖收集起来,等到setter调用后,更新相关的依赖项。

计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算,所以使用computed会更加节约内存。

Vue 为什么不允许动态添加根级响应式 property?

一方面消除了依赖项追踪系统中的一类边界情况,也使Vue实例能更好地配合类型检查系统工作。因为在data对象上才能让Vue将它转换为响应式的数据。

另一方面是在data中提前声明所有的响应式property,会使组件状态的结构更加清晰,便于维护。

Vue Dep.target为什么需要targetStack栈结构来管理?

从Vue2.0开始,一个状态所绑定的依赖不再是具体的dom节点,而是一个组件。视图被抽象为一个 render 函数,一个 render 函数只会生成一个 watcher,其处理机制可以简化理解为:

renderRoot () {
    ...
    renderMy ()
    ...
}

在 Vue2 中组件数的结构在视图渲染时就映射为 render 函数的嵌套调用,有嵌套调用就会有调用栈。当 evaluate root 时,调用到 my 的 render 函数,此时就需要中断 root 而进行 my 的 evaluate,当 my 的 evaluate 结束后 root 将会继续进行,这就是 target stack 的意义。

computed 和 watch 有什么区别及运用场景?

区别

computed:依赖于其他属性值,并且它的值是有缓存的。

只有它依赖的属性值发生了变化,下一次获取computed属性值的时候才会重新计算这个属性的值。

watch 监听器:更多的是观察的作用,无缓存性。

当监听的数据发生变化后才会执行回调进行后续操作。

应用场景

computed:

  • 需要进行数值计算
  • 这个计算依赖于其他的数据

因为computed属性值具有缓存特性,可以避免每次获取值时重新计算。

watch:

  • 在数据变化时执行异步
  • 在数据变化时执行开销较大的操作

因为watch允许我们执行异步操作(发起请求),限制我们执行该操作的频率,并在得到最终结果之前设置中间状态,而计算属性无法做到。