Vue2.0源码阅读计划(二)——响应式原理

——慢慢来,一切会比较快

前言

继上篇《Vue2.0源码阅读计划(一)——工具函数》之后,我们来探究响应式原理,这是vue的核心所在。本篇采取模块化的阅读方式link,一定要对照着源码来阅读。对于源码,重在理解思想,不必一行行死抠代码,要学会学习。思想是境界上的提升,有了思想的高度,就不怕不会做。
这块的文章真的不好写,一定要自己结合代码多思考,欢迎指教???。

引子

源码中文件第一行的注释/* @flow */,是因为Vue2.0源码采用的flow做的静态类型检查,并没有用typescript,所以行首会有注释,目的就是启用静态类型检查,flow的写法与typescript大体差不多,所以源码阅读起来不要有太大的困惑。

var vm = new Vue({
  data: {
    a: 1
  }
})
 

我们是这样初始化一个Vue实例的,当然在单文件组件中我们的data会定义为一个函数,这是因为单文件组件是组件,组件是会复用的。

扩展

vue单文件组件通过vue-loader解析,而vue-loader做的事只是把.vue文件中的templatestyle编译到.js(编译到render函数),并混合到你在.vueexport出来的Object中。产出的js只是导出了一个符合component定义的Object

Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,这个过程就是 Vue模板编译过程。

模板编译后续篇章会讲,继续正文。

初始化完成后vm.a就是响应式的了。我们要探究这中间发生了什么?所以我们需要看Vue这构造函数到底做了什么。我们先找到Vue的构造函数link:

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)
}
 

其中options就是我们传入的包含data、computed、methods...的对象,上面构造函数的重点在this._init(options)这一行,this指代Vue实例,所以我们需要去Vue的原型上找到定义所在。

_init

_init定义在init.js文件,我们一起看一下:

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
 

Vue 的初始化逻辑写的非常清楚:合并配置,初始化生命周期,初始化事件中心,初始化渲染,调用beforeCreate钩子函数,初始化注入、初始化状态等等。

initState()

因为本篇探究响应式原理,所以我们重点关注initState(vm)函数,具体如下:

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)
  }
}
 

initState中初始化了props、methods、data、computed、watch五项,不一一分析。

initData()

我们重点分析initData

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // 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 */)
}
 

initData首先对data的类型进行判断,如果是函数就调用getData函数拿到返回值,然后调用isPlainObject函数判断data是否为一个朴素的对象,isPlainObject内部采用Object.prototype.toString.call()进行的判断。下面紧接着遍历data的属性,不能与methodsprops的属性重名,然后通过proxyvm._data代理到vm上,这也就是我们属性明明是在data上定义的却能在this上拿到的原因。注意这里的proxy函数并不是ES6中的Proxy,是个自定义函数,大小写开头区别开,只是名字类似。proxy函数内部通过对象的访问器属性代理过去,自行查阅。

函数的最后执行了observe函数,该函数就是对数据添加变化侦测响应式的关键所在,因为ES5Object.defineProperty()只能针对对象,无法作用于数组,所以ObjectArray的变化侦测是有区别的,下面分开解析。

对象的变化侦测

observer文件夹下,一共有这么6个文件,这就是响应式的核心代码。
Vue2.0源码阅读计划(二)——响应式原理

三个关键角色:

  • Observer: 它的作用是给对象的属性添加gettersetter,用于依赖收集和派发更新
  • Dep: 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个Dep实例(里面subsWatcher实例数组),当数据有变更时,会通过dep.notify()通知各个watcher
  • Watcher: 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcheruser watcher)三种

observe()

index.js文件中,我们找到observe函数,我们看看这个函数:

function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  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)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
 

首先对传入的值进行判断,如果不是一个对象或者是VNode类型(准确说应该是VNodeprototype是否出现在value的原型链上)就直接返回。然后通过__ob__属性去判断是否已经添加过响应式了,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例并返回。

问题1:value为什么不允许是VNode类型?

我的理解是,VNode不允许是响应式的,我们通过产生VNode实例的createElement函数来看(定义render函数时的第一参数),源码中判断了第二参数data不能为响应式的,这是因为datavnode的渲染过程中可能会被改变,如果是一个监听属性,就会触发监控,从而产生不可预估的问题,data就是VNode类中的一个属性,在new VNode()时会作为第二参数传入,所以不允许为VNode添加响应式。大致看下createElement的实现:

export function _createElement(
    context: Component,
    tag ? : string | Class < Component > | Function | Object,
    data ? : VNodeData,
    children ? : any,
    normalizationType ? : number
): VNode | Array < VNode > {
if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
    `Avoid using observed data object as vnode data:   ${JSON.stringify(data)}n` +
    'Always create fresh vnode data objects in each render!',
    context)
    
    return createEmptyVNode()
 }
...
}
 

注意:Object.isExtensible(value)这个判断很有用

一定要利用好这个特性哦,上篇文章我放过个很有用的例子:

new Vue({
    data: {
        // vue不会对list里的object做getter、setter绑定
        list: Object.freeze([
            { value: 1 },
            { value: 2 }
        ])
    },
    mounted () {
        // 界面不会有响应
        this.list[0].value = 100;

        // 下面两种做法,界面都会响应
        this.list = [
            { value: 100 },
            { value: 200 }
        ];
        this.list = Object.freeze([
            { value: 100 },
            { value: 200 }
        ]);
    }
})
 

当你需要定义一个比较复杂的对象但又不依赖它的响应式时就很有用,比如在绘制echarts时,我就经常见有人把那么复杂的一个options定义在data中。因为vue会给data数据的所有子对象递归绑定gettersetter,数据量大的时候影响性能是必然的。

Observer()

继续正文,我们接着来看Observer的实现:

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
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  ......
}
 

new Observer()时执行constructor()函数,注意这里有个dep属性赋值为new Dep(),然后在value上注入属性__ob__值为当前Observer的实例,表明已经添加过响应式了。接着if判断了值为数组的情况,本节我们只看对象的,走入else分支,执行walk函数,walk()函数很简单就是遍历value的所有可枚举属性并执行defineReactive()函数。于是重点落在了defineReactive()函数。

defineReactive()

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

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  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) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
 

defineReactive 的功能从名字就能看出来:定义一个响应式对象,给对象动态添加 gettersetter。注意这里一开始也执行了new Dep(),然后拿到传入键的属性描述符,判断configurable是否为false,因为false了我们就无法再在下面使用Object.defineProperty方法重新定义访问器属性了。接着获取访问器属性中的getset,如果get不存在,就直接通过obj[key]的形式拿到vallet childOb = !shallow && observe(val)递归子属性,将子属性也变为响应式的,这就是为什么我们访问或修改 obj 中一个嵌套较深的属性,也能触发 gettersetter

重点来了,定义getter,先看首尾,拿到value,return出去,保持了获取数据默认行为,中间的部分为收集依赖。我们先将依赖暂且认为是保存在Dep.target上的一个东西,判断依赖存在就调用depend方法进行收集,里面还包含了子属性的依赖收集。 定义setter,同样先拿到value,比较新旧值,不同再继续往下,略过自定义setter,判断如果getter存在且setter不存在(也就是说你定义访问器属性的时候只定义了get却没有定义set),直接结束,否则向下递归新值的子属性将其也变为响应式,最后触发当前属性的依赖。

注意,vue源码中基本没写过匿名函数,从定义getter、setter就能看出来,这是一个很好的习惯。

至此,我们已经大概了解了vue的响应式原理,但目前我们只知道是通过dep.depend()去收集依赖,通过dep.notify()去触发依赖,所以留下最大的疑问是:

  1. Dep是做什么的?
  2. 依赖到底是什么?
  3. 依赖具体怎么去收集或触发的?

下面一一解析。

Dep

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 () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
 

我们需要注意的是target这个静态属性,它规定为Watcher类型,这是一个全局唯一 Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组,subs为存放依赖的地方。

问题2: Watcher就是依赖吗?

Watcher并不是依赖,你可以理解Watcher为依赖的代理执行者,每一个依赖都对应一个Watcher,收集依赖时收集Watcher就可以,触发依赖时通过Watcher去代理执行。

问题3:依赖到底是什么?

很简单一句话:谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher实例。vue2.0版本依赖的粒度大小为中等组件级,即一个状态所绑定的依赖为一个组件。所以可以理解依赖为使用了某个状态的当前组件,当状态改变时,通过Watcher代理通知到组件,组件内部再使用虚拟dom进行patch,然后触发界面组件展示的地方重新渲染。

说了这么多Watcher,它是什么,我们一起看看:

Watcher

class Watcher {
  ......

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    ......
    
    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()
      : ''
    // parse expression for getter
    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()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  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
  }
  
  ......
}
 

文中省略了部分代码,我们简单分析下实例化Watcher的过程中都发生了什么。首先判断是否为渲染watcher(初始化Vue实例时mounted阶段就是渲染watcher),所以this._watcher存放的是renderWatcherthis._watchers会存放上面说的全部三种类型watcher。下面我们结合实例化renderWatcher的那部分源码来继续看Watcher的实例化过程会好懂些(在new Vue()的过程中会实例化一个renderWatcher):

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
 

没看过模板编译patch部分源码的同学可以简单认为updateComponent是一个更新组件的函数,准确点来说updateComponent函数就是生成组件的vnode并进行patch然后渲染的那个函数,当然首次渲染并不会进行patch,因为没有oldVNode与之对比。上面dep.notify最后就是通知它执行。

上面Watcher实例化走到对expOrFn的判断,此时expOrFn等于updateComponent,是函数类型,赋值给this.getter,最后调用this.get(),先看一下pushTarget的定义:

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

执行pushTargetDep.target置为当前Watcher的实例,然后执行this.getter也就是updateComponent,在进行模板编译的过程中会对data上的属性访问,触发getter完成依赖收集。其实在这一步走完的时候,当前组件就已经渲染完成了,最后下面的就是执行popTargetDep.target置空。

派发更新

依赖收集完了,我们再回来看派发更新。我们更新状态,触发setter,执行dep.notify,源码中notify的定义如下:

notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
}
 

主要做了一件事,就是遍历subs(收集到的依赖)调用update方法,update方法定义如下:

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
}
 

这理判断了lazy属性与sync属性:lazy属性对应的是computed watcher,会从缓存中去拿;sync对应user watcher,不把更新watcher放到nextTick队列 而是立即执行更新。因为我们分析的是render watcher,所以逻辑走进else里面,关于三种类型的watcher之后我会单独开一篇。解析queueWatcher:

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false

function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // 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)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
 

这里引入了一个队列的概念,这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue

这理首先用 has 对象保证同一个 Watcher 只添加一次;接着走 flushing 的判断;最后通过 waiting 保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次。

flushSchedulerQueue

接下来我们来看 flushSchedulerQueue的实现:

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    
    ......
  }
  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}
 

这里首先对队列进行了排序,这么做主要有以下要确保以下几点(代码块中注释的翻译):

  1. 组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。
  2. 用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的(initComputed() > initWatch() > render watch)。
  3. 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。

接着遍历队列,拿到对应的 watcher,对watcher.before进行判断,这里就是执行beforeUpdate钩子函数的地方,然后执行watcher.run()。这里需要注意一个细节,在遍历的时候每次都会对 queue.length 求值,因为在 watcher.run() 的时候,很可能用户会再次添加新的 watcher,这样会再次执行到 queueWatcher,走进上面if (!flushing)else分支中:

else {
  // 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)
}
 

这里会从后往前找,找到第一个待插入 watcherid 比当前队列中 watcherid 大的位置,把 watcher 按照 id的插入到队列中,因此 queue 的长度发生了变化。

然后我们来看watcher.run()

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) {
          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)
        }
      }
    }
  }
 

run方法很简单,就是执行this.get()(我们之前存的updateComponent函数),进行patch触发界面更新。下面的逻辑主要判断了新旧值是否相等、新值是否为对象、deep是否为true,然后传入新旧值作为第一第二参数执行相应的回调函数,对于user watcher特别添加了try...catch处理。

最后执行resetSchedulerState()将控制流程状态的一些变量恢复到初始值,把 watcher 队列清空。

nextTick的实现

我们分析完了flushSchedulerQueue,再回过头来看nextTick的实现,我们毕竟是在nextTick里面调用执行的flushSchedulerQueue

let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
 

Event Loop机制,理解起来就很简单。为了保证浏览器和移动端兼容,vue不得不做了microtaskmacrotask的兼容(降级)方案,优先支持哪个就用哪个,所以timerFunc赋值得看浏览器看设备。将flushSchedulerQueue再搞个箭头函数压入callbacks中,对pending的判断保证timerFunc只执行一次,timerFunc无论是使用宏任务还是微任务都会在下一个 tick 执行 flushCallbacksflushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。

这里使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 依次执行完毕。

上面的话可能有些绕,举个例子分析一下:

methods: {
    todo () {
        this.a = 1
        this.b = 2
    }
}
 

假设我点击一个按钮触发了todo函数,更新a、b的值,为a赋值的时候,触发一次setter
调用一次nextTick,为b赋值的时候,触发一次setter,再调用一次nextTick,假设支持promise,第一次调用nextTick后,就将pending就设为了true,导致第二次再调用nextTick时只会往callbackspush,不会再走后面从而产生新的微任务,所以在这一轮宏任务执行完毕时,上面callbacks中此时应该存放了两个值,但只产生了一个微任务。这也就理解了上面说的避免了开启多个异步任务

因为存在异步队列的机制,所以我们此时直接访问dom就还是之前未更新的,为了确保我们想要访问到的是更新后的dom,我们一般需要采用this.$nextTick()的形式(当然你也可以写setTimeout或者Promise.resolve().then(),但是this.$nextTick()就好在做了平台兼容,我不用就是我傻),通过this.$nextTick()会在之前的callbacks中再push一次,等timerFunc执行起来就走的是同步代码,前面的a、b两个依赖触发updateComponent函数后已经重新渲染界面了,我们this.$nextTick()中的回调最后执行,就保证了拿到的是更新后的dom

至此派发更新完毕。

数组的变化侦测

上面已经说了,数组与对象的变化侦测不同是因为Object.defineProperty()引起的。我们平时都是如下定义数组的:

data(){
  return {
    arr: [1, 2, 3]
  }
}
 

我们按照之前的逻辑为arr添加了响应式,但[1, 2, 3]作为arr的子属性会递归添加响应式,当再次走到new Observer()的时候,如下:

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
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
}
 

走进了Array的判断中,我们只看protoAugment(value, arrayMethods)这个分支:

function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
 

很简单就是改变了数组原型的指向,所以再来看arrayMethods

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original 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)
    // notify change
    ob.dep.notify()
    return result
  })
})
 

arrayProto保留了原来Array的原型,arrayMethods则是以arrayProto为原型创建出来的对象,接下来列出了Array原型中可以改变数组自身内容的7个方法,分别是:push、pop、shift、unshift、splice、sort、reverse。然后遍历它们,目的就是添加拦截器,采用重写操作数组的方法,以达到在不改变原有功能的前提下,为其新增一些其他功能。因为其他方法并没有操作Array基本都是遍历查找之类的最后生成新的数组并返回,所以做拦截没有任何意义。

拦截器的第一步就执行了数组原有的方法,首先保证了原有的功能,还用最开始举的例子,那这里面的this此时是指向arr的,arr已经是响应式的了,先用它拿到Observer的实例ob,然后针对push、unshift、splice三个可以新增元素的方法,拿到新增的元素inserted,调用ob.observeArray方法,该方法如下:

observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
}
 

很简单,就是遍历为其添加响应式,因为新加入进来的元素可能是对象类型的。ob.observeArray()之后再执行ob.dep.notify(),触发依赖更新。

至此数组的变化侦测分析完毕,此时你应该可以理解我下面这个例子了:

export default {
    data() {
        return {
            arr: [1, 2, 3, { x: 1 }]
        }
    },
    methods: {
        todo() {
            /* 不要一起测试,因为下面的依赖更新会触发整个组件的重新渲染 */
            this.arr[0] = 4 // 单独测试:界面不会变化
            this.arr[3].x = 5 // 单独测试:界面会进行响应式变化
            this.arr = [1, 2, 3] // 单独测试:界面会进行响应式变化
        }
    }
}
 

问题4:很多人难以理解在defineReactive里面已经有一个Dep实例了,为什么在Observer里面最开始还要创建一个Dep实例?

其实他们的着重点是不同的。Observer实例的dep是一个对象有一个,可以从$set$delete方法里看到,这个dep只有在对象新加属性或者删除属性时才会触发更新,而defineReactive中的dep是对象的每个键上都有一个,在这个键被重新赋值之后会触发更新。

补充:观察者模式

相信大家在面试的时候,经常被问到vue的响应式原理,其实耐住性子读完上面就已经难不住你了。但在这之前,大家的回答可能大致是这样的:采用了观察者模式,所以我这里还是做个简单的补充介绍吧。观察者模式是一种设计模式。

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。

使用场景:当对象间存在一对多关系时,则使用观察者模式。
意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

vue采用了观察者模式,很适合。观察者模式分被观察者与观察者,毫无疑问我们在data中定义的对象就是被观察者,观察者则是在模板中需要响应的数据。观察者又名发布-订阅者模式,我们以报社为例,报社每天需要发布新的新闻杂志并给到订阅者,那报社怎么知道要将报纸派发给哪些人?所以报社会有注册机构,你先注册成为我们的用户,然后我们才会给你派发报纸。对应到vue,就指依赖收集派发更新

结语

文中有不对的地方欢迎指正,大家一起努力呀!!!

参考:
Vue源码系列-Vue中文社区
Vue.js 技术揭秘

(0)
上一篇 2021年5月26日 下午7:00
下一篇 2021年5月26日 下午7:23

相关推荐

发表回复

登录后才能评论