vue响应式源码解读

190 阅读4分钟

前言:

我们在使用vue框架开发项目的过程中与响应式数据是离不开的,我们经常会遇到很多与此相关的问题,例如:为什么后设置的对象属性不是响应式数据,生命周期函数的执行次序,亦或是知道如何去解决但是只知晓解决方式却不知其原因,等等诸如此类的问题,都是不明响应式原理所致。

一,初始化

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

这段代码大家可能再熟悉不过了,一切都是从这开始,我们只知道new一个Vue然后传入一个对象参数就开始写代码了,其他的他都会帮我们去做,其实这个构造函数内部做了很多事情。如果我们去查看源码就会发现这个Vue构造函数的庐山真面。

function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
}

// init代码内部
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    vm._self = vm
    initState(vm)
  }
}

export function initState (vm: Component) {
  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 /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

很明显Vue 内部是调用init方法初始化数据 而init方法是挂载在 Vue 原型 供 Vue 实例调用 它内部最主要的是调用initState 函数 进而去分别调用数据的处理函数去处理。而data 里面的属性交给initData 函数去做处理。

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // proxy data on instance
  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)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

这个函数里面做了两件事情

  • 数据代理:调用proxy函数把data数据代理到vm 这也就是我们为什么可以使用this.a,访问到data的数据。
  • 数据观测:对数据进行观测(响应式核心)。

二,数据劫持

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    if (Array.isArray(value)) {
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

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 () {
      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) {
      const value = getter ? getter.call(obj) : val
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}


export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  return ob
}

Observer 函数内部调用walk函数对对象上的所有属性依次进行观测,然后调用 defineReactive 函数去进行数据劫持(此处也是实现响应式的重点)我们都知道Vue是调用 Object.defineProperty 去进行数据劫持实现响应式的,这便是出处。

使用 Object.defineProperty 对数据的读写进行劫持,给每个属性添加 getter 和 setter方法用于依赖收集和通知更新,当读取属性时使用 dep.depend 去收集依赖,当设置属性时使用dep.notify去通知。如果值是一个对象,则递归调用 observe 方法,保证子属性都能变成响应式。

三,依赖收集(Dep)

依赖收集,就是将依赖某个属性的代码保存,当后期对这个数据修改的时候触发所有依赖这个属性的依赖回调代码。当数据劫持时给每一个属性创建一个Dep 用于依赖收集(watcher)

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    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()
    }
  }
}

通过双向收集机制建立关联。这种设计确保了数据变更时能精确通知到依赖它的观察者,同时观察者也能管理自己的依赖关系。

双向收集

Dep.target.addDep(this) 当前Watcher 记录 Dep 收集到自己的deps 数组记录所有相关的 Dep 实例,

this.subs.push(watcher) Dep 记录 Watcher 将所有相关的watcher 记录到subs 数组中

精准依赖管理
  • 当数据变化时,Dep 能准确通知到所有关联的 Watcher
  • 当 Watcher 销毁时,能主动从所有 Dep 中移除自己(避免内存泄漏)

四,观察者(Watcher)

初始化的时候去订阅数据,将自己与数据建立联系,当订阅的数据发生变化时回调方法更新视图。

export default class Watcher {
  vm: Component;
  cb: Function;
  id: number;
  getter: Function;
  value: any;
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
  ) {
    this.vm = vm
    vm._watchers.push(this)
    this.cb = cb
    this.id = ++uid // uid for batching
    this.deps = []
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      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
        )
      }
    }
    this.value = this.lazy ? undefined : this.get()
  }

  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 {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

在数据劫持的 get 方法中调用,谁引用了该属性,谁就依赖了该属性。当读取属性时使用 dep.depend 而他并不是直接去收集依赖,而是addDep去将watcher添加到subs。

Dep.target

其实就相当于一个全局变量,当一个Watcher开始获取数据的值时,pushTarget 函数将Dep.target 设置为当前watcher,这样在数据劫持时候就可以将其添加到数据对应的Dep

五,派发更新

当修改数据的时候数据劫持里面的set方法执行 执行过程如下

  • 判断如果该属性修改的新值和保存的旧值是一样,直接返回,不一样则替换
  • 执行该属性对应的依赖Dep.notify派发更新
  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()
    }
  }
  
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
 }
  • 遍历subs下面所有watcher,watcher 即为模板编译时每个用到data属性的地方都会生成一个watcher, 执行其update 方法内部调用run 方法 里面又调用cb(更新页面的render函数) 。

  • 在调用render函数,函数里的值就会拿到更新后的值,然后去生成新的vnode,然后去patch 将vnode 转为真实dom挂载,如此页面便更新完成 。

六,全流程分析

acc3b7c623b2b3756c8e84b6375d2129~480x480.png

  1. vm实例化时候会使用Object.defineProperty 对每一个属性进行劫持,生成与之相关联的Dep。
  2. 在模板解析的过程中会对每一个用到的属性值都创建一个watcher,这个watcher 读取此属性值会触发此属性的get方法从而将其收集到Dep中的subs数组中。
  3. 在修改此属性值的时候又会触发其set方法 调用dep.notify()遍历subs中的所有 Watcher,执行其update 函数更新。
  4. update 函数 调用run函数,进而调用其cd 回调函数完成更新。

注意:依赖收集这个过程发生在数据被读取的时候,而不是在数据劫持或模板编译的时候。数据劫持只是设置getter和setter,而模板编译是将模板转换为可执行的渲染函数,真正的依赖收集是在渲染函数执行时,访问数据属性触发的。

依赖收集是动态的,发生在组件渲染或计算属性被求值的时候。例如,当渲染函数执行并访问某个数据属性时,该属性的getter被触发,此时Dep.target(当前的Watcher)会被收集到该属性的Dep中。

结语

本文主要介绍了vue内部是如何将数据变为响应式数据并更新视图的,希望对诸位会有所帮助,如果其中有表述不周及错误之处,还望诸位加以提点,我会持续修改。