data的值 如何初始化vue_理解Vue响应式系统

本文深入探讨Vue响应式系统,包括数据劫持、_init初始化过程、Observer类、defineReactive方法以及依赖收集和变更通知机制。通过对Vue实例创建、data选项的处理和Watcher实例的创建,揭示了Vue如何实现数据的响应式更新。
摘要由CSDN通过智能技术生成

深入理解 Vue 响应式系统

理解 Vue 响应式原理,到 computed、vuex 原理

前言

众所周知,一说到 vue 的响应式系统,就能马上想到 Object.defineProperty、数据劫持、getter/setter 这些内容,是的,毕竟 官方文档 已经把基本原理介绍的很清楚了

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。

了解了基本工作原理,但是我仍然有两个问题

  • 内部是如何实现追踪依赖和通知变更的?
  • 了解了 data 选项如何实现响应式,那么 computed 的原理是什么?
  • vuex 的 state 也是响应式的,其内部原理又是什么?

工作流程

引用官方文档一段内容

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

再上官网一张图

994908ca3334257e4f71d67e14b278f0.png

官方文档解释实在是太清晰了,我觉得我再多说一句都是废话了

到这里就可以知道了,响应式系统内部是依靠 Watcher 来实现的,那接下来就来看看 Watcher 的实现?不,现在直接看还有点懵,我们先按文档介绍的过程来逐步分析

  1. 遍历 data 选项的所有属性,把这些属性转为 getter/settter,即所谓的数据劫持
  2. 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。
  3. 当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

实现细节

数据劫持

Vue 将 data 的属性转为 getter/setter 的工作,是在初始化一个 Vue 实例的时候完成的,接下来快速过一遍初始化的过程,找到我们要看的地方。

初始化 Vue 实例 _init

传入 options 后,调用 this._init 并传入 options,initMixin 的作用就是在 Vue 的原型对象扩展了 _init 方法

function Vue (options) {
  // ... 省略
  this._init(options)
}
initMixin(Vue)

来看 initMixin,做了三件事

  1. 处理options,这里只需知道,传入的 options,被合并到了 vm.$options 里了
  2. 初始化和调用生命周期函数,initState 就是对状态进行初始化的
  3. 挂载组件实例,对!就是文档说的那个组件实例,可以猜测这里和 Watcher 有关系
export function initMixin (Vue: Class<Component>) {
    Vue.prototype._init = function () {
        // 处理 options
        vm._isVue = true
        if (options && options._isComponent) {
            initInternalComponent(vm, options)
        } else {
            vm.$options = mergeOptions(
                resolveConstructorOptions(vm.constructor),
                options || {},
                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')

        // 挂载组件
        if (vm.$options.el) {
            vm.$mount(vm.$options.el)
        }
    }
}

初始化状态 initState

可以看到,传入的options合并到 vm.$options 后,所有选项就都从 $options 获取了

initState 依次对 props, methods, data, computed, watch 进行初始化,这些都是我们熟悉的 API 了,initData 就是对 data 选项进行数据劫持的地方了

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

初始化 data 选项

传入 data 选项也要经历检查和初始化

  1. 获取 data 对象,如果是函数,则获取函数返回的结果,data必须为 plain object
  2. 将data的属性代理到 Vue 实例上,这就是能直接通过 vm.a 访问 data 的属性的原因 详见文档
  3. observe 观察数据!马不停蹄看下来!!
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
  }
  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 (props && hasOwn(props, key)) {
      // ...省略警告提示
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

observe

observe 函数主要工作是:对于可扩展的对象或数组,在其内部生成一个Observer实例并返回,包括以下几点

  • 对传入的 values (可以是data 选项,或者子对象、子数组,普通属性)进行类型判断,非对象或VNode实例,则返回空,
  • 根据 __ob__ 属性判断是否已设置观察者,有则直接返回
  • 判断类型为数组或对象,且可扩展的,使用 new 创建 Observer 实例,传入当前 value,返回该实例对!象
那么,问题来了 __ob__ 是什么?哪里来的? Observer 实例对象又是什么?有什么用?
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
}

class Observer

这是 Observer 的源码

  • def(value, '__ob__', this) 就是将当前的 Observer 实例挂到 value.__ob__ 上,每个对象(或数组)会对应一个 Observer 实例,保存在自身的 __ob__ 属性上,可以方便获取到
  • Observer 会 new 一个 Dep 实例,并保存到 this.dep 上,这个的作用稍后讲
  • 如果value是数组,则对数组的 变异方法进行包裹 ,然后调用 observeArray 遍历数组调用 observe
  • 如果value是对象,则遍历对象的属性,调用 defineReactive ,定义响应式属性

值得注意的是

Vue 不会对数组的索引调用 defineReactive,要知道 Object.definePrototype 是可以对数组索引值的变动的,但由于性能代价和用户收益不成正比,所以这里直接跳过了
Observer 实例会保存一个 Dep 实例,Dep 是 dependent 的缩写,表示依赖,一个对象对应一个 dep,就是一个依赖
export 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 (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

设置响应式属性,就是把属性转为 getter/setter

代码有点多,分步来看,先看整体,参数有点多,目前只需用到前两个

export function defineReactive (obj, key, val, customSetter, shallow) {
    // ...
}

接下来是函数内部

创建一个 Dep 实例,由于后面的getter/setter 有访问到这个变量,会形成一个闭包,上面说到 dep 是依赖,也就是说,一个属性就是一个依赖

const dep = new Dep()

判断是否可设置 getter/setter ,因此使用 Object.frezze 可以阻止修改现有的属性,也意味着响应系统无法再追踪变化。

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

预存

const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
  val = obj[key]
}

上面说到 observe 会创建并返回一个 Observer 实例对象,那这里就是对子对象递归设置观察者,并获取到观察者实例,下面会有用

let childOb = !shallow && observe(val)

定义存取描述符 getter/setter,里面做了这几件事

  • 上面预存是为了这里正常完成对属性值的读写
  • setter 中判断如果新值和旧值相等,则不会触发更新,newVal !== newVal 是考虑 NaN 的情况
  • setter 中,childOb = !shallow && observe(newVal) 如果修改的值是对象或数组,则需要对其进行 observe
  • 然后接下来就是依赖收集通知变更
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
    // 考虑到 NaN !== NaN 的情况
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return
    }
    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()
  }
})

依赖收集

这里说的依赖,就是把当前属性作为一个依赖(dep)收集起来,如果这个属性的值变化了,就去通知订阅了这个依赖的订阅者

我们把依赖收集的代码抽出来看

  • dep 是上面说到的 Dep 实例,dep.denpend() 就是收集依赖了
  • childOb 是一个子对象的 Observer 实例,我们知道,它内部也保存了一个 Dep 实例,可以递归收集依赖

那么问题来了,先把问题抛出来,后面来逐个解决

  • 依赖收集到哪去,订阅者是谁?
  • Dep.target 是什么?到目前为止还没看过这个变量,这里应该怎么收集依赖?
  • 对于子对象,又递归收集了一遍依赖,是否会重复?
if (Dep.target) {
  dep.depend()
  if (childOb) {
    childOb.dep.depend()
    if (Array.isArray(value)) {
      dependArray(value)
    }
  }
}

通知变更

哪个属性值被修改了,就由它自身去发出通知

同样把问题先抛出来

  • 如何通知变更
dep.notify()

小结

回看上面的分析步骤,第一步,遍历 data 选项的所有属性,转为 getter/setter 已经完成,留下的问题就是如何收集依赖和通知变更,所有的问题都指向 Dep.targetDep 、以及一开始说到的 Watcher, 这三者是什么关系,又是如何互相配合的?

创建 Watcher 实例

接下来看第二步

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。

回看 _init 方法最后一步是挂载组件

// 挂载组件
if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

组件挂载,即经历了编译渲染成为真实DOM,这个过程会去读取 data 选项的属性值,这个时候就会触发 getter ,然后收集依赖

$mount 定义在 src/platforms/web/entry-runtime-with-compiler.js 这里主要是将 template 转为 render ,然后调用 mount

mount 其实是 src/platforms/web/runtime/index.js 上定义的 $mount 方法,这样做是为了方便复用,不同的平台都基于这个来封装,这个 $mount 直接调用了 mountComponent

接下来看看 mountComponent 方法

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // ...
  callHook(vm, 'beforeMount')
  // ...
  const 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 && !vm._isDestroyed) {
        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
}

mountComponent 就是用来挂载组件实例的,首先调用了 beforeMount,然后 new Watcher,最后调用 mounted ,因此,组件实例挂载前会创建一个 Watcher 实例,并完成挂载

那么来看看 new Watcher 传入什么参数

  • vm: vue 实例
  • updateComponent: 这个方法调用了 _update, 传入了 render 方法执行后的结果,因此这个方法是用于挂载和更新组件实例的,执行render方法就会触发依赖收集
  • noop: 不执行操作
  • before: 方法内判断组件实例已挂载且未销毁的情况下,调用 beforeUpdate ,可以猜测,组件实例更新前会调用这个方法

再来看看 Watcher 的源码

export default class Watcher {
  constructor (vm, expOrFn, cb, options) {
    if (typeof expOrFn === 'function') {
        this.getter = expOrFn
    } else {
        // ...
    }
    this.value = this.lazy ? undefined : this.get()
  }
}

这里省略了大部分的源码,上面组件挂载前 new Watcher 之后,主要是执行了上面这部分代码,可以看到这里把传入的 updateComponent 保存在 this.getter,最后调用了 this.get, 下面来看看 get

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (err) {
  } finally {
    // ...
    popTarget()
    this.cleanupDeps()
  } 
}

上面代码中,主要有以下几个操作

  1. 执行 this.getter(),即上面的 updateComponent
updateComponent 是用来挂载和更新组件的,保存在 Watcher 实例里,由 Watcher 来管理执行更新的时机,这也印证了上面说到的: 当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。 那么, Watcher 如何被准确通知到呢?答案是依赖 Dep.target 来确定数据对应的 Watcher 实例
  1. 执行 this.getter 的前后分别 pushTarget(this)popTarget() ,这里的 this 其实就是当前的 Watcher 实例
这两个方法是全局方法,是用于修改全局变量 Dep.target 的,依赖收集的时候,会判断是否有 Dep.target ,有才会进行依赖收集,而 Dep.target 就是每次执行 getter 方法时的 Watcher 实例(组件实例对应的是 updateComponent 方法)
而执行 updateComponent 方法又会执行 vm._render(),这个时候就会触发数据的 getter ,然后开始收集依赖。那么,收集依赖的过程是怎样的呢? 答案是依赖数据内部各自持有的 Dep 实例

```javascript Dep.target = null const targetStack = []

export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target console.log('targetStack:', targetStack) }

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

  1. 最后执行了 this.cleanupDeps

结合 updateComponentbefore 可以看出,组件的挂载和更新,是在 Watcher 实例里面执行的,这也对应了开头说的第三个步骤

这个的作用需等到 依赖收集讲完再来看~

======== 未完待续 =========

依赖收集

通知变更

computed 原理

Vuex 原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值