Vue 源码分析响应式原理

220 阅读7分钟

简单版

首先对data/props进行监听(defineProperty),get时收集依赖,set时派发更新。每一个属性都有自己的Dep(收集依赖和派发更新的容器),记录着哪些组件(渲染watcher)依赖该属性,当属性值更新后,告诉所有依赖该属性的组件重新渲染。举个栗子:

首先对data进行监听,然后开始渲染组件 我们有很多组件,一个一个去渲染。当Title组件渲染发现需要属性值data.title时(标题是{{data.title}}),就会触发title属性的get,title属性的Dep中的subs就会把当前的Dep.target(Title组件的渲染watcher)收集进来。当data.title的值改变后,会触发title属性的set,会通知(Title组件的渲染watcher)重新渲染。

大致思想就是上面描述的, 下面看下代码:

对data进行defineProperty监听

function _initData(data) {
    const keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i])
    }
}
function defineReactive(data, key) {
    let val =  data[key]
    // 每个key都有自己的dep
    const dep = new dep()
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        dep.depend()
        return val
      },
      set(newVal) {
        dep.notify()
        val = newVal
      }
    })
    // val 是对象时候递归监听
    if(isObejct(val)) {
      _initData(val)
    }
}

Dep.target一定是一个watcher,为了解决组件嵌套问题,定义了一个targetStack(target栈),记录target(当前是哪个watcher)的信息。 进入父组件,调用pushTarget(target) (target = 父渲染watcher) , Dep.target = 父渲染watcher。 此时发现一个子组件需要渲染,又会调用pushTarget(target) (target = 子渲染watcher), Dep.target = 子渲染watcher。 子组件渲染完成后,调用popTarget(), 此时回到父组件继续渲染其他元素。Dep.target = 父渲染watcher。 Dep.target顺序为: 父->子->父。 (需要结合watcher的get函数理解上面这一段)

let depId = 0
class Dep {
  constructor() {
    this.id = depId++
    this.subs = []
  }
  depend() {
    if(Dep.target) {
      this.subs.push(Dep.target)
    }
  }
  notify() {
    this.subs.forEach(watcher  => {
      watcher.updata()
    })
  }
}
// 同一时间只有一个Watcher
Dep.target = null
const targetStack = []

function pushTarget(target) {
  targetStack.push(target)
  Dep.target = target
}
function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length-1]
}
  1. Watcher, 进入调用pushTarget,完成前调用popTarget。
class Watcher {
  constructor(getter) {
    this.getter = getter
    this.get()
  }
  get() {
    pushTarget(this)
    this.value = this.getter()
    popTarget()
    return this.value
  }
  updata() {
    this.get()
  }
}

此时, 一个简单的响应式就实现了。下面看一下源码中的响应式是如何实现的。

源码分析

先来一张整体图:

defineProperty

从new Vue开始,对Vue原型添加了一些属性/方法,然后调用了初始化函数 _init

function Vue (options) {
  this._init(options)
}
initMixin(Vue) // Vue.prototype._init 
stateMixin(Vue) // Vue.prototype.$watch, Object.defineProperty($data/$props) 
eventsMixin(Vue) // Vue.prototype.$on / $once / $off / $emit
lifecycleMixin(Vue) // Vue.prototype._update / $forceUpdate / $destroy
renderMixin(Vue) // Vue.prototype.$nextTick / _render

export default Vue

_init(options) 1. mergeOptions,2. initState 3. $mount

记住这个顺序后面有用, initState负责对数据defineProperty监听,$mount负责触发get收集依赖

Vue.prototype._init = function (options?: Object) {
    const vm = this
    // 每一个组件都有一个唯一的_uid
    vm._uid = uid++
    // mergeOptions中会normalizeProps/normalizeInject/normalizeDirectives
    // 合并options props/inject/directives 都有多种写法,为了后面处理方便,会提前统一格式化成object类型
    vm.$options = mergeOptions(...)
    // 各种初始化
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm) // 初始化 props 和 data
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    // 挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
}

initState(vm),初始化props/methods/data/computed/watch 在初始化props和data时候进行defineProperty监听

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

关键的地方终于到了, 对props进行监听

initProps, 对每个props值进行 defineReactive(props, key, value) 还有一些额外的操作, (1)缓存数组keys(2)验证props的格式并让 _props 可以访问到每个 prop (3)把_props代理到vm上, 及this.xxx 就可以取到props的值

initProps还有一个特别之处就是在开发环境时, defineReactive会传入第4个参数警告函数。 主要作用是防止子组件修改父组件传入的porps。 具体可以看: juejin.cn/post/684490…

  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // 缓存props数组
  const keys = vm.$options._propKeys = []
  for (const key in propsOptions) {
    keys.push(key)
     // 验证 prop 并让 _props 可以访问到每个 prop
    const value = validateProp(key, propsOptions, propsData, vm)
    // Object.defineProperty 监听每一个prop
    defineReactive(props, key, value)
    // 可以让 vm._props.x 通过 vm.x 访问
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)

上面对props进行监听, 这里对data进行监听

initData,检测是否与props/methods重名,把数据代理到_data上, 主要observe(data, true)

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm) // data可能是函数
    : data || {}
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  // 检测data 是否与props/method 重名
  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)
    }
  }
  // 监听 data
  observe(data, true /* asRootData */)
}

observe(data, true),主要的 ob = new Observer(value)

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // data 不是对象 或 是vnode  
  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
}

new Observer(value) 只处理对象,会给每一个监听过的data 使用defineProperty加一个不可遍历的属性__ob__ , 如果是对象直接遍历监听, 如果是数组,特殊处理: 修改数组原型指向, 并重写了数组的一些方法('push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse')

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
    // 用defineProperty给每个值都设置一个__ob__属性,值为this,且不可枚举 
    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)
    }
  }

  /**
   * 监听所有属性。当值类型为Object时才应调用此方法
   */
  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])
    }
  }
}

不管props还是data, 最后都是对其中属性进行 defineReactive, 并且递归监听统一放在defineReactive中处理, 若是对象则重新observer该对象。

如果key的configurable为false则不进行监听, 因此可以冻结一个对象(freeze)让Vue不监听它, 达到一定的性能优化(列表数据仅展示而不会修改,则不需要监听它)

function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 创建依赖实例,通过闭包的方式让 set get 函数使用
  const dep = new Dep()
  // 如果key的configurable为false则不进行监听, 因此可以冻结一个对象(freeze)让Vue不监听它, 达到一定的性能优化(列表数据仅展示而不会修改,则不需要监听它)
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // 获取自定义的 getter 和 setter
  const getter = property && property.get
  const setter = property && property.set
  // 如果没有getter且只传了两个参数(第三个是val)
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 如果 val 是对象的话递归监听
  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) {
         // 处理数组和vue.set(它们会手动触发dep.notify())
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // 判断值是否发生变化
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 派发更新
      dep.notify()
    }
  })
}

Dep

(1) 有一个 subs 负责收集某个属性所有依赖的watcher

(2) depend/addSub: 添加watcher,简洁版时,触发属性get,直接向subs中添加一个watcher, 源码中dep.depend-->wacther.addDep-->dep.addSub,多绕了一层, 主要作用是优化处理,不重复添加依赖

(3) removeSub:删除watcher, Watcher.cleanupDeps删除无用依赖时候使用

(4) notify触发属性set时调用, 会触发watch.update

(5) Dep 还有一个静态属性 Dep.target 记录当前的watcher, 配合pushTarget/popTarget 使用

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()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher

(1) new Watcher 会进行一系列赋值, 并执行this.get(), get中pushTarget -> getter -> popTarget -> cleanupDeps,

什么时候会 new Watcher? 最上面_init中 vm.$mount中会执行mountComponent,和每个组件第一次渲染也会执行mountComponent, mountComponent中会执行 new Watcher, 并将updateComponent = () => { vm._update(vm._render(), hydrating) } 当作getter传入 (即this.get()中取值执行的函数) 。 执行 this.value = this.get() 就会执行updateComponent,然后执行vm._render()(vnode->dom),就会触发属性get, 收集当前的渲染watcher。

(2) newDepIds/newDeps depIds/deps addDep/cleanupDeps 所有配合使用保证不重复添加watcher和每次set完删除无用watcher

下面例子中, A组件首次渲染时, isShow/two 的dep 中就会收集A组件作为依赖, 当它们值发生改变,会派发通知A组件重新渲染。

当改变isShow=true,如果有 cleanupDeps, 就会让two的dep删除A组件, 变成 isShow/one的dep 中收集A组件作为依赖, 此时two变化A组件不需要重新渲染

但如果没有 cleanupDeps, 就会变成 isShow/one/two 的dep 中都会收集A组件作为依赖,此时two改变还会重新渲染A组件,但这其实是不应该的(A组件在isShow=true时, 与two的值无关)

// A组件
<div v-if="isShow">
    {{one}}
</div>
<div v-else>
    {{two}}
</div>

data() {
    return {
        isShow: false,
        one: 'one',
        two: 'two'
    }
}

(3) update, 被监听的属性set时,就会触发watcher.update 一般情况,会把当前watcher推入一个watcher队列中(queueWatcher)

(4) queueWatcher 有一个全局队列 queue, 会收集当前event loop中所有需要重新渲染的watcher, 在下一轮事件循环中刷新这个queueWatcher: nextTick(flushSchedulerQueue)

(5) flushSchedulerQueue 先对queue进行排序(1.保证渲染从父到子 2.user watcher先执行 3.如果父组件watcher.run()销毁了, 子组件就没必要执行了), 然后遍历执行watcher.run()

(6) run 再次this.get(), 并触发cb,传入新值与旧值(我们watch可以拿到新值与旧值的原理)

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    this.cb = cb
    this.id = ++uid // uid for batching
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
   
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.get()
  }
    get () {
        pushTarget(this)
        let value
        const vm = this.vm
        value = this.getter.call(vm, vm)
        popTarget()
        this.cleanupDeps()
        }
        return value
    }
    // 保证不重复添加watcher
    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)
          }
        }
    }
    // 删除无用watcher 交换 newDepIds/depIds newDeps/deps 的值
    cleanupDeps () {
        let i = this.deps.length
        while (i--) {
          const dep = this.deps[i]
          if (!this.newDepIds.has(dep.id)) {
            dep.removeSub(this)
          }
        }
        let tmp = this.depIds
        this.depIds = this.newDepIds
        this.newDepIds = tmp
        this.newDepIds.clear()
        tmp = this.deps
        this.deps = this.newDeps
        this.newDeps = tmp
        this.newDeps.length = 0
    }
    update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
    }

参考文章:

juejin.cn/post/684490…

juejin.cn/post/684490…

ustbhuangyi.github.io/vue-analysi…