跳至主要內容
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?

4563博客

全新的繁體中文 WordPress 網站
  • 首頁
  • Svelte 的异步更新实现原理
未分類
18 4 月 2021

Svelte 的异步更新实现原理

Svelte 的异步更新实现原理

資深大佬 : djyde 0

原载于我的独立博客: https://lutaonan.com

在 我对 Svelte 的看法 一文里,我分析了 Svelte 在编译时实现 Reactive 的原理。在这篇文章,我将分析在 Svelte 里更新一个状态 (state) 值后更新到 UI 的这一过程。

阅读本文前,你应该至少:

  • 读过 我对 Svelte 的看法
  • 明白 JavaScript 中的 事件循环 机制

原理分析

为了保持简单,先从一个和 Svelte 无关的例子讲起:

// 假设我们正在实现一个 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: 1 setCount(2) //=> update ui with count: 2 setCount(3) //=> update ui with count: 3 

这样实现很简单,但是有一个严重的问题:连续的状态更新会连续触发 updateUI, 性能会非常糟糕。解决这个问题的方法是:把同一个事件循环里的所有状态更新造成的 UI 更新统一合并( batch )到下一个事件循环里统一执行。做法很简单:把 updateUI 放到一个 microtask 里就行。

// 基于 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 一个 update function setCount(newVal) {   count = newVal  scheduleUpdate() }  setCount(1) setCount(2) setCount(3) //=> update ui with count: 3 

这样,在同一个事件循环里,多个状态更新只会触发一次 UI 更新。

现在假设页面上有一个 h1, updateUI 中会更新它:

let count = 0 const h1 = document.querySelector('h1')  function updateUI() {   updateScheduled = false  h1.innerHTML = `${count}` }  setCount(1) setCount(2) setCount(3) //=> update ui with count: 3  

So far so good. 但是相信不少人年轻的时候曾经写过这样的代码:

setCount(1) setCount(2) setCount(3) console.log(h1.innerHTML) //=> 0 

在 setCount(3) 后, h1.innerHTML 竟不是预期中的 3. 仔细一想,当然了,updateUI 是在下一个事件循环才触发的啊。

为了可以在 setCount 后拿到更新后正确的值,我们可以把关于 UI 的操作也放到下一个事件循环才执行。为了方便,我们可以写一个 tick 函数:

function tick() {   return new Promise.resolve() }  async () => {   setCount(1)   setCount(2)   setCount(3)   await tick()   console.log(h1.innerHTML) //=> 3 } 

Svelte 的实际做法

回到 Svelte:

<script>     let count = 0 </script>  <div>     <span>{count}</span>     <button on:click={() => count++}>+</button>     <button on:click={() => count--}>-</button> </div> 

这个组件会被编译成一个 fragment (你不需要读懂下面的代码):

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]; }  

不要感到害怕,一个 Svelte Fragment 实际上是一个函数返回几个必要的方法:

function createFragment(ctx) {   return {     // 创建 DOM 的方法     c(): {},     // 把 DOM mount 到节点的方法,以及事件绑定     m(): {},     // DOM 节点更新的方法     p(): {},   // unmount 的方法     d() {}   } } 

这里的 p(), 就是类似上文提到的 updateUI.

而 instance 则是 <script> 之中定义的变量和一些 event handlers. $$invalidate(0, count--, count) 类似上文提到的 setCount. 在真实的 Svelte 中整个状态更新的流程简单地来说就是:

  1. 用户点击 button, 触发 $$invalidate(0, count--, count)
  2. 触发 schedule_update(), 通知框架这个 fragment 需要被更新(make_dirty()),框架会维护一个 dirty_components 的数组
  3. 事件循环结束后,触发更新(flush),遍历 dirty_components, 触发每一个 component 的 p()
大佬有話說 (4)

  • 資深大佬 : Wichna

    赞

  • 主 資深大佬 : djyde

    注:文中对事件循环的表达有误,在博客中已修改。

  • 資深大佬 : FightPig

    我发现 vue3 的 setup 新提案 ref 写法有点像 Svelte 了,

  • 資深大佬 : phithon

    文章不错

文章導覽

上一篇文章
下一篇文章

AD

其他操作

  • 登入
  • 訂閱網站內容的資訊提供
  • 訂閱留言的資訊提供
  • WordPress.org 台灣繁體中文

51la

4563博客

全新的繁體中文 WordPress 網站
返回頂端
本站採用 WordPress 建置 | 佈景主題採用 GretaThemes 所設計的 Memory
4563博客
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?
在這裡新增小工具