nsdata *data 直接赋值_<Vue 源码笔记系列1>Data 的初始化与依赖收集的准备工作

1. 前言

原文发布在语雀上,地址在这里。语雀带有大纲,阅读起来可能更舒服。

<Vue 源码笔记系列1>Data 的初始化与依赖收集的准备工作 · 语雀​www.yuque.com
0c0d7e21e87da0964d63bb386c05c1c1.png

React、Vue、Angular 流行框架让开发者从抠 DOM 变化中解脱出来。我们可以将主要的精力放在数据的变化即业务逻辑上,框架会依据数据变化灵活更新视图。

三个框架在实现上各有不同,那么 Vue 是如何做到数据变化自行更新视图呢?

要实现上述目标我们需要解决两个问题:

  1. 如何监听到数据变化
  2. 数据变化后应该更新哪些视图

2. 基础 Object.defineProperty

defineProperty 定义 get 与 set 回调,读取值的时候触发 get,改变其值时触发 set。这是一切的基础,这里不详细讲了,网上关于这部分的资源实在太多。

3. 大致流程图

先将函数执行顺序图放在这里,不需要立即了解里面的细节,放在这里只是方便接下来的阅读作为对照。

3b7c5b1cd13042c42fd0dfa79f4233ed.png

data 初始化始于 initData

// src/core/instance/state.js

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
    )
  }
  // ...
  // observe data
  observe(data, true /* asRootData */)
}

上半部分,let data = vm.$options.data 获取到 data,$options 为我们实例化 Vue 时传入的参数,当然Vue 并不会原原本本地使用我们传入的参数,会有些合并的处理,我们以后再讲。

之后判断 data 是否是纯对象,如果不是生产环境就会在打印错误。<br />isPlainObject 其实很简单,源码如下:

/**
 * Strict object type check. Only returns true
 * for plain JavaScript objects.
 */
export function isPlainObject (obj: any): boolean {
  return _toString.call(obj) === '[object Object]'
}

中间我们省略了一部分代码,大致作用是代理 data,与本文主题关系不大,以后会单独讲。

最后一行代码才是本文的核心,调用 observe 工厂函数并传入 data。observe(data, true / asRootData /)

4. observe 工厂函数

先来看下 observe 的代码:

// core/observer/index.js

/**
 * 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
}

可以看到 observe 有返回值 ob,虽然在 initData 中并没有使用,但是在之后的其他地方会有使用,我们稍微注意下即可。

9 到 11 行代码:

if (!isObject(value) || value instanceof VNode) {
  return
}

如果传入的值 value 不是对象,或者 value 是 VNode 的实例,直接 return。此时返回值为 undefined。

12 到 23 行

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

判断 value 是否存在 ob 属性,并且为 Observer 实例,是则返回该属性,这么做的目的在于防止重复依赖。<br />否则进入下一个判断<br />当 value 为数组或者纯对象时,new Observer,并且赋值给 ob,作为函数的返回值。

综上可以看出来 observe 的返回值有两种,一个是 undefined,一个是 Observer 的实例。

observe 函数只干了两件事:

  1. 调用 new Observer(value)
  2. 返回 Observer 实例 ob

注意:
只有数组和纯对象才会往下走 new Observer,进而有机会

下一步我们看看 class Observer 干了些什么。

5. Observer 观察者类

Observer 源码如下:

// src/core/observer/index.js

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    // ...
  }

  walk (obj: Object) {
    // ...
  }

  observeArray (items: Array<any>) {
    // ...
  }
}

可以看到 Observer 类有三个实例属性 value、dep、vmCount。一个构造函数。两个实例方法 walk、observeArray。

5.1 构造函数

构造函数代码如下:

constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__', this)
  if (Array.isArray(value)) {
    const augment = hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}

第3行:<br />this.dep = new Dep()<br />Dep 中会存储依赖,也包含触发依赖更新的方法,之后我们会详细讲,这里我们先认为他是一个盒子,提供一个 depend 方法收集相关依赖,提供 notify 用以遍历所有依赖依次触发。

第5行:<br />def(value, ‘ob‘, this)<br />给 value 添加 ob 属性,值为 Observer 实例。<br />如果对上一节 observe 工厂函数段落还有印象的话,我们提到过这么一段代码:

// 来自 observe 方法
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
  ob = value.__ob__
}

当时我们提过这个判断是为了防止重复依赖,这里 value.ob 的值来源就是 def(value, ‘ob’, this) 。<br />def 方法源码如下:

/**
 * Define a property.
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

使用 defineProperty 为对象添加属性,并且可以设置该属性是否可遍历,默认为 false 不可遍历。这里使用 def 的主要目的就是防止遍历到 ob 属性。

6 到 14 行:

if (Array.isArray(value)) {
  const augment = hasProto
    ? protoAugment
    : copyAugment
  augment(value, arrayMethods, arrayKeys)
  this.observeArray(value)
} else {
  this.walk(value)
}

这段逻辑比较简单,value 为数组调用 this.observeArray 否则调用 this.walk 。

5.2 实例方法 this.walk

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

遍历对象,对每一个属性调用 defineReactive

<a name="UqlBS"></a>

5.3 实例方法 this.observeArray

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

遍历数组,对数组的每一项重新走 observe, 这里又回到了 第四节 observe 工厂函数。

<a name="nu2oI"></a>

5.4 小结

Observer 主要干了以下几件事:

  1. 给传进来的 value 添加 ob 属性,值为自身实例
  2. value 为对象时为每一个属性调用 defineReactive
  3. value 为数组时为每一项调用 observe

假如现在有 data 如下:

const data = {
  data1: 1,
  data2: {
    a: 2
  },
  data3: [ 3, { b: 4 } ],
}

经过处理后

const data = {
  data1: 1,     // 基本数据类型,在 observe 时直接 return,没有 __ob__
  data2: {
    a: 2,
    __ob__: { value, dep, vmCount }
  },
  data3: [
    3,
    { b: 4, __ob__: { value, dep, vmCount } }
    // __ob__: { value, dep, vmCount }  这里__ob__ 为 data3 的属性,而不是数组的(ˇˍˇ) 想~
   ],
  // __ob__ 不可枚举
  __ob__: {
    value: data,
    dep: Dep 实例 new Dep(),
      vmCount: 0,
  }
}

6. defineReactive 函数

Observer 类实例化过程中会调用 defineReactive , 先看一下该方法的源码:

// src/core/observer/index.js

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
      // ...
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // ...
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

大致能看到,这个方法主要目的将 data 的属性变为响应式属性(添加 get set 方法)。

第 10 行:<br />const dep = new Dep()<br />Observer 类的构造函数有这样一句 this.dep = new Dep() ,当时我们讲 Dep 提供 depend 方法收集依赖,提供 notify 遍历依赖并触发。但是这两个 dep 仍有些不同。稍后我们再讲。

12 到 15 行:

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

getOwnPropertyDescriptor 为 js 方法,用于获取属性描述对象。接着判断如果 configurable 为 false,直接终止。因为 configurable 为 false 时是无法设置 get set 的。

18 到 22 行:

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

我们先缓存 obj[key] 原本的 get set 方法。<br />接下来的判断条件比较奇怪,我们先看后边的 arguments.length = 2 ,这个比较好理解,只有两个参数时,我们才需要使用 val = obj[key] 获取值。<br />另一个判断条件 (!getter || setter) ,如果用户之前已经定义了 obj[key] 的 get 属性,那么我们就不再使用 val = obj[key] 取值,因为这里取值将会触发用户定义的 get 方法。当真正使用值的时候,我们调用缓存的 getter 来取值。<br />这样会产生一个问题,用户已经定义过 get 时,我们的 val 为 undefined,会导致下一步 childOb 为 undefined。也就是说我们不会对这个已定义 get 的属性深度观测。<br />还有另一个问题,当我们将 obj[key] 变为可观测后,会定义 get set 方法,仅仅按照有 get 就不进行深度观测,这里就会有问题,所以这里又补上了 如果有 set 的话,也要取值,进行深度观测。<br />最终判断条件就成了 (!getter || setter) && arguments.length = 2

第 24 行:<br />let childOb = !shallow && observe(val)<br />又又又调用 observe 工厂函数。我们多次提到过,observe 传入的 val 不是数组和纯对象时,返回 undefined,是数组或纯对象时,返回 val.ob 属性,该属性为 Observer 实例,具有 dep 属性,为 Dep 实例。<br />执行这一句就是要对数组和纯对象进行深度观测。

剩下的部分:<br />定义 get set 方法。<br />先来看看 get:

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

Dep.target 就是我们需要收集的依赖,之后会详细讲。<br />调用 dep.depend() 来收集依赖。注意这里的 dep 为第 10 行代码声明的 dep,当时我们说它和 Observer 类 this.dep = new Dep() 生成的 dep 不太一样。这里的 dep 在 get set 中被使用,作为闭包存在,它针对的是纯对象的每一个属性,每一个属性都有,不论它的值是数字、字符串这样的基本数据类型还是数组、对象。<br />Observer 实例上的 dep,因为只有数组、纯对象有 ob 属性,所以只有数组、纯对象有该 dep。<br />当 childOb 存在时,我们再次将该依赖收集到 childOb。<br />如此一来对于数组对象来说岂不是同一个依赖被收集了两次?没错,在闭包 dep 中一次,在 childOb 中一次。为什么要收集两次呢?<br />假设有如下 data:

const data = {
  a: {
   b: 1
  }
}

首先是 observe(data)<br />接着 Vue 会遍历 data,对每一个属性调用 observe。即在 defineReactive 中 childOb = observe(data.a),获得其_ob 属性,我们记为 childOb1<br />进入 observe(data.a),会给 data.a 添加 _ob 属性。此时 data 如下:

const data = {
  a: {
   b: 1
   __ob__: { value, dep, vmCount }  // 注意此时,dep 只是空壳子,因为我们还没有调用 depend 收集依赖
  }
  // 这里也有 __ob__,为了简化描述,我们将重点放在 data.a 所以这里的 __ob__ 我们略过不谈
}

接着往下走,new Observer 的时候会遍历 data.a ,调用 defineReactive ,也就是对 data.a.b 调用 defineReactive, 在 defineReactive 中我们使用闭包保存了 data.a.b 的依赖。此时的 childOb = observe(data.a.b) 为 undefined。我们记为 childOb2<br />执行完毕后,会返回到刚刚进入的节点接着往下走,此节点已在上方标注为蓝色。返回到这里后,childOb1 的值也就出现了,为 data.a.ob。<br />再往下走,进入 get 设置

if (childOb) {
  childOb.dep.depend()   // 之前只是定义了 obj[key].__ob__.dep,这里才是真正收集依赖
  // ...
}

调用 data.a.ob 的 dep 的 depend 方法收集依赖。<br />如此依赖,针对 data.a 我们既有闭包收集的依赖,又有 data.a.ob 收集的依赖。因为为 object 添加或者删除属性时,set 方法不会被触发,所以我们使用 ob 保存一份依赖,当 obj.a 添加或者删除属性的时候,手动触发依赖。这才有了 Vue.set Vue.delete 方法。<br />另外针对数组的 push shift 等会改变原有数组的变异方法,Vue已经做了代理,调用这些方法时,Vue 会帮我们手动触发 ob 保存的依赖。

我们还剩下一句没讲:

if (Array.isArray(value)) {
  dependArray(value)
}

dependArray 其实很简单

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

如果value 是数组的话,我们需要遍历数组,对数组的每一项收集一次依赖。

再看看 set:

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()
  }
  if (setter) {
    setter.call(obj, newVal)
  } else {
    val = newVal
  }
  childOb = !shallow && observe(newVal)
  dep.notify()
}

4 5 6 行:<br />如果 newVal = value,即值没有改变的时候,放弃执行。<br />newVal ! newVal && value !== value 如果值为 NaN,也放弃执行更新。

接下来就是执行 setter,设置新值。<br />针对新值再次调用 observe 进行观测。<br />最后调用闭包 dep.notify() 通知更新。

defineReactive 方法主要作用是将数据变为可观测的。它针对object每个属性生成闭包保存依赖,并且调用数组或纯对象数据的 ob.dep.depend 再另存一份依赖以供 Vue.set Vue.delete 已经数组的编译方法使用。

7. 小结

结合第三节的流程图,我们通篇主要涉及的只有三个点。

  1. observe 工厂函数。针对数组、纯对象,调用 new Observer,并返回 Observer 实例,其他数据类型返回 undefined。
  2. Observer 类。为数据添加ob属性,值为自己的实例。针对对象类型数据的每一个属性调用 definedReactive,针对数组类型数据,调用 observe。
  3. definedReactive 方法。为数据设置 get set,使其可观测,并且产生闭包,保存依赖。同时触发Observer 类为数据添加的ob属性上的 dep.depend,另存一份依赖,以供 Vue.set Vue.delete 代理数组变异方法时使用。

到此,算是大致过了一遍 data 的初始化,其实 data 的初始化就是为其依赖收集做准备工作。注意,我们只是准备好了去收集依赖,也即定义好了 get。下一步需要有人触发 get,比如 render?这些我们放在另外的章节来讲。

8. 参考文献

  1. Vue 技术内幕
  2. 【Vue原理】响应式原理 - 白话版
  3. 【Vue原理】依赖收集 - 源码版之基本数据类型
  4. 【Vue原理】依赖收集 - 源码版之引用数据类型
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值