为了调试源码,我们首先克隆一份 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(文件比较大,我们只关注核心即可)。
- 这里打包使用到了对应子包 package.json 下的自定义属性 buildOptions,顺便提一嘴,package.json 中 main 字段指的是 require 引用执行的入口文件,module 指的是 import 引用入口文件,unpkg 字段代表不用打包文件,需要手动在 script.src 引入这个入口。
- 打包入口文件为 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>
- 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 方法,这里出现了一些核心方法,createSetter 和 createGetter,我们先看 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;