Vue3 源码剖析之 reactivity 模块

95 阅读10分钟

为了调试源码,我们首先克隆一份 Vue3 的代码,本文使用的是 Vue@3.2-beta.4 版本,官方仓库

// depth 1 代表只克隆下包含最近一次 commit,不会携带历史提交记录
git clone https://github.com/vuejs/vue-next.git --depth 1 

打包配置分析

我们先来看 package.json 中的 dev 打包命令,把打包文件改成 reactivity,我们这里先分析 reactivity 响应式模块哦。

node scripts/dev.js

scripts/dev.js

// 开启子进程 可以用于执行命令
const execa = require('execa')
const { fuzzyMatchTarget } = require('./utils')
// 获取命令行参数
const args = require('minimist')(process.argv.slice(2)) 
// 如果没有传参,默认以 vue 为入口,这里改成 'reactivity'
const target = args._.length ? fuzzyMatchTarget(args._)[0] : 'reactivity' 
const formats = args.formats || args.f
const sourceMap = args.sourcemap || args.s
const commit = execa.sync('git', ['rev-parse', 'HEAD']).stdout.slice(0, 7)

execa(
  'rollup', // 开启子进程 执行 roolup 命令
  [
    '-wc',  // 使用监控和配置文件的方式
    '--environment', // 指定打包后文件运行的环境(esm, cjs, iife)
    [
      `COMMIT:${commit}`,
      `TARGET:${target}`, // 要打包的文件
      `FORMATS:${formats || 'global'}`, // 打包方式,默认global(iife)
      sourceMap ? `SOURCE_MAP:true` : `` // 添加 sourceMap
    ]
      .filter(Boolean) // 过滤空选项
      .join(',') // 拼接
  ],
  {
    stdio: 'inherit' // 子进程可以在父进程进行输出打印
  }
)

rlolup.config.js

rollup 执行命令依赖了它的配置文件(rollup -c),我们来看下 rollup.config.js(文件比较大,我们只关注核心即可)。

  1. 这里打包使用到了对应子包 package.json 下的自定义属性 buildOptions,顺便提一嘴,package.json 中 main 字段指的是 require 引用执行的入口文件,module 指的是 import 引用入口文件,unpkg 字段代表不用打包文件,需要手动在 script.src 引入这个入口。
  2. 打包入口文件为 src/index.ts
rollup.config.js
// @ts-check
import path from 'path'
// rollup 中处理 ts
import ts from 'rollup-plugin-typescript2' 
// 替换环境变量的插件 兼容浏览器 process.env 不存在的情况
import replace from '@rollup/plugin-replace' 
// 实现对 json 导入支持
import json from '@rollup/plugin-json'

if (!process.env.TARGET) {
  throw new Error('TARGET package must be specified via --environment flag.')
}

const masterVersion = require('./package.json').version
// 获取装载各种子包的 packages 目录(典型的 monorepo 组织代码方式)
const packagesDir = path.resolve(__dirname, 'packages') 
// 获取要打包的文件
const packageDir = path.resolve(packagesDir, process.env.TARGET)
// 根据当前打包的目录解析路径查找文件,进行方法封装
const resolve = p => path.resolve(packageDir, p)
// 拿到当前打包文件对应的 package.json
const pkg = require(resolve(`package.json`))
// 当前打包文件对应的 package.json 中自定义配置 buildOptions
const packageOptions = pkg.buildOptions || {}
// 获取配置的打包输出的文件名,默认自子文件夹名
const name = packageOptions.filename || path.basename(packageDir)

// ensure TS checks only once for each build
let hasTSChecked = false

const outputConfigs = { // 输出文件配置
  'esm-bundler': {
    file: resolve(`dist/${name}.esm-bundler.js`),
    format: `es`
  },
  'esm-browser': {
    file: resolve(`dist/${name}.esm-browser.js`),
    format: `es`
  },
  cjs: {
    file: resolve(`dist/${name}.cjs.js`),
    format: `cjs`
  },
  global: {
    file: resolve(`dist/${name}.global.js`),
    format: `iife`
  },

  // runtime-only builds, for main "vue" package only
  'esm-bundler-runtime': {
    file: resolve(`dist/${name}.runtime.esm-bundler.js`),
    format: `es`
  },
  'esm-browser-runtime': {
    file: resolve(`dist/${name}.runtime.esm-browser.js`),
    format: 'es'
  },
  'global-runtime': {
    file: resolve(`dist/${name}.runtime.global.js`),
    format: 'iife'
  }
}

const defaultFormats = ['esm-bundler', 'cjs'] // 默认打包格式
const inlineFormats = process.env.FORMATS && process.env.FORMATS.split(',')
const packageFormats = inlineFormats || packageOptions.formats || defaultFormats
const packageConfigs = process.env.PROD_ONLY
  ? []
  : packageFormats.map(format => createConfig(format, outputConfigs[format]))

if (process.env.NODE_ENV === 'production') {
  packageFormats.forEach(format => {
    if (packageOptions.prod === false) {
      return
    }
    if (format === 'cjs') {
      packageConfigs.push(createProductionConfig(format))
    }
    if (/^(global|esm-browser)(-runtime)?/.test(format)) {
      packageConfigs.push(createMinifiedConfig(format))
    }
  })
}

export default packageConfigs

// 创建 packageConfigs
function createConfig(format, output, plugins = []) {
  if (!output) {
    console.log(require('chalk').yellow(`invalid format: "${format}"`))
    process.exit(1)
  }

  output.exports = 'auto' // 默认导出
  output.sourcemap = !!process.env.SOURCE_MAP // 配置 sourceMap
  output.externalLiveBindings = false

  // 以下是一些环境变量控制
  const isProductionBuild = 
    process.env.__DEV__ === 'false' || /\.prod\.js$/.test(output.file)
  const isBundlerESMBuild = /esm-bundler/.test(format)
  const isBrowserESMBuild = /esm-browser/.test(format)
  const isNodeBuild = format === 'cjs'
  const isGlobalBuild = /global/.test(format)
  const isCompatBuild = !!packageOptions.compat
  const isCompatPackage = pkg.name === '@vue/compat'

  if (isGlobalBuild) {
    output.name = packageOptions.name // 是 global(iife), 配置名字(window[name])
  }

  const shouldEmitDeclarations =
    pkg.types && process.env.TYPES != null && !hasTSChecked

  const tsPlugin = ts({ // TS 插件配置
    check: process.env.NODE_ENV === 'production' && !hasTSChecked,
    tsconfig: path.resolve(__dirname, 'tsconfig.json'),
    cacheRoot: path.resolve(__dirname, 'node_modules/.rts2_cache'),
    tsconfigOverride: {
      compilerOptions: {
        sourceMap: output.sourcemap,
        declaration: shouldEmitDeclarations, // 是否生成 ts 的 .d.ts 文件
        declarationMap: shouldEmitDeclarations
      },
      exclude: ['**/__tests__', 'test-dts'] // 排除测试目录
    }
  })
  // we only need to check TS and generate declarations once for each build.
  // it also seems to run into weird issues when checking multiple times
  // during a single build.
  hasTSChecked = true

  // 入口文件 区分运行时和完整代码
  let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts` 

  // the compat build needs both default AND named exports. This will cause
  // Rollup to complain for non-ESM targets, so we use separate entries for
  // esm vs. non-esm builds.
  if (isCompatPackage && (isBrowserESMBuild || isBundlerESMBuild)) {
    entryFile = /runtime$/.test(format)
      ? `src/esm-runtime.ts`
      : `src/esm-index.ts`
  }

  let external = []

  if (isGlobalBuild || isBrowserESMBuild || isCompatPackage) {
    if (!packageOptions.enableNonBrowserBranches) {
      // normal browser builds - non-browser only imports are tree-shaken,
      // they are only listed here to suppress warnings.
      external = ['source-map', '@babel/parser', 'estree-walker']
    }
  } else {
    // Node / esm-bundler builds.
    // externalize all deps unless it's the compat build.

    // es6 或者 cjs 环境下,需要把一些依赖包做下 external,不要把所有包打到一起。
    // 比如 reactivity 包引用 shared 包,不应该把 shared 打到 reactivity 中
    // 防止打包文件过大
    external = [
      ...Object.keys(pkg.dependencies || {}),
      ...Object.keys(pkg.peerDependencies || {}),
      ...['path', 'url', 'stream'] // for @vue/compiler-sfc / server-renderer
    ]
  }

  // the browser builds of @vue/compiler-sfc requires postcss to be available
  // as a global (e.g. http://wzrd.in/standalone/postcss)
  output.globals = {
    postcss: 'postcss'
  }

  const nodePlugins =
    packageOptions.enableNonBrowserBranches && format !== 'cjs'
      ? [
          // @ts-ignore
          require('@rollup/plugin-commonjs')({
            sourceMap: false
          }),
          // @ts-ignore
          require('rollup-plugin-polyfill-node')(),
          require('@rollup/plugin-node-resolve').nodeResolve()
        ]
      : []

  // createConfig 函数 最终的返回的配置项
  return {
    input: resolve(entryFile),
    // Global and Browser ESM builds inlines everything so that they can be
    // used alone.
    external,
    plugins: [
      json({
        namedExports: false
      }),
      tsPlugin,
      createReplacePlugin( // 替换环境变量
        isProductionBuild,
        isBundlerESMBuild,
        isBrowserESMBuild,
        // isBrowserBuild?
        (isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) &&
          !packageOptions.enableNonBrowserBranches,
        isGlobalBuild,
        isNodeBuild,
        isCompatBuild
      ),
      ...nodePlugins,
      ...plugins
    ],
    output,
    onwarn: (msg, warn) => {
      if (!/Circular/.test(msg)) {
        warn(msg)
      }
    },
    treeshake: {
      moduleSideEffects: false
    }
  }
}

function createReplacePlugin(
  isProduction,
  isBundlerESMBuild,
  isBrowserESMBuild,
  isBrowserBuild,
  isGlobalBuild,
  isNodeBuild,
  isCompatBuild
) {
  const replacements = {
    __COMMIT__: `"${process.env.COMMIT}"`,
    __VERSION__: `"${masterVersion}"`,
    __DEV__: isBundlerESMBuild
      ? // preserve to be handled by bundlers
        `(process.env.NODE_ENV !== 'production')`
      : // hard coded dev/prod builds
        !isProduction,
    // this is only used during Vue's internal tests
    __TEST__: false,
    // If the build is expected to run directly in the browser (global / esm builds)
    __BROWSER__: isBrowserBuild,
    __GLOBAL__: isGlobalBuild,
    __ESM_BUNDLER__: isBundlerESMBuild,
    __ESM_BROWSER__: isBrowserESMBuild,
    // is targeting Node (SSR)?
    __NODE_JS__: isNodeBuild,

    // 2.x compat build
    __COMPAT__: isCompatBuild,

    // feature flags
    __FEATURE_SUSPENSE__: true,
    __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,
    __FEATURE_PROD_DEVTOOLS__: isBundlerESMBuild
      ? `__VUE_PROD_DEVTOOLS__`
      : false,
    ...(isProduction && isBrowserBuild
      ? {
          'context.onError(': `/*#__PURE__*/ context.onError(`,
          'emitError(': `/*#__PURE__*/ emitError(`,
          'createCompilerError(': `/*#__PURE__*/ createCompilerError(`,
          'createDOMCompilerError(': `/*#__PURE__*/ createDOMCompilerError(`
        }
      : {})
  }
  // allow inline overrides like
  //__RUNTIME_COMPILE__=true yarn build runtime-core
  Object.keys(replacements).forEach(key => {
    if (key in process.env) {
      replacements[key] = process.env[key]
    }
  })
  return replace({
    // @ts-ignore
    values: replacements,
    preventAssignment: true
  })
}

function createProductionConfig(format) {
  return createConfig(format, {
    file: resolve(`dist/${name}.${format}.prod.js`),
    format: outputConfigs[format].format
  })
}

function createMinifiedConfig(format) {
  const { terser } = require('rollup-plugin-terser')
  return createConfig(
    format,
    {
      file: outputConfigs[format].file.replace(/\.js$/, '.prod.js'),
      format: outputConfigs[format].format
    },
    [
      terser({
        module: /^esm/.test(format),
        compress: {
          ecma: 2015,
          pure_getters: true
        },
        safari10: true
      })
    ]
  )
}

现在我们的关注点可以放在打包的入口文件 packages/reactivity/src/index.ts 了。

reactivity 包分析

入口文件 reactivity/src/index.ts

reactivity/src/index.ts 这个入口文件很简单,就是暴露了一些 api

packages/reactivity/src/index.ts
export {
  ref,
  shallowRef,
  isRef,
  toRef,
  toRefs,
  unref,
  proxyRefs,
  customRef,
  triggerRef,
  Ref,
  ToRefs,
  UnwrapRef,
  ShallowUnwrapRef,
  RefUnwrapBailTypes
} from './ref'  // 暴露一些 ref 相关 api
export {
  reactive,
  readonly,
  isReactive,
  isReadonly,
  isProxy,
  shallowReactive,
  shallowReadonly,
  markRaw,
  toRaw,
  ReactiveFlags,
  DeepReadonly,
  UnwrapNestedRefs
} from './reactive' // 暴露一些 reactive 相关 api
export {
  computed,
  ComputedRef,
  WritableComputedRef,
  WritableComputedOptions,
  ComputedGetter,
  ComputedSetter
} from './computed' 
export { deferredComputed } from './deferredComputed'
export {
  effect,
  stop,
  trigger,
  track,
  enableTracking,
  pauseTracking,
  resetTracking,
  ITERATE_KEY,
  ReactiveEffect,
  ReactiveEffectRunner,
  ReactiveEffectOptions,
  EffectScheduler,
  DebuggerOptions,
  DebuggerEvent
} from './effect'
export {
  effectScope,
  EffectScope,
  getCurrentScope,
  onScopeDispose
} from './effectScope'
export { TrackOpTypes, TriggerOpTypes } from './operations'

响应式 reactivity/src/reactive.ts

我们来看下 reactive 文件,yarn dev 启动服务,reactivity 根目录新建 index.html,引入 reactivity.global.js

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <script src="./dist/reactivity.global.js"></script>

  <script>
    let { reactive, readonly} = VueReactivity;

  </script>
</body>

</html>
  1. reactive 方法
export function reactive(target: object) {
  // 针对特殊场景,reactive(readonly(obj))
  // 如果这个对象已经被 readonly 代理了,则直接返回该 proxy
  // 访问 ReactiveFlags.IS_READONLY 属性,会取 proxy 的 __v_isReadonly 属性
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }

  // 创建响应式对象
  return createReactiveObject(
    target,
    false,
    mutableHandlers, // 数组和对象的拦截器
    mutableCollectionHandlers, // map 和 set 的拦截器(包含弱引用) 
    reactiveMap
  )
}

示例,同一个对象被 readonly 代理后将代理对象传入 reactive 会直接返回代理对象。

let obj = { name: 'ys' }
let proxy1 = readonly(obj);
let proxy2 = reactive(proxy1);
console.log(proxy1 == proxy2); // true

下跳到 createReactiveObject 方法:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 如果不是对象 直接返回
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }

  // ReactiveFlags.RAW = "_v_raw"
  // 获取 target._v_raw 会走 target 的 get 方法,该方法内部判断,
  // 如果获取的 key 是 "_v_raw",则返回当前 target 的代理状态
  // 同理 __v_isReadonly __v_isReactive __v_skip 都一样哦
  // 如果当前对象是 reactive 代理后的对象,进行 readonly 二次代理,否则直接返回它
  // 也就是兼容了 radeOnly(reactive(obj)) 写法
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  // 看当前目标有没有被当前代理类型代理过.. proxyMap 缓存是传过来的
  // 分为四种 reactiveMap shallowReactiveMap readonlyMap shallowReadonlyMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) { // 如果被代理过,直接返回存在过的代理对象
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  // 判断对象状态,是否可扩展(有没有冻结变量之类的操作哦)
  // 如果能被扩展,调用 Object.prototype.toString 获取对象类型并返回
  const targetType = getTargetType(target) // 看看能否被代理
  // 如果不能 返回对象本身
  if (targetType === TargetType.INVALID) {
    return target
  }

  // 创建代理 
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 当前 Map 缓存 target 和 代理结果
  proxyMap.set(target, proxy)
  // 返回代理
  return proxy
}

1. 多次进行 reactive 代理,返回的都是上次代理后的值,且 reactive 代理后,能继续被 readonly 代理 2. 冻结(不能被代理)的对象直接返回对象本身

let obj = { name: 'ys' }
let proxy1 = reactive(obj);
let proxy2 = reactive(proxy1);
let proxy3 = reactive(proxy2);
let proxy4 = readonly(proxy3);

console.log(proxy1 === proxy2); // true
console.log(proxy2 === proxy3); // true

proxy4.name = 'sy'

let proxy5 = reactive(Object.freeze({ a: 1 }));
console.log(proxy5); // { a: 1 } 不会被代理,返回源对象

我们的普通类型(非集合)都是通过 baseHandlers 对象来实现代理,而 baseHandler 就是以下几个对象,作为 createReactiveObject 参数传递到函数中的。

// 其实以下这些就是 new Proxy 第二个参数
import {
  mutableHandlers, // reactive 对应的代理方式
  readonlyHandlers, // readonly 对应的代理方式
  shallowReactiveHandlers, // shallowReactive 对应的代理方式
  shallowReadonlyHandlers // shallowReadonly 对应的代理方式
} from './baseHandlers'

代理方法 reactivity/src/baseHandlers.ts

const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

//...

const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)

//...

// 我们拿 reactive 来举例
export const mutableHandlers: ProxyHandler<object> = {
  get, // get 劫持函数!!
  set, // set 劫持函数!!
  deleteProperty,
  has,
  ownKeys
}

我们的关注点放在 get 和 set 方法,这里出现了一些核心方法,createSettercreateGetter,我们先看 createGetter 1. get 方法针对数组进行了八个内置方法的重写,其中查找方法('includes', 'indexOf', 'lastIndexOf')为了触发某些场景下的更新,针对每个数组下标进行了依赖收集,修改方法('push', 'pop', 'shift', 'unshift', 'splice')加了可以 暂停/恢复 依赖收集的锁,而收集依赖方法(track)会判断是否要收集依赖 2. 读取 Symbol 上或者原型链上的属性,不进行依赖收集,深度代理会递归进行代理。

// 对数组进行点操作(arr.shift),会触发 get 方法,重写数组八种方法
// <div>{{ proxy([1, 2, 3]).includes('a') }}<div> 正常情况下,数组元素改变,不会触发更新,
// 因为数组被我们代理了,并且劫持了数组的 get 方法,but 并没有对数组下标进行收集。
function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  // instrument identity-sensitive Array methods to account for possible reactive
  // values
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      const arr = toRaw(this) as any // 拿到当前操作的数组源对象
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, TrackOpTypes.GET, i + '') // 每个数组下标都进行依赖收集
      }
      // we run the method using the original args first (which may be reactive)
      const res = arr[key](...args) // 还是会调用原有的方法
      if (res === -1 || res === false) {
        // if that didn't work, run it again using raw values.
        return arr[key](...args.map(toRaw))
      } else {
        return res
      }
    }
  })
  // instrument length-altering mutation methods to avoid length being tracked
  // which leads to infinite loops in some cases (#2137)

  // 当我们调用数组  push 时,它不光会触发下标读取,还会触发 length 读取
  // 会导致下面这种无限更新,互相修改 length,这里相当于加个锁
  // const arr = reactive([])

  // watchEffect(() => {
  //   arr.push(1);
  // })

  // watchEffect(() => {
  //   arr.push(2);
  // })

  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      pauseTracking() // 暂停收集依赖 shouldTrack = false
      const res = (toRaw(this) as any)[key].apply(this, args) // 对源数组执行方法
      resetTracking() // 恢复收集依赖 shouldTrack = true
      return res
    }
  })
  return instrumentations // 最终返回重写的对象
}

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      // 访问 target.__v_isReactive 会走这里
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      // 访问 target.__v_isReadonly 会走这里
      return isReadonly
    } else if (
      // 访问 target.__v_raw 会走这里,判断是否有被代理,有代理返回源对象
      // 这也是 toRaw 方法的逻辑哦
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }

    // 判断是否是数组
    const targetIsArray = isArray(target)

    // 非仅读的 target,而且是数组,重写了 'includes', 'indexOf', 'lastIndexOf'
    // push', 'pop', 'shift', 'unshift', 'splice' 八个方法
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // 如果 get 取值是特殊的方法,不会对这些方法进行依赖收集,而是重写增强了他们
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    // 是不是 symbol,如果是内置的 symbol 上的属性或者是原型链,直接返回
    // arr[Symbol.xxx] 这样不用收集依赖,不然太损耗性能辣
    // builtInSymbols 获取 Symbol 上的属性,isNonTrackableKeys 获取原型链
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    // 不是仅读的,就收集依赖
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    // 浅的直接返回,不需要递归代理
    if (shallow) {
      return res
    }

    // 是 ref,会把除数组元素的值进行拆包(不用写 .value)
    if (isRef(res)) { 
      // ref unwrapping - does not apply for Array + integer key.
      // 不是数组或者取的key不是数字字符串,应该拆包
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    // get 取出来的属性的值仍是对象,进行递归代理
    if (isObject(res)) { 
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

小试牛刀

  let { reactive, toRaw, markRaw } = VueReactivity;
// toRaw 返回源对象,markRaw 代表对象不能被代理,返回源对象
// markRaw 会给对象增加 _v_skip(不能被扩展,同冻结处理一致)
let obj = { name: 'ys' }

let proxy = reactive(obj);
console.log(obj === toRaw(proxy)); // true

let obj2 = { name: 'sy' }
let proxy2 = reactive(markRaw(obj2))
console.log(obj2, proxy2); // {name: "sy", __v_skip: true}
console.log(obj2 === proxy2); // true



// ref 的拆包
let r = reactive({
  name: ref('ys') // r.name.value 太啰嗦了
})
// 访问属性发现是 ref,会带上 .value 取值,这时候 r.name 仍然是个响应式
console.log(r.name);

// reactive 包裹着的数组内元素如果有 ref,不会拆包,比如我想数组第 0 项是响应式
// 你如果拆包的话,我取第一个 0 项元素就是个没有代理过的原始值,这不合理
let r2 = reactive([ref[0], 1, 2, 3])
console.log(r2[0]); // RefImpl {__v_isRef: true, _value: "hello", ...}

接下来分析 createSetter 方法

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // 获取旧值
    let oldValue = (target as any)[key] 
    if (!shallow) { 
      // 深层代理对象时,如果设置的值是个已经代理的对象话,"还原值" 进行操作以节约性能
      // 如果传入的也是个代理后的值,可以直接用
      // let proxy = reactive({ a: { val: 1 }})
      // proxy.a = reactive({ name: b }) // 代理后的值
      // 这种情况,直接全部还原,让 proxy.a = { name: b } 即可。
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      // 源对象不是数组,老值是 ref,新值不是 ref,直接赋值 return 出去。
      // let proxy = reactive({ a: ref(1) })   proxy.a = 100;
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        // 把老的 ref 的 value 改成新 value,让新 value 也是个 ref
        oldValue.value = value 
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    // 针对原型链是 proxy 的情况,不要触发多次更新
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 新增
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 修改
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

请看以下代码:

// 设置值的特性(原值赋值,保持 ref)
let proxy1 = reactive({ name: 'ys', age: ref(11) });

proxy1.name = reactive({ str: 'sy' }); // 恢复原值赋值

proxy1.age = 12; // 原来是 ref,继续包成 ref
console.log(proxy1)


// 针对原型链是 proxy 的情况,不要使 set 触发多次更新
let obj = {};
let proto = { a: 1 }
let proxyProto = new Proxy(proto, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log(proxyProto, receiver == myProxy)
    if (receiver == proxyProto) {
      // 触发更新
    }
    return Reflect.set(target, key, value, receiver)
  }
})

Object.setPrototypeOf(obj, proxyProto); //原型链 obj.__proto__ = proxyProto

// 代理一下
let myProxy = new Proxy(obj, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log(receiver === myProxy)
    // 这里设置原型链上的 a,receiver 是 myProxy,为了防止在原型链触发更新
    // 源码对这个做了处理 target === toRaw(receiver)
    console.log(myProxy == receiver) // true
    return Reflect.set(target, key, value, receiver)
  }
})

myProxy.a = 100

响应式核心 reactivity/src/effct.ts

effct 在我们响应式声明中扮演极其重要的角色,他相当于 vue2 中的 watcher,用于 set 操作后,做实际的更新操作。

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  if ((fn as ReactiveEffectRunner).effect) { // 如果已经是 effct 了
    // 把 effct 中的原函数拿出来再次生成新的 effct 
    // 就是不用原来的,埃?就是玩儿
    fn = (fn as ReactiveEffectRunner).effect.fn 
  }

  const _effect = new ReactiveEffect(fn) // 创建响应式 effct
  if (options) {
    extend(_effect, options) 
    // 如果有 scope 属性,记录 effct 作用域
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    // 如果没传 lazy,执行创建完的 effct 方法
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

该函数调用了 _effect.run 方法,_effct 是类 ReactiveEffect 的实例。

export class ReactiveEffect<T = any> {
  active = true // 默认 effct 是激活状态
  deps: Dep[] = [] // 收集的属性

  // can be attached after creation
  computed?: boolean // 计算属性 effct
  allowRecurse?: boolean 
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope | null
  ) {
    recordEffectScope(this, scope)
  }

  run() {
    if (!this.active) {
      // 如果是非激活状态 执行挂载的原函数 fn,默认没有执行
      return this.fn()
    }
    // 防止 effct 中同时存在取值 & 赋值,无限循环
    // 判断当前 effct 不在栈中,才继续执行
    if (!effectStack.includes(this)) { 
      try {
        effectStack.push((activeEffect = this))
        enableTracking() // 启动收集


        trackOpBit = 1 << ++effectTrackDepth

        if (effectTrackDepth <= maxMarkerBits) {
           // 这里做标记 effct 内的取值,会给属性收集 { n:2, w:0 },存在 Set 收集的 effct 中?
           // 下次执行该 effct 会做清空操作,给属性值刷为 { n: 0, w: 2 }
           // 在进行 effct 的取值,取到继续刷 n = 2
           // 如果 n !== w,则标记为应该干掉的属性依赖收集!从依赖表中干掉
           // 结合下面栗子理解,这是 3.2 才更新出的优化
          initDepMarkers(this)
        } else { // 超过 30 层 effct 就直接清理
          cleanupEffect(this)
        }
        return this.fn()
      } finally {
        if (effectTrackDepth <= maxMarkerBits) {
          finalizeDepMarkers(this)
        }

        trackOpBit = 1 << --effectTrackDepth

        resetTracking() // 恢复收集
        effectStack.pop() // 在栈中弹出
        const n = effectStack.length
        activeEffect = n > 0 ? effectStack[n - 1] : undefined // 还原 activeEffct为栈顶元素
      }
    }
  }

  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

栗子来了

const state = reactive({ name: 'ys', age: 11 })
effect(() => { // 1.默认执行时 需要 收集 name:{n:2,w:2} 和 age {n:0,w:2} 
  console.log('effect')
  if (state.name === 'ys') {
    // 第一次进来 第二次不进来
    console.log(state.age);
  }
})
// 我改了name后,需要重新渲染重新调用effect,(把name和age的依赖全部清空) 取name了 但是没取age (age不需要依赖收集了)
state.name = 'sy';
state.age = 200;