Svelte 的异步更新实现原理
Apr 11, 2021 · 5 分钟阅读
在 我对
阅读本文前,你应该至少:
- 读过 我对
Svelte 的看法 - 明白
JavaScript 中的 事件循环 机制
原理分析
为了保持简单,先从一个和
// 假设我们正在实现一个 counter, 只有一个 state,就是 count, 它是一个 number:let count = 0
// 我们可以实现一个 setCount, 来改变 count 的值,顺便执行更新 UI:function setCount(newVal) { count = newVal updateUI()}
function updateUI() { console.log("update ui with count:", count)}
setCount(1) //=> update ui with count: 1setCount(2) //=> update ui with count: 2setCount(3) //=> update ui with count: 3
这样实现很简单,但是有一个严重的问题:连续的状态更新会连续触发updateUI
// 基于 Promise 实现一个把函数放到 microtask 里的函数function createMicroTask(fn) { Promise.resovle().then(fn);}
let updateScheduled = false;function scheduleUpdate() { if (!updateScheduled) { // 当首次 schedule 时,把 updateUI 放到 microtask 中 createMicroTask(updateUI) updateScheduled = true; }}
function updateUI() { updateScheduled = false console.log("update ui with count:", count)}
// 在 setCount 时,不再直接触发 updateUI, 而是 schedule 一个 updatefunction setCount(newVal) { count = newVal scheduleUpdate()}
setCount(1)setCount(2)setCount(3)//=> update ui with count: 3
这样,在同一个事件循环里,多个状态更新只会触发一次
现在假设页面上有一个h1
updateUI
let count = 0const h1 = document.querySelector('h1')
function updateUI() { updateScheduled = false h1.innerHTML = `${count}`}
setCount(1)setCount(2)setCount(3)//=> update ui with count: 3
setCount(1)setCount(2)setCount(3)console.log(h1.innerHTML) //=> 0
在 setCount(3)
后, h1.innerHTML
竟不是预期中的updateUI
是在同步代码执行完后,开始执行
为了可以在 setCount
后拿到更新后正确的值,我们可以把关于tick
函数:
function tick() { return new Promise.resolve()}
async () => { setCount(1) setCount(2) setCount(3) await tick() console.log(h1.innerHTML) //=> 3}
Svelte 的实际做法
回到
<script> let count = 0</script>
<div> <span>{count}</span> <button on:click={() => count++}>+</button> <button on:click={() => count--}>-</button></div>
这个组件会被编译成一个
function create_fragment(ctx) { let div; let span; let t0; let t1; let button0; let t3; let button1; let mounted; let dispose;
return { c() { div = element("div"); span = element("span"); t0 = text(/*count*/ ctx[0]); t1 = space(); button0 = element("button"); button0.textContent = "+"; t3 = space(); button1 = element("button"); button1.textContent = "-"; }, m(target, anchor) { insert(target, div, anchor); append(div, span); append(span, t0); append(div, t1); append(div, button0); append(div, t3); append(div, button1);
if (!mounted) { dispose = [ listen(button0, "click", /*click_handler*/ ctx[1]), listen(button1, "click", /*click_handler_1*/ ctx[2]) ];
mounted = true; } }, p(ctx, [dirty]) { if (dirty & /*count*/ 1) set_data(t0, /*count*/ ctx[0]); }, i: noop, o: noop, d(detaching) { if (detaching) detach(div); mounted = false; run_all(dispose); } };}
function instance($$self, $$props, $$invalidate) { let count = 0; const click_handler = () => $$invalidate(0, count++, count); const click_handler_1 = () => $$invalidate(0, count--, count); return [count, click_handler, click_handler_1];}
不要被吓到,一个
function createFragment(ctx) { return { // 创建 DOM 的方法 c(): {}, // 把 DOM mount 到节点的方法,以及事件绑定 m(): {}, // DOM 节点更新的方法 p(): {}, // unmount 的方法 d() {} }}
这里的p()
updateUI
而 instance
则是 <script>
之中定义的变量和一些$$invalidate(0, count--, count)
setCount
- 用户点击
button, 触发$$invalidate(0, count--, count)
- 触发
schedule_update()
, 通知框架这个fragment 需要被更新(make_dirty()
) ,框架会维护一个dirty_components
的数组 - 同步代码执行完后,开始执行
microtask, 触发更新(flush
) ,遍历dirty_components
, 触发每一个component 的p()
