在《都2020年了你还不知道Svelte》这个系列里,我们将会介绍 Svelte 框架的基本用法、特性、优缺点、基本原理和源码分析。
目录
- 第一篇 ?? 《都2020年了你还不知道Svelte(1)—— 初识Svelte》
- 第二篇 ?? 《都2020年了你还不知道Svelte(2)—— 更新渲染原理》
- 第三篇 ?? 《都2020年了你还不知道Svelte(3)—— 脏值检测、DOM更新》(未完待续)
- 第四篇 ?? 《都2020年了你还不知道Svelte(4)—— 长列表处理》(未完待续)
- 第五篇 ?? 《都2020年了你还不知道Svelte(5)—— 初探编译原理》(未完待续)
- 第六篇 ?? 《都2020年了你还不知道Svelte(6)—— 实现一个Svelte 编译器》(未完待续)
相比于 Vue、React, Svelte 更新渲染过程非常简单,简单到,你只需要看完这篇就可以彻底的弄懂。
整体更新渲染流程
Svelte 是如何更新数据,渲染结果的呢?Svelte 整体的更新渲染流程是怎么样子的呢?
接下来,我们将编译一个最简单的例子,深入Svelte 的每一行源码。
例子的代码:
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 | <main> <div> {name} </div> <button on:click={onClick}>click</button> </main> <script> let name = 'hello' function onClick() { name = name + 's' } </script> |
上面代码: 当点击按钮时,
我梳理了 Svelte 打包后代码的整体流程,发现还是比较简单的,用画图表示如下:
这里需要注意的是,虽然
svelte 声称自己无运行时代码,但并不意味着,打包出来的产物完全不会加入其他代码。svelte 还是会打包进去一些工具性的代码,相比于 react 和 vue, 代码体积还是会小很多。
-
click 事件发生之后,会发生两件事情: 1. name 变量被修改:name=name + s 2. 调用$$invalidate 方法。 -
$$invalidate 方法又调用了make_dirty 方法,make_dirty 是记住已经脏的数据,里面做了两件事情,比较复杂,后面会讲到。 -
数据弄脏之后,不是立马更新,而是
schedule_update() 方法把flush 回调推到16ms 一帧的最后去执行。 -
flush 被执行时,遍历所有的diry_components 中的组件,调用他们的.p() 方法。.p() 方法是编译生成的,里面有一个神奇的 if 判断,如果走进判断体,则调用setData 方法更新DOM节点
看上去一头雾水?没关系,只需要有一个大致的印象,下面会详细的介绍每一个过程
click 事件发生后
首先,我们在浏览器里面执行的并不是我们写的代码,而是 Svelte 编译修改之后的。
增加了一个
1 2 3 4 5 6 7 8 9 | function instance($$self, $$props, $$invalidate) { let name = "hello"; let handleClick = () => { $$invalidate(0, name = name + "s"); }; return [name, handleClick]; } |
$$invalidate
1 2 3 4 | function $$invalidate(i, value) { make_dirty(component, i); } |
也就是调用
make_dirty
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 | function make_dirty(component, i) { // 如果 component.$$.dirty[0] if (component.$$.dirty[0] === -1) { // dirty_components 记录了,脏组件 dirty_components.push(component); schedule_update(); component.$$.dirty.fill(0); } // 下面代码的原理是,通过二进制的值来记录脏数据 // 不要尝试看懂,下面会讲 component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); } |
之后,调用了
schedule_update
1 2 3 4 | function schedule_update() { resolved_promise.then(flush); } |
背后其实是,让
一帧 16ms 之内发生的任务的顺序
-
响应用户输入事件(scroll / click / key)
-
Javascript 执行 -
requestAnimation /Intersection Observer cb -
布局 Layout
-
绘制 Paint
-
如果
16ms 还有盈余,调用requestIdleCallback ,若没有,会被饿死,通过第二个参数指定一个到时必处理 -
宏任务 (
setTimeout /MessageChannel.onMessage ) -
微任务 (
Promise.then() )
flush
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 | function flush() { // 如果正在 flushing , 就退出 if (flushing) { return; } flushing = true; do { for (let i = 0; i < dirty_components.length; i += 1) { const component = dirty_components[i]; update(component.$$); } flushing = false; } function update($$) { // 先假设 $$.fragment 都不是 null if ($$.fragment !== null) { $$.update(); // ~~~~~~~~~~~~~~~ before_update 生命周期 ~~~~~~~~~~~~~~~ run_all($$.before_update); const dirty = $$.dirty; // 所有必须的 更新,必须要更新了,调用 p 方法 $$.fragment && $$.fragment.p($$.ctx, dirty); // ~~~~~~~~~~~~~ after_update 生命周期 ~~~~~~~~~~~~~~~ $$.after_update.forEach(add_render_callback); } } |
.p 方法
1 2 3 4 5 6 7 8 9 10 | p(ctx, [dirty]) { // & 是位运算 if (dirty & 1) { // set_data 就是把 dom 节点的 data 值更新 set_data(t1, ctx[0]) }; if (dirty & 1) { set_data(t3, ctx[0]) }; }, |
上面神奇的
整体流程小节
你可能还是一头雾水,最大的困惑在于Svelte 是如何根据脏数据更新DOM节点的。为了彻底理解这块的逻辑,请务必要看下面这一小节。
Svelte 脏数据更新DOM 原理
任何一个现代前端框架,都需要记住哪些数据更新了,把更新了的数据视为脏数据,然后根据脏数据计算出最新的dom状态。
Svelte使用 位掩码(bitMask) 的技术来跟踪哪些值是脏的,即自组件最后一次更新以来,哪些数据发生了哪些更改。
位掩码是一种将多个布尔值存储在单个整数中的技术,一个比特位存放一个数据是否变化,一般
用大白话来讲,你有A、B、C、D 四个值,那么二进制
这种表示法,可以最大程度的利用空间。为啥这么说呢?
比如说,十进制数字
从左边数第一位、第二位是1,意味着第一个值A 和第二个值B是脏数据;其余位都是0,意味着其余数据都是干净的。
JS 的限制
但是,js 的二进制有31位限制(32位,减去1位用来存放符号)。
也就是说,如果 Svelte 采用二进制位存储的方法,那么只能在一个 Svelte 组件中存 31个数据。
但肯定不能这样,对吧?
Svelte 采用数组来存放,数组中一项是二进制
这个数组就是
一探究竟 component.$$.dirty
上文中,我们说到
我们模拟一个 Svelte 组件,这个 Svelte 组件会修改33个数据。
我们打印出每一次
wtf ?? 看上去,根本找不出一些规律。
Svelte 正是用
如果我们把上面数组中的值,都十进制转为二进制来,再来观察一下:
上面数组中的每一项中的每一个比特位,如果是1,则代表着该数据是否是脏数据。如果是脏数据,则意味着更新。
-
第一行
["0000000000000000000000000000001", "0000000000000000000000000000000"] , 表示第一个数据脏了,需要更新第一个数据对应的dom节点 -
第二行
["0000000000000000000000000000011", "0000000000000000000000000000000"] , 表示第一个、第二个数据都脏了,需要更新第一个,第二个数据对应的dom节点。 -
……
当一个组件内,数据的个数,超出了
如何设置为脏数据
原理上,设置为脏数据,比较简单,就是把
如果你不想了解具体的代码, 下面这一小节《如何设置为脏数据》可以先不看,== 比较复杂。
只需要了解,设为脏数据,就是
component.$$.dirty 数组二进制对应的比特位设置为1svelte 作者通过一系列位操作符来做到这一点
实际上,这段逻辑对应的源码非常精炼和复杂,就下面一行代码:
1 | component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); |
上面的代码是啥意思呢?把指定bit位上代表的记为脏数据
(i/31)|0 啥意思?
如果你觉得很不好理解,可以简单的理解为
component.$$.dirty[(i/31)|0] 啥意思 ?
还记得我们的
1 2 | // component.$$.dirty ["0000000000000000000000000000001", "0000000000000000000000000000000"] |
1 <<(i%31) 啥意思?
先讨论
比如说
下面是
1 2 3 | 3 的二进制表示为 0000 0011 所有位数左移2位变为 0000 1100 转变为十进制后为 12( 相当于 3 * 4, 也就是变大了 2 的2次方倍 ) |
可以这么理解,二级制后所有位数左移n位, 其效果相当于
假设
|= 啥意思?
1 2 | Operator: x |= y Meaning: x = x | y |
注意哦。
我们最初的代码是这样的:
1 | component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); |
为了好理解,可以改写成下面这个样子:
1 2 | let item = component.$$.dirty[(i / 31) | 0] item = item | (1 << (i % 31)); |
上文说过,假设 i 为 3, 1 << (i % 31) 计算为 8, 二进制表示为
假如 item 二进制表示为
也就是说, 将指定的那一位位设置为1,无论其值之前是多少。
【如何设置为脏数据】小节
总之这一小节,你只需要了解到,
更新哪些DOM节点
上面一个小节,我们知道了, Svelte 会用
那么,有了脏数据之后,
为了测试,我们模拟一个
编译出来的
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 | p(ctx, dirty) { if (dirty[0] & /*name1*/ 1) set_data(t1, /*name1*/ ctx[0]); if (dirty[0] & /*name2*/ 2) set_data(t3, /*name2*/ ctx[1]); if (dirty[0] & /*name3*/ 4) set_data(t5, /*name3*/ ctx[2]); if (dirty[0] & /*name4*/ 8) set_data(t7, /*name4*/ ctx[3]); if (dirty[0] & /*name5*/ 16) set_data(t9, /*name5*/ ctx[4]); if (dirty[0] & /*name6*/ 32) set_data(t11, /*name6*/ ctx[5]); if (dirty[0] & /*name7*/ 64) set_data(t13, /*name7*/ ctx[6]); if (dirty[0] & /*name8*/ 128) set_data(t15, /*name8*/ ctx[7]); if (dirty[0] & /*name9*/ 256) set_data(t17, /*name9*/ ctx[8]); if (dirty[0] & /*name10*/ 512) set_data(t19, /*name10*/ ctx[9]); if (dirty[0] & /*name11*/ 1024) set_data(t21, /*name11*/ ctx[10]); if (dirty[0] & /*name12*/ 2048) set_data(t23, /*name12*/ ctx[11]); if (dirty[0] & /*name13*/ 4096) set_data(t25, /*name13*/ ctx[12]); if (dirty[0] & /*name14*/ 8192) set_data(t27, /*name14*/ ctx[13]); if (dirty[0] & /*name15*/ 16384) set_data(t29, /*name15*/ ctx[14]); if (dirty[0] & /*name16*/ 32768) set_data(t31, /*name16*/ ctx[15]); if (dirty[0] & /*name17*/ 65536) set_data(t33, /*name17*/ ctx[16]); if (dirty[0] & /*name18*/ 131072) set_data(t35, /*name18*/ ctx[17]); if (dirty[0] & /*name19*/ 262144) set_data(t37, /*name19*/ ctx[18]); if (dirty[0] & /*name20*/ 524288) set_data(t39, /*name20*/ ctx[19]); if (dirty[0] & /*name21*/ 1048576) set_data(t41, /*name21*/ ctx[20]); if (dirty[0] & /*name22*/ 2097152) set_data(t43, /*name22*/ ctx[21]); if (dirty[0] & /*name23*/ 4194304) set_data(t45, /*name23*/ ctx[22]); if (dirty[0] & /*name24*/ 8388608) set_data(t47, /*name24*/ ctx[23]); if (dirty[0] & /*name25*/ 16777216) set_data(t49, /*name25*/ ctx[24]); if (dirty[0] & /*name26*/ 33554432) set_data(t51, /*name26*/ ctx[25]); if (dirty[0] & /*name27*/ 67108864) set_data(t53, /*name27*/ ctx[26]); if (dirty[0] & /*name28*/ 134217728) set_data(t55, /*name28*/ ctx[27]); if (dirty[0] & /*name29*/ 268435456) set_data(t57, /*name29*/ ctx[28]); if (dirty[0] & /*name30*/ 536870912) set_data(t59, /*name30*/ ctx[29]); if (dirty[0] & /*name31*/ 1073741824) set_data(t61, /*name31*/ ctx[30]); if (dirty[1] & /*name32*/ 1) set_data(t63, /*name32*/ ctx[31]); if (dirty[1] & /*name33*/ 2) set_data(t65, /*name33*/ ctx[32]); } |
我们一起来看,但其实一分析,发现这一坨代码很好理解:有
上面代码中的
1 2 | // dirty === component.$$.dirty ["0000000000000000000000000000001", "0000000000000000000000000000000"] |
上面代码中的
上面代码中的
还是一头雾水不要慌,我们拿上面代码里第4行举例子:
1 | if (dirty[0] & /*name3*/ 4) set_data(t5, /*name3*/ ctx[2]); |
这里的
只有当4 转变为二进制是
当我们分析到这里,已经看出了一些眉目,让我们站在更高的一个层次去看待这 30多行代码: 它们其实是保存了这33个变量 和 真实DOM 节点之间的对应关系,哪些变量脏了,Svelte 会走入不同的
这 30多行代码,是Svelte 编译了我们写的Svelte 组件之后的产物,在Svelte 编译时,就已经分析好了,数据 和 DOM 节点之间的对应关系,在数据发生变化时,可以非常高效的来更新DOM节点
【更新哪些DOM节点】小节
一个前端框架,不管是
vue, react 是通过 virtualDom 来 diff 计算出更新哪些dom节点更划算。
而
至于,Svelte 的脏值检测、如何封装更新DOM的方法,将会在下一个小节介绍。
最后,字节跳动大量招人人人人!!
字节跳动(杭州|北京|上海)大量招人。福利超级棒,薪资水平秒BAT。上班不打卡、每天下午茶、免费零食无限供应、免费三餐(我念下菜单,大闸蟹鲍鱼扇贝海鲜烤鱼片黑椒牛柳咖喱牛肉麻辣小龙虾)。
免费健身房、入职配16寸顶配最新mbp、每月还有租房房补。
这次真的机会多多,年后研发人数要扩招n倍,技术氛围好,大牛多,加班少,还犹豫什么?快发简历到下方邮箱,就现在!
仅仅是一小部的JD, 更多的欢迎加微信~
前端部分JD
-
高级前端(北京)?? https://job.toutiao.com/s/WXjEDn
-
前端跨端应用(北京)?? https://job.toutiao.com/s/WXBG1s
-
前端实习生(北京)?? https://job.toutiao.com/s/WXBqne
-
前端实习生(杭州)?? https://job.toutiao.com/s/WXM4oX
-
高级前端(杭州)?? https://job.toutiao.com/s/WXfj3x
-
高级前端(上海)?? https://job.toutiao.com/s/WX88rF
JAVA 部分JD
- JAVA(北京)?? https://job.toutiao.com/s/WX2wPC
持续招聘大量前端、服务端、客户端、测试、产品,实习社招都阔以
加微信 dujuncheng1,可以聊天聊地聊人生,请注明来自掘金以及要投递哪里的岗位