svelte框架浅析

响应式

svelte的响应式并非Vue型(数据读取时收集依赖、数据更新时劫持操作),也非React的用于数据更新API(调用后引起视图更新),而是将赋值语句编译后生成一个函数$$invalidate(flag, assignment);,通常该函数的调用是在事件处理函数之类的交互更新函数内,通过$$invalidate的调用,触发视图更新流程。例如下面的代码:

组件编译前代码

xml 复制代码
<script>
    let name;
    let num = 1;
    const toggleName = () => {
        name = 'Svelte';
    }
    const addNum = () => {
        num += 1;
    }
    const updateNameAndNum = () => {
        name = 'solid.js';
        num = 10;
    }
</script>

<main>
    <h1 class="hello">Hello {name}!</h1>
    <p class="num">num is  {num}</p>
    <button on:click={updateNameAndNum}>update</button>
</main>

<style>
    main {
        text-align: center;
        padding: 1em;
        max-width: 240px;
        margin: 0 auto;
    }

    h1 {
        color: #ff3e00;
        text-transform: uppercase;
        font-size: 4em;
        font-weight: 100;
    }
</style>

组件编译后,赋值语句被替换后的代码

ini 复制代码
const updateNameAndNum = () => {
  $$invalidate(0, name = 'solid.js');
  $$invalidate(1, num = 10);
};

$$invalidate(flag, assignment)

flag:每个变量都有唯一的flag来追踪,这样哪个变量有变化,就去更新哪个变量对应的视图,这种颗粒度比Vue的组件颗粒度细很多,从而排除了虚拟DOM层的diff运算。

对于flag机制,svelte的每个组件实例都有一个dirty属性来记录哪些变量有更新(即所谓脏数据),dirty属性是一个数组,直观的设计就是每个变量按顺序记录,存入dirty数组。例如上面的例子,name -> 0,num -> 1,那么name -> dirty[0]num -> diryt[1]。组件在更新视图时,如果dirty[0] = 1,说明name有更新,更新name对应的视图,以此类推。但是这种方案占用的内存空间比较大,每个变量要占用一个数组元素,svelte采用了位来跟踪变量,每一位可以跟踪一个变量,那么一个字节的8位可以跟踪8个变量,内存占用远低于直接使用数组的索引。

位掩码方案跟踪变量

变量仍然按照从0递增1的方式跟踪,但是在dirty数组中需要经过一定的位运算来保存。

这里JavaScript在处理整数的位运算时,总是将操作数转换为32位整数,也就是说任何整数参与位运算时都会先转换为32位整数,所以用位来标记变量时,dirty[0]的值最大就是32位,一个索引最多能保存32个变量

因此,svelte在保存flag时,用了下列的位运算

css 复制代码
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));

(i / 31) | 0:本质上就是取i / 31的整数部分,每32个变量占据一个索引

1 << (i % 31):将1左移(i % 31)位,也就是0 -> 0000 0001, 1 -> 0000 0010, 2 -> 0000 0100, 3 -> 0000 1000等等,由于是31个位,所以这里使用有符号左移即可。

在保存位运算之前,先执行了component.$$.dirty.fill(0),让所有索引的值都为0,0与任何数做位或运算,都是任何数。这样就保存了该变量的flag。

assignment:变量的赋值语句,值为变量最新的值

Prop与单向数据流

通常,prop是单向数据流,即父组件给子组件传递prop,prop的值只有父组件可以更改,子组件不能更改,否则无法分辨该prop的变化到底是父组件还是子组件导致的更新,不利于排查数据更新的问题,尤其是大型项目里数据复杂多变的情况。

但是,在svelte中,传统的prop思维模式下,prop的值(这里的说法不严谨,客观来将,父组件传给prop的值没有被修改,而是子组件修改了自己内部的值,因为子组件的变量名和prop名是一样的)可以被子组件所修改。看下面的例子:

xml 复制代码
// 子组件,使用export声明了currentProp变量用来接收父组件传入的值
// 如果不声明则无法接收父组件传递的值
<script>
    // `currentProp` is updated whenever the prop value changes...
    export let currentProp;

    // ...but `initial` is fixed upon initialisation
  // of the component because it uses `const` instead of `$:`
  const initial = currentProp;
    const updateCurrent = () => {
        let i = 0;
        const colors = ['red', 'yellow', 'green'];
        return () => {
            console.log('thing component current changed', i, currentProp);
            currentProp = colors[i];
            i ++;
            if (i > 2) {
                i = 0;
            }
        }
    }
</script>

<div>
    <span style="background-color: {initial}">initial</span>
    <span style="background-color: {currentProp}">current</span>
    <button on:click={updateCurrent()}>thing current change</button>
    <p class="thing">Thing Component</p>
</div>

<style>
// 省略
<style>

<script>
    import Thing from "./Thing.svelte";
    let bgColor = 'lightblue';
    const updatebgColor = () => {
        let i = 0;
        const colors = ['darkred', 'orange', 'darkgreen'];
        return () => {
            console.log('app component bgColor changed', i, bgColor);
            bgColor = colors[i];
            i ++;
            if (i > 2) {
                i = 0;
            }
        }
    }
</script>

<main>
    <Thing currentProp={bgColor} --color="red" />
    <button on:click={updatebgColor()}>update bgColor</button>
    <p>bgColor value: {bgColor}</p>
</main>

<style>
// 省略
</style>

子组件可以通过updateCurrent方法来更新currentProp的值,从而更新视图,但此时父组件的bgColor值未更新。

子组件使用内部变量来接收父组件的值,后面子组件更新内部的变量,只要有赋值语句就会触发视图更新,与父组件无关。

请看编译后的运行时(在instance之类的方法中)

ini 复制代码
// 从父组件传递的props对象中解构出currentProp变量
let { currentProp } = $$props;

const updateCurrent = () => {
            let i = 0;
            const colors = ['red', 'yellow', 'green'];

            return () => {
                // 给函数内部变量赋值,触发更新,都与父组件无关
                $$invalidate(0, currentProp = colors[i]);
                i++;

                if (i > 2) {
                    i = 0;
                }
            };
        };

父组件可以通过updatebgColor方法来更新bgColor的值,从而更新视图,效果是一样的。

ini 复制代码
let bgColor = 'lightblue';

const updatebgColor = () => {
            let i = 0;
            const colors = ['darkred', 'orange', 'darkgreen'];

            return () => {
                console.log('app component bgColor changed', i, bgColor);
                $$invalidate(1, bgColor = colors[i]);
                i++;

                if (i > 2) {
                    i = 0;
                }
            };
        };

// 父组件的p函数用来更新视图,这里的thing_changes和thing.$set(thing_changes)用来更新子组件
// 父组件内部保存有子组件实例,调用$set更新子组件,从而触发this.$$set($$props);
// this.$$set()又会调用invalidate(0, currentProp = $$props.currentProp)
p: function update(ctx, [dirty]) {
                if (!current || dirty & /*name*/ 1) set_data_dev(t1, /*name*/ ctx[0]);
                if (!current || dirty & /*num*/ 4) set_data_dev(t5, /*num*/ ctx[2]);
                const thing_changes = {};
                if (dirty & /*bgColor*/ 2) thing_changes.currentProp = /*bgColor*/ ctx[1];
                thing.$set(thing_changes);
                if (!current || dirty & /*bgColor*/ 2) set_data_dev(t17, /*bgColor*/ ctx[1]);
            },

组件编译结果

每个组件都继承在svelteComponent,在构造函数中会调用init初始化方法来初始化组件实例,给实例挂载核心的$$属性。

开发者的组件代码编译后分为两部分

create_fragment函数

负责组件与DOM相关的部分,包括创建DOM节点、挂载DOM、卸载DOM、更新DOM等,可以理解为.svelte单文件中的模板部分编译结果。

instance函数

负责组件的脚本部分,即script标签的编译结果,包含了组件内变量,方法,这些变量和方法都是开发者通过script脚本的代码赋予给组件实例的扩展。

运行时执行流程

svelte打包后,会将svelte框架运行时和组件代码打成一个bundle包,组件相关的代码包含了SvelteComponent的代码以及开发者定义的组件编译后代码,其他的为框架运行时任务调度和一系列工具函数;

运行时代码由一系列DOM操作函数和任务调度函数组成。

SvelteComponent和组件运行时相关代码

  • 提供组件的基类

  • 提供组件的工具函数,如init函数,用于组件实例的初始化

工具函数

DOM更新的工具函数

创建DOM节点、移除DOM节点、DOM属性更新、style对象更新、事件监听等等一系列工具函数;

挂载组件、卸载组件函数

任务调度

任务调度是视图更新的核心功能,通过flush函数,遍历dirtyComponents(含有脏数据的组件,即需要更新视图的组件),调用组件各自的更新方法来更新视图。在此期间,调用生命周期方法注册的回调函数。在after_update注册的回调函数中如果更新数据,就会引发循环更新问题,在flush函数中也通过缓存方案解决了。

挂载时的执行流程

从根组件的实例初始化开始(即init函数调用),到instance函数调用(instance函数处理开发者写的组件代码,返回变量和更新变量的函数),然后调用create_fragment函数(函数内包含子组件的初始化,保留了子组件实例的引用),返回的对象包含了组件的创建、挂载、更新等方法,根组件实例$$.fragment保存了该对象,用于之后的创建、挂载、更新等操作。最后调用mount_component函数,传入根组件实例、挂载点等参数,开始挂载流程。

mount_component流程

根组件实例调用m方法实现挂载操作,父组件的m方法中,调用了mount_component函数来挂载子组件,这样逐步递归调用,实现整个挂载流程。在挂载完毕后,将onMount生命周期函数注册的回调函数和after_update注册的回调函数都增加到render_callback中。最后,在mount_component递归调用完毕后,调用flush函数,将render_callback中的回调函数一一执行完毕。

更新流程

更新是由$$invalidate(flag, assignment)触发,其中assignment赋值语句的值是变量最新值,调用make_dirty函数,将包含脏数据的组件和组件内的脏数据都保存,脏数据组件保存到dirty_components数组中。同时开启了异步的flush流程。

scss 复制代码
function make_dirty(component, i) {
        if (component.$$.dirty[0] === -1) {
            dirty_components.push(component);
            schedule_update();
            component.$$.dirty.fill(0);
        }
        component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
    }

flush流程

遍历脏组件,调用update函数对脏组件进行更新。update函数内先执行beforeUpdate回调函数,将组件的dirty数组保留副本后还原dirty数组为[-1],然后执行各组件的p方法(传入组件的ctx和dirty副本,ctx包含所有组件内的变量),根据dirty副本去执行对应变量的DOM更新方法。接着把afterUpdate注册的回调都放到render_callback数组,执行binding_callbacks的回调函数,然后执行render_callbacks里的回调,最后执行flush_callbacks的回调函数。

  • 回调函数的执行顺序

    • beforeUpdate的回调函数,父组件先于子组件

    • bind:this的回调函数,子组件先于父组件

    • afterUpdate的回调函数,父组件先于子组件,除非在挂载阶段是子组件先于父组件(因为子组件先挂载完成)

  • 回调函数可能引起新的更新

    • beforeUpdate的回调函数中,如果引起新的更新,会将脏组件置于dirty_components中,但不会引发新的flush调用,因为update_scheduled此时为true,只有update_scheduled为false才会引发flush调用。然后继续遍历dirty_components,这样就可以把新引起的更新也加入到此次更新流程中。

    • bind:this 的回调函数无法触发flush调用

    • afterUpdate的回调函数不会被调用两次

相关推荐
heroboyluck7 天前
Svelte 核心语法详解:Vue/React 开发者如何快速上手?
前端·svelte
hboot12 天前
还不会Svelte?快来一起学习吧🤓
前端·svelte
姜 萌@cnblogs1 个月前
开源我的一款自用AI阅读器,引流Web前端、Rust、Tauri、AI应用开发
rust·web·tauri·svelte
冴羽2 个月前
SvelteKit 最新中文文档教程(23)—— CLI 使用指南
前端·javascript·svelte
冴羽2 个月前
SvelteKit 最新中文文档教程(22)—— 最佳实践之无障碍与 SEO
前端·javascript·svelte
冴羽2 个月前
SvelteKit 最新中文文档教程(21)—— 最佳实践之图片
前端·javascript·svelte
冴羽2 个月前
SvelteKit 最新中文文档教程(20)—— 最佳实践之性能
前端·javascript·svelte
冴羽yayujs2 个月前
SvelteKit 最新中文文档教程(19)—— 最佳实践之身份认证
前端·javascript·vue.js·react.js·前端框架·svelte·sveltekit
冴羽2 个月前
SvelteKit 最新中文文档教程(19)—— 最佳实践之身份认证
前端·javascript·svelte
冴羽2 个月前
SvelteKit 最新中文文档教程(18)—— 浅层路由和 Packaging
前端·javascript·svelte