再了解svelte组件渲染过程之前,我们先看一下我们熟悉的vue组件渲染过程,做个比较
vue组件渲染过程
借助vue-loader
我们可以把一个vue文件编译成一个组件的配置项对象,其中的template
会被编译成render
函数。
然后在我们new Vue(options)
的时候传入组件配置项对象,组件内部进行一系列的初始化操作,并在相应的时机执行生命周期钩子函数。有了组件实例之后就可以对数据进行很好的管理。然后进入$mount
阶段,首先执行template
编译生成render
函数生成vnode
(这也是svelte
和vue
最大的不同,svelte
直接编程成真实的dom
操作,而vue
则是通过vnode
做了一层适应多平台的抽象,并在后续更新时进行diff
优化),然后对vnode
一层层patch
成真实的dom
,借用黄轶大佬的一张图
svelte组件渲染过程
svelte组件初始化渲染过程
首先我们看一下svelte
文件会被编程成什么
通过官网的演示实例,我们可以看到svelte文件被编译成了以文件名命名的类,如:
App
,并且继承SvelteComponent
(实现了$destroy
,$on
,$set
三个通用方法)
当我们new App(options)
的时候,会执行init(component, options, instance, create_fragment, not_equal, props, dirty = [-1])
方法,先介绍下init
中每个参数的用意
- component: 当前类的实例
- options: 实例化组件的配置, 详见
- instance:svelte文件中script代码块会编译成的instance函数,返回用于模板标签中的动态数据(为什么上图示例编译的instance是null,因为生产环境编译时如果script代码块中没有定义可修改变量的函数会将这些变量编译为全局的,省略instance函数,一个小的性能优化点)
- create_fragment:创建包含
create
(创建dom节点),mount
(挂载dom节点),update
(更新dom节点),destory
(销毁dom节点)等钩子对象 - not_equal:判断两个变量是否相同
- props:
- dirty:用于匹配需要更新的某个节点 下面我们分析一下init函数渲染过程的主逻辑,防止干扰,其他的逻辑的代码先删除了,会在后面的章节中介绍
function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
...
// 定义一个对象,管理动态数据,生命周期数组等
const $$ = component.$$ = {
fragment: null,
ctx: null,
// state
props,
update: noop,
not_equal,
bound: blank_object(),
// lifecycle
on_mount: [],
on_destroy: [],
before_update: [],
after_update: [],
context: new Map(parent_component ? parent_component.$$.context : []),
// everything else
callbacks: blank_object(),
dirty,
skip_bound: false
};
...
//调用instance函数,返回作用于html模板中的动态数据,挂载到ctx中
$$.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;
})
: [];
...
//返回用于操作真实dom的对象,含有create,mount,update,destory钩子函数
$$.fragment = create_fragment ? create_fragment($$.ctx) : false;
...
//target为需要挂载的dom元素
if (options.target) {
// 调用create钩子生成dom节点
$$.fragment && $$.fragment.c();
// 调用mount钩子挂载dom节点
mount_component(component, options.target, options.anchor);
}
...
}
instance:script代码块中编译生成的函数
写了个简单的例子,看下编译之后的结果
首先调用
instance($$self,$$props, $$invalidate)
函数返回模板中用到的动态数据数组,挂载到$$.ctx
上,(self
是组件实例,props
是options
中传入的props
,invalidate
是更新dom的回调函数),后续修改后会重新赋值给$$.ctx
对应的值,所以用$$.ctx
中管理标签模板中动态数据。
create_fragment:html模板编译生成的函数
create_fragment($$.ctx)
返回了包含create
(创建dom节点),mount
(挂载dom节点),update
(更新dom节点),destory
(销毁dom节点)等操作dom的钩子对象。
然后执行$$.fragment.c()
,创建dom元素,text静态和动态节点。
然后执行到mount_component
方法中fragment.m(target, anchor)
,会把create
中生成的元素挂载到目标元素上,初始化的渲染过程也就结束了。
svelte组件更新过程
当我们改变一个动态数据的时候,会怎么触发更新呢
首先,看下上图中script代码块中changeNmae
会被编译成什么
function changeName() {
$$invalidate(0, name = "!!!");
}
调用该函数会触发invalidate
回调
(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;
})
not_equal
中判断更新前后值是否相同,并替换$$.ctx
中储存的旧值,如果新旧值不同并且ready
为true
(初始化渲染完成)执行make_dirty(component, i)
函数
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));
}
dirty_components
用于储存需要更新的组件,然后执行schedule_update()
function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
也就是会在微任务时异步触发flush
更新,update_scheduled
用于控制flush
函数只执行一次
我们在看一下,这句的作用
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31))
也就是会把需要更新的ctx中索引i转化成匹配更新dom节点的dirty
值,并赋给dirty[0]
,例如下:0=>1,1=>2,2=>4
function update(ctx, [dirty]) {
if (dirty & /*name1*/ 1) set_data_dev(t0, /*name1*/ ctx[0]);
if (dirty & /*name2*/ 2) set_data_dev(t1, /*name2*/ ctx[1]);
if (dirty & /*name3*/ 4) set_data_dev(t2, /*name3*/ ctx[2]);
}
这样我们在更新时,根据匹配的dirty的值更新哪个节点。
然后进入flush方法,下面是做了简化后的flush函数
function flush() {
if (flushing)
return;
flushing = true;
do {
// first, call beforeUpdate functions
// and update components
for (let i = 0; i < dirty_components.length; i += 1) {
const component = dirty_components[i];
set_current_component(component);
update(component.$$);
}
set_current_component(null);
dirty_components.length = 0;
} while (dirty_components.length);
update_scheduled = false;
flushing = false;
}
在flush
函数中会遍历dirty_components
,调用update(component.$$)
更新组件。
function update($$) {
...
const dirty = $$.dirty;
$$.dirty = [-1];
$$.fragment && $$.fragment.p($$.ctx, dirty);
...
}
在update
中会执行$$.fragment.p($$.ctx, dirty)
触发编译生成的fragment.update
钩子,并根据传入的dirty
匹配需要更新的dom节点
function update(ctx, [dirty]) {
if (dirty & /*name1*/ 1) set_data_dev(t0, /*name1*/ ctx[0]);
if (dirty & /*name2*/ 2) set_data_dev(t1, /*name2*/ ctx[1]);
if (dirty & /*name3*/ 4) set_data_dev(t2, /*name3*/ ctx[2]);
}
这就完成了一次最简单的组件更新过程
总结
svelte
强大之处在于编译能力,首先会把html模板
编译成create_fragment
函数,用于创建,挂载,更新和销毁dom节点,并把script中的代码块
编译成instance
函数,在操作动态数据的地方用invalidate
函数包裹,用于更新组件。而vue则是通过render
函数生成抽象层vnode
再patch
成真实的dom
,update
的时候对前后vnode
进行diff
比较,更新有差异的dom节点。而svelte则是直接把模板编译成操作真实dom
的语句,性能上省去了vnode
的过程。
谢谢大家的观看,有问题忘指正。