Vue源码分析 - 响应式系统(上)

34 阅读38分钟

前言

在上一篇文章中,我们已经了解了 Vue 变化侦测的基本流程,如果你还没看,建议先看看(Vue源码分析 - 响应式系统(前言篇)掘金),因为这有助于你更高效的理解下边要分析的源码,那我们就进入正题,开始分析源码吧~

在从入口到 new Vue 构造函数的完整流程中那篇文章中我们知道 Vue 做了一系列初始化,在 initMixin 中调用了initState 函数,它是初始化状态的核心方法(初始化主要包括 data、props、computed、watch)。

在开始前,我们先来分析实现变化侦测的 proxy 函数,核心的特性就是我们上一篇文章提到的 Object.defineProperty。

proxy

const sharedPropertyDefinition = {
  enumerable: true, // 属性可枚举
  configurable: true, // 属性可配置
  get: noop, // get 函数
  set: noop // set 函数
}
/**
 * target:目标对象
 * sourceKey:源对象属性 key
 * key:目标对象属性 key
 */
export function proxy(target: Object, sourceKey: string, key: string) {
  // 属性描述符 get 函数
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }
  // 属性描述符 set 函数
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val
  }
  // 当访问目标对象的 key 属性时,会触发 get 函数,返回源对象 sourceKey 的 key 属性值
  // 同样 set 函数也会往源对象 sourceKey 的 key 属性上赋值
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

我们先明确下:在 initData 和 initProps 函数里调用 proxy 函数时传入的 target 参数是 vm,在组件里 vm 就是 this

proxy 函数很简单,就是使用 Object.defineProperty 在 target(vm / this) 上定义 key 属性,然后属性描述符是 sharedPropertyDefinition 对象,sharedPropertyDefinition 对象上有 get、set 函数,假设现在的 sourceKey 是 _props,当我们使用 this.xxx 访问时会触发 get 拦截函数,实际上返回的是 this._props.xxx,当我们使用 this.xxx = val 赋值时,会触发 set 拦截函数,实际上是给 this._props.xxx 赋值。说白了就是做了一层中间的代理。

我们就顺着 initState 内部的初始化流程来分析:

image.png

首先是 vm.$options 获取了 vm 实例上的配置项,比如我们写一个组件:

<template>
    <div>{{ name }}</div>
</template>
<script>
export default {
    data() {
        return {
            name: 'hello, world'
        }
    },
    props: {
        age: {
            type: Number;
            default: 18
        }
    },
    computed: {},
    watch: {},
    methods: {
        getName() {
            return this.name;
        }
    }
}
</script>

那获取的 vm.$options 就包含了上边定义的 data、props、methods、computed、watch 所有选项。

首先判断的是 props 是否为空,不为空的情况下就调用 initProps,第一个参数是 vm,第二个参数是当前的 props 选项。

// 如果有 props,调用 initProps 初始化(此处的 props 是做了归一化后的)
if (opts.props) initProps(vm, opts.props)

initProps

initProps 中的第一行是获取 vm.$options 上的 propsData,propsData 是指父组件传递给子组件的 props 数据,父组件没有传就是空对象 image.png 接着定义 props 以及 vm._props 属性,默认值是一个调用 shallowReactive 处理的空对象 image.png shallowReactive 会基于传入的这个空对象浅拷贝一个新对象,然后仅将新对象的第一层变为响应式,比如下边例子,obj 对象的第一层是响应式的,即修改 a 的值会通知依赖该数据的视图更新,而修改 obj.b.c 则不会通知依赖更新。

obj = {
    a: 1,
    b: { // 第二层往下都不具备响应式
      c: 2
    }
}

接着定义 keys 和 vm.$options._propKeys 属性为空数组: image.png

定义 isRoot 属性,取值取决于 vm.$parent,!vm.$parent 表示 vm 实例没有父实例了,那当前 vm(组件实例)就是根实例。如果不是根实例 !root,会调用 toggleObserving(false) 禁用监视。为什么要有监视呢?因为当父组件传给子组件的 propsData 变化了,可以侦测到并且让子组件重新渲染。但如果当前 vm 实例不是父级,那就关闭监视,因为当前子组件的 props 上的属性已经在父组件中已经递归监视过了,没必要重复递归监视。

image.png

接着枚举 propsOptions 上的属性,往 keys 数组中推送 key 属性,然后调用 validateProp 逐个属性进行校验 image.png

validateProp

接收四个参数:

  • key 当前属性
  • propsOptions 当前的 props 选项
  • propsData 父组件传的 props 数据
  • vm 当前实例
// 获取当前属性值
const prop = propOptions[key]
// absent:不存在 / 缺席的
// 这里表示这个 key 属性不在 propsData 自身上,不会往原型链上去找
const absent = !hasOwn(propsData, key)
// 拿到父组件传的属性值
let value = propsData[key]

hasOwn 会判断当前属性是否是当前对象的自有属性,什么是自有属性,就是不会往原型链上查找,仅在对象自身查找,如果对象自身没有就返回 false,反之就是 true。

absent 这个单词直译过来就是缺席的/不存在的,也就是这个属性在 propsData 对象自身未找到,那 absent 就是 true,表示缺席的。

接着是下面这段判断逻辑:

// 判断 prop.type 是不是布尔类型
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) { // 是布尔类型
    // propsData 中有这个 key 并且这个 prop 属性上的 default 属性不在 prop 属性本身,可能通过原型链继承而来的
    if (absent && !hasOwn(prop, 'default')) {
      // 属性值赋值为 false
      value = false 
    } else if (value === '' || value === hyphenate(key)) { // 如果 propsData 中属性的值是空串或者属性值是一个预定义的 key
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      // 只有在布尔类型优先级更高的情况下,才允许将空字符串或同名变量转换为布尔值
      // 判断 prop.type 是不是字符串类型
      const stringIndex = getTypeIndex(String, prop.type)
      // prop.type 不为 String 类型或者 prop.type 的布尔类型所在索引小于字符串类型
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        // 属性值赋值为 true
        value = true
      }
    }
  }

getTypeIndex 函数接收两个参数,第一个是设定的类型,第二个是期望的类型,也就是当前属性值的 type 属性,注意这里的 props 已经经过归一化了(在 mergeOptions 函数中调用过 normalizeProps 归一化)。每个 prop 属性对应属性值是一个对象(里边含有 type 属性和 default 属性)。

/**
 * 
 * @param type 默认 type
 * @param expectedTypes 期望的 type
 * @returns 相同 type 的索引
 * - 不是数组:如果两个 type 相同返回 0,反之就返回 1
 * - 是数组:找到相同 type 时所在数组位置上的索引,找不到返回 -1 
 */
function getTypeIndex(type, expectedTypes): number {
  // 不是数组的前提下
  if (!isArray(expectedTypes)) {
    // 判断是不是相同的类型,相同返回 0,不同返回 1
    return isSameType(expectedTypes, type) ? 0 : -1
  }
  // 如果是数组,遍历返回相同类型所在数组位置上的索引,找不到返回 -1
  for (let i = 0, len = expectedTypes.length; i < len; i++) {
    if (isSameType(expectedTypes[i], type)) {
      return i
    }
  }
  return -1
}

首先判断属性值的 type 属性是不是一个数组,不是的情况下,返回 0 或 1,取决于调用 isSameType 函数的返回值。

const functionTypeCheckRE = /^\s*function (\w+)/
function getType(fn) {
  // 有捕获组情况下,第一个参数是正则匹配到的所有内容,第二个参数是捕获组的内容
  const match = fn && fn.toString().match(functionTypeCheckRE)
  // 返回函数名称或者空串
  return match ? match[1] : ''
}

function isSameType(a, b) {
  return getType(a) === getType(b)
}

getType 接收 fn 参数,调用 toString 后然后匹配 functionTypeCheckRE 正则,这个正则会通过函数字符串去匹配函数名称,比如我们上边传的第一个参数是 Boolean,Boolean 是 JavaScript 的内置函数,调用 toString 会返回如下函数字符串:

console.log(Boolean.toString()) // function Boolean() { [native code] }

那么 match[1] 就会匹配到函数名称 Boolean 了。所以isSameType 最终就是通过判断两个类型对应函数字符串的函数名称是否相等来作为返回值,也就是说如果属性值的 type 也是 Boolean 类型的话,那 isSameType 就返回 true,最终 getTypeIndex 就返回 0,如果 prop 的 type 属性值不是 Boolean 就不相等,返回 1。

那么对于传给 getTypeIndex 的第二个参数,也就是当前属性值的 type 属性是个数组,这种情况也不少见,比如我们在组件中这样写:

<script>
export default {
    props: {
        flag: {
            type: [Boolean, String];
            default: false
        }
    },
}
</script>

那就走下边遍历数组的逻辑,同样去数组中找类型为设定的 Boolean 的元素,找到就返回其在数组中的索引。以上边例子为例,匹配上的数组元素是第一个,也就是索引 0。

function getTypeIndex(type, expectedTypes): number {
  ...
  // 如果是数组,遍历返回相同类型所在数组位置上的索引,找不到返回 -1
  for (let i = 0, len = expectedTypes.length; i < len; i++) {
    if (isSameType(expectedTypes[i], type)) {
      return i
    }
  }
  return -1
}

如果都没匹配上就返回 -1。 分析了这么久 getTypeIndex 函数,核心就是判断第一和第二个参数类型是否一致,一致说明 type 属性符合 Boolean 类型,booleanIndex 就是 0,不一致返回 -1,下边判断 booleanIndex > -1(也就是 type 是 Boolean 的情况)

 if (booleanIndex > -1) { // 是布尔类型
    // 父组件没有这个属性且属性值没有设置 default 默认值
    if (absent && !hasOwn(prop, 'default')) {
      // 属性值赋值为 false
      value = false 
     } else if (xxx) {...}
 }

上边就是父组件没有传这个 prop 属性且子组件的 props 上这个属性对应的属性值没有指定 default 默认值,那就将 value 设为 false。这很好理解,你没有默认值,而且你还是 Boolean 类型的,那我就给你默认值 false 吧。

接着是 else if 分支,判断 value 是不是空串的(父组件传的 prop 属性值)或者传的 value 是不是和调用 hyphenate 函数返回的值严格相等

if (booleanIndex > -1) { // 是布尔类型
    // propsData 中有这个 key 并且这个 prop 属性上的 default 属性不在 prop 属性本身,可能通过原型链继承而来的
    if (absent && !hasOwn(prop, 'default')) {
      // 属性值赋值为 false
      value = false 
    } else if (value === '' || value === hyphenate(key)) { // 如果 propsData 中属性的值是空串或者属性值是一个预定义的 key
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      // 只有在布尔类型优先级更高的情况下,才允许将空字符串或同名变量转换为布尔值
      // 判断 prop.type 是不是字符串类型
      const stringIndex = getTypeIndex(String, prop.type)
      // prop.type 不为 String 类型或者 prop.type 的布尔类型所在索引小于字符串类型
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        // 属性值赋值为 true
        value = true
      }
    }
  }

看下 hyphenate 函数:

/**
 * \B 表示非单词开头的
 * ([A-Z]) 表示匹配大写字母
 * $1 表示捕获组的内容
 */
const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cached((str: string): string => {
    // 将字符串的捕获部分替换成连字符 - 以及捕获的那个单词,最后还要转为小写形式
  return str.replace(hyphenateRE, '-$1').toLowerCase() // fooBar => foo-bar
})

其实就是将驼峰命名转换为短横线命名。上边调用 hyphenate 传入的参数是 key 属性,那么 value === hyphenate(key) 即表示 value 值是不是和其短横线形式的属性名严格相等,满足其中一种都能进判断,调用 getTypeIndex,这次是传入 String,找到 prop.type 中类型同为 String 的索引位置。如果小于 0 或者 找到的 stringIndex 要大于 booleanIndex,说明 booleanIndex 在 stringIndex 前面,将 value 设为 true。booleanIndex < stringIndex 的情况如下:

<script>
export default {
    props: {
        flag: {
            type: [Boolean, String];
            default: false
        }
    },
}
</script>

也就是 Boolean 类型的索引是 0,String 类型的索引是 1,0 < 1,所以 Boolean 在前边优先级会更高,那将 value 默认设为 true。

我们来捋一下这两个 if 分支,首先 booleanIndex > -1,即当前属性声明的 type 为布尔类型,第一个分支是父组件没传这个属性值且当前组件声明的属性内也没有 default 提供默认值,这种情况直接赋值 false 默认值。第二个分支就可能是父组件传了,或者当前组件声明了 default 默认值,这时候判断传的或是 default 设定的 value 是不是个空串或是 value 和 key 属性名的短横线命名形式严格相等,如果满足其中一个条件,获取这个属性对应 type 的 stringIndex,如果获取不到或者优先级比 booleanIndex 小(即 boolean < stringIndex,谁在前谁大),那就赋予 value 默认值 true。

image.png 不进入判断那就是 booleanIndex < 0 的情况,也就是属性声明的 type 属性值不是布尔类型,判断value 是不是 undefined(即父组件没有传),如果是则调用 getPropDefaultValue 来获取一个默认值。 getPropDefaultValue 接收当前 vm 实例,prop 选项,key 属性,这里传的 prop 选项就是当前的 prop 属性(属性内有 type、default 属性)

// check default value
// 检查默认值,当父组件没有传递 props 属性
if (value === undefined) { // prop.type 不是布尔类型的情况 value 可能为 undefined
    /**
     * value 取值:
     *   - prop 上没有 default 属性,为 undefined
     *   - 上一次的默认值
     *   - 调用 default 指定的工厂函数
     */
    value = getPropDefaultValue(vm, prop, key)
    ...
}
/**
 * Get the default value of a prop.
 */
function getPropDefaultValue(
  vm: Component | undefined,
  prop: PropOptions,
  key: string
): any {
  // no default, return undefined
//   对象上没设置 default 属性,返回 undefined
  if (!hasOwn(prop, 'default')) {
    return undefined
  }
//   获取设置的默认 default 值
  const def = prop.default
  // warn against non-factory defaults for Object & Array
//   如果是一个对象,抛出错误信息,对象必须使用工厂函数来返回默认值
  if (__DEV__ && isObject(def)) {
    warn(
      'Invalid default value for prop "' +
        key +
        '": ' +
        'Props with type Object/Array must use a factory function ' +
        'to return the default value.',
      vm
    )
  }
  // the raw prop value was also undefined from previous render,
  // return previous default value to avoid unnecessary watcher trigger
  if (
    vm &&
    vm.$options.propsData &&
    vm.$options.propsData[key] === undefined &&
    vm._props[key] !== undefined
  ) {
    // 之前缓存的数据
    return vm._props[key]
  }
  // call factory function for non-Function types
  // a value is Function if its prototype is function even across different execution context
//   如果默认值是个工厂函数且 props 的 type 属性不是一个函数,拿到调用该函数的返回值,反之就去 default 属性值本身
  return isFunction(def) && getType(prop.type) !== 'Function'
    ? def.call(vm)
    : def
}

getPropDefaultValue 函数也很简单,属性本身也没有设置 default 属性就返回 undefined,有的话就校验设定的这个 default 值是不是一个对象形式,是的话要抛出错误提示,因为 default 默认值是对象或数组的情况,必须要写成一个工厂函数然后返回这个对象或数组的形式。校验结束后下边就尝试从 vm._props 中找这个 key 对应的值,_props 是之前初始化时缓存过的,有的话就返回之前缓存的这个值。没有继续往下看,最后会根据这个 default 默认值的类型来决定返回值,是一个工厂函数就返回调用绑定好上下文 vm 的这个函数的结果,不是函数就直接返回 default 默认值本身。

接着是这段代码:

// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)

上面已经对父组件传来的 value 值进行一系列校验和赋予默认值了,接着需要对这个 value 值进行监视,当这个 value 值发生变化时才能侦测到从而重新渲染子组件。

下面就调用 assertProp 进行断言

if (__DEV__) {
    assertProp(prop, key, value, vm, absent)
}

传入的参数,prop:属性值(是一个对象包含 type、required、default),key:属性名,value:处理后的父组件传过来的属性值,vm:当前实例,absent:父组件是否传了该 prop 属性,分析代码:

// 属性值的 required 设为 true 且父组件没有传这个属性,抛出警告信息
if (prop.required && absent) {
    warn('Missing required prop: "' + name + '"', vm)
    return
}
// 最终这个 value 是 null 且属性值的 required 为 false 非必传,直接返回不往下走
if (value == null && !prop.required) {
    return
}

如果属性的 required 为 true(即必传)且 absent 为 true(表明父组件没有传),这种情况抛出警告。接着经过前面处理 value 还为 null,那就是一种情况,父组件没有传这个属性,且 prop 属性的 required 为 false(即非必传)直接返回,不往下走了。

// 属性的 type 属性
let type = prop.type
/**
 * 两种情况:
 *   - valid 取值为 true:没有 type 或者 type 是 true
 *   - valid 取值为 false:有 type 或者 type 不是 true
 *  */ 
let valid = !type || (type as any) === true
const expectedTypes: string[] = []
// 如果有 type
if (type) {
    // 不是一个数组
    if (!isArray(type)) {
        // 放到数组中
        type = [type]
    }
    // 遍历 type 数组,前提是 valid 为 false
    for (let i = 0; i < type.length && !valid; i++) {
        const assertedType = assertType(value, type[i], vm)
        expectedTypes.push(assertedType.expectedType || '')
        valid = assertedType.valid
    }
}

assertType 函数,传入参数 value:经过处理的父组件传的值,type[i]:当前数组中第 i 个位置 type 的值,vm:当前实例

/**
 * 
 * @param value 当前父组件传的值
 * @param type 当前 type 数组索引上元素的类型
 * @param vm 当前组件实例
 * @returns {valid, expectedType}
 */
// 断言类型:父组件传值的类型和子组件期望类型相等 valid 为 true,反之为 false
function assertType(
  value: any,
  type: Function,
  vm?: Component
): {
  valid: boolean
  expectedType: string
} {
  let valid
  const expectedType = getType(type)
  // 判断是不是 String|Number|Boolean|Function|Symbol|BigInt 类型中的一个
  if (simpleCheckRE.test(expectedType)) {
    // 父组件传的值的类型
    const t = typeof value
    // 父组件传值所属类型和子组件接收的属性类型转为小写后如果严格相等那就为 true
    valid = t === expectedType.toLowerCase()
    // for primitive wrapper objects 对于原始的包装对象
    // 类型不相等且子组件接收的类型是一个对象
    if (!valid && t === 'object') {
      // 试图从原型链中寻找这个实例值
      valid = value instanceof type
    }
  } else if (expectedType === 'Object') { // 子组件接收一个 Object 类型对象
    valid = isPlainObject(value) // 如果父组件传的 value 也是对象类型就为 true
  } else if (expectedType === 'Array') { // 子组件接收一个 Array 类型对象
    valid = isArray(value) // 如果父组件传的 value 也是数组类型就为 true
  } else {
    try {
      // 子组件接收的 expectedType 类型不为以上几种
      valid = value instanceof type // 直接判断是不是 type 的一个实例,是就为 true
    } catch (e: any) {
      // 如果接收类型 type 不是一个有效的构造函数,会抛出警告
      warn('Invalid prop type: "' + String(type) + '" is not a constructor', vm)
      valid = false
    }
  }
  return {
    valid,
    expectedType
  }
}

看这个函数我们知道返回值是个对象,会赋值给了assertedType,将对象的 expectedType 属性推到 expectedTypes 数组中,valid 赋值为 assertedType.valid,也就是说遍历的时候会判断 type 数组中当前位置元素的类型是否和父组件传入的 value 值的类型一致,一致的就会推入到 assertedType 中,然后 valid 为 true,说明匹配上了,所以循环就不会继续。

// 遍历 type 数组,前提是 valid 为 false
for (let i = 0; i < type.length && !valid; i++) {
    const assertedType = assertType(value, type[i], vm)
    expectedTypes.push(assertedType.expectedType || '')
    valid = assertedType.valid
}
  // 判断 expectedTypes 只要有一个元素 haveExpectedTypes 就是 true
  const haveExpectedTypes = expectedTypes.some(t => t)
  // valid 为 false 表示未找到匹配的 type,而且 haveExpectedTypes 还为 true,也就是有传值过来,但是没匹配上,抛出错误提示
  if (!valid && haveExpectedTypes) {
    warn(getInvalidTypeMessage(name, value, expectedTypes), vm)
    return
  }
  // 获取 props 上自定义的 validator 属性
  const validator = prop.validator
  // 如果有自定义 validator 校验器的话
  if (validator) {
    // 调用 validator 函数,传入参数 value(父组件传过来的值),看是否符合 validator 校验器的要求 
    if (!validator(value)) {
      warn(
        'Invalid prop: custom validator check failed for prop "' + name + '".',
        vm
      )
    }
  }

总结一下 assertProp 断言 prop 属性的流程:

  • required 属性值校验,看是否必传
  • 校验父组件传过来的 value 对应的类型和组件中声明的 type 类型是否一致,不一致的话抛出错误提示
  • 触发自定义的 validator 校验器(如果有的话),校验不通过的话抛出错误提示

validateProp 函数最终返回 value,也就是父组件传过来的值。

至此,validateProp 的内部逻辑我们也分析完了。我们来总结下 validateProp 函数。

  • 通过 booleanIndex 值判断用户 prop 属性的 type 是否包含布尔类型,根据情况给 value 设定默认的布尔值
  • prop 属性的 type 属性不包含 Boolean(即 booleanIndex < 0),且父组件没有传 value,调用 getPropDefaultValue 函数,判断 prop 自身有没有 default 属性,没有就直接返回 undefined,有的话进行校验,设置好默认值并开启监视
  • 调用 assertProp 断言 prop(包含 required 是否必传校验、传的 value 对应类型是否符合 prop 属性的 type、触发自定义校验器如果有提供的话)
  • 返回这个 value

那我们回到 initProps 的逻辑中,刚刚是看到枚举 propsOptions 中的属性,先将属性推到 keys 数组中,随后调用 validateProp 校验 prop,返回处理好的 value,接着又是一系列判断

if (__DEV__) {
  const hyphenatedKey = hyphenate(key)
  if (
    isReservedAttribute(hyphenatedKey) ||
    config.isReservedAttr(hyphenatedKey)
  ) {
    warn(
      `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
      vm
    )
  }
  defineReactive(
    props,
    key,
    value,
    () => {
      if (!isRoot && !isUpdatingChildComponent) {
        warn(
          `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
          vm
        )
      }
    },
    true /* shallow */
  )
} else {
  defineReactive(props, key, value, undefined, true /* shallow */)
}

hyphenate 函数,将 key 转换为连字符形式,比如:fooBar 转换为 foo-bar, isReservedAttribute 函数就是判断你这个 key 是不是 Vue 中预定义的属性名了,比如:key、ref、slot、slot-scope、is 这些都是预定义的属性,如下

export const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')

config.isReservedAttr 函数也是判断是不是预定义的属性,如下

export const isReservedAttr = makeMap('style,class')

makeMap 函数在之前章节介绍过了,makeMap 会将你传入的参数构建成一个 map,那么当你调用 isReservedAttribute 或是 isReservedAttr 时,传入的 key 属性就会去这个 map 上做映射,映射上了说明属性名称重复了,那很显然要抛出错误提示的,不能使用预定义的属性作为组件的 prop 属性名。

往事俱备,只欠东风,我们可以发现 initProps 函数前面操这么多心思都是在做一系列校验,那么真正核心的就是要给属性添加响应式了,父组件传给子组件的各种 prop,在子组件能正常使用的同时,当父组件传递给子组件的这个值发生变化要能得知并触使子组件重新渲染视图。说白了就是我们之前提到的变化侦测。

属性添加响应式主要是调用 defineReactive 来实现,这个函数我们留到后面再来看,因为它还牵扯到 Dep、Watcher、Observer 类。我们现在只要知道调用 defineReactive 函数,给每个属性添加了响应式,这样当属性值发生变化时,会通知关联的视图更新。

接着下面这段就是调用 proxy 做代理,它的作用和原理在上边 proxy 函数的分析中我们已经弄得很清晰了,vm._props[key] 能直接通过 this[key] 访问了。

if (!(key in vm)) {
  proxy(vm, `_props`, key)
}

最后调用 toggleObserver(true) 重新开启监视,因为在初始化前判断了当前不是根组件就关掉了,那么初始化结束后要重新开启。

initMethods

initMethods 就比较简单了,先获取 vm 上的 props 选项,因为后面要校验 method 方法名称是否和 props 上重复了,接着枚举 methods 上的属性(方法)

每个方法的值必须是一个函数,否则抛出错误提示信息

// 初始化方法
function initMethods(vm: Component, methods: Object) {
  // 获取 props
  const props = vm.$options.props
  // // 遍历 methods 对象上的属性(方法)
  for (const key in methods) {
    if (__DEV__) {
      // 开发者环境,属性值必须是一个函数,否则抛出警告
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[
            key
          ]}" in the component definition. ` +
            `Did you reference the function correctly?`,
          vm
        )
      }
     ...
  }
}

调用 hasOwn 传入当前 method 属性(方法名),如果在 props 选项自身上找到了,说明方法名和某个 prop 属性名重复了,抛出错误提示

// 如果 props 中已存在同名属性,则抛出警告
if (props && hasOwn(props, key)) {
  warn(`Method "${key}" has already been defined as a prop.`, vm)
}

其次再校验这个 key(方法名)是不是预定义的关键字,如果是的话,抛出错误提示

// 如果 属性名(即函数名)是预定义的关键字属性名,则抛出警告
if (key in vm && isReserved(key)) {
  warn(
    `Method "${key}" conflicts with an existing Vue instance method. ` +
      `Avoid defining component methods that start with _ or $.`
  )
}

最后这一句是核心,往 vm 上添加该方法名称对应的函数,如果属性值不是一个函数就给它一个默认的 noop 函数,noop 函数是一个函数体为空的函数(什么也不做),如果属性值是一个函数,则赋值为通过 bind 为这个函数绑定上下文 vm 返回的新函数,如下:

vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)

bind 函数很简单,就是判断浏览器是否支持 bind 函数,支持就直接用,不支持就使用 polyfillBind 函数,polyfillBind 就是通过判断参数来选择使用 call 或 apply。

function polyfillBind(fn: Function, ctx: Object): Function {
  function boundFn(a: any) {
    const l = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }

  boundFn._length = fn.length
  return boundFn
}

function nativeBind(fn: Function, ctx: Object): Function {
  return fn.bind(ctx)
}

// @ts-expect-error bind cannot be `undefined`
export const bind = Function.prototype.bind ? nativeBind : polyfillBind

总结下 initMethods 的流程:

  • 获取 props 选项,枚举 methods 上属性
  • 校验属性名称合法性
  • 在 vm 实例上绑定该属性对应函数

initData

initData 的整体流程如下:

  • 获取 data 选项
  • data 和 vm._data 重新赋值,如果 data 是一个函数,赋值为调用 getData 函数的返回值,否则直接赋值 data 本身
  • 判断如果 data 不是对象,抛出错误提示
  • 获取 data 对象的属性键名列表、vm 上的props 选项、methods 选项,开启 while 循环,每次以当前 data 中的一个属性来进行名称校验,先后与 methods 和props 选项做校验,只要 data 上的属性名称和 methods 或 props 选项上的属性名称重复就抛出错误提示。最后调用 isReserved 校验是不是预定义的关键字,不是的话就调用 proxy 对属性做一层代理,使其能直接通过 this.xxx 访问
  • 调用 observe 函数给 data 中的各个属性添加响应式(observe 函数在后边再分析)
function initData(vm: Component) {
  let data: any = vm.$options.data
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
  if (!isPlainObject(data)) {
    data = {}
    __DEV__ &&
      warn(
        'data functions should return an object:\n' +
          'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      )
  }
  // proxy data on instance
  // data 对象中的属性键名列表
  const keys = Object.keys(data)
  const props = vm.$options.props
  // methods 对象
  const methods = vm.$options.methods
  let i = keys.length
  // 循环 data 对象中的属性键名列表
  while (i--) {
    const key = keys[i]
    if (__DEV__) {
      // 如果 methods 对象存在且对象上也有 key 这个同名属性,发出警告 ==> data 中已经有同属性声明了
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    // 如果 props 对象存在且对象上也有 key 这个同名属性,发出警告 ==> props 中已经有同属性声明了
    if (props && hasOwn(props, key)) {
      __DEV__ &&
        warn(
          `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm
        )
    } else if (!isReserved(key)) { // 判断是不是 Vue 中预定义的关键字
      // 如果不是,则调用 proxy 做一层代理,使其能直接通过 this.xxx 访问
      proxy(vm, `_data`, key)
    }
  }
  // observe data(使 data 上的属性都具备响应式) 
  const ob = observe(data)
  ob && ob.vmCount++
}

initComputed

initComputed 的整体流程如下:

  • 定义一个 watchers 常及为 vm._computedWarchers 共同赋值为空对象,后续主要用来存储各个属性对应的 watcher 实例
  • 定义 isSSR 属性用于判断是不是服务端渲染环境
  • 枚举 computed 选项上的属性,获取当前枚举属性的属性值 userDef,定义 getter 函数,赋值取决于这个属性值 userDef 自身是不是一个函数,是的话说明自身就是个 getter,反之就是个对象,获取对象上的 get 函数作为 getter,赋值后的 getter 函数如果为空,抛出错误提示,如果不是服务端渲染,new Watcher 创建一个 watcher 实例放到常量对象 watchers 中,最后判断 vm 实例上是否已经存在该 key,不存在则调用 defineComputed 函数给属性添加响应式,如果存在,则判断该 key 是否和 vm 上的哪个选项中的属性同名,是的话会抛出错误提示
function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = (vm._computedWatchers = Object.create(null))
  // computed properties are just getters during SSR
  // 是否处于服务端渲染环境                                                                               
  const isSSR = isServerRendering()

  // 枚举计算属性选项上的每个属性
  for (const key in computed) {
    const userDef = computed[key]
    // 获取计算属性的 getter 函数 
    const getter = isFunction(userDef) ? userDef : userDef.get
    if (__DEV__ && getter == null) {
      warn(`Getter is missing for computed property "${key}".`, vm)
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      // 给计算属性添加一个 Watcher 实例
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    // key 属性不在 vm 实例上
    if (!(key in vm)) {
      // 给属性添加响应式
      defineComputed(vm, key, userDef)
    } else if (__DEV__) { // vm 实例上存在该属性且在开发者环境
      if (key in vm.$data) { // key 已经存在于 vm.$data 上
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) { // key 已经存在于 vm.props 上
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      } else if (vm.$options.methods && key in vm.$options.methods) { // key 已经存在于 vm.methods 上
        warn(
          `The computed property "${key}" is already defined as a method.`,
          vm
        )
      }
    }
  }
}

getter 取值我们可以根据平常定义 computed 属性的两种方式来理解:

方式一:computed 属性的定义是一个函数(这种情况下执行函数会返回值,即函数自身就是一个 getter)

<script>
export default {
    computed: {
      fullName() {
        return this.firstName + ' ' + this.lastName
      },
    }
}
</script>

方式二:computed 属性得定义是一个对象,对象中有 get/set 函数,计算属性得 get 函数作为 getter

<script>
export default {
    computed: {
      fullName: {
        get() {
          return this.firstName + ' ' + this.lastName
        },
        set() {
          // ...
        }
      }
    }
}
</script>

initWatch

initWatch 的整体流程如下:

  • 枚举 watch 选项上的属性
  • 获取当前 watch 属性的值 handler
  • 如果 handler 是数组,遍历数组调用 createWatcher 函数,不是数组就直接调用 createWatcher 函数
function initWatch(vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

watch 的写法也很多种:

export default {
  data() {
    return {
      items: ['apple', 'banana']
    }
  },
  watch: {
    // 1. 函数写法
    items(newVal, oldVal) {
      console.log('变化了')
    },
    
    // 2. 对象写法
    items: {
      handler(newVal, oldVal) {
        console.log('变化了')
      },
      deep: true
    },
    
    // 3. 字符串写法(方法名)
    items: 'handleItemsChange',
    
    // 4. 数组写法(多个处理器)- 这就是 isArray(handler) 判断的情况!
    items: [
      // 可以是函数
      function(newVal, oldVal) {
        console.log('处理器1')
      },
      // 可以是对象
      {
        handler(newVal, oldVal) {
          console.log('处理器2')
        },
        deep: true
      },
      // 可以是方法名
      'handleItemsChange'
    ]
  },
  methods: {
    handleItemsChange(newVal, oldVal) {
      console.log('方法被调用')
    }
  }
}

createWatcher:

  • 首先判断属性值是不是一个对象,是的话这个对象作为 options,然后取这个 handler 选项里的 handler 作为 handler 函数
  • 如果 handler 是一个字符串,则从 vm 实例上获取该 handler 字符串对应的函数
  • 最后调用 vm.$watch 函数
function createWatcher(
  vm: Component,
  expOrFn: string | (() => any),
  handler: any,
  options?: Object
) {
  // 判断 handler 是不是一个对象
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler // 对象中的 handler 函数
  }
  // 针对 watch 选项某个属性的属性值是一个字符串,字符串里就是个函数
  if (typeof handler === 'string') {
    handler = vm[handler] // vm 实例上该 handler 字符串对应的函数
  }
  return vm.$watch(expOrFn, handler, options)
}

watch 属性的对象写法 handler 就放在对象中,字符串写法通常指向的就是 vm 实例上的某个方法

export default {
  data() {
    return {
      items: ['apple', 'banana']
    }
  },
  watch: {
    // 对象写法
    items: {
      handler(newVal, oldVal) {
        console.log('变化了')
      },
      deep: true
    },
    
    // 字符串写法(方法名)
    items: 'handleItemsChange',
  },
  methods: {
    handleItemsChange(newVal, oldVal) {
      console.log('方法被调用')
    }
  }
}

defineReactive

我们在分析 initProps 函数知道最后会遍历 props 的所有属性,然后调用 defineReactive 函数给每个属性添加响应式,如下:

defineReactive(
    props,
    key,
    value,
    () => {
      // 当前实例不是根节点并且不在更新子组件,抛出警告
      if (!isRoot && !isUpdatingChildComponent) {
        warn(
          `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
          vm
        )
      }
    },
    true /* shallow */
    )

传递的第一个参数是整个 props 选项,第二个参数是当前的属性,第三个参数是当前属性对应的取值,第四个参数是个回调函数。第五个参数传了布尔值 true。

接着我们来分析 defineReactive 函数的内部逻辑:

/**
 * Define a reactive property on an Object.
 */
// 在当前对象上定义一个响应式属性
export function defineReactive(
  obj: object, // 目标对象
  key: string, // 属性
  val?: any, // 属性值
  customSetter?: Function | null, // 自定义 setter 函数
  shallow?: boolean, // 是否浅层次响应式,即只监视对象第一层
  mock?: boolean,
  observeEvenIfShallow = false
) {
  const dep = new Dep()

  //... 一系列处理逻辑 

  return dep
}

看到它刚开始就 new Dep(),上篇文章提到过,每个属性都会对应一个 Dep,每个 Dep 维护着一个 subs 订阅者列表,Dep 这个类会在下面分析。继续看 defineReactive 内部处理:

/**
   * Object.getOwnPropertyDescriptor:返回一个直接存在于对象而不存在于原型链中的属性
   */
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果对象不允许配置,直接返回
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 属性的 getter 函数
  const getter = property && property.get
  // 属性的 setter 函数
  const setter = property && property.set
  // 没有 getter 函数或者有 setter 且 val 是个空对象或者这个函数只传了前两个参数
  if (
    (!getter || setter) &&
    (val === NO_INITIAL_VALUE || arguments.length === 2)
  ) {
    // val 值就等于 obj[key]
    val = obj[key]
  }

  let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)

首先调用 Object.getOwnPropertyDescriptor 获取对象上这个属性的描述符对象,判断描述符上的 configurable 是不是 false,false 就表示不允许给这个属性进行任何配置,那下面要进行的响应式也不用走了,直接返回。接着取这个属性描述符上的 get 和 set 函数,然后判断传的 val 是空对象或者只传了前两个参数,那要给 val 赋予默认值。下面给 childOb 这个变量赋值,它的作用我们在后边会提到,暂且不管。

接下来就是真正的给这个属性添加响应式了,进行变化侦测,使用 Object.defineProperty,定义 get 函数,当某个地方读取这个属性时就会触发 get 函数

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 拿到老的 getter 中的值
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        if (__DEV__) {
          // 收集当次目标的依赖
          dep.depend({
            target: obj,
            type: TrackOpTypes.GET,
            key
          })
        } else {
          dep.depend()
        }
        if (childOb) {
          childOb.dep.depend()
          if (isArray(value)) {
            dependArray(value)
          }
        }
      }
      // 根据 val 类型返回对应的值
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {  
        // ...一系列操作
        // 核心:通知依赖项执行更新操作
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
        // ...
    }
  })

get 函数首先判断该属性自身是否有 getter 函数,有就调用它拿到 value 值,没有的话值就是 val(我们从参数传过来的),然后判断 Dep.target,Dep.target 其实就是当前读取该属性的目标,那这个目标我们上篇文章提到,其实就是各种 Watcher,比如模板中的 Watcher、computed Watcher、自定义 Watcher。然后 dep.depend 收集依赖,dep.depend 会把当前访问该属性的 Watcher 添加到订阅者列表 subs 中,从而保证在修改该属性时触发的 set 函数中,调用 dep.notify() 能遍历订阅者列表然后通知各个依赖去触发更新。这就实现响应式了。

前面我们已经分析完 props、data、watch、computed 的初始化流程,都是进行一系列初始化和校验,最后遍历选项上的属性来添加响应式,实现响应式的核心就是 defineReactive 函数,上边我们也看到了,要实现变化侦测,就要在属性被访问时将访问该属性的目标收集起来,待属性更新时通知收集的目标,那么谁来收集、收集的目标及通知目标是谁?在 Vue 里有专门的类来负责这些事情,接下来就来看做这些事的专属类。

Dep

所在文件(src/core/observer/dep.ts)

Dep 类中定义的属性如下:

// 目标为静态的,仅在当前类中可使用,而且它集成了 DepTarget 提供的接口
static target?: DepTarget | null
// id 唯一标识
id: number
// subs 表示订阅者列表,不止一个
subs: Array<DepTarget | null>
// pending subs cleanup
_pending = false    

target 声明为类的静态属性,id 是 Dep 实例的唯一标识,subs 是 Dep 实例的订阅者列表(收集的目标),_pending 表示当前是否正在移除订阅者。

new Dep() 的时候会执行 constructor 构造函数,默认当前实例的 id 等于 uid++,保证 id 唯一,uid 在文件顶部声明,默认是 0,每创建一个 Dep 实例就会加一,随后给这个 Dep 实例初始化一个订阅者列表。

constructor() {
    // 每一个 dep 实例的 id 都会在原有的 uid 基础上加一
    this.id = uid++
    // 默认新的 dep 实例的订阅者列表是空的
    this.subs = []
}

addSub 很简单,就是将接收的 sub 订阅者推入到订阅者列表中。removeSub 函数接收一个订阅者,将订阅者列表中接收的这个订阅者所在位置元素置为 null,然后 _pending 置为 true,随后将当前 Dep 实例也就是 this 推入 pendingCleanupDeps 列表中,以便之后在需要的时候清理一下当前 Dep 实例的 subs 列表,也就是将 subs 列表中那些为 null 或 undefined 的元素过滤掉。

// 添加订阅者
  addSub(sub: DepTarget) {
    // 将当前订阅者添加进订阅者列表中
    this.subs.push(sub)
  }

  // 删除订阅者
  removeSub(sub: DepTarget) {
    // #12696 deps with massive amount of subscribers are extremely slow to
    // clean up in Chromium
    // to workaround this, we unset the sub for now, and clear them on
    // next scheduler flush.
    // 将订阅者在订阅者列表的位置元素设为 null
    this.subs[this.subs.indexOf(sub)] = null
    // 如果当前不处于 _pending 状态
    if (!this._pending) {
      // 设为 _pending 状态
      this._pending = true
      // 将当前 dep 实例加入 pendingCleanupDeps 列表中
      pendingCleanupDeps.push(this)
    }
  }

下面的 cleanupDeps 就是负责清理收集在 pendingCleanupDeps 列表中的 Dep 实例上 subs 列表中为 null/undefined 的元素。

const pendingCleanupDeps: Dep[] = []

export const cleanupDeps = () => {
  for (let i = 0; i < pendingCleanupDeps.length; i++) {
    const dep = pendingCleanupDeps[i]
    dep.subs = dep.subs.filter(s => s)
    dep._pending = false
  }
  pendingCleanupDeps.length = 0
}

遍历 pendingCleanupDeps 列表,上边收集的是各个调用过 removeSub 的 Dep,既然调用过 removeSub 即表明当前 Dep 实例的 subs 列表中某个订阅者被置为 null 了,当调用次数越多,subs 数组上很多位置上都是 null,这其实是很耗费性能和内存的,所以调用 removeSub 时顺便收集下这个 Dep 实例,在合适时机清理一下不必要的引用(即为 null 的元素),dep.subs = dep.sub.filter(s => s) 这一过滤操作就会将订阅者列表中为 null/undefined 的元素过滤掉。你可能会疑惑这里的 dep 为什么能访问到 subs,那是在 removeSub 中将 this(当前 Dep 实例)推入到 pendingCleanupDeps 列表中了,所以自然能访问到该实例的 subs 列表,遍历的最后将 当前 Dep 实例的 _pending 恢复为 false,表示当前 Dep 实例不在移除订阅者的状态,循环结束后就将所有收集的 Dep 实例上 subs 中为 null/undefined 元素清理掉了,最后将 pendingCleanupDeps 长度置为 0,清空 pendingClenupDeps 列表

depend 函数会先判断当前 Dep.target 存不存在,存在的话就调用其 addDep 方法,Dep.target 我们知道是各种 Watcher,所以 addDep 也是 Watcher 实例的方法,我们只要知道 Dep.target.addDep 最终会调用 addSub 将 Dep.target(也就是Watcher)作为订阅者传入进来,从而能将目标(Watcher)收集到 subs 订阅者列表中,具体这里的 Dep.target.addDep 等讲到 Watcher 类就清楚了。现在我们只要知道能将这个依赖(目标 Watcher)收集到订阅者列表中。

// 收集订阅者依赖
depend(info?: DebuggerEventExtraInfo) {
    if (Dep.target) {
      // 将当前 dep 实例添加进 Dep.target 中
      Dep.target.addDep(this)
      // 判断为开发者环境并且参数 info 不为空并且 Dep.target.onTrack 不为空
      if (__DEV__ && info && Dep.target.onTrack) {
        // 调用 Dep.target.onTrack 并传入一个对象
        /**
         * onTrack:
         *   effect: any
         *   target: object
             type: TrackOpTypes | TriggerOpTypes
             key?: any
             newValue?: any
             oldValue?: any
        **/
        Dep.target.onTrack({
          effect: Dep.target,
          ...info
        })
      }
    }
}

notify 函数主要用来通知依赖变更,通过遍历订阅者列表调用它们身上的 update 函数来更新,首先过滤掉 Dep 实例中 subs 列表中为 null/undefined 的元素,接着判断在开发者环境且 !config.async(即当前不是异步的情况),订阅者列表按照 id 从小到大排序,接着遍历订阅者列表,然后调用每个订阅者身上的 update 函数来更新了,订阅者就是各种 Watcher,所以 update 方法也是 Watcher实例上的。

// 通知订阅者
notify(info?: DebuggerEventExtraInfo) {
    // stabilize the subscriber list first 稳定订阅者列表
    const subs = this.subs.filter(s => s) as DepTarget[]
    // 开发者环境并且 config.async 属性为假,也就是不是异步,那就是同步
    if (__DEV__ && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      // 根据 id 来排序,保证以正确的顺序触发
      subs.sort((a, b) => a.id - b.id)
    }
    // 遍历订阅者列表
    for (let i = 0, l = subs.length; i < l; i++) {
      // 拿到其中一个
      const sub = subs[i]
      // 如果是开发者环境并且有 info 参数
      if (__DEV__ && info) {
        // 当前订阅者有 onTrigger 情况下去调用
        sub.onTrigger &&
          sub.onTrigger({
            effect: subs[i],
            ...info
          })  
      }
      // 最后触发订阅者的更新
      sub.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: Array<DepTarget | null | undefined> = []

// 推送并更新目标
export function pushTarget(target?: DepTarget | null) {
  // 往目标栈推送当前目标
  targetStack.push(target)
  // 更新当前 Dep.target
  Dep.target = target
}

// 移除目标栈最后一个元素 & 更新 Dep.target
export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

比较简单,就是把目标(Watcher)往 targetStack 目标栈推送(pushTarget)或者将其从中移除(popTarget),并更新当前的 Dep.target。

observe

接着我们来分析 observe 函数,在 initData 的最后调用它来监视 data 对象

image.png image.png 接收三个参数,第一个参数必传,是要监视的对象;第二、三个参数非必传,第二个参数表示是否浅层次监视对象,浅层次即只有对象的第一层具备响应式;第三个参数是用于服务端渲染的。

首先校验传的 value 对象不为空且 value 对象上自身有 _ob_ 属性且这个属性还得是 Observer 的实例,也就是通过 new Observer 得到的,如果是这种情况,那说明这个 value 对象已经监视过了,不需要重复监视,返回这个监视对象上的 _ob_ 属性。

// 判断这个 value 是否有 __ob__ 这个属性
if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    return value.__ob__ // 有 __ob__ 属性表明已经是监视对象,直接返回
}

下边就是 value 对象不是监视对象的情况,先走一系列判断,

  • shouldObserve 我们在 initProps 中分析过了,就是子组件的 props 在父组件中已经递归监视过了,所以在子组件 props 初始化过程中需要关闭,待初始化完成再开启。shouldObserve 默认值是 true,所以这里初始化 data 时 shouldObserve 就是 true
  • ssrMockReactivity 不传就是空,!isServerRendering() 表示当前不是服务端渲染环境
  • isArray(value) || isPlainObject(value) 判断 value 必须为对象或数组
  • Object.isExtensible(value) 判断 data 对象是否可扩展(即能正常增删改)
  • __v_skip 属性在 initMixin 作为 vm 的属性,默认值是 true,而且官方注释也很明显,就是为了避免 vm 实例被监视。所以其他对象或属性默认都没有这个属性
  • !isRef(value) 表示对象不是一个 ref 实例
  • !(value instanceof VNode) 表示 value 对象不是一个 vnode 节点

满足以上的判断条件,就能够对该对象进行监视了,返回 new Observer 创建的实例对象。

if (
    shouldObserve &&
    (ssrMockReactivity || !isServerRendering()) &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value.__v_skip /* ReactiveFlags.SKIP */ &&
    !isRef(value) &&
    !(value instanceof VNode)
  ) {
    return new Observer(value, shallow, ssrMockReactivity)
  }

Observer

通常不仅仅针对单个属性做侦测,会涉及对象或数组的参与,这时候需要一个专门的类,可以实现侦测整个对象或数组中的属性或元素,Observer 类就做到了。

首先,Observer 类上有两个属性

  • dep:Dep 实例
  • vmCount:表示当前 vm 实例个数也就是组件的个数,每个组件对应一个 vm 实例,初始化 data 对象的时候会将创建的 Observer 实例上的 vmCount++

当 opts.data 为空,也就是组件内定义的 data 选项上没有属性,也会默认初始化一个空对象,并将返回的 Observer 实例上的 vmCount++,如下: image.png

如果有 data 选项,则初始化逻辑在 initData 函数中,最后会调用 observe 函数返回一个监视对象,给监视对象上的 vmCount++:

image.png

然后是构造函数的初始化:

  • newDep():实例化一个 Dep 实例
  • 当前 Observer 实例的 vmCount 初始化为 0
  • def(value, '_ob_', this):使用 Object.defineProperty 往 value 对象上定义了一个 _ob_ 属性,而且属性描述符的 enumerable 设为 false,也就是不可枚举,在 initData 中这里的 value 代表的就是 data 对象,那么它现在身上多了一个 _ob_ 属性,而且还是不可枚举的,这样当我们遍历 data 对象上的属性时就不会访问到 _ob_ 这个属性了
/**
 * Define a property.
 */
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
  // 在 obj 目标对象上定义 key 属性并设置属性描述符
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable, // 属性不可枚举,避免遍历到
    writable: true,
    configurable: true
  })
}
  • isArray(value):判断 value 是不是一个数组
    • 情况一:是数组,先判断当前不是 mock 环境,然后判断 hasProto,hasProto 在(core/util/env.ts 中),
// can we use __proto__?
export const hasProto = '__proto__' in {}

很简单就是看运行的环境支不支持原型的 _proto_ 属性,如果支持那就是每个对象身上都会有一个 _proto_ 属性。将 arrayMethods 赋值给 value._proto_,arrayMethods 在同级目录下的 array.ts 文件中,我们看到该文件中有这么两行:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

arrayProto 赋值为 JavaScript 内置数组的原型,即拥有内置数组原型上的所有属性,然后将 arrayProto 作为原型创建一个新对象赋值给 arrayMethods 并导出,这样给 value._proto_ 赋值的就是一个拥有 JavaScript 内置数组所有属性的原型。

这里我们想下,在对象处理上,当我们往对象上添加一个属性或者修改某个属性时,会被 setter 函数拦截从而触发更新,但是 Array.prototype 中向数组添加或删除元素的方法本身并没有侦测这一特性,没有对应的 getter/setter 来收集和更新,所以单纯用原生 JavaScript 上内置数组原型中的方法来访问或修改数组中的元素并不具备响应式,这里 Vue 通过自定义的方法去覆盖原生数组的原型方法,然后在自定义方法中实现侦测(就是在保证原有数组方法功能正常外还会有额外的一些操作的自定义方法)。代码也是在(core/instance/observer/array.ts 文件中),不过我们会在下一章再进行详细分析。

接着回到 Observer 类的 constructor 初始化函数中,对于 value 是数组且环境支持原型的情况我们分析完了,接着是不支持原型 _proto_ 属性的情况,那就没办法直接将原生数组内置属性方法赋值给 value._proto_ 了,这时候只能采取遍历原生数组内置方法然后将它们逐个添加到 value 的属性上。

for (let i = 0, l = arrayKeys.length; i < l; i++) {
    const key = arrayKeys[i] // 当前数组方法
    def(value, key, arrayMethods[key]) // 将方法添加到 value 的属性上
}

arrayKeys 如下:

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

arrayMethods 分析过了,就是拷贝了原生数组原型的新对象,Object.getOwnPropertyNames 会返回静态对象上所有自有属性,即 arrayKeys 就是 arrayMethods 上所有的自有属性的集合。遍历 arrayKeys,然后调用 def 函数往 value 上添加这些属性,而且这些属性不可枚举。这样子 value 上也有原生数组原型上的所有方法了。只不过前者是在 value 的 __proto 上,而后者是直接添加在 value 上。

因为数组中可能存放的是对象,那也要侦测,!shallow 表示要深层次监测,那就调用 observeArray 函数将 value 传入,observeArray 函数也很简单,就是遍历 value 上所有属性(不可枚举属性不会遍历出来),然后递归调用 observe 函数监视每个属性(对象)。这样即使数组中有对象,这个对象也会变成监视对象,这样该对象发生任何变化也能侦测到了。

/**
* Observe a list of Array items.
*/
observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i], false, this.mock)
    }
}
  • 情况二:不是数组,那就只能是对象了,直接获取 value 对象上的所有可枚举属性列表 keys,然后遍历 keys 列表,调用 defineReactive 给每个属性添加响应式。
/**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  const keys = Object.keys(value)
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
  }

现在看 Observer 类的整体代码就很清晰了:

export class Observer {
  dep: Dep
  vmCount: number // number of vms that have this object as root $data
  constructor(public value: any, public shallow = false, public mock = false) {
    // this.value = value
    this.dep = mock ? mockDep : new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (isArray(value)) {
      if (!mock) {
        // 对 value 上的数组属性方法添加响应式
        if (hasProto) {
          /* eslint-disable no-proto */
          ;(value as any).__proto__ = arrayMethods
          /* eslint-enable no-proto */
        } else {
          for (let i = 0, l = arrayKeys.length; i < l; i++) {
            const key = arrayKeys[i]
            def(value, key, arrayMethods[key])
          }
        }
      }
      // 监视 value 上的属性
      if (!shallow) {
        this.observeArray(value)
      }
    } else {
      /**
       * Walk through all properties and convert them into
       * getter/setters. This method should only be called when
       * value type is Object.
       */
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
      }
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i], false, this.mock)
    }
  }
}

总结一下 Observer 类,给传入的 value 对象创建一个 Dep 实例以及添加一个 _ob_ 属性,表明为已侦测对象,然后判断 value 类型,数组的情况下,为 value 数组添加原生内置数组原型上的各种方法,针对那些能往数组添加/修改/删除元素的方法做了拦截(保证原有方法功能基础上进行扩展),使该添加操作能侦测到,具体实现会在下篇文章分析,对于数组中的元素也可能是对象,调用 observeArray 实现深度侦测;如果 value 是对象,直接遍历对象的可枚举属性,调用 defineReactive 给每个属性添加响应式。

childOb

在我们分析的 defineReactive 函数中,childOb 直接跳过了,因为它涉及到 observe 函数,observe 函数又会涉及到 Observer 类,现在 observe 函数和 Observer 类我们分析完了,再回过头来看 childOb 会更容易理解,顺便加深我们对 observe、Observer、defineReactive 的印象。我们举个例子先:

<template>
    <div>{{ hobbies[0] }}</div>
</template>
export default {
    data() {
        return {
            hobbies: ['reading', 'coding']
        }
    }
}

上边在 data 声明了一个 hobbies 属性,它是一个数组,按照 Vue 初始化流程,首先在 initState 的 initData 最后,调用了 observe 函数,将这个 data 对象作为实参传入,刚开始 data 对象还不是一个监视的对象(没有 _ob_ 属性),observe 函数会使用 new Observer 创建一个监视对象返回,那么在 Observer 类中先是为这个 data 对象创建一个关联 Dep 实例,然后给 data 对象添加一个 _ob_ 监视标志,随后判断 value 类型,这里我们的 data 是对象,会枚举 data 对象的每个属性,这里枚举出 hobbies 属性,调用 defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock),value 是 data 对象,key 是 hobbies、NO_INITIAL_VALUE 是文件上方声明的常量空对象,defineReactive 先为这个 hobbies 属性创建一个关联的 Dep 实例,然后获取 data 对象上这个 hobbies 的属性描述符,包括 get 和 set 函数,默认没有设置是空,所以 val 赋值为 data['hobbies'],也就是属性值 val 是上边的 ['reading', 'coding'] 数组,shallow 传的是 false,也就是要深层次监视,接着 childOb 赋值为调用 observe 函数返回值,我们知道 observe 函数最终会返回一个监视对象,这里调用 observe 函数时是将 val(数组['reading', 'coding'])传入,它本身没有 _ob_ 标志,所以会 new Observer(),为这个 val(数组['reading', 'coding'])创建一个关联 Dep 实例,添加 _ob_ 监视标志,接着 value 是数组,会为其的原型 _proto_(如果有的话)或者自身添加上原生 JavaScript 内置数组的各种方法,最后判断 !shallow,传的 false,也就是深层次,所以会调用 observeArray(value) ,这个函数会递归数组中的元素然后调用 observe 函数,observe 函数内部添加监视的前提是你得是个对象或数组,上边 hobbies 数组中元素是原始类型,很显然不需要添加监视了,不做任何处理。

我们来梳理下上边例子,首先在 observe 中为 data 对象创建一个关联的 Dep 实例并标志为监视对象,随后枚举 data 上的属性调用 defineReactive,defineReactive 首先为 hobbies 属性创建一个关联 Dep 实例,val 就是 data['hobbies'] 是个数组,然后调用 observe 函数监视这个数组,Observer 类会给这个数组创建一个关联的 Dep 实例并添加 _ob_ 标志。这个数组创建的监视对象赋值给了 childOb。

当模板中访问 hobbies[0] 时,会先触发 hobbies 的 getter 进行依赖收集,此时的 dep 就是 hobbies 关联的 Dep 实例,收集这个 render watcher,随后判断了 childOb 有没有,这里的 childOb 就是数组 ['reading', 'coding'] 关联的监视对象,也会让这个监视对象上的 Dep 实例收集这个 render watcher,随后还有判断 value 是不是数组,是的话调用 dependArray,dependArray 会遍历数组元素,判断是不是监视对象,是的话会递归对象或数组中所有监视对象进行收集依赖,这样就实现了对象或者对象中任何一个属性发生变化,都会触发 render watcher(视图) 的更新。

所以总结 childOb 的作用,一个对象或数组中往往不止一层,里边可能嵌套其他的属性或对象,当我们访问对象上某个属性时,先触发对象上的 getter 进行依赖收集,这时候不仅仅是这个对象自身的 Dep 实例要收集依赖,还需要让访问的这个子属性也一起收集这个依赖,这样当子属性发生变化时,也能通知收集的这个依赖去触发更新,这就是 childOb 的作用。

Watcher

Watcher 类上的属性就有很多了,我们来看 constructor 构造函数的初始化

参数:

  • vm:当前实例
  • expOrFn:表达式或函数
  • cb:回调函数
  • options:配置项
  • isRenderWatcher:是不是渲染的 Watcher

将 Watcher 实例的 vm 属性设为当前 vm(组件) 实例,且是渲染 Watcher 的情况下,将当前 Watcher 实例保存在 vm 的 _watcher 属性上

if ((this.vm = vm) && isRenderWatcher) {
  vm._watcher = this
}

接着判断有传 options 选项,进行实例属性的初始化赋值操作,如果没有传 options,会默认将 deep、user、lazy、sync 设为 false

if (options) {
  this.deep = !!options.deep
  this.user = !!options.user
  this.lazy = !!options.lazy
  this.sync = !!options.sync  
  this.before = options.before
  if (__DEV__) {
    this.onTrack = options.onTrack
    this.onTrigger = options.onTrigger
  }
} else {
  this.deep = this.user = this.lazy = this.sync = false
}

下面是也是默认的初始化属性,dirty 主要却决于 lazy 的值,deps、newDeps、depIds、newDepIds 这四个就是 Watcher 实例依赖的 Dep 信息,在添加订阅者时会记录依赖的 Dep,移除订阅者时会清理掉。

this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.post = false
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = __DEV__ ? expOrFn.toString() : ''

expOrFn 如果是函数,直接赋值给 getter 函数,反之可能就是字符串表达式了,这时候需要调用 parsePath 解析下,解析完 getter 为空抛出错误提示

if (isFunction(expOrFn)) {
  this.getter = expOrFn
} else {
  // expOrFn 不是函数情况,调用 parsePath 解析
  this.getter = parsePath(expOrFn)
  if (!this.getter) {
    this.getter = noop
    __DEV__ &&
      warn(
        `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
        vm
      )
  }
}

比如我们自定义的一个 watch:

export default {
    data() {
        return {
            num: {
                a: {
                    b: 1 
                }
            }
        }
    },
    watch: {
        'num.a.b': {
            handler(val) {
               console.log(val)
            }
        }
    }
}

这里监视的是一串字符串表达式,parsePath 就是解析这个字符串表达式用的,该函数首先定义一个正则表达式 bailRE,如果传入参数 path 匹配上表达式,说明是不符合条件的,直接返回。接着按照符号 . 进行分割,放到 segments 数组中,根据上边例子分割好的 segements 就是 ['num', 'a', 'b'],然后返回一个函数,接收 obj 对象,遍历 segements 数组逐个读取数据 obj[segements[i]],最后返回值 obj 就是 obj[segements['b']] 对应得值了。

/**
 * Parse simple path.
 */
/**
 * ^ 表示否定
 * 判断所有不在 unicodeRegExp.source 中以及 . $ _  的字符 
 */
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath(path: string): any {
  // 测试通过不符合条件,直接返回
  if (bailRE.test(path)) {
    return
  }
  // 按照 . 字符划分字符串
  const segments = path.split('.')
  // 返回一个函数
  return function (obj) {
    // 遍历划分的字符数组
    for (let i = 0; i < segments.length; i++) {
      // 如果调用这个函数没传参数直接返回
      if (!obj) return
      // 给这个参数对象赋值为当前字符在该对象对应的值
      obj = obj[segments[i]]
    }
    // 返回这个对象
    return obj
  }
}

最后给 value 赋值,this.lazy 为 true,表示懒执行,首次取值为 undefined,对于 computed watcher 其传的 options 中就要 lazy 属性,值为 true,其他的 Watcher 默认调用 get 函数来取值。

this.value = this.lazy ? undefined : this.get()

get 函数的实现就很巧妙了,首先调用 pushTarget 将当前 Watcher 实例推入到栈中并设置为当前的 Dep.target 为它,然后执行 Watcher 实例的 getter 函数,这时候会触发依赖收集,而此时的 Dep.target 恰恰是当前 Watcher 实例,刚好能把它收集到订阅者列表中,待 getter 函数执行完后,判断 deep 是否为 true,也就是是否深度监视,如果为 true,调用 traverse 递归遍历 value 上的所有属性,将每个嵌套属性都收集为“深层”依赖项,然后再调用 popTarget 函数将当前 Watcher 实例出栈,更新 Dep.target 为上一次列表中得最后一个,最后调用 cleanupDeps 清理一下当前 Watcher 实例关联的 Dep 信息,因为当前 Watcher 实例已经执行完 getter 函数了,也已经 popTarget 出栈了,所以需要清理掉它关联的 Dep 信息,避免后续重复收集依赖。

/**
   * Evaluate the getter, and re-collect dependencies.
   */
  // 评估 getter 并且重新收集依赖
  get() {
    // 推送 watcher 实例
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e: any) {
      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
  }

接着来看 addDep,在 Dep 类的 depend 函数中收集依赖时调用,将 Dep 实例传过来了。先获取 dep 的 id,然后判断新的 newDepsId 集合中存不存在该 id,不存在就存进去,然后 newDeps 推入这个 Dep 实例,为了后续不依赖这个 Dep 时,可以找到它然后调用它的 removeSub 方法来移除当前 Watcher 实例(订阅者)(也就是断开关系)。最后判断 depIds 老的集合中存不存在,要是老的也不存在,那就说明这个依赖还没收集过,调用 dep 的 addSub 方法,将当前 Watcher 实例作为订阅者加到 Dep 实例的订阅者列表中。

/**
* Add a dependency to this directive.
*/
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)
      }
    }
}

组件的渲染对应一个render Watcher,render Watcher 的渲染函数就是 updateComponent,比如我们在模板写这么一段:

<template>
  <div>{{ msg }}</div>
  <div>{{ msg }}</div>
</template>
<script>
export default {
    data() {
        return {
            msg: 'hello, world'
        }
    }
}
</script>

这个渲染函数中就包含模板中访问的两次 msg 属性,也就是对应 Watcher 里的 getter 函数,而当前的 Dep.target 就是这个 render Watcher,当读取第一个 msg 属性时,会收集依赖,Dep.target.addDep 函数会触使 render Watcher 将该 Dep 实例及其 id 分别存到 newDeps、newDepIds 中,因为初始它们都是空,且 depIds 也是空,会将 render Watcher 添加进 Dep 实例的 subs 订阅者列表中。当第二次访问 msg 属性时,同样触发其 getter 函数进行依赖收集,因为一个属性对应一个 Dep 实例,那这时候第二次访问的 msg 和上一次访问的 msg 实际上是同一个 Dep 实例,所以在 newDeps 和 newDepIds 其实已经保存了该 Dep 实例的信息,直接跳过它。

cleanupDeps 用来清理无关的依赖,说白了就是将当前 Watcher 实例与其依赖的所有 Dep 实例断开关系。遍历 deps 列表,判断新的 newDepIds 中存不存在当前 deps 列表中某个 Dep 实例对应 id,如果没找到,说明新的 Watcher 已经不依赖 deps 列表中的这个 Dep 实例了,那将自身(当前 Watcher)从 dep 实例的订阅者列表中移除(dep.removeSub(this))。接着就是新老依赖列表做交换,新的列表放老的列表了,然后新的列表清空。

/**
 * Clean up for dependency collection.
 */
cleanupDeps() {
    let i = this.deps.length
    while (i--) {
        const dep = this.deps[i]
        if (!this.newDepIds.has(dep.id)) {
            dep.removeSub(this)
        }
    }
    let tmp: any = 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 方法,先判断 lazy 的值为 true 的话,将 dirty 属性设为 true,这个主要用在 computed watcher,computed watcher 的取值是惰性的,只有当依赖发生变化时才会重新计算。lazy 为 false 再判断 sync 是否为 true,即是否要同步更新,为 true 的话就执行 run 方法。再接着既不是惰性也不是同步的,调用 queueWatcher 函数,将当前 Watcher 实例(this)作为实参传入。

/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
update() {
    /* istanbul ignore else */
    if (this.lazy) {
        this.dirty = true
    } else if (this.sync) {
        this.run()
    } else {
        queueWatcher(this)
    }
}

run 方法同步执行更新的操作,并没有任何异步的操作逻辑,先判断 active 值为 true,即表示是当前活动的 watcher,调用 get 方法获取新的值,判断新老值不同或者新值是个对象,或者深度监视了,记录下老值,然后将新值赋给老值,判断是不是用户自定义的 watcher,即 user 为 true,是的话调用 invokeWithErrorHandling 函数,不是的话直接调用绑定了当前 Watcher 实例上的 vm 属性作为上下文的 cb 函数,将新老值传入。

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

来看 queueWatcher 函数(在 src/core/observer/scheduler.ts)文件中,scheduler 是调度器的意思,整个文件都是有关调度的逻辑,我们先顺着上边的函数调用先来分析 queueWatcher 函数。

首先看官方在 queueWatcher 函数上写的注释,意思是:将一个 watcher 推入一个专门存放 watcher 的队列中,具有重复 ID 的任务将被跳过,除非在队列清空时进行推送操作。

它接收一个 watcher 作为参数。主要流程:先获取该 watcher 的 id 属性,判断 has 对象中该 id 上的取值是否为空,不为空说明已经存在该 id 对应的 watcher 了,直接返回。下面继续判断,这个 watcher 是不是当前 Dep.target 且 watcher 上的 noRecurse 属性为 true,noRecurse 为不递归的意思,默认不传是 false,所以 noRecurse 为 true 表示不递归,那也直接返回,不做任何操作。接着将 has 对象上该 id 作为属性其值设为 true。接着判断 flushing 是否为 false,为 false 就将这个 watcher 推到 queue 队列中,flushing 标志位表示当前是否正在清空队列,如果 flushing 为 true,说明当前正在清空队列,那么就要根据当前 watcher 的 id 在队列中找到合适的位置,然后插入进去(两个条件,第一个就是 当前 i 索引要大于 index,index 是当前正在清除的 watcher 的索引,以及当前队列中 i 位置上的 id 大于当前 watcher 的 id,那就 i--,直到找到合适的位置,然后调用 splice 将这个 watcher 放到 i + 1 的位置,也就是刚好满足条件 i 的后一个位置)。最后判断 waiting 是否为 false,为 false 就将 waiting 设为 true,然后判断 config.async 是否为 false,为 false 表示要同步更新,直接调用 flushSchedulerQueue 函数清空队列然后直接返回,反之就是异步更新了,将 flushSchedulerQueue 函数作为参数作为 nextTick 函数的实参,nextTick 函数会进行异步调度,在合适时机触发这个回调清空队列。

/**
 * 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) {
    return
  }

  if (watcher === Dep.target && watcher.noRecurse) {
    return
  }

  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 (__DEV__ && !config.async) {
      flushSchedulerQueue()
      return
    }
    nextTick(flushSchedulerQueue)
  }
}

currentFlushTimestamp 赋值为 getNow 函数返回的结果,就是执行这个函数时的时间戳,flushing 设为 true,表示当前正在清空队列,声明一个 watcher、id 属性,将 queue 队列按照 sortCompareFn 函数排序,会根据 watcher 的 id 进行排序。然后遍历 queue 队列,开始执行,此处官方也注释了表示不要缓存长度,因为随着执行现有的 Watcher 过程中,可能会有更多 Watcher 被添加进来。我们知道在 queueWatcher 中会将新的 watcher 插入到当前 queue 队列的合适位置中,所以 queue 长度是随时可能变化的。遍历时,先获取当前 index 索引位置上的 watcher,判断它有没有 before 函数,有的话先执行,before 函数就是在更新前要做的一些事,一般用不到,接着将 watcher 的 id 赋值给变量 id,然后将 has 对象上的 id 属性对应值赋为 null,因为下面马上要执行该 watcher 了,可以先清掉了。赋值为 null 后,下边就执行该 watcher 上的 run 方法。接着检测是否存在循环更新,就是我这个 watcher 执行完,理论上 has 中该 id 对应值为 null 了,但如果检测到还不为空,说明执行过程又再次推入,这样每个 watcher 的执行都会推一个新的进来,这样停不下来的,circular 就记录着该 watcher 对应 id 的引用次数,MAX_UPDATE_COUNT 是设置的最大引用次数 100,当一个 watcher 引用自身超过这个设定的阈值,说明它存在无限循环了,抛出错误提示,这里也是一个优化,如果不设置这个最大引用次数的阈值,那么很可能导致栈溢出,浏览器崩溃。最后进行一些钩子函数的更新,先拷贝一份 activatedChildren 和 queue,然后调用 resetSchedulerState 重置状态,会将用到的变量、数组全部置为初始状态,然后调用 callActivatedHooks、callUpdatedHooks 执行钩子函数,最后调用 cleanupDeps 清理依赖。下边还有个判断 devtools 开发者工具的,如果有开启,需要触发一下 flush 函数刷新。 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(sortCompareFn)

  // 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) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (__DEV__ && 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)
  cleanupDeps()

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

最后我们来看异步调度的核心函数 nextTick,所在文件:(src/core/util/next-tick.ts)

看 nextTick 函数:

export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        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
    })
  }
}

接收 cb 回调函数作为第一个参数,第二个参数 ctx 为调用该函数时的上下文

核心逻辑:声明 _resolve 变量,将回调函数 cb 放到 callbacks 数组中,然后判断 pending 是否为 false,如果不是,说明已经有一个异步任务在执行了,不处理,如果 pending 为 false,说明当前没有异步任务在执行,将 pending 设为 true,然后调用 timerFunc 函数,timerFunc 函数是异步调度的核心函数,下边我们来看 timerFunc 函数。

首先文件顶部有几个变量声明:

  • 向外暴露一个 isUsingMicroTask 表示是否使用微任务,默认为 false
  • callbacks 是一个数组,用来存放回调函数
  • pending 表示是否在等待异步任务执行,默认为 false
  • timerFunc 是一个函数,用来执行异步任务,默认是一个未赋值的变量

flushCallbacks 函数

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

将标志位 pending 设为 false,表示当前没有异步任务在执行了,将 callbacks 数组拷贝一份,清空 callbacks 数组,然后遍历拷贝的数组,依次执行数组中的回调函数。

接下来的一系列判断会根据不同的环境选择相应的异步调度策略,然后包裹成一个函数赋值给 timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  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)
  }
  isUsingMicroTask = true
} else if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

首先判断当前执行环境中 Promise 不为 undefined 且是原生 JavaScript 中的 Promise,即当前环境支持 Promise,那就优先使用它作为异步策略,Promise.resolve 会返回一个 Promise 实例,接着给 timerFunc 赋值为一个函数,这个函数执行会在 p.then 时执行 flushCallbacks 函数,如果当前环境是 iOS,则调用 setTimeout,因为 iOS 的 webview 有 bug,Promise.then 不会立即执行,而是会延迟到下一个事件循环,所以这里加一个空 setTimeout,保证 flushCallbacks 函数立即执行,isUsingMicroTask 设为 true,表示当前使用的是微任务。

如果当前环境不支持 Promise,先判断非 IE 环境 且支持 MutationObserver 这个特性的情况,声明一个变量 counter 默认为 1,通过 new MutationObserver 构造函数创建一个监听实例 observer,给构造函数传的是 flushCallbacks 回调,这个回调会在监视的内容发生变化后执行,接着基于 counter 变量创建一个文本节点,然后调用 observer 实例的 observe 监视方法,第一个参数是要监视的节点,就是刚创建的文本节点,第二个参数是提供应报告哪些 DOM 突变的选项,设置了 characterData: true,就是监控指定目标节点(其后代节点)对该节点或多个节点内字符数据的变化。在这里只要监视的 counter 文本节点内容发生变化就会重新执行 flushCallbacks 回调。接着将 timerFunc 赋值为一个函数,执行该函数会先改变 counter 的值,然后将更新的 counter 值赋值给 textNode.data,这样文本节点内容发生变化就会触发 observer 的回调函数,执行 flushCallbacks 函数,最后将 isUsingMicroTask 设为 true,表示当前使用的是微任务。

接着就是 Promise 和 MutationObserver 都不支持的情况,检查 setImmediate 是否可用,如果可用,将 timerFunc 赋值为一个函数,执行该函数会调用 setImmediate,将 flushCallbacks 函数放到任务队列中,等待执行。

最后就是以上都不支持的情况,使用 setTimeout 兜底,将 timerFunc 赋值为一个函数,执行该函数会调用 setTimeout,将 flushCallbacks 函数放到任务队列中,等待执行。

分析完 timerFunc 的实现,知道它会根据不同的环境选择不同的异步策略,保证 flushCallbacks 函数在下一个事件循环中执行,而 flushCallbacks 函数会依次执行 callbacks 数组中的回调函数。timerFunc 就是 nextTick 实现异步调度的核心方法。

以上就是本篇文章的所有内容了,对你有帮助的话点赞支持下,下篇我们来分析 object 和 array 变化侦测有什么不同,以及 Vue 为我们提供的一些响应式 API 的实现。