svelte组件渲染过程

1,688 阅读4分钟

再了解svelte组件渲染过程之前,我们先看一下我们熟悉的vue组件渲染过程,做个比较

vue组件渲染过程

借助vue-loader我们可以把一个vue文件编译成一个组件的配置项对象,其中的template会被编译成render函数。 然后在我们new Vue(options)的时候传入组件配置项对象,组件内部进行一系列的初始化操作,并在相应的时机执行生命周期钩子函数。有了组件实例之后就可以对数据进行很好的管理。然后进入$mount阶段,首先执行template编译生成render函数生成vnode(这也是sveltevue最大的不同,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是组件实例,propsoptions中传入的propsinvalidate是更新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中储存的旧值,如果新旧值不同并且readytrue(初始化渲染完成)执行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函数生成抽象层vnodepatch成真实的dom,update的时候对前后vnode进行diff比较,更新有差异的dom节点。而svelte则是直接把模板编译成操作真实dom的语句,性能上省去了vnode的过程。

谢谢大家的观看,有问题忘指正。