Svelte笔记三:runtime源码解读

svelte 的源码很简单是由两大部分组成,compiler 和 runtime。

compiler 就是一个编译器将 svelte 模版语法转换为浏览器能够识别的代码。而 runtime 则是在浏览器中帮助业务代码运作的运行时函数。所以说 runtime 是 svelte 框架最核心的部分,它也解释了 svelte 是如何在没有 virtual dom 的情况下也照样运行的。今天我们 review 一下 runtime 代码。

svelte 的 runtime 主要由 fragment 和 component 组成,而 component 是包含了 fragment。它们有着独立的生命周期,将逻辑层和渲染层分离。

Fragment

Svelte 官方 example提供了 compile 出来的 Js output。这些 output 就是运行在浏览器的源码,根据内容知道 svelte 的基本运作,让开发者清楚它内部的每一步运作。下面这个栗子很简单,就是对 hello 的一个字符串插值。而 name 是一个变量。

<script>
  let name = "world";
</script>

<h1>Hello {name}!</h1>

编译出来的结果:

/* App.svelte generated by Svelte v3.24.0 */
import {
  SvelteComponent,
  detach,
  element,
  init,
  insert,
  noop,
  safe_not_equal,
} from "svelte/internal";

function create_fragment(ctx) {
  let h1;

  return {
    c() {
      h1 = element("h1");
      h1.textContent = `Hello ${name}!`;
    },
    m(target, anchor) {
      insert(target, h1, anchor);
    },
    p: noop,
    i: noop,
    o: noop,
    d(detaching) {
      if (detaching) detach(h1);
    },
  };
}

let name = "world";

class App extends SvelteComponent {
  constructor(options) {
    super();
    init(this, options, null, create_fragment, safe_not_equal, {});
  }
}

export default App;

编译出来的结果就是有一个初始化函数,叫 create_fragment,它是用于 dom 的初始挂载。它使用了 element 函数,通过查阅源码src/runtime/internal/dom,我们知道它的作用就是用来创建 h1 标签实例,并且填入可变内容。除了element之外,还有spacetextsvg_element等都是用于生成真实 dom,分别是对空格,纯文本,svg 进行生成处理。

export function element<K extends keyof HTMLElementTagNameMap>(name: K) {
  return document.createElement<K>(name);
}

export function text(data: string) {
  return document.createTextNode(data);
}

export function space() {
  return text(" ");
}

create_fragment 的过程还包含有c,m,p,i,o,d等特殊名称的函数,这些函数并非编译混淆,而是 Fragment 内部的生命周期缩写。Fragment 指得是真实 dom 的节点,它拥有着独立的生命周期和属性。源码中src/runtime/internal/Component介绍了它的定义,它是一个真实的 dom 元素集合,它的属性并非组件属性(如下方 ts 类型定义),分别包含了create, claim, hydrate, mount, update, mesure, fix, animate, intro, outro, destory,组件的真实变化会影响 Fragment 的变化,Fragment 的变化影响真实的 dom,从上面例子看在 create 的过程中它创建了 h1 标签,在 mount 的过程将刚才创建的 h1 挂载到页面中,在 update 的过程没有任何操作任何操作只有回调钩子,在 detach 的过程销毁该 Fragment。

interface Fragment {
  key: string | null;
  first: null;
  /* create  */ c: () => void;
  /* claim   */ l: (nodes: any) => void;
  /* hydrate */ h: () => void;
  /* mount   */ m: (target: HTMLElement, anchor: any) => void;
  /* update  */ p: (ctx: any, dirty: any) => void;
  /* measure */ r: () => void;
  /* fix     */ f: () => void;
  /* animate */ a: () => void;
  /* intro   */ i: (local: any) => void;
  /* outro   */ o: (local: any) => void;
  /* destroy */ d: (detaching: 0 | 1) => void;
}

Component

SvelteComponent 则是包含了 svelte 组件内置的属性和生命周期,它们与 Fragment 的属性和生命周期是息息相关,SvelteComponent 是依赖于 Fragment,组件的变化会触发 Fragment 的变化。它是一个相辅相成的组合。源码中还有 SvelteComponent 和 SvelteElement 的细分,不同点在于 Web Component 的组件的支持,这里就不再展开。

Component 拥有四个生命周期,分别是 mount,beforeUpdate, afterUpdate,destory。没有 create 阶段是因为 svelte 没有 virtual dom。所以在组件层面,它没有像 vue 那么复杂。

数据流

react 的单向数据流,vue 的双向绑定,那么 svelte 是怎么样实现数据流的呢?

下面是我们业务中经常见到的代码,点击按钮请求数据然后设置到变量,触发 dom 内容的变化。svelte 的写法形似 vue 的写法,但是它的 runtime 原理并没有双向绑定。编译后的代码除了有上面所说的 create 和 mount 等 fragment 生命周期属性外,其他代码更多表现了数据流的形式。

<script>
  let num = 1;

  async function handleClick() {
    const res = await fetch(`tutorial/random-number`);
    const text = await res.text();

    if (res.ok) {
      num = text;
      return text;
    } else {
      throw new Error(text);
    }
  }
</script>

<button on:click="{handleClick}">generate random number</button>

<p>The number is {num}</p>

编译出来的结果:

/* App.svelte generated by Svelte v3.24.0 */
import {
  SvelteComponent,
  append,
  detach,
  element,
  init,
  insert,
  listen,
  noop,
  safe_not_equal,
  set_data,
  space,
  text,
} from "svelte/internal";

function create_fragment(ctx) {
  let button;
  let t1;
  let p;
  let t2;
  let t3;
  let mounted;
  let dispose;

  return {
    c() {
      button = element("button");
      button.textContent = "generate random number";
      t1 = space();
      p = element("p");
      t2 = text("The number is ");
      t3 = text(/*num*/ ctx[0]);
    },
    m(target, anchor) {
      insert(target, button, anchor);
      insert(target, t1, anchor);
      insert(target, p, anchor);
      append(p, t2);
      append(p, t3);

      if (!mounted) {
        dispose = listen(button, "click", /*handleClick*/ ctx[1]);
        mounted = true;
      }
    },
    p(ctx, [dirty]) {
      if (dirty & /*num*/ 1) set_data(t3, /*num*/ ctx[0]);
    },
    i: noop,
    o: noop,
    d(detaching) {
      if (detaching) detach(button);
      if (detaching) detach(t1);
      if (detaching) detach(p);
      mounted = false;
      dispose();
    },
  };
}

function instance($$self, $$props, $$invalidate) {
  let num = 1;

  async function handleClick() {
    const res = await fetch(`tutorial/random-number`);
    const text = await res.text();

    if (res.ok) {
      $$invalidate(0, (num = text));
      return text;
    } else {
      throw new Error(text);
    }
  }

  return [num, handleClick];
}

class App extends SvelteComponent {
  constructor(options) {
    super();
    init(this, options, instance, create_fragment, safe_not_equal, {});
  }
}

export default App;

代码中,handleClick 函数被封装在一个名为instance的方法当中,而它的入参当中有个$$invalidate的回调函数,字面意思就是“使之不能再使用”,用于变量的设置,把接口异步获取的数据设置回调函数当中。而它在组件的调用如下,重点在于回调函数当中,instance只会在初始化的时候调用,但是回调函数$$invalidate可以在各种异步情况调用。它会触发make_dirty的方法,使得schedule_update。在一个微任务当中,触发flush将一段时间内的变量操作都执行掉。实现变量的处理,flush 函数的具体实现请查看源码(src/runtime/internal/Component.ts)。flush 的过程中会触发 Fragment 的 update 以及 Component 的 update。

$$.ctx = instance
  ? instance(component, prop_values, (i, ret, ...rest) => {
      const value = rest.length ? rest[0] : ret;
      if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = value))) {
        if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
        if (ready) make_dirty(component, i);
      }
      return ret;
    })
  : [];

由此可见,svelte 是单向数据流,很多数据工作已经在 compile 的过程当中已经完成。runtime 更多是服务于浏览器层面的数据流转化。

题外话

shopee,又称虾皮,是一家腾讯投资的跨境电商平台。这里加班少,技术氛围好。如果想和我并肩作战一起学习,可以找我内推。邮箱weiping.xiang@shopee.com,非诚勿扰。