前几篇文章已经构建了较为完善的响应式系统,并且实现了计算属性 computed
以及调度器、懒执行等特性。这一篇文章就来实现另外一个重要的功能—— watch
监听。
watch 的基础原理
简单实现
watch
无非是实现这样一个功能:对于指定的响应式变量,当该变量发生变化时能够执行指定的回调函数。这一个需求完全可以使用调度器
scheduler 来实现。例如下面的代码,通过给 effect
传入一个读取 data.cnt
的函数,来使 data.cnt
与副作用函数进行绑定。然后在 scheduler
上指定回调函数,每当
data.cnt
发生变化时就执行指定的回调函数。
1 2 3 4 5 6 7 8 9 10 const data = reactive ({ cnt : 0 })effect (() => { data.cnt }, { scheduler : () => { console .log ('data.cnt changed' ) } })
递归监听所有属性
上一节中实现监听的方式有一个明显的不足,那就是必须指定要监听响应式数据的哪个属性,否则其他属性的变化不会触发绑定的回调函数。但实际上我们希望所监听变量的任何属性发生变化都能触发回调。这就自然而然想到使用递归的方法来遍历变量的所有属性。我们如此封装
watch
函数,其中 traverse(obj)
用来递归的读取
obj
的所有属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function watch (obj, handler ) { effect ( () => traverse (obj), { scheduler : hanlder } ) }function traverse (obj, seen = new Set () ) { if (typeof obj !== 'object' || value === null || seen.has (obj)) return seen.add (obj) for (const k in obj) { traverse (obj[k]) } return obj }
有了以上的封装我们就可以直接调用 watch
来自动追踪响应式数据的所有变化了:
1 2 3 4 watch (obj, () => { console .log ('obj changed' ) })
getter
Vue 中对于 watch
方法还提供了 getter
的使用方式,它的调用方式如下。通过 getter
返回一个响应式数据,watch 仅仅监听 getter
返回的值,并且默认不会进行深层监听。
1 2 3 4 5 6 watch ( () => obj.cnt , () => { } )
为 watch
添加 getter
的实现非常简单,即通过判断 watch
的第一个参数是不是函数
(getter
),如果是函数则直接将 getter
作为副作用函数传给
effect
,否则使用上面的递归处理。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function watch (source, handler ) { let getter if (typeof source === 'function' ) { getter = source } else { getter = () => traverse (source) } effect ( getter, { scheduler : hanlder } ) }
watch 的功能优化
获得“新旧值”
这里实现 Vue.js 中回到函数中获取新旧值参数的功能。这里主要利用到
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 function watch (source, handler ) { let getter if (typeof source === 'function' ) { getter = source } else { getter = () => traverse (source) } let oldVal, val const effectFn = effect ( getter, { lazy : true , scheduler ( ) { val = effectFn () handler (oldVal, val) oldVal = val } } ) oldVal = effectFn () }
通过以上的封装,watch
所监听的数据只有发生变化时才会执行回调函数。我们同样可以像 Vue.js
中为其设置 immediate
参数来使其立即执行。
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 watch (source, handler, options ) { let getter if (typeof source === 'function' ) { getter = source } else { getter = () => traverse (source) } let oldVal, val const job = ( ) => { val = effectFn () handler (oldVal, val) oldVal = val } const effectFn = effect ( getter, { lazy : true , scheduler : job } ) if (options.immediate ) { job () } else { oldVal = effectFn () } }
小结
本文实现了 Vue.js 数据侦听方法 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 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 let activeEffect;const effectStack = [];const bucket = new WeakMap ();const data = reactive ({ foo : 1 , bar : 2 });watch ( () => data.bar , (old, val ) => { console .log (`data change from ${old} to ${val} ` ); }, { immediate : true } );setTimeout (() => { data.bar = 100 ; }, 2000 );function watch (source, cb, options = {} ) { let getter; if (typeof source === "function" ) { getter = source; } else { getter = () => traverse (source); } let oldVal, val; const job = ( ) => { val = effectFn (); cb (oldVal, val); oldVal = val; }; const effectFn = effect (getter, { lazy : true , scheduler : job }); if (options.immediate ) { job (); } else { oldVal = effectFn (); } }function traverse (obj, seen = new Set () ) { if (typeof obj !== "object" || obj === null || seen.has (obj)) return ; for (const k in obj) { traverse (obj[k]); } return obj; }function computed (getter ) { let dirty = true ; let cache; const effectFn = effect (getter, { lazy : true , scheduler : () => { dirty = true ; trigger (obj, "value" ); } }); const obj = { get value () { if (dirty) { cache = effectFn (); dirty = false ; } track (obj, "value" ); return cache; } }; return obj; }function reactive (obj ) { return new Proxy (obj, { get (target, key ) { track (target, key); return Reflect .get (target, key); }, set (target, key, value ) { const res = Reflect .set (target, key, value); trigger (target, key); return res; } }); }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.depSets .push (deps); }function trigger (target, key ) { let depsMap = bucket.get (target); if (!depsMap) return ; let deps = depsMap.get (key); if (!deps) return ; const depsToRun = new Set (deps); depsToRun.forEach ((fn ) => { if (fn === activeEffect) return ; if (fn.options .scheduler ) { fn.options .scheduler (fn); } else { fn (); } }); }function effect (fn, options = {} ) { const effectFn = ( ) => { cleanup (effectFn); activeEffect = effectFn; effectStack.push (effectFn); const res = fn (); effectStack.pop (); activeEffect = effectStack[effectStack.length - 1 ]; return res; }; effectFn.depSets = []; effectFn.options = options; if (!options.lazy ) { effectFn (); } return effectFn; }function cleanup (effectFn ) { effectFn.depSets .forEach ((deps ) => { deps.delete (effectFn); }); effectFn.depSets .length = 0 ; }
参考资料
[1] vuejs/core: 🖖 Vue.js is a progressive, incrementally-adoptable
JavaScript framework for building UI on the web.[EB/OL]. [2023-09-22].
https://github.com/vuejs/core .
[2] Vue.js - 渐进式 JavaScript 框架 | Vue.js[EB/OL]. [2023-09-22]. https://cn.vuejs.org/ .
[3] 霍春阳. Vue.js设计与实现[M/OL]. 人民邮电出版社, 2022[2023-09-19].
https://book.douban.com/subject/35768338/ .
[4] JavaScript | MDN[EB/OL]. 2023-04-10 . https://developer.mozilla.org/zh-CN/docs/Web/JavaScript .