阅读 711

VUE源码系列二:Vue响应式原理解析(附超详细源码注释和原理解析)

来点鸡汤

考59分比考0分更遗憾,最痛苦的不是曾经拥有,而是差一点就可以。

前言

上一篇我们深入分析了数据驱动视图渲染(juejin.im/post/5e06b4…)的原理以及源码解析,感兴趣的可以去瞅一眼,那么这一次我们接着套路,讲一下数据是如何驱动视图更新的,也就是Vue的响应式原理,let go!

案例

<div id="app" @click="changeName">
  {{ name }}
</div>

var app = new Vue({
  el: '#app',
  data: {
    name: 'bo.yang'
  },
  methods: {
    changeName() {
      this.name = 'biaohui'
    }
  }
})
复制代码

案例解析:当我们修改this.name的时候,视图会接着修改为biaohui。

响应式对象

用过Vue的童鞋都知道Vue实现响应式原理是通过Object.defineProperty()这个API,不熟悉这个API的童鞋自行查看MDN(developer.mozilla.org/zh-CN/docs/…

Object.defineProperty(obj, prop, descriptor)
复制代码

其中核心参数是属性描述符descriptor,它是一个对象,里边最重要的属性就是
get(给属性提供getter方法,当访问该属性的时候会触发getter)
set(给属性提供setter方法,当我们去修改属性的时候会触发setter)

一旦对象有了getter和setter,那么我们就会把它叫做响应式对象。

initState

Vue初始化的时候会执行_init()方法,上一篇文章讲过,_init()方法会执行initState(),源码地址:src/core/instance/state.js

/**
 * 初始化状态
 */
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  /*初始化props*/
  if (opts.props) initProps(vm, opts.props)
  /*初始化方法*/
  if (opts.methods) initMethods(vm, opts.methods)
  /*初始化data*/
  if (opts.data) {
    initData(vm)
  } else {
    /*该组件没有data的时候绑定一个空对象*/
    observe(vm._data = {}, true /* asRootData */)
  }
  /*初始化computed*/
  if (opts.computed) initComputed(vm, opts.computed)
  /*初始化watchers*/
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
复制代码

先来分析initProps和initData

initProps

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  /* 循环props,进行响应式绑定 */
  for (const key in propsOptions) {
    defineReactive(props, key, value)
    if (!(key in vm)) {
      /* 把vm._props.key全都代理到vm.key上 */
      proxy(vm, `_props`, key)
    }
  }
}
复制代码

initData

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  //遍历data中的数据
  while (i--) {
    const key = keys[i]
    /* 把vm._data.key全都代理到vm.key上 */
    proxy(vm, `_data`, key)
  }
  /*从这里开始我们要observe了,开始对数据进行绑定,这里有尤大大的注释asRootData,这步作为根数据,下面会进行递归observe进行对深层对象的绑定。*/
  observe(data, true /* asRootData */)
}
复制代码

我们可以看出,initProps和initData都是对props和data绑定到this上,对props进行响应式绑定,并监听data。接下来看一下proxy和observe

proxy

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  /* proxy(vm, `_data`, key),可以看出get直接输出vm._data.key的值 */
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  /* 把val(新值)附给vm._data.key */
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  /* 最后用sharedPropertyDefinition去描述vm的key属性 */
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码

observe

/**
 * 监听数据变化
 * @param {object} value    data||props等
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  /* 如果data本身存在观察属性,直接返回 */
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    // ...
  ) {
    /* 否则重新实例化一个Observer */
    ob = new Observer(value)
  }
  return ob
}
复制代码

observe就是通过Observer这个类给data或props绑定监听

Observer

Observer 是一个类,它的作用是通过defineReactive给对象的属性添加 getter 和 setter,用于依赖收集和派发更新

/**
 * 给对象的属性添加getter和setter,用于依赖收集和派发更新
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; 

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    /* 把自身实例(Observer)添加到value的__ob__属性上 */
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      /*
          如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。
          这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。
      */
      if (hasProto) {
        /* 直接覆盖原型的方法来修改目标对象 */
        protoAugment(value, arrayMethods)
      } else {
        /*定义(覆盖)目标对象或数组的某一个方法*/
        copyAugment(value, arrayMethods, arrayKeys)
      }

      /*如果是数组则需要遍历数组的每一个成员进行observe*/
      this.observeArray(value)
    } else {
      /*如果是对象则直接walk进行绑定*/
      this.walk(value)
    }
  }

  /* 给对象的每个属性添加响应式,注意这里只循环绑定了对象的第一层属性 */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  /* 给数组的每个成员添加监听 */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
复制代码

defineReactive

defineReactive 的功能就是定义一个响应式对象,给对象动态添加 getter 和 setter
源码: src/core/observer/index.js

/* 定义一个响应式对象,给对象添加setter和getter */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)

  const getter = property && property.get
  const setter = property && property.set

  /* 递归调用observe,使得obj对象无论层次多么深,都会将所有属性变成响应式的 */
  let childOb = !shallow && observe(val) // 获取具有__ob__属性的数据
  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
      // 新老值相等,就不再执行setter
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      /* 给新值添加__ob__属性 */
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
复制代码

以上涉及到了Vue响应式原理的两个概念,分别是依赖收集和派发更新,接下来我们详细说一下:

依赖收集(Dep)

我们发现defineReactive首先实例化了一个类Dep,它是依赖收集的核心,它也是对Watcher的一种管理,脱离Watcher,Dep将没啥意义,先看一下Dep定义:

Dep

源码:src/core/observer/dep.js

export default class Dep {
  static target: ?Watcher; // 静态属性target(Watcher类型)
  id: number;
  subs: Array<Watcher>; // 一个(Watcher类型)数组
  
  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    /* 为后续数据变化时候能通知到哪些 subs 做准备 */
    this.subs.push(sub)
  }

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

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

  notify () {
    /* 所有Watcher的实例对象 */
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
复制代码

分析:上篇在Vue mount的过程中有一段逻辑

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
/* 实例化Watcher,触发它的get方法 */
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
复制代码

再看看Watcher是啥玩意儿,只看一些关键代码
源码:src/core/observer/watcher.js

Watcher

export default class Watcher {
  vm: Component;
  deep: boolean;
  sync: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    vm._watchers.push(this)
    /* 监听器的options */
    if (options) {
      this.deep = !!options.deep // 深度监听
      // ...
      this.sync = !!options.sync // 在当前 Tick 中同步执行 watcher 的回调函数,否则响应式数据发生变化之后,watcher回调会在nextTick后执行;
    }
    /* Watcher实例持有Dep的实例的数组 */
    this.deps = [] // 老的Dep集合
    this.newDeps = [] // 触发更新生成的新的Dep集合
    this.depIds = new Set()
    this.newDepIds = new Set()

  get () {
    /* 收集Watcher实例,也就是Dep.target */
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      /* this.getter对应就是我们上篇讲到的vm._update(vm._render(), hydrating),_update会生成VNode,在这个过程中会访
      问vm上的data,这时候就触发了数据对象的getter,defineReactive中可以发现每个getter都持有一个dep,
      因此在触发getter的时候会触发Dep的depend方法,也就触发了Watcher的addDep方法 */
      value = this.getter.call(vm, vm)
    } finally {
      /* 把 Dep.target 恢复成上一个状态,因为当前 vm 的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变 */
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  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)) {
        /* 把当前的 watcher 订阅到这个数据持有的 dep 的 subs, 目的是为后续数据变化时候能通知到哪些 subs 做准备 */
        dep.addSub(this)
      }
    }
  }
}
复制代码

分析: 收集依赖的目的是为了当这些响应式数据发生变化,触发它们的 setter 的时候,能知道应该通知哪些订阅者去做相应的逻辑处理,我们把这个过程叫派发更新,其实 Watcher 和 Dep 就是一个非常经典的观察者设计模式的实现

派发更新(setter)

defineReactive方法里的setter就是派发更新的,它的核心是dep.notify(),来看一下:
源码:src/core/observer/dep.js

 notify () {
    /* 所有Watcher的实例对象 */
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
复制代码

遍历所有watcher实例,并执行Watcher的update方法:
源码:src/core/observer/watcher.js

update () {
    // ...
    queueWatcher(this)
  }
复制代码

看一下queueWatcher定义:
源码: src/core/observer/scheduler.js

/* 异步执行Dom更新的时候所执行的方法 */
/*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  /* 保证同一个 Watcher 只添加一次 */
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      /* 异步执行flushSchedulerQueue */
      nextTick(flushSchedulerQueue)
    }
  }
}
复制代码

解析: 这是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue,再来看一下flushSchedulerQueue定义:

/*
nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers
主要目的是执行Watcher的run函数,用来更新视图
*/
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  /*
    给queue排序,这样做可以保证:
    1.组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。
    2.一个组件的用户的自定义watcher(user watchers)比Vue内部的渲染watcher(render watcher)先运行,因为user watchers往往比render watcher更早创建
    3.如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过。
  */
  queue.sort((a, b) => a.id - b.id)
  
  /*这里不缓存queue.length是因为在执行watcher.run()的时候,可能用户会再次添加新的 watcher,
  更多的watcher对象可能会被push进queue*/
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    /*将has的标记删除*/
    has[id] = null
    /*执行watcher的run方法*/
    watcher.run()
    /*
      在测试环境中,检测watch是否在死循环中
      比如这样一种情况
      watch: {
        test () {
          this.test++;
        }
      }
      持续执行了一百次watch代表可能存在死循环
    */
    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 updatedQueue = queue.slice()
  /*重置调度者的状态*/
  resetSchedulerState()

  /*使子组件状态都改编成active同时调用activated钩子*/
  callActivatedHooks(activatedQueue)
  /*调用updated钩子*/
  callUpdatedHooks(updatedQueue)
}
复制代码

接下来看一下watcher.run()的定义:
源码:src/core/observer/watcher.js

run () {
      // 新value值
      const value = this.get()
      // 老value值
      const oldValue = this.value

      // set new value
      this.value = value
      /* 触发更新 */
      this.cb.call(this.vm, value, oldValue)
  }
复制代码

解析:执行了Watcher的回调cb,并有新值value和旧值oldValue,这就是我们watch的时候能拿到的那俩值的原因啦。

总结

在组件初始化渲染的时候我们去把它的data和props进行响应式绑定,并将所有视图中用到的数据进行依赖收集,将这些数据添加订阅属性(__ob__),以便在更新的时候知道需要更新那些数据,当我们去修改相应data的时候,会执行setter方法进行派发更新,紧接着触发了所有观察者(Watcher)的update方法,然后执行Watcher的run方法,在run方法里,去执行每个Watcher的回调触发视图更新。

下期预告

Vue的nextTick实现原理(juejin.im/post/5e1ae6…)

关注下面的标签,发现更多相似文章
评论