Svelte框架
响应式
svelte的响应式并非Vue型(数据读取时收集依赖、数据更新时劫持操作),也非React的用于数据更新API(调用后引起视图更新),而是将赋值语句编译后生成一个函数$$invalidate(flag, assignment);
,通常该函数的调用是在事件处理函数之类的交互更新函数内,通过$$invalidate
的调用,触发视图更新流程。例如下面的代码:
组件编译前代码
<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>
组件编译后,赋值语句被替换后的代码
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时,用了下列的位运算
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名是一样的)可以被子组件所修改。看下面的例子:
// 子组件,使用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之类的方法中)
// 从父组件传递的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的值,从而更新视图,效果是一样的。
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流程。
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回调函数不会被调用两次
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ji-chu/82004.html