1

  整个vue架构看下来,主要分为三个部分:响应式原理,编译器的实现 以及 patch算法。
  以下是对响应式原理的个人理解,看下来,大致是利用 发布-订阅模式 + 异步更新
  当我们初始化一个vue实例并把它绑定到相应的DOM节点时,其实已经完成了属性响应式的设定。
  所以我们从VUE的构造函数入手。
  打开 core/index.js

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue

发现构造函数的根,其实在core/instance/index.js

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue

我们不细化到每一个函数,一句话,core/instance/index.js 丰满 Vue.prototype ;而 initGlobalAPI(Vue) 丰满Vue构造函数,即全局API。
以下是丰满后的Vue.prototype和Vue构造函数:

// initMixin(Vue)    src/core/instance/init.js **************************************************
Vue.prototype._init = function (options?: Object) {}
// stateMixin(Vue)    src/core/instance/state.js **************************************************
Vue.prototype.$data
Vue.prototype.$props
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {}
// eventsMixin(Vue)    src/core/instance/events.js **************************************************
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
// lifecycleMixin(Vue)    src/core/instance/lifecycle.js **************************************************
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
// renderMixin(Vue)    src/core/instance/render.js **************************************************
// installRenderHelpers 函数中
Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
Vue.prototype._g = bindObjectListeners
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
// core/index.js 文件中
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})
// 在 runtime/index.js 文件中
Vue.prototype.__patch__ = inBrowser ? patch : noop
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
// 在入口文件 entry-runtime-with-compiler.js 中重写了 Vue.prototype.$mount 方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // ... 函数体
}
// initGlobalAPI
Vue.config
Vue.util = {
  warn,
  extend,
  mergeOptions,
  defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
Vue.options = {
  components: {
    KeepAlive
    // Transition 和 TransitionGroup 组件在 runtime/index.js 文件中被添加
    // Transition,
    // TransitionGroup
  },
  directives: Object.create(null),
  // 在 runtime/index.js 文件中,为 directives 添加了两个平台化的指令 model 和 show
  // directives:{
  //  model,
  //  show
  // },
  filters: Object.create(null),
  _base: Vue
}
// initUse ***************** global-api/use.js
Vue.use = function (plugin: Function | Object) {}
// initMixin ***************** global-api/mixin.js
Vue.mixin = function (mixin: Object) {}
// initExtend ***************** global-api/extend.js
Vue.cid = 0
Vue.extend = function (extendOptions: Object): Function {}
// initAssetRegisters ***************** global-api/assets.js
Vue.component =
Vue.directive =
Vue.filter = function (
  id: string,
  definition: Function | Object
): Function | Object | void {}
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
// entry-runtime-with-compiler.js
Vue.compile = compileToFunctions

以一个例子为引

<div id="app">{{test}}</div>
var vm = new Vue({
    el: '#app',
    data: {
        test: 1
    }
})

我们知道,实例化一个vue实例的关键,在this._init(options)上,所以让我们走进这个函数。
core/instance/init.js

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  vm._uid = uid++// vue实例ID
  // 开发环境下打开性能追踪-init,compile,render,patch
  let startTag, endTag
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    startTag = `vue-perf-start:${vm._uid}`
    endTag = `vue-perf-end:${vm._uid}`
    mark(startTag)
  }
  // 以下就是被追踪性能的代码
  vm._isVue = true// 标志该组件是一个vue实例
  // merge options
  if (options && options._isComponent) {// 不存在_isComponent属性,走else分支
    initInternalComponent(vm, options)
  } else {
    // 初始化并丰满$options属性
    vm.$options = mergeOptions(// 1.规范化属性名 2.合并对象产生新对象
      resolveConstructorOptions(vm.constructor),// 解析构造函数options
      options || {},
      vm
    )
  }
  if (process.env.NODE_ENV !== 'production') {
    initProxy(vm)// 对vue实例的渲染做一个代理过滤
  } else {
    vm._renderProxy = vm
  }
  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)
  }
}

这个函数一共做了几件事:

1.给实例添加ID和标识为VUE实例的标志位。
2.初始化并丰满实例的$options属性。
vm.$options = mergeOptions(// 1.规范化属性名 2.合并对象产生新对象
  resolveConstructorOptions(vm.constructor),// 解析构造函数options
  options || {},
  vm
)

相当于

vm.$options = mergeOptions(
  // resolveConstructorOptions(vm.constructor)
  {
    components: {
      KeepAlive
      Transition,
      TransitionGroup
    },
    directives:{
      model,
      show
    },
    filters: Object.create(null),
    _base: Vue
  },
  // options || {}
  {
    el: '#app',
    data: {
      test: 1
    }
  },
  vm
)

所以我们得去了解mergeOptions函数的实现(core/util/options.js)
主要做了两件事:
(1)规范化属性。
(2)利用相应的合并策略函数合并属性。

export function mergeOptions (
  parent: Object,// 构造函数的options
  child: Object,// 初始化vue实例时传入options
  vm?: Component// vue实例
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)// 检验options.components组件名称合法性
  }
  // 处理VUE.extend的情况,合并子类构造函数的options
  if (typeof child === 'function') {
    child = child.options
  }
  // 规范化属性,因为开发者有多种定义方式,需要统一
  normalizeProps(child, vm)// 统一成对象的形式
  normalizeInject(child, vm)// 规范化为对象语法 inject和provide配合使用
  normalizeDirectives(child)// directive-注册局部指令
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {// 判断一个属性是否是对象自身的属性(不包括原型上的)
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat// 调用属性相对应的策略函数,不存在则调用默认策略
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

需要了解引用的strats策略对象(const strats = config.optionMergeStrategies,来自于 core/config.js ),引入在当前文件core/util/options.js中,该strats策略对象是空对象,需要在该文件中慢慢丰满自己。
我们只分析两个策略函数,其他便不再赘述了。
(1) 默认合并策略函数

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

其实很简单,同一个属性,只要子选项不是 undefined 那么就是用子选项,否则使用父选项。
(2) data属性的合并策略函数

// 定义data属性的策略函数-最终把data属性定义成一个函数,执行该函数才能得到真正的数据对象
strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) { // 当前处理的是子组件-说明当前处理的是VUE.extend的情况
    if (childVal && typeof childVal !== 'function') {// 子组件的data属性必须存在且为函数  
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )
      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }
  return mergeDataOrFn(parentVal, childVal, vm)
}

发现mergeDataOrFn的返回值便是data属性策略函数,所以进入该函数。

export function mergeDataOrFn (
  parentVal: any,// Vue.options 的data对象
  childVal: any,// 参数 options 的data对象
  vm?: Component
): ?Function {
  if (!vm) {// 处理VUE.extend的情况
    if (!childVal) {// 子类不存在data对象,直接返回父类的data对象
      return parentVal
    }
    if (!parentVal) {// 父类不存在data对象,直接返回子类的data对象
      return childVal
    }
    return function mergedDataFn () {
      return mergeData(// 返回真正的数据对象,去除重复属性
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,// 调用子类的data函数
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal// 调用父类的data函数
      )
    }
  } else {// 初始化实例走该分支
    return function mergedInstanceDataFn () {// 返回一个函数,执行后就是真正的data数据对象
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)// 返回真正的数据对象,去除重复属性
      } else {
        return defaultData
      }
    }
  }
}

到此我们发现,data属性的策略函数,执行后会将data属性定义成一个函数,只有执行该函数才能得到真正的数据对象。
已下是vm.$options的截图
image.png

3.对vue实例的渲染做一个代理过滤。
4.预处理+调用钩子函数。

在这一步,包含着的initState(vm)函数,是实现响应式的根。

进入core/instance/state.js

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

我们以initData(vm)为切入点,看看vue是如何对data属性实现响应式的。

function initData (vm: Component) {
  let data = vm.$options.data// 此时的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]
    // props优先级 > data优先级 > methods优先级
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {// 避免method和data具有同名属性
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {// 避免props和data具有同名属性
      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)) {// 键名不为保留字
      // 在vue实例对象上添加代理访问数据对象的同名属性
      proxy(vm, `_data`, key)// vm._data.x => vm.x
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

这个函数主要做了以下几件事:
(1)获取真正的data数据对象,因为vm.$options.data是一个函数。
(2)避免data中的属性与props和methods同名。
(3)对vue实例的属性添加一层基本代理,使vm.key指向vm._data.key。

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

(4)监测data对象,使之成为响应式。

进入observe函数(core/observer/index.js)

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__// 被观测过的对象都会带有__ob__属性
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue// 避免vue实例被监测
  ) {
    ob = new Observer(value)// 为data数据对象建立一个监测对象
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

这个函数的核心,是new Observer(value),所以进入Observer的构造函数。

export class Observer {
  value: any;// data数据对象
  dep: Dep;// 属于该数据对象的依赖(watcher)收集筐
  vmCount: number; // number of vms that has this object as root $data
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)//给数据对象定义一个__ob__属性,指向当前的observer实例,且该属性不可枚举
    if (Array.isArray(value)) {// 处理数组对象
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {// 处理纯对象
      this.walk(value)
    }
  }

根据数据对象data的类型,分为两个分支(处理纯对象和处理数组)。
结合当前的例子,我们先只看处理纯对象的情况:

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

对data数据对象中的每一个属性,调用defineReactive()方法,将数据对象的数据属性转换为访问器属性,该方法是整个响应式的核心。

// 让属性成为响应式
export function defineReactive (
  obj: Object,// data
  key: string,// 属性名
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 一个key 对应一个 dep 
  //每一个数据字段都通过闭包引用着属于自己的 dep 常量
  const dep = new Dep()
  // 获取属性描述对象-之前设置的第一层基本代理
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {// 判断属性是否是可配置的
    return
  }
  // 缓存原来设置的get set函数
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {// 如果val本身拥有get函数但没有set,就不会执行深度监测
    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// 调用缓存的基本get函数 vm.x=>vm._data.x
      if (Dep.target) {// 要被收集的依赖-观察者watcher
        dep.depend()// 将依赖收集到闭包dep的筐中
        if (childOb) {//val.__ob__
          childOb.dep.depend()//在使用 $set 或 Vue.set 给数据对象添加新属性时触发
          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)) {//NaN
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)// vm._data.x => vm.x
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)//监测新值
      dep.notify()// 触发dep筐中依赖
    }
  })
}

主要做了两件事
(1)声明一个属于属性自己的dep收集依赖筐(闭包)。
(2)改进属性的get set代理。
get代理:返回值+收集依赖

get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val// 调用缓存的基本get函数 vm.x=>vm._data.x
  if (Dep.target) {// 要被收集的依赖-观察者watcher
    dep.depend()// 将依赖收集到闭包dep的筐中
    if (childOb) {// 指向val.__ob__,处理深度监测的问题
      childOb.dep.depend()// 在使用 $set 或 Vue.set 给数据对象添加新属性时触发
      if (Array.isArray(value)) {// 处理属性为数组的情况
        dependArray(value)
      }
    }
  }
  return value
}

当获取test属性时,先判断当前存不存在要被收集的依赖(watcher对象),如果有,调用自己对应dep的depend()方法来收集依赖。(下面两个if分支在此例中不涉及)
进入core/obsever/dep.js

export default class Dep {
  static target: ?Watcher;// 静态对象,全局只有一个
  id: number;// dep唯一标识
  subs: Array<Watcher>;// 监测此dep实例的观察者们
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {// 收集依赖(watcher) 
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {// 触发依赖
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()// 调用每一个观察者的update方法
    }
  }
}

dep.depend()就是把当前的存储在全局的Dep.target对象(watcher)添加到dep的观察者数组中。
再看看watcher对象的addDep方法,进入core/obsever/watcher.js

addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {// 避免重复收集dep
    this.newDepIds.add(id)
    this.newDeps.push(dep)// 将当前dep对象添加到watcher实例自己的newDeps数组中
    if (!this.depIds.has(id)) {
      dep.addSub(this)// 将当前watcher实例添加到dep实例的观察者数组中
    }
  }
}

暂时不用去考虑if分支的作用,无非是做一些性能的优化,该方法最终的效果,就是让当前的dep实例和当前的watcher实例都彼此包含。换句话说,dep实例在收集依赖(观察者)的同时,依赖也保存了dep实例。
set代理:设置新值+触发依赖

set: function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val// 获取原有值
  /* eslint-disable no-self-compare */
  // 只有当原有值与新值不等时才触发set代理
  if (newVal === value || (newVal !== newVal && value !== value)) {// NaN
    return
  }
  /* eslint-enable no-self-compare */
  if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
  }
  if (setter) {
    setter.call(obj, newVal)// 调用缓存的基本set函数 vm.x => vm._data.x 
  } else {
    val = newVal
  }
  childOb = !shallow && observe(newVal)//监测新值
  dep.notify()// 触发dep筐中依赖
}

但修改test属性时,会先判断是否等于旧值,若不等,则设置新值且调用自己对应dep的notify()方法来触发依赖。
进入core/obsever/dep.js

notify () {// 触发依赖
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()// 调用每一个观察者的update方法
  }
}

发现无非是调用该dep实例存储的每一个依赖(watcher)的update方法。

update () {
  /* istanbul ignore else */
  if (this.computed) {// 处理计算属性
    // A computed property watcher has two modes: lazy and activated.
    // It initializes as lazy by default, and only becomes activated when
    // it is depended on by at least one subscriber, which is typically
    // another computed property or a component's render function.
    if (this.dep.subs.length === 0) {
      // In lazy mode, we don't want to perform computations until necessary,
      // so we simply mark the watcher as dirty. The actual computation is
      // performed just-in-time in this.evaluate() when the computed property
      // is accessed.
      this.dirty = true
    } else {
      // In activated mode, we want to proactively perform the computation
      // but only notify our subscribers when the value has indeed changed.
      this.getAndInvoke(() => {
        this.dep.notify()
      })
    }
  } else if (this.sync) {// 同步更新
    this.run()
  } else {
    queueWatcher(this)// 将当前观察者对象放到一个异步更新队列
  }
}

在此例中,会将当前依赖(watcher)放入一个异步更新队列中。但这块并不是我们响应式流程的重点,无非是对触发依赖的性能优化,通过上一个if分支我们知道,最终所有的依赖(watcher)都会执行自己的run方法。

run () {
  if (this.active) {// 当前依赖(watcher)为激活状态
    this.getAndInvoke(this.cb)
  }
}

进入getAndInvoke函数,这也是我们依赖触发的尽头

getAndInvoke (cb: Function) {
  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// 深度检测标志位,默认true
  ) {
    // set new value
    const oldValue = this.value
    this.value = value
    this.dirty = false
    if (this.user) {
      try {
        cb.call(this.vm, value, oldValue)// 调用watcher构造函数中设置的回调
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      cb.call(this.vm, value, oldValue)
    }
  }
}

进入watcher.get()方法

get () {
  pushTarget(this)// 将此watcher实例设置为Dep.Target
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)// 获取组件中观察的属性值-获取时同时会触发属性的get
  } 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()// 还原Dep.Target
    this.cleanupDeps()// 清空newDeps
  }
  return value
}

发现无非是把当前watcher设置为Dep.Target,再重新收集一次依赖。
所以getAndInvoke函数,无非做了两件事:重新收集依赖(其实还包含了视图更新)以及触发相应回调函数。
现在,我们了解了响应式原理的实现机制,但我们发现,在initData()阶段,只是对每个属性配置了相应的代理,那么在哪调用get,set? 初始的Dep.Target又是谁? 响应式修改数据后又是怎样自动更新视图的?

答案在_init()方法的最后一句代码:
if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

接下来,我们将解析vue.prototype.$mount()方法,即整个响应式的起点

进入platforms/web/runtime/index.js

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined// 获取真实DOM节点
  return mountComponent(this, el, hydrating)// 真正的挂载工作
}

这是vue.prototype.$mount()方法的第一层封装,我们还得到src/platforms/web/entry-runtime-with-compiler.js,在这一层封装,我们加入了编译模板的功能。

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

看下来,在这层封装中,无非是对没有render函数的vue实例,通过template编译出render函数。

最终还是要调用 mountComponent(this, el, hydrating)。即这个函数才是真正的挂载函数,之前的操作无非是为了给它提供渲染函数。

进入core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`
      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)
      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

在该函数中,总共做了几件事:

1.判断渲染函数是否存在,不存在定义为一个空VNode节点。

2.定义并初始化updateComponent函数。

3.为updateComponent函数声明一个观察者对象。

我们先看updateComponent函数,发现无论是执行 if 语句块还是执行 else 语句块,最终 updateComponent 函数的功能是不变的。都是以 vm._render() 函数的返回值作为第一个参数调用 vm._update() 函数,即把渲染函数生成的虚拟DOM渲染成真正的DOM

不深究那两个子函数,可以简单地认为:

vm._render 函数的作用是调用 vm.$options.render 函数并返回生成的虚拟节点(vnode)。

vm._update 函数的作用是把 vm._render 函数生成的虚拟节点渲染成真正的 DOM。

接着,我们看创建观察者的部分,这也是真正触发响应式的关键。

创建 Watcher 观察者实例将对 updateComponent 函数求值,我们知道 updateComponent 函数的执行会间接触发渲染函数(vm.$options.render)的执行,而渲染函数的执行则会触发数据属性的 get 拦截器函数,从而将依赖(观察者)收集,当数据变化时将重新执行 updateComponent 函数,这就完成了重新渲染。同时我们把上面代码中实例化的观察者对象称为渲染函数的观察者。

最后,我们进入core/observer/watcher.js做最后的探索。

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  computed: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  dep: Dep;
  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
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed 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 = function () {}
        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
        )
      }
    }
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
  }

当前的expOfFn指向updateComponent函数,此时被赋值到this.getter。关注到最后一行的this.value=this.get();

get () {
  pushTarget(this)// 将此watcher实例设置为Dep.Target
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)// 获取组件中观察的属性值-获取时同时会触发属性的get
  } 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()// 还原Dep.Target
    this.cleanupDeps()// 清空newDeps
  }
  return value
}

发现当前的渲染函数观察者被设置到Dep.Target上了,且触发this.getter就是执行updateComponent函数,执行时会触发相应属性的get代理。

现在一切都捋顺了,我们来梳理一下流程。

我们new一个vue实例的同时,会触发_init函数,这个函数做了几件事:

(1)初始化并丰满vm.$options属性。

(2)调用相应的初始化函数,让数据成为响应式。其中关键的initState(vm),会让设置的data,methods,props对象,成为响应式。

例如其中的initData(vm),会调用observe(data),即为data数据对象建立一个监测对象observer,具体的就是为纯对象调用walk(data)(数组对象递归调用observe()),该函数内部就是为data对象中的每一个属性调用defineReactive(obj,key)函数,使该属性成为响应式。而属性成为响应式的关键,就是

 1)为每个属性设置对应的dep,由它来做发布信息的工作。

 2)设置get代理来收集依赖。

 3)设置set代理来触发依赖。

(3)将vue实例挂载到dom节点上。我们在这一步为渲染函数设置了一个观察者watcher,这也是我们整个响应式的起点。我们在挂载的时候,会调用渲染函数,从而触发相关数据属性的 get 拦截器函数,从而将当前依赖(渲染函数观察者)收集,此时,每一个属性都收集着当前渲染函数观察者。未来,当数据变化时将触发依赖的update()函数->run()->getAndInvoke(),其中的this.get(),会重新执行 updateComponent 函数,这就完成了重新渲染。


Zack921
0 声望1 粉丝

一个前端。