框架设计概览 权衡的艺术 命令式和声明式 命令式 关注过程(jquery)
声明式 关注结果(vue)
Vue.js 帮我们封装了过程,内部实现是命令式的,而暴露给用户的却更加声明式
性能与可维护性的权衡 声明式代码的性能不优于命令式代码的性能
命令式代码的更新性能消耗 = 直接修改的性能消耗
声明式代码的更新性能消耗 = 直接修改的性能消耗 + 找出差异的性能消耗
但声明式代码的可维护性更强
设计框架时 要保证可维护性的同时让性能损失最小化
虚拟 DOM 的性能到底如何 虚拟 DOM 就是为了最小化找出差异 这一步的性能消耗而出现的 (diff算法)
创建页面差异不大
更新页面性能差异大
更新页面的性能效果总结
运行时和编译时 纯运行时,没有编译的过程,没办法分析用户提供的内容
纯编译时,可以分析用户提供的内容,性能更好,但用户提供的内容必须编译后才能用,有损灵活性
Vue.js 是运行时 + 编译时的架构
框架设计的核心要素 提供用户的开发体验 更友好的警告
自定义输出形式
控制框架代码的体积 利用 Tree-shaking 机制,配合构建工具预定义常量的能力,如预定义 __DEV__常量,实现仅在开发环境中打印警告信息,从而控制线上代码体积
良好的 Tree-Shaking 利用 /*#__PURE__*/ 注释 来让 webpack 相信这段代码不会产生副作用,从而 tree-shaking 掉
副作用:当调用函数的时候会对外部产生影响(例如修改了全局变量)
Vue.js 3 源码中,基本都是在一些顶级调用的函数中使用 /*#__PURE__*/ 注释掉
框架应该输出怎样的构建产物 为了让用户能够通过 <script> 标签直接引用并使用,需要输出 IIFE 格式的资源
为了让用户能够通过 <script type=”module”> 引用并使用,需要输出 ESM 格式的资源
ESM格式的资源有两种:
esm-browser.js 用于浏览器 使用 __DEV__ true/false 来判断环境
esm-bundler.js 用于打包工具 使用 process.env.NODE_ENV !== ‘production’ 来判断
为了让用户能够通过在服务端通过
1 const Vue = require ('vue' )
使用,需要输出 CommonJS 格式的资源(一般用于服务端渲染)
特性开关 Vue.js 提供了很多功能,如果不需要可以通过特性开关关闭,这样打包时代码就不会出现在最终资源中,文件体积减小
比如 Vue3.js 中我们仍可使用 options API,可以关闭,通过 __VUE_OPTIONS_API__ 控制。
实现特性开关的方法和 __DEV__ 是一样的,本质上是通过 rollup.js(webpack) 的预定义常量插件来实现
可以使用 webpack.DefinePlugin 插件来控制是否开启
1 2 3 new webpack.DefinePlugin ({ __VUE_OPTIONS_API__: JSON .stringify(true ) })
错误处理 Vue3.js 中将错误处理程序封装为一个函数 callWithErrorHandling
还提供了 registerErrorHandler 函数,用户可以使用它注册错误处理程序
良好的 TS 支持 使用 TS 编写框架 和 框架对 TS 类型 支持友好是完全不同的
Vue3.js 的设计思路 声明式描述 UI 描述 UI 的两种方式:模板和虚拟 DOM
模板
1 2 3 4 <h1 @click="handler"> <span> </span> </h1>
虚拟DOM
1 2 3 4 5 6 7 8 9 const title = { tag : 'h1' , props : { onClick : handler }, children : [ {tag : 'span' } ] }
h 函数就是一个辅助创建虚拟 DOM 的工具函数
1 2 3 4 5 export default { render ( ) { return h ('h1' , {onClick : handler}) } }
等价于
1 2 3 4 5 6 7 8 export default { render ( ) { return { tag : 'h1' , props : { onClick : handler } } } }
Vue.js 会根据组件的 render 函数的返回值拿到虚拟 DOM,然后就可以把组件的内容渲染出来了
初始渲染器 渲染器 的作用就是把 虚拟 DOM 转为 真实 DOM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function renderer (vnode, container ) { const el = document .createElement (vnode.tag ) for (const key in vnode.props ) { if (/^on/ .test (key)) { el.addEventListener ( key.substring (2 ).toLowerCase (), vnode.props [key] ) } } if (typeof vnode.children === 'string' ) { el.appendChild (document .createTextNode (vnode.children )) } else if (Array .isArray (vnode.children )) { vnode.children .forEach (child => render (child, el)) } container.appendChild (el) }
组件的本质 组件就是一组 DOM 元素的封装
可以定义一个函数来代表组件
1 2 3 4 5 6 7 8 9 const MyComponent = function ( ) { return { tag : 'div' , props : { onClick : () => alert ('hello' ) }, children : 'click me' } }
组件的返回值也是虚拟 DOM
定义虚拟 DOM 来描述组件
1 2 3 const vnode = { tag : MyComponent }
为了能够渲染组件,需要渲染器的支持,修改前面的 renderer 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 function mountElement (vnode, container ) { const el = document .createElement (vnode.tag ) for (const key in vnode.props ) { if (/^on/ .test (key)) { el.addEventListener ( key.substring (2 ).toLowerCase (), vnode.props [key] ) } } if (typeof vnode.children === 'string' ) { el.appendChild (document .createTextNode (vnode.children )) } else if (Array .isArray (vnode.children )) { vnode.children .forEach (child => render (child, el)) } container.appendChild (el) }mountComponent (vnode, container ) { const subtree = vnode.tag () renderer (subtree, container) }function renderer (vnode, container ) { if (typeof vnode.tag === 'string' ) { mountElement (vnode, container) } else if (typeof vnode.tag === 'function' ) { mountComponent (vnode, container) } }
模板的工作原理
编译器分析动态内容 代码中有静态内容(如写死的字符串),有动态内容(如双向绑定的变量)
动态内容改变时,渲染器会自动寻找变更点,找起来比较麻烦。
如果编译器有能力分析动态内容,获取更多信息,就会方便很多。
编译器能够识别出哪些是静态属性,哪些是动态属性(比如,模板上有冒号的属性就是动态的(双向绑定),没有的就是静态的),在生成虚拟 DOM 的时候就会携带额外的 patchFlags 信息。(patchFlags 应该是 vue3 新增的)
总结编译与渲染流程
响应系统 响应式系统的作用与实现 副作用函数 effect函数的执行会直接或间接影响其他函数的执行,这时我们说 effect 函数产生了副作用。
例如一个函数修改了全局变量,其实也是一个副作用。
响应式系统的工作流程 当读取操作发生时,将副作用函数收集到”桶“中
当设置操作发生时,从”桶“中去除副作用函数并执行
设计一个完善的响应系统 结构上来说,有三个层级。
代码中有很多对象,一个对象有很多属性,一个属性又有很多副作用函数。
当我们修改对象上的一个属性时,理应只能触发这个键上的副作用函数。
建立 weakmap - map - set 的数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const obj = new Proxy (data, { get (target, key ) { track (target, key) return target[key] }, set (target, key, newVal ) { target[key] = newVal trigger (target, key) } })function track (target, key ) { if (!activeEffect) return let depsMap = bucket.get (target) if (!depsMap) { bucket.set (target, (depsMap = new Map ())) } let deps = depsMap.get (key) if (!deps) { depsMap.set (key, (deps = new Set ())) } deps.add (activeEffect) }function trigger (target, key ) { const depsMap = bucket.get (key) if (!depsMap) return const effects = depsMap.get (key) effects && effects.forEach (fn => fn ()) }
分支切换与cleanup 数据变更后,一些副作用函数和key之间的关系应该被切断,但遗留了下来,导致了不必要的更新。
解决方案是,在每次副作用函数执行时,先把它从所有与之关联的依赖集合中删除。当副作用函数执行完毕后,会重新建立联系,但在新的联系中不会包含遗留的副作用。
为了方便从所有依赖集合中删除副作用函数的操作,需要给每个副作用函数创建一个数组,在track函数中,将依赖放入相应副作用函数的数组。
然后建立一个 cleanup 函数,用于执行清除操作。
1 2 3 4 5 6 7 8 9 10 11 function cleanup (effectFn ) { for (let i = 0 ; i < effectFn.deps .length ; i++) { const deps = effectFn.deps [i] deps.delete (effectFn) } effectFn.deps .length = 0 }
响应的 trigger 函数需要做一些改动,否则会出现无限循环的场景。这和 forEach 的性质有关。
嵌套的 effect 和 effect 栈 effect 应设计为可嵌套的,因为存在组件嵌套的场景。
维持原先的写法会导致,内部的 effect 函数覆盖了外部的 effect 函数,因为同一时刻只能存在一个 activeEffect。
为了解决这个问题,需要一个副作用函数栈 effectStack。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 let activeEffectconst effectStack = []function effect (fn ) { const effectFn = ( ) => { cleanup (effectFn) activeEffect = effectFn effectStack.push (effectFn) fn () effectStack.pop () activeEffect = effectStack[effectStack.length - 1 ] } effectFn.deps = [] effectFn () }
避免无限递归循环 1 2 3 effect (() => { obj.foo = obj.foo + 1 })
这种自增的情况下,副作用函数执行,触发obj.foo的get行为,将当前副作用函数放入obj.foo的桶中,然后触发obj.foo的set行为,取出当前副作用函数执行,但由于当前副作用函数还在执行中,就要开始下一次的执行,就会无限递归调用自己。
解决方案:在 trigger 动作发生时增加守卫条件:如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function trigger (target, key ) { const depsMap = bucket.get (key) if (!depsMap) return const effects = depsMap.get (key) const effectsToRun = new Set () effects && effects.forEach (effectFn => { if (effectFn !== activeEffect) { effectsToRun.add (effectFn) } }) effectsToRun.forEach (effectFn => effectFn ()) }
调度执行 可调度,指的是当 trigger 动作出发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。
解决方案:为 effect 函数设计一个选项参数 options,允许用户指定调度器。trigger 函数中遍历执行副作用函数前做一个调度器是否存在的判断。
计算属性 computed 和 lazy 懒执行(lazy) 有些场景下,我们并不希望副作用函数立即执行,而是希望它在需要的时候才执行,例如计算属性。(依赖修改时不会立即执行,只有读取计算属性的值时才执行)
可以通过在 options 中添加 lazy 属性来达到目的
1 2 3 4 5 6 7 8 9 effect ( () => { console .log (obj.foo ) }, { lazy : true } )
当 options.lazy 为 true 时,则不立即执行副作用函数,而是将封装的副作用函数 effectFn 作为返回值返回。
在 effectFn 中执行真正的副作用函数 fn 时,将其结果保存,并作为返回值返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 let activeEffectconst effectStack = []function effect (fn ) { const effectFn = ( ) => { cleanup (effectFn) activeEffect = effectFn effectStack.push (effectFn) const res = fn () effectStack.pop () activeEffect = effectStack[effectStack.length - 1 ] return res } effectFn.options = options effectFn.deps = [] if (!options.lazy ) { effectFn () } return effectFn () }
缓存性(dirty) 读取计算属性值时,如果不是第一次获取且依赖的属性没有变化,则不应该再次执行副作用函数(effectFn),而是直接使用缓存。
增加 dirty 标志标识依赖是否有更新,副作用函数执行后,dirty 置为 false。利用 scheduler,在依赖改变时将 dirty 置 true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function computed (getter ) { let value let dirty = true const effectFn = effect (getter, { lazy : true , scheduler ( ) { dirty = true } }) const obj = { get value () { if (dirty) { value = effectFn () dirty = false } return value } } return obj }
计算属性太懒了 1 2 3 4 5 6 7 const sumRes = computed (() => obj.foo + obj.bar )effect (() => { console .log (sumRes.value ) }) obj.foo ++
执行上述代码,发现并不会打印任何东西
因为虽然 obj.foo 改变了,但计算属性懒,并没有重新求值,所以 sumRes 没变,所以没有执行副作用函数(打印sumRes)。
解决方案:
读取计算属性时,手动调用 track 函数进行追踪
计算属性依赖数据变化时,手动调用 trigger 函数触发响应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function computed (getter ) { let value let dirty = true const effectFn = effect (getter, { lazy : true , scheduler ( ) { dirty = true trigger (obj, 'value' ) } }) const obj = { get value () { if (dirty) { value = effectFn () dirty = false } track (obj, 'value' ) return value } } return obj }
watch 本质上就是利用了 effect 以及 options.scheduler 选项
watch的使用:
1 2 3 4 5 6 7 8 9 10 11 12 watch (obj, () => { console .log ("数据变了" ) })watch ( () => obj.foo , () => { console .log ("obj.foo 的值改变了" ); } )
watch的实现:
1.处理source是对象还是getter的能力(将对象也包装成getter)
2.回调中拿到oldValue和newValue的能力(利用effect函数的lazy配置项)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 function watch (source, cb ) { let getter if (typeof source === 'function' ) { getter = source } else { getter = () => traverse (source) } let oldValue, newValue const effectFn = effect ( () => getter (), { lazy : true , scheduler ( ) { newValue = effectFn () cb (newValue, oldValue) oldValue = newValue } } ) oldValue = effectFn () }
立即执行及回调执行时机 立即执行 1 2 3 4 5 watch (obj, () => { console .log ("数据变了" ) }, { immediate : true })
回调执行时机 1 2 3 4 5 6 7 8 watch (obj, () => { console .log ("数据变了" ) }, { flush : 'pre' })
watch改良 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 function watch (source, cb ) { let getter if (typeof source === 'function' ) { getter = source } else { getter = () => traverse (source) } let oldValue, newValue const job = ( ) => { newValue = effectFn () cb (newValue, oldValue) oldValue = newValue } const effectFn = effect ( () => getter (), { lazy : true , scheduler : () => { if (options.flush === 'post' ) { const p = Promise .resolve () p.then (job) } else { job () } } } ) if (options.immediate ) { job () } else { oldValue = effectFn () } }
过期的副作用 假设多次改变obj,那么会发送多个请求并赋给res,因为请求是异步的,不确定哪个请求先完成,所以 res 的值可能和理想中的不一致。
1 2 3 4 5 watch (obj, async () => { const res = await fetch ('/path/to/request' ) finalData = res })
我们需要想办法,让之前没用的副作用过期。
解决方案:watch 的回调函数接收第三个参数 onInvalidate,我们可以使用 onInvalidate 函数注册一个回调,这个回调会在当前副作用过期时执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let finalDatawatch (obj, async (newValue, oldValue, onInvalidate) => { let expired = false onInvalidate (() => { expired = true }) const res = await fetch ('/path/to/request' ) if (!expired) { finalData = res } })
onInvalidate的实现
在 watch 内部每次检测到变更后,在副作用函数重新执行前,先调用我们通过 onInvalidate 函数注册的过期回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 function watch (source, cb ) { let getter if (typeof source === 'function' ) { getter = source } else { getter = () => traverse (source) } let oldValue, newValue let cleanup function onInvalidate (fn ) { cleanup = fn } const job = ( ) => { newValue = effectFn () if (cleanup) { cleanup () } cb (newValue, oldValue, onInvalidate) oldValue = newValue } const effectFn = effect ( () => getter (), { lazy : true , scheduler : () => { if (options.flush === 'post' ) { const p = Promise .resolve () p.then (job) } else { job () } } } ) if (options.immediate ) { job () } else { oldValue = effectFn () } }
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 let activeEffectconst effectStack = []function effect (fn ) { const effectFn = ( ) => { cleanup (effectFn) activeEffect = effectFn effectStack.push (effectFn) const res = fn () effectStack.pop () activeEffect = effectStack[effectStack.length - 1 ] return res } effectFn.options = options effectFn.deps = [] if (!options.lazy ) { effectFn () } return effectFn () }const obj = new Proxy (data, { get (target, key ) { track (target, key) return target[key] }, set (target, key, newVal ) { target[key] = newVal trigger (target, key) } })function track (target, key ) { if (!activeEffect) return let depsMap = bucket.get (target) if (!depsMap) { bucket.set (target, (depsMap = new Map ())) } let deps = depsMap.get (key) if (!deps) { depsMap.set (key, (deps = new Set ())) } deps.add (activeEffect) activeEffect.deps .push (deps) }function trigger (target, key ) { const depsMap = bucket.get (key) if (!depsMap) return const effects = depsMap.get (key) const effectsToRun = new Set () effects && effects.forEach (effectFn => { if (effectFn !== activeEffect) { effectsToRun.add (effectFn) } }) effectsToRun.forEach (effectFn => { if (effectFn.options .scheduler ) { effectFn.options .scheduler (effectFn) } else { effectFn () } }) }function cleanup (effectFn ) { for (let i = 0 ; i < effectFn.deps .length ; i++) { const deps = effectFn.deps [i] deps.delete (effectFn) } effectFn.deps .length = 0 }function computed (getter ) { let value let dirty = true const effectFn = effect (getter, { lazy : true , scheduler ( ) { dirty = true trigger (obj, 'value' ) } }) const obj = { get value () { if (dirty) { value = effectFn () dirty = false } track (obj, 'value' ) return value } } return obj }function watch (source, cb ) { let getter if (typeof source === 'function' ) { getter = source } else { getter = () => traverse (source) } let oldValue, newValue let cleanup function onInvalidate (fn ) { cleanup = fn } const job = ( ) => { newValue = effectFn () if (cleanup) { cleanup () } cb (newValue, oldValue, onInvalidate) oldValue = newValue } const effectFn = effect ( () => getter (), { lazy : true , scheduler : () => { if (options.flush === 'post' ) { const p = Promise .resolve () p.then (job) } else { job () } } } ) if (options.immediate ) { job () } else { oldValue = effectFn () } }function traverse (value, seen = new Set () ) { if (typeof value !== 'object' || value === null || seen.has (value)) return seen.add (value) for (const k in value) { traverse (value[k], seen) } }
非原始值的响应式方案 理解 proxy 和 reflect 代理,指的是对一个对象基本语义的代理,允许我们拦截并重新定义对一个对象的基本操作。
基本语义:读取属性值、设置属性值、调用函数
reflect 在数据响应式中的作用
reflect.get() 有第三个参数 receiver,可以理解为函数调用过程中的 this。
reflect.get(target, key, receive) 替代 target[key],可以避免 this 错误问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const obj = { foo : 1 , get bar () { return this .foo } }const p = new Proxy (obj, { get (target, key, receiver ) { track (target, key) return Reflect .get (target, key, receiver) } })
JS对象及Proxy 的工作原理 内部方法 对象的实际语义是由对象的内部方法指定的。
内部方法指的是当我们对一个对象进行操作时在引擎内部调用的方法,这些方法对于 JS 使用者来说是不可见的。
例如 访问 obj.foo 时,引擎内部会调用 [[GET]] 这个内部方法来读取属性值。
ES2022规范要求的所有必要的内部方法:
还有两个额外的必要内部方法:
如果一个对象需要作为函数调用,那么这个对象就必须部署内部方法[[Call]]。
内部方法具有多态性:
不同类型的对象可能部署了相同的内部方法,却具有不同的逻辑。
例如:普通对象和 Proxy 对象,都部署了 [[Get]] 这个内部方法,但它们的逻辑不同
常规对象与异质对象 满足以下三点要求的对象就是常规对象:
1.对于前11个内部方法,必须使用 ECMA 规范 10.1.x 节给出的定义实现
2.对于[[Call]],必须使用 ECMA 规范 10.2.1 节给出的定义实现
3.对于[[Construct]],必须使用 ECMA 规范 10.2.2 节给出的定义实现
如果有一点不满足就是异质对象。
Proxy 对象的 [[Get]] 没有使用 ECMA 规范的 10.1.8 节给出的定义实现,所以是一个异质对象。
代理透明性质 如果在创建代理对象时没有指定对应的拦截函数,例如没有指定 get() 拦截函数,那么当我们通过代理对象访问属性值时,代理对象的内部方法 [[Get]] 会调用原始对象的内部方法 [[Get]] 来获取属性值。
创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为的,而不是用来指定被代理对象的内部方法和行为的。
Proxy 对象部署的所有内部方法
[[Call]] 和 [[Construct]] 只有当被代理对象是函数和构造函数时才会部署
举例:
1 2 3 4 5 6 const p = new Proxy (obj, { deleteProperty (target, key ) { return Reflect .deleteProperty (target, key) } })
如何代理Object 读取 1.访问属性:obj.foo
2.判断对象或原型上是否存在给定的 key:key in obj
3.使用 for…in 循环遍历对象:for (const key in obj){}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const p = new Proxy (obj, { get (target, key, receiver ) { track (target, key) return Reflect .get (target, key, receiver) }, has (target, key ) { track (target, key) return Reflect .has (target, key) }, ownKeys (target ) { track (target, ITERATE_KEY ) return Reflect .ownKeys (target) } })
设置 1.新增属性
2.修改属性值
新增属性会对 for…in 循环产生影响(增加循环次数),所以需要触发与 ITERATE_KEY 关联的 副作用函数
修改属性值不需要触发与 ITERATE_KEY 关联的 副作用函数
我们可以使用 Object.prototype.hasOwnProperty 来判断当前操作属性是否已经存在于目标对象上,从而区分新增和修改
1 2 3 4 5 6 7 8 9 10 11 12 13 const p = new Proxy (obj, { set (target, key, newVal, receiver ) { const type = Object .prototype .hasOwnProperty .call (target, key) ? 'SET' : 'ADD' const res = Reflect .set (target, key, newVal, receiver) trigger (target, key, type) return res } })
删除 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const p = new Proxy (obj, { deleteProperty (target, key ) { const hadKey = Object .prototype .hasOwnProperty .call (target, key) const res = Reflect .deleteProperty (target, key) if (res && hadKey) { trigger (target, key, 'DELETE' ) } return res } })
设置和删除都会向 trigger 传第三个参数 type,所以 trigger 函数也要做修改
如果 type 是 ADD/DELETE,则需要触发与 ITERATE_KEY 相关联的副作用函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function trigger (target, key ) { const depsMap = bucket.get (key) if (!depsMap) return const effects = depsMap.get (key) const effectsToRun = new Set () effects && effects.forEach (effectFn => { if (effectFn !== activeEffect) { effectsToRun.add (effectFn) } }) if (type === 'ADD' || type === 'DELETE' ) { const iterateEffects = depsMap.get (ITERATE_KEY ) iterateEffects && iterateEffects.forEach (effectFn => { if (effectFn !== activeEffect) { effectsToRun.add (effectFn) } }) } effectsToRun.forEach (effectFn => { if (effectFn.options .scheduler ) { effectFn.options .scheduler (effectFn) } else { effectFn () } }) }