Vue响应式分析

87 阅读3分钟

示例HTML

<!DOCTYPE html>
<html>

<head>
  <title>Vue.js</title>
  <!-- Delete ".min" for console warnings in development -->
  <script src="../../dist/vue.js"></script>
</head>

<body>
  <div id="app">
    {{message}  
  </div>

  <script>
    var app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue!'
      },
      created() {
        this.message = '你好'
      }
    })
  </script>
</body>

</html>

接下来我们就要分析在created钩子中改变this.message是如何影响视图变化的

响应式数据的核心defineReactive

在vue中所有对于响应式数据的定义最终都要进入这个方法,这个方法通过Object.defineProperty这个方法定义属性的getter和setter,通过在getter收集依赖,在setter触发依赖来实现响应式数据更新的过程。

我们来看看初次渲染时有哪些位置进入了这个方法。

  1. initRender
    • defineReactive(vm, '$attrs')
    • defineReactive(vm, '$listeners')
  2. initState -> initData -> observe -> new Observer -> observer.walk -> defineReactive
    • 这个过程是对data中的数据进行响应化处理

initState完毕之后,我们再次进入这个方法是通过callHook(vm, 'created'),因为

/**
 * Define a reactive property on an Object.
 */
export 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()
    }
  })
}

当对data中的数据set值时,会调用reactiveSetter方法,如果该值是对象,还会递归响应化子对象,这部分通过observe方法实现。

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export 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
}

通过调用dep实例的notify方法通知所有订阅这个数据的watcher,调用watcher的update方法,从而实现数据的响应式更新。而getter是通过在render function中获取data中的数据来实现依赖收集的。

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
}
  
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

我们在实例化render watcher的时候,调用了this.get方法,dep.target就是这个渲染watcher,然后在调用render function时就可以找到这个watcher,并且调用defineReactive作用域中的dep.depend,调用watcher的addDep,更新watcher的newDepIds属性和newDeps属性,如果这个dep没出现过,那么调用dep的addSub,将watcher作为dep管理的subs(watcher数组)被添加。

depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
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)) {
        dep.addSub(this)
      }
    }
  }

如果后面通过事件将这个值改变从而调用setter时,我们就可以调用dep的notify方法遍历subs,并调用watcher中的update方法:

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

会把watcher入队:

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    // 避免watcher重复入队
    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)
    }
  }
}

会在nextTick调用flushSchedulerQueue:

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
      // flushSchedulerQueue
        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
    })
  }
}

timerfunc定义如下,调度了一个微任务,微任务回调执行flushCallbacks

timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }

函数调用栈为空时,执行微任务flushCallbacks:

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

callbacks只包含一个函数就是我们之前push的这个函数

() => {
    if (cb) {
      try {
      // flushSchedulerQueue
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  }

所以我们执行flushSchedulerQueue方法:

/**
 * Flush both queues and run the watchers.
 */
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) {
      // 对于renderwatcher有
      // 调用before就是触发beforeUpdate的hook
      //new Watcher(vm, updateComponent, noop, {
        //before () {
          //if (vm._isMounted && !vm._isDestroyed) {
            //callHook(vm, 'beforeUpdate')
          //}
        //}
      //}, true /* isRenderWatcher */)
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    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
      }
    }
  }

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

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

进入watcher.run方法,run方法首先调用get方法,于是又重新触发一波渲染,即执行update(vm.render())方法,修改过后的值便重新显示在页面上。

/**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  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) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

总结

响应式数据更新的核心时defineReactive,通过在数据更改时触发setter,调用dep.notify方法,dep管理很多个观测这个数据的watcher,通过触发watcher的update方法,实际上queueWatcher,并在nextTick方法中将回调函数保存在回调队列中,同时利用primise调度一个微任务flushCallbacks,当函数调用栈为空时,执行微任务flushCallbacks,从而执行原来保存的所有callback,即flushSchedulerQueue,这个queue中的元素是watcher实例,调用watcher的run方法,进而调用get方法,触发重新渲染update(vm.render())。