58南浔做网站上海企业网站定制

当前位置: 首页 > news >正文

58南浔做网站,上海企业网站定制,php 手机网站开发,大连企业网站模板建站目录 编写 reactive 的函数签名处理对象的其他行为拦截 in 操作符拦截 for…in 循环delete 操作符 处理边界新旧值发生变化时才触发依赖的情况处理从原型上继承属性的情况处理一个对象已经是代理对象的情况处理一个原始对象已经被代理过一次之后的情况 浅响应与深响应代理数组… 目录 编写 reactive 的函数签名处理对象的其他行为拦截 in 操作符拦截 for…in 循环delete 操作符 处理边界新旧值发生变化时才触发依赖的情况处理从原型上继承属性的情况处理一个对象已经是代理对象的情况处理一个原始对象已经被代理过一次之后的情况 浅响应与深响应代理数组数组的索引与 length遍历数组数组的查找方法隐式修改数组长度的原型方法 结语 编写 reactive 的函数签名 前面我们已经实现了一个基础的响应式数据但是这是一种比较简陋的方法而且使用起来书写相对繁琐所以我们先构造一个 reactive 的函数签名如下 function get(target, key, receiver) {track(target, key)return Reflect.get(target, key, receiver) }function set(target, key, newVal) {target[key] newValtrigger(target, key)return true }function reactive(value) {// 简单做一个基础拦截if (typeof value ! object || value null) {console.warn(value 必须是一个对象)return value}const proxy new Proxy(value, { get, set })return proxy }const state reactive({ name: zs, age: 18 })effect(() {console.log(\({state.name}今年\){state.age}岁了) })state.age上述这段代码中我们将 get 和 set 的逻辑抽离了出去这样代码会更优雅一点同时简单的封装了一下 reactive创建一个可响应的数据也方便一点。 处理对象的其他行为 这个行为具体的可以查阅文档得知https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#object_internal_methods 拦截 in 操作符 前面我们处理的只有 get 和 set也简单的把 get 就看做是读set 看做是写而在响应式系统中读是一个宽泛的概念比如 in 操作符也是一个读的操作例如 effect((){foo in obj })而这种读的操作在响应式系统中也应该被拦截而如果想拦截 in 这个读的操作就需要使用内部方法 [[HasProperty]]而这个内部方法在 Proxy 中对应的则是 has这些在 ECMA 或者 MDN 文档中都可以找到有兴趣的自行翻阅即可。 已经知道了对应的拦截器那实现也就非常简单了如下 function has(target, key) {console.log(in 被拦截了)track(target, key)return Reflect.has(target, key) }function reactive(value) {if (typeof value ! object || value null) {console.warn(value 必须是一个对象)return value}const proxy new Proxy(value, { get, set, has })return proxy }const state reactive({ name: zs, age: 18 }) effect(() {age in state })我们来查看一下输出的结果如图 拦截 for…in 循环 for…in 循环对应的内部方法为 [[OwnPropertyKeys]]对应 Proxy 中的 ownKeys我们先看一下实现代码如下 const ITERATE_KEY Symbol(iterate) function ownKeys(target) {// 手动构造了一个 key让其与副作用函数关联track(target, ITERATE_KEY)return Reflect.ownKeys(target) }在这段代码中与之前不一样的则是手动的创建一个 key这是为什么呢我们设计响应式依赖收集数据结构中需要一个 key而 ownKeys 方法确并没有提供 key 的参数只有 target为什么不提供 key 呢合理吗 其实是合理的因为不同于 get/set 的操作我们总能精准的知道当前读取的是对象的那个属性而 ownKeys 则是用来获取一个对象的所有属于自己的键值这个操作是没有和任何具体的键值进行绑定所以不提供 key 是合理的。 因此我们如果想复合我们设计的数据结构只有手动的构造一个 key 来实现依赖的关联。 那我们来看看是否现在可以达到我们预期的效果代码如下 const state reactive({ name: zs, age: 18 }) effect(() {for (const key in state) {console.log(key)} }) state.address shanghai这段代码我们的预期是先输出 name、age然后添加了一个 address 时肯定会对 for…in 循环产生印象那么应该在输出一个 address我们看一下执行的结果如图 预期中的 address 并没有出现这是为什么呢因为这一部分触发的其实 set我们给 set 方法添加一句打印如下 function set(target, key, newVal) {console.log(set: , key)target[key] newValtrigger(target, key)return true }查看一下运行结果如图 但是在 set 中触发key 是 address这和我们设置的 ITERATE_KEY 是一点关系都没有所以应该如何触发呢我们可以尝试给 ITERATE_KEY 单开一个 Set 集合在执行即可如下 function trigger(target, key) {let depsMap targetMap.get(target)if (!depsMap) returnlet deps depsMap.get(key)// 取得 ITERATE_KEY 的依赖let iterateDeps depsMap.get(ITERATE_KEY)const effetsToRun new Set()deps deps.forEach(effectFn {if (effectFn ! activeFn) {effetsToRun.add(effectFn)}})// 除了加入当前 key 的依赖还要加入 ITERATE_KEY 的依赖iterateDeps iterateDeps.forEach(effectFn {if (effectFn ! activeFn) {effetsToRun.add(effectFn)}})effetsToRun.forEach(fn {if (fn.options fn.options.scheduler) {fn.options.scheduler(fn)} else {fn()}}) }此时我们写一段测试代码如下 const state reactive({ name: zs, age: 18 }) effect(() {for (const key in state) {console.log(key)} }) console.log(**添加*) state.address shanghai结果如图 但是现在还有一个问题就是无差别触发比如我重新修改年龄那么此时并不会对这个迭代行为产生影响那么就不应该触发所以我们需要识别 set 触发时当前的 key 是新增的还是重新赋值基于这点我们就可以优化一下代码如下 function trigger(target, key, type) {let depsMap targetMap.get(target)if (!depsMap) returnlet deps depsMap.get(key)const effetsToRun new Set()deps deps.forEach(effectFn {if (effectFn ! activeFn) {effetsToRun.add(effectFn)}})// 只有添加属性才会触发 ITERATE_KEY 的副作用函数if (type ADD) {let iterateDeps depsMap.get(ITERATE_KEY)iterateDeps iterateDeps.forEach(effectFn {if (effectFn ! activeFn) {effetsToRun.add(effectFn)}})}effetsToRun.forEach(fn {if (fn.options fn.options.scheduler) {fn.options.scheduler(fn)} else {fn()}}) }/* … /function set(target, key, newVal, receiver) {// 根据当前对象有没有这个 key 来区分是新增还是修改const type Object.prototype.hasOwnProperty.call(target, key) ? SET : ADDconst result Reflect.set(target, key, newVal, receiver)if (!result) returntrigger(target, key, type)return result }/ … */const state reactive({ name: zs, age: 18 }) effect(() {for (const key in state) {console.log(key)} }) console.log(修改) state.age console.log(添加*) state.sex 男查看一下输出的结果如图 此时就是符合我们预期的修改不触发添加才触发。 当然为了更好的维护我们通常会将这个维护成一个枚举值如下 delete 操作符 delete 在 Proxy 对应的方法则是 deleteProperty因此我们在其中书写对应的逻辑即可如下 function deleteProperty(target, key) {// 检测属性是否存在const hadKey Object.prototype.hasOwnProperty.call(target, key)const result Reflect.deleteProperty(target, key)// 属性存在和删除成功则触发依赖if (hadKey result) {trigger(target, key, TriggerType.DELETE)}return result }这里其他的没有特殊的地方唯有在 trigger 的时候我们传递了一个类型这是因为当属性删除的时候也会印象 for…in 的迭代行为所以需要这个类型如下 function trigger(target, key, type) {let depsMap targetMap.get(target)if (!depsMap) returnlet deps depsMap.get(key)const effetsToRun new Set()deps deps.forEach(effectFn {if (effectFn ! activeFn) {effetsToRun.add(effectFn)}})// 添加判断条件if (type TriggerType.ADD || type TriggerType.DELETE) {let iterateDeps depsMap.get(ITERATE_KEY)iterateDeps iterateDeps.forEach(effectFn {if (effectFn ! activeFn) {effetsToRun.add(effectFn)}})}effetsToRun.forEach(fn {if (fn.options fn.options.scheduler) {fn.options.scheduler(fn)} else {fn()}}) }那么基于修改后的代码我们可以来测试一下测试1如下 const state reactive({ name: zs, age: 18, sex: 男 }) effect(() {console.log(effect, state.sex) })delete state.sex结果如图 是达到我们的预期了在来看看第二个情况如下 const state reactive({ name: zs, age: 18, sex: 男 }) effect(() {for (const key in state) {console.log(effect: , key)} }) console.log(*******) delete state.sex结果如图 也是符合我们的预期的成功触发了 for…in 的迭代行为。 处理边界 新旧值发生变化时才触发依赖的情况 现在我们的只要触发了 set 就会进行依赖的派发而在正确的逻辑中如果新旧值不一样则无需触发我们写一段代码测试一下如下 const state reactive({ name: zs, age: 18, sex: 男 }) effect(() {console.log(effect:, state.age) }) state.age 18测试结果如图 所以我们需要对触发的新旧值进行判断代码如下 function set(target, key, newVal, receiver) {// 获取旧值const oldVal target[key]const type Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADDconst result Reflect.set(target, key, newVal, receiver)if (!result) return// 新旧值不相等则触发依赖if (oldVal ! newVal) {trigger(target, key, type)}return result }我们在执行一样的测试代码如图 现在我们使用的判断是 这个方式是会存在一些问题的我们看一段测试结果如下 console.log(NaN NaN) // false console.log(0 -0) // true console.log(Object.is(NaN, NaN)) // true console.log(Object.is(0, -0)) // false一个值从 NaN 变为 NaN 不会对我们的结果产生影响所以要看做一样的值而 0 和 -0 则会影响比如在某些数学运算和函数中0 和 -0 可能会产生不同的结果。例如在某些情况下计算 1 / 0 和 1 / -0 会得到正无穷大和负无穷大所以应该是不一样的因此这里需要将 换成 Object.is 判断如下 function set(target, key, newVal, receiver) {const oldVal target[key]const type Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADDconst result Reflect.set(target, key, newVal, receiver)if (!result) returnif (!Object.is(oldVal, newVal)) {trigger(target, key, type)}return result }处理从原型上继承属性的情况 话不多说我们看一段测试代码如下 const obj {} const proto { bar: 1 }const child reactive(obj) const parent reactive(proto) // 将 parent 作为 child 的原型 Object.setPrototypeOf(child, parent)effect(() {console.log(child.bar) }) child.bar 2执行结果如图 从结果不难发现修改 bar 的值之后竟然触发了两次副作用函数这是为什么呢让我们顺着代码的执行过程来解析一下。 首先我们知道如果一个对象身上没有某个属性的话会顺着原型链网上查找这里 child.bar 实际上是 parent.bar 的值所以输出的值是1而这个查找的过程第一次查找 child 时就会触发 child 的 get 拦截此时 key 为 bar我们的 child 时一个响应式数据就会发生一次依赖的收集而在 child 上没有找打则去查找 parent那么 parent.bar 也是一个读取行为则也会触发 parent 的 get 拦截且 parent 也是一个响应式数据那么也会造成一次依赖的收集此时就会导致收集的依赖关系如下 child|–bar|–effect parent|–bar|–effect不过这些还不足以解释为什么会触发两次那么我们再来看 child.bar 2 这句代码会发生什么事情。 首先设置 bar 的值时候一定会触发一次 child 的 set 拦截这里就可以知道次数 1哪还有一次从何而来呢 关于这一段在 ECMA 中可以找到调用内部方法 [[Set]] 时的执行过程如图 这一段解释的意思大概是如果设置的属性不存在于对象上的话则会取得其原型并调用原型的 [[Set]] 方法也就是 parent 的 [[Set]] 内部方法吗而由于 parent 也是一个响应式数据那么也会也会触发一次 set 拦截而前面我们也分析了依赖收集的关系parent 也收集了这个 bar 属性看到这里就很明显了这就是第二次执行的由来。 知道了问题之后我们就可以思考如何解决。尽然是两次执行我们只需要屏蔽掉其中一次即可而具体屏蔽那一次肯定是除第一次之后的都屏蔽掉因为原型只要想这个链条上可以不止两个。 那如何让其只执行第一次呢这个我们就要把视线回到 receiver 上我们来看一下不同时候触发的 set 的拦截里面 receiver 都是那个我们添加一句打印如下 function set(target, key, newVal, receiver) {console.log(set-target: , target)console.log(set-receiver: , receiver)const oldVal target[key]const type Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADDconst result Reflect.set(target, key, newVal, receiver)if (!result) returnif (!Object.is(oldVal, newVal)) {trigger(target, key, type)}return result }// 为了方便查看打印添加一个 name 属性表示 const obj { name: obj } const proto { bar: 1, name: proto }const child reactive(obj) const parent reactive(proto) Object.setPrototypeOf(child, parent)effect(() {console.log(child.bar) }) child.bar 2打印结果如图 通过这个打印可以看到target 第一次是 obj 原始对象第二次为 proto 原始对象而反之 receiver 两次都是代理对象 child因此我们只要添加一个判断条件当 receiver 这个代理的原始对象等于 target 那么才触发更新如果不是则表示是原型上的则不派发。 有了这个思路之后我们的问题就是如何在代理身上得到代理的原始对象不幸的是原生的 proxy 并没有提供这样的属性但是我们可以自己解决代码如下 // 获取原始对象时的 key const RAW_KEY Symbol(raw)function get(target, key, receiver) {// 只要 key 为 RAW_KEY就返回原始对象if (key RAW_KEY) {return target}track(target, key)return Reflect.get(target, key, receiver) }有了这个 RAW_KEY 之后就可以在代理身上拿到其所代理的原始对象代码如下 function set(target, key, newVal, receiver) {const oldVal target[key]const type Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADDconst result Reflect.set(target, key, newVal, receiver)if (!result) return// receiver[RAW_KEY] 表示所代理原始对象若两者相等则表示 receiver 是 target 的代理对象if (receiver[RAW_KEY] target) {if (!Object.is(oldVal, newVal)) {trigger(target, key, type)}}return result }/* … */const obj {} const proto { bar: 1 }const child reactive(obj) const parent reactive(proto) Object.setPrototypeOf(child, parent)effect(() {console.log(child.bar) }) console.log(修改) child.bar 2结果如图 现在触发的次数就正常了。 处理一个对象已经是代理对象的情况 如果当一个对象已经是代理对象了那么按照逻辑来说就应该直接返回这个代理对象而不需要在做一次代理而实现这一点也很简单可以参考设置 RAW_KEY 的方式如下 // 添加是否是代理对象的标识 key const IS_REACTIVE Symbol(isReactive)function reactive(value) {if (typeof value ! object || value null) {console.warn(value 必须是一个对象)return value}if()const proxy new Proxy(value, { get, set, has, ownKeys, deleteProperty })// 给完成代理的对象添加一个标识表示是一个代理对象proxy[IS_REACTIVE] truereturn proxy }// 判断一个值是否是响应式对象 function isReactive(value) {return typeof value object value ! null !!value[IS_REACTIVE] }/* … */const o1 reactive({name: zs }) const o2 reactive(o1) const o3 reactive({name: zs }) console.log(o1 o2) // true console.log(o1 o3) // falseo1 等于 o2 就表示了不会对本身就是一个代理对象的数据进行二次代理。 处理一个原始对象已经被代理过一次之后的情况 若一个原始对象 obj 已经被代理过一次之后再次使用代理的时候也不应该在进行代理而是返回之前代理完成的对象代码如下 // 缓存已经代理过的对象 const reactiveMap new WeakMap()function reactive(value) {if (typeof value ! object || value null) {console.warn(value 必须是一个对象)return value}// 以 value 为 key从缓存中取出对应的代理对象如果有责返回缓存的代理对象不然进行代理if (reactiveMap.has(value)) {return reactiveMap.get(value)}if (isReactive(value)) return valueconst proxy new Proxy(value, { get, set, has, ownKeys, deleteProperty })proxy[IS_REACTIVE] true// 将代理好的对象缓存起来reactiveMap.set(value, proxy)return proxy }const obj { name: zs } const p1 reactive(obj) const p2 reactive(obj) console.log(p1 p2) // truep1 与 p2 相等则表示没有重复对同一个原始对象进行代理。 浅响应与深响应 我们来看一段示例代码 const obj {foo: {bar: 1} } const p1 reactive(obj)effect(() {console.log(p1.foo.bar) })console.log(修改) p1.foo.bar 2再来看一下输出的结果如图 修改 p1.foo.bar 的值并没有导致副作用函数再次执行那么就表示目前我们的 reactive 还是一个浅响应的foo 的值还是一个普通对象而非代理的响应式对象因此我们需要给它变成响应式对象。 这点我们可以在 get 拦截器中完成当检测到 Reflect.get 返回的值是一个对象时那么就再次进行代理完成递归式的处理不过我们这个行为是懒的只要当用到了这个属性才会被深度代理如下 function get(target, key, receiver) {if (key RAW_KEY) {return target}track(target, key)// 得到本次获取的原始值const result Reflect.get(target, key, receiver)// 若是一个对象则进行代理否则直接返回此值if (typeof result object result ! null) {return reactive(result)}return result }现在我们在运行上面的例子查看一下结果如图 但是并不是是什么时候都需要进行深度响应此时我们就需要一个方法来完成只进行浅响应基于这个情况shallowReactive 应运而生基于此我们可以将 reactive 函数在做一层封装将其抽离出来如下 // 因为是抽离出来的 get所以如果想拿到 isShallow 的值就需要在封装一层 function baseGet(isShallow) {return function get(target, key, receiver) {if (key RAW_KEY) {return target}track(target, key)const result Reflect.get(target, key, receiver)// 如果 isShallow 为 true 表示只需要做到浅响应即可因此直接返回 result 即可if (isShallow) return resultif (typeof result object result ! null) {return reactive(result)}return result} }// 深响应 function reactive(value) {return createReactiveObject(value) }// 浅响应 function shallowReactive(value) {return createReactiveObject(value, true) }// 将逻辑再做一次抽离放入 createReactiveObject 函数中 // - 使用 isShallow 参数来区分是深响应还是浅响应默认为 false 表示进行深响应处理 function createReactiveObject(value, isShallow false) {if (typeof value ! object || value null) {console.warn(value 必须是一个对象)return value}if (reactiveMap.has(value)) {return reactiveMap.get(value)}if (isReactive(value)) return value// 通过 baseGet 返回具体的 get 拦截回调函数const proxy new Proxy(value, { get: baseGet(isShallow), set, has, ownKeys, deleteProperty })proxy[IS_REACTIVE] truereactiveMap.set(value, proxy)return proxy }添加一段测试代码看看是否完成了浅响应如下 const obj {a: {b: 100} } const p1 shallowReactive(obj) effect(() {console.log(p1.a.b) }) console.log(修改*) p1.a.b查看结果看看是否修改值之后是否会触发更新如图 我们在测试一下只修改第一层的属性是否会发生更新代码如下 const obj {a: 1 } const p1 shallowReactive(obj) effect(() {console.log(p1.a) }) console.log(修改**) p1.a结果如图 代理数组 JavaScript 中万物皆对象分为常规对象和异质对象Proxy 就是一个异质对象而数组也是。 所以需要针对数组进行单独的处理但是因为也是对象所以之前大部分的实现都是可用的如下 const arr [1, 2, 3] const a1 reactive(arr)effect(() {console.log(effect: , a1[0]) })a1[0] 4测试结果如图 数组的索引与 length 通过前文我们知道通过索引访问是可以建立响应式关系的但是如果设置的索引大于当前数组的 length则会导致隐式的修改了 length 的值如下 const arr [1] const a1 reactive(arr)effect(() {console.log(effect: , a1.length) })// 设置索引为1的值则长度也变为 2 a1[1] 4 console.log(a1.length)但是目前这样并不会触发副作用函数的重新执行所以我们需要自己来进行一些处理通过这个例子我们可以知道当设置的索引大于或者等于数组的长度的时候就是新增的所以操作类型我们还需要针对数组做一层判断如下 // 表示这些key忽略 const noWarnKey [RAW_KEY, IS_REACTIVE, ITERATE_KEY] function baseSet(isReadonly) {return function set(target, key, newVal, receiver) {const oldVal target[key]const type Array.isArray.isArray(target)? // 如果代理的目标是数组则检测 key 是否小于 target.length// 如果小于则修改的是数组中已经存在的元素触发 SET 事件否则触发 ADD 事件key target.length? TriggerType.SET: TriggerType.ADD: Object.prototype.hasOwnProperty.call(target, key)? TriggerType.SET: TriggerType.ADDconst result Reflect.set(target, key, newVal, receiver)if (!result) returnif (receiver[RAW_KEY] target) {if (!Object.is(oldVal, newVal)) {trigger(target, key, type)}}return result} }当然这里的判断只是一个粗浅的判断只是为了讲清楚这个原理实际的实现需要更换一下如下 function baseSet(isReadonly) {return function set(target, key, newVal, receiver) {const oldLen Array.isArray(target) ? target.length : undefinedconst oldVal target[key]let type Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADDconst result Reflect.set(target, key, newVal, receiver)if (!result) returnconst newLen Array.isArray(target) ? target.length : undefinedif (receiver[RAW_KEY] target) {if (!Object.is(oldVal, newVal)) {// 设置时如果满足以下条件则操作类型是 ADD// 1、target 是数组// 2、key 不是 length// 3、旧长度小于新长度if (Array.isArray(target) key ! length newLen oldLen) {type TriggerType.ADD}// 派发更新trigger(target, key, type)}}return result} }而有了这个操作的类型之后我们就可以进一步的修改 trigger 里面的代码如下 function trigger(target, key, type) {let depsMap targetMap.get(target)if (!depsMap) returnlet deps depsMap.get(key)const effetsToRun new Set()deps deps.forEach(effectFn {if (effectFn ! activeFn) {effetsToRun.add(effectFn)}})// 如果是一个数组且是新增元素if (Array.isArray(target) type TriggerType.ADD) {// 则去除 length 的依赖加入执行集合const lengthEffects depsMap.get(length)lengthEffects lengthEffects.forEach(effectFn {if (effectFn ! activeFn) {effetsToRun.add(effectFn)}})}if (type TriggerType.ADD || type TriggerType.DELETE) {let iterateDeps depsMap.get(ITERATE_KEY)iterateDeps iterateDeps.forEach(effectFn {if (effectFn ! activeFn) {effetsToRun.add(effectFn)}})}effetsToRun.forEach(fn {if (fn.options fn.options.scheduler) {fn.options.scheduler(fn)} else {fn()}}) }此时在执行原来的测试代码如图 此时因为设置了索引导出数组长度隐式增加也可以触发响应式了解决完成这个我们在来看看直接修改 length 属性也会隐式的影响数组元素例如 const arr [A] const a1 reactive(arr)effect(() {// 访问数组第一个元素console.log(effect: , a1[0]) })// 将数组的长度设置0则会清空数组 a1.length 0 console.log(a1.length)测试结果如图 此时并没有触发副作用函数的触发因此不妨来猜想一些情况假设将 length 设置为 100那么会对 a1[0] 造成影响吗并不会也就说只有 length 设置的值小于或者等于当前索引才应该去触发响应而为了实现这一点我们还需要给 trigger 传递第四个参数如下 function trigger(target, key, type, newValue) {let depsMap targetMap.get(target)if (!depsMap) returnlet deps depsMap.get(key)const effetsToRun new Set()addEffects(effetsToRun, deps)// 如果target是数组并且 key 是 lengthif (Array.isArray(target) key length) {// 对于索引大于或者等于当前 length 的新值的元素就将其取出并添加到 effetsToRun 中等待执行// - 假设值原数组为 [1,2,3,4,5]设置 length 为 2// - 那么新数组就会删减为 [1,2]则对于索引大于等于 2 的元素 [3,4,5] 就都被删除了不存在了肯定也要触发依赖// - 而对于索引小于 2 的元素 [1,2] 则是存在的没有改变所以不需要触发依赖depsMap.forEach((deps, key) {if (key newValue) {addEffects(effetsToRun, deps)}})}if (Array.isArray(target) type TriggerType.ADD) {const lengthEffects depsMap.get(length)addEffects(effetsToRun, lengthEffects)}if (type TriggerType.ADD || type TriggerType.DELETE) {let iterateDeps depsMap.get(ITERATE_KEY)addEffects(effetsToRun, iterateDeps)}effetsToRun.forEach(fn {if (fn.options fn.options.scheduler) {fn.options.scheduler(fn)} else {fn()}}) }// 将添加的逻辑抽离出来 function addEffects(effetsToRun, effects) {if (!effects) returneffects.forEach(effectFn {if (effectFn ! activeFn) {effetsToRun.add(effectFn)}}) }function baseSet(isReadonly) {return function set(target, key, newVal, receiver) {//if (receiver[RAW_KEY] target) {if (!Object.is(oldVal, newVal)) {if (Array.isArray(target) key ! length newLen oldLen) {type TriggerType.ADD}// 传递第四个参数-本次修改的新值trigger(target, key, type, newVal)}}return result} }此时我们再次运行测试代码结果如图 成功的触发了副作用函数数组清空值为 undefined 也是符合我们的预期的。 遍历数组 在日常的代码开发中我们都会尽量避免使用 for…in 来遍历数组但是在语法上是支持这样遍历的因此我们需要再之前编写的 ownKeys 方法中在做出一些修改之前我们为了实现 for…in 遍历一个对象对象的属性增加或者删除都会重新触发统一将 key 设置为了 ITERATE_KEY(Symbol(‘iterate’))但是这是为了适应普通对象的所以如果是数组的话要针对 key 做出一些处理如下 function ownKeys(target) {// 如果是数组则使用 length 为 key否则使用 ITERATE_KEYconst key Array.isArray(target) ? length : ITERATE_KEYtrack(target, key)return Reflect.ownKeys(target) }此时我们写一段测试代码测试一下 const arr reactive([A, B]) effect(() {console.log(effect触发)for (const key in arr) {console.log(key)} }) console.log(**修改length) arr.length 0结果如图 相比 for…in 来说我们更加常用的是 for…of而 for…of 采用的是可迭代协议具体这块的知识在这里不做赘述但是无需任何改动for…of 就是可以正常工作的因为迭代器本身就是基于数组长度和索引来迭代的而这些在之前我们就都已经处理好了。 不过在 for…of 会读取一次 Symbol.iterator这个并不需要跟踪收集依赖还需要多做一次处理如下 function baseGet(isShallow, isReadonly) {return function get(target, key, receiver) {if (key RAW_KEY) {return target}// 为什么直接把 symbol 去掉了呢因为 for in 本身也不会迭代 Symbolif (!isReadonly typeof key ! symbol) {track(target, key)}const result Reflect.get(target, key, receiver)if (isShallow) return resultif (typeof result object result ! null) {return isReadonly ? readonly(result) : reactive(result)}return result} }测试代码如下 const arr reactive([A, B]) effect(() {console.log(effect触发)for (const item of arr) {console.log(item)} }) console.log(修改length**) arr.length 0结果如图 数组的查找方法 可以触发的部分这里就不做展示仅展示无法触发响应的案例如下 const obj {} const arr reactive([obj])console.log(arr.includes(obj)) // false为什么存在的值但是却表示无法找到呢在 ECMA 规范中表明includes 方法会通过索引获取值而我们这里使用的是 arrarr 是一个代理在代理中如果得到这个值是一个对象的话则会对这个对象进行代理那么这个 obj 就变为了 objProxy Proxy(obj)这两者之间肯定是不相等的所以这里 arr 通过 includes 查找的时候includes 内部通过遍历取值对比实际执行判断时 arr[0] objarr[0]是objProxy 那肯定就会返回 false这里我们尽然说是因为再次代理的原因是不是使用浅响应就可以找到呢是可以的代码如下 const obj {} const arr shallowReactive([obj]) console.log(arr.includes(obj)) // true// 我们也可以直接使用 arr[0] 来直接得到这个代理对象因为之前我们处理过了不会重复代理一个对象所以不会得到两个代理对象如果没有做这个那么得到的还是 false const obj {} const arr reactive([obj]) console.log(arr.includes(arr[0])) // true哪知道问题的原因之后应该如何解决呢这个解决方案其实也不难includes 内部的 this 指向的代理后的 arr如果从这里找不到我们从代理的原始数组里面在找一次即可所以一次找不到那就在找一次也因此我们需要重新一下 includes 方法如下 // 创建对象以重写数组方法名为 key并绑定执行函数 const arrayInstrumentations {} ;[includes, indexOf, lastIndexOf].forEach(key {arrayInstrumentations[key] function (…args) {// this — proxy// 1、在 proxy 里面找一次const proxyResult Array.prototype[key].apply(this, args)// 如果在代理中找到的结果为 true 或者不等于 -1表示找到了直接返回if ((typeof proxyResult boolean proxyResult true) ||(typeof proxyResult number proxyResult ! -1)) {return proxyResult}// 2、在原始数组中找一次const rawResult Array.prototype[key].apply(this[RAW_KEY], args)// 直接返回原始数组中的结果return rawResult} })function baseGet(isShallow, isReadonly) {return function get(target, key, receiver) {if (key RAW_KEY) {return target}// 如果访问的是重新的数组方法则直接使用重新的方法if (Array.isArray(target) arrayInstrumentations.hasOwnProperty(key)) {return Reflect.get(arrayInstrumentations, key, receiver)}const noKeys [IS_REACTIVE, RAW_KEY, Symbol.iterator]if (!isReadonly !noKeys.includes(key)) {track(target, key)}const result Reflect.get(target, key, receiver)if (isShallow) return resultif (typeof result object result ! null) {return isReadonly ? readonly(result) : reactive(result)}return result} } 执行结果如图 隐式修改数组长度的原型方法 这些方法主要是指数组的栈方法例如push、pop、shift、unshift。除此之外还有 splice 也会修改数组的长度。 此时我们来看一段测试代码如下 const arr reactive([A]) // 第一个 effect effect(() {arr.push(B) }) // 第二个 effect effect(() {arr.push© })此时运行代码会得到一个栈溢出的错误如图 我们来解读一下这个执行过程 这是因为push 会读取 length 属性所以当第一个 effect 执行之后就会将 length 加入到一个依赖关系中即进行了依赖收集而第二个 effect 执行的时候也会读取 length 属性除了读取之外它还会设置 length 属性当第二个 effect 在设置 length 属性时就会将 length 关联的副作用函数取出执行其中就包括第一个 effect而此时我们的第二个 effect 还并没有执行完成就执行起来了第一个 effect第一个 effect 又再次执行又设置了 length 属性触发 set又要取出全部与 length 关联的副作用函数此时这写函数里面就包括第二个然后一直重复这个循环就导致了栈溢出 解决方法也简单问题就是不停的收集依赖导致的只要在这些方法运行期间暂停依赖收集那么就算改变 length 属性也会因为停止了依赖收集而略过等待这些方法执行完成就可以恢复依赖收集代码如下 // 是否应该收集依赖 let shouldTrack true// 暂停收集依赖 function pauseTracking() {shouldTrack false }// 恢复收集依赖 function resumeTracking() {shouldTrack true }const arrayInstrumentations {} // 在重写一些方法 ;[push, pop, shift, unshift, splice].forEach(key {// 这些会改动数组的长度造成额外的依赖收集因此在这些方法运行期间暂停依赖的收集arrayInstrumentations[key] function (…args) {// 暂停依赖收集pauseTracking()const result Array.prototype[key].apply(this, args)// 恢复依赖收集resumeTracking()return result} })function track(target, key) {// 停止收集依赖期间或者 activeFn 为空则不收集if (!shouldTrack || !activeFn) returnlet depsMap targetMap.get(target)if (!depsMap) {depsMap new Map()targetMap.set(target, depsMap)}let deps depsMap.get(key)if (!deps) {deps new Set()depsMap.set(key, deps)}deps.add(activeFn)activeFn.deps.push(deps) }此时在运行测试案例就不会出现栈溢出的错误了 结语 篇幅原因这里仅展示代理对象和数组两种最为常见的数据结构实现代理 Set 和 Map 将在后续的作为扩展章节展示