【Vue2.x原理剖析一】响应式原理

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

前置

源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结回看过程和注释收获更大

Vue核心

数据驱动视图变化

响应式基本原理

  • 对象通过Object.defineProperty进行get、set拦截
  • 数组通过重写数组的七中修改方法完成对数组的监听
  • 采用观察者模式实现依赖收集和派发更新

数据初始化

new Vue({
  el: "#app",
  router,
  store,
  render: h => h(App)
})

vue其实就是一个构造函数,在实例化过程中,对数据进行初始化,initMixin把_init方法挂载到Vue原型上供Vue调用

// src/core/instance/index.js
...
function Vue (options) { //vue构造函数
  this._init(options)
}

initMixin(Vue) // Vue.prototype._init
...
export default Vue

_init方法会对一些声明周期等做初始化,其中就包括初始化状态

// /src/core/instance/init.js
export function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    const vm = this
    ...
    // 初始化状态
    initState(vm)
    ...
  }
}

initState方法中,就包括了一些对prop、methods、data等的初始化,初始化的顺序为prop -> methods -> data -> computed -> watch,在初始化data时,就会对data里的数据进行代理

// src/core/instance/state.js
export function initState (vm) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm) //数据处理
  } else {
    observe(vm._data = {}, true)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

function initData (vm) {
  let data = vm.$options.data
  // 定义私有变量_data
  data = vm._data = typeof data === 'function'? getData(data, vm): data || {}
  // proxy data on instance
  // 循环判断,props和methods定义的变量不能和data里定义的变量相同,因为他们最后都会挂载到vue实例vm上
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      //代理,将data挂载到vue实例vm上
      proxy(vm, `_data`, key)
    }
  }
  // 对data做劫持
  observe(data, true /* asRootData */)
}

对对象的数据劫持

在初始化数据时,会对数据进行劫持,如果是对象类型才能被检测,同时在Observer类中,会声明一个__ob__(通过修改属性描述符来标记数据不会被枚举)属性用来标记数据是否被监听过,如果监听过就不再进行监听。

// /src/core/observer/index.js
export class Observer {
  constructor (value) {
    this.value = value
    this.dep = new Dep()  //数据可能是数组或者对象,如果是数组此处会给数组添加一个dep
    this.vmCount = 0
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods) // __proto__
      } else {
        copyAugment(value, arrayMethods, arrayKeys) //循环每一项赋值
      }
      this.observeArray(value) //观测数组每一项
    } else {
      this.walk(value)// 对对象的每一项进行劫持
    }
  }

  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  // 对数组的每一项检查如果是对象类型,则进行监听
  observeArray (items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

...
// 如果是对象类型就递归判断劫持
export function observe (value, asRootData) {
  if (!isObject(value) || value instanceof VNode) { //如果是虚拟节点也不用观测
    return
  }
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && //isExtensible表示可以被defineProperty
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
...

通过defineReactive方法对对象不断的进行递归进行劫持,利用Object.defineProperty中的gettersetter对数据进行拦截

// /src/core/observer/index.js
export function defineReactive (obj,key,val,customSetter,shallow) {
  const dep = new Dep()
  // 在data定义的对象中,可自定义getter和setter
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) { // 可配置才能加defineProperty
    return
  }
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  // 核心方法,进行get,set劫持
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // 将dep添加到watcher里,同时将watcher添加到dep中
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // 如果新值和旧值一样则返回
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      //vm.a = 1 => vm.a = [1,2,3]
      childOb = !shallow && observe(newVal) //当赋值一个新值时 需要重新监控 并且更新childOb
      // 通知dep里存在的watcher去更新
      dep.notify()
    }
  })
}

对数组的数据劫持

初始化Observer类和对对象的递归劫持过程中会判断数据是否为数据,如果是数组会通过对数组方法的劫持来监听数组(切片编程),也会递归对数组中不是基本数据类型的数据进行观测
只有这七种方法会改变数组,所以Vue对这七种方法进行了重写,目的是对新增的内容进行观测,同时派发更新

// /src/core/observer/array.js
import { def } from '../util/index'
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) {
    const result = original.apply(this, args)
    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)
    // 通知相关watcher更新
    ob.dep.notify()
    return result
  })
})

watcher和Dep

将watcher当做观察者,需要订阅数据的变动,当数据变动之后,通知watcher去执行某些方法

// /src/core/observer/watcher.js
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

export default class Watcher {
  constructor (vm,expOrFn,cb,options,isRenderWatcher) {
    this.vm = vm
    ...
    vm._watchers.push(this) //为了能强制更新
    // options
    this.deps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'? expOrFn.toString(): ''
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
      }
    }
    // 默认初始化会执行this.get()
    this.value = this.lazy? undefined: this.get()
  }

  get () {
    // 在调用方法之前将当前watcher实例放在全局Dep.target上
    pushTarget(this)
    let value
    ...
    // 如果watcher是渲染watcher,那么就相当于执行vm._update(vm._render())这个方法执行的时候会取值,实现依赖收集
    value = this.getter.call(vm, vm)
    ...
    // 在调用方法之后把当前watcher实例从全局Dep.target移除
    popTarget()
    return value
  }
  // 将dep放到deps里面,同时保证同一个dep只被保存到watcher一次,同一个watcher也只保存在dep一次
  addDep (dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // 将当前watcher添加到dep的subs容器里
        dep.addSub(this)
      }
    }
  }

  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 异步执行
      queueWatcher(this)
    }
  }

  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

}

Dep可以看做是观察者模式里的被观察者,在subs容器里收集watcher,当数据变动时同时自身subs所有的watcher去更新

// /src/core/observer/dep.js
let uid = 0
export default class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }

  removeSub (sub) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() //调用watcher更新
    }
  }
}
// 全局watcher指向,初始状态是null
Dep.target = null
const targetStack = []

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

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

watcher在初次数据渲染时为渲染watcher

依赖收集

对象依赖收集: 在defineReactive函数里新建一个Dep类(每一个属性都会有一个Dep类用于存放当前watcher),在Object.defineProperty()进行get拦截时会调用dep中的depend方法,将dep保存在watcher中,同时,将watcher也保存在dep维护的栈中

// /src/core/observer/index.js
export function defineReactive (obj,key,val,customSetter,shallow) {
  const dep = new Dep()
  ...
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      ...
    }
  })
}

数组依赖收集: 与对象收集依赖同理,唯一不同的是会判断数组里是否嵌套了对象类型,如果嵌套了就再拦截

...
// 例obj为{a:[1,2,3]},key为a
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          // 对嵌套数组里的对象进行拦截,例:{a:[{b:1},2]}
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      ...
    }
  })
...

派发更新

对象的更新: 在设置data里的属性值时会触发set拦截,如果赋值的是一个对象会对对象再进行劫持,**调用dep.notify()**方法去让存在当前属性dep里的所有watcher进行更新操作

// /src/core/observer/index.js
export function defineReactive (obj,key,val,customSetter,shallow) {
  const dep = new Dep()
  ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
     ...
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      ...
      //vm.a = 1 => vm.a = [1,2,3]
      childOb = !shallow && observe(newVal) //当赋值一个新值时 需要重新监控 并且更新childOb
      dep.notify()
    }
  })
}
// /src/core/observer/dep.js
// notify通知所有watcher更新 
export default class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }
...
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() //调用watcher更新
    }
  }
}
...

数组的更新: 通过重写的7种数组方法更改值时,就会被拦截到,同时去调用dep.notify()去通知所有watcher更新

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    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
  })
})

注意点

  • 由于利用Object.defineProperty递归对对象进行观测,所以对象的层级过深会导致性能受影响
  • 此时的观测是基于data定义的对象属性(observe(data)初始化之后执行,之后不再执行),如果后期给对象新增/删除属性时,Vue是拦截不到的,也就是说不是响应式的,只有对象本身就存在的属性修改才会被劫持。
  • 可以使用$set让对象自己去notify,用户对对象属性赋值新对象,vue会对这个对象进行观测
  • 通过数组索引修改数组或者改变数组长度,是不会触发更新的,通过7种重写的方法更新视图
  • 数组重写的7种方法push、shift、unshift、pop、reverse、sort、splice
  • Vue支持在data属性中自定义settergetter,在通过Object.defineProperty进行劫持时会进行合并

总结

new vue()的过程中,vue会对传入的options进行初始化,里边包括对data定义的数据的初始化,同时对数据进行观测(observe(date))。对于对象,通过Object.defineProperty()进行getset劫持,在初始化渲染页面时,会触发get劫持,此时在get方法中会将当前的渲染watcher存放在属性的dep中,同时还会将dep存放在watcher中,当通过赋值等操作对data里的属性值做更改是,会触发set劫持,此时会触发属性里的dep.notify()方法,让存在dep中的所有watcher进行更新操作。对于数组,get拦截过程和对象拦截过程相似,Vue重写了数组的七种方法,所以,通过这七种方法更新数组值时,会调用属性的dep.notify()方法通知所有watcher进行更新操作。

猜你喜欢

转载自juejin.im/post/7126081397021736968