svelte组件渲染过程

时间:2021-1-8 作者:admin

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

vue组件渲染过程

借助vue-loader我们可以把一个vue文件编译成一个组件的配置项对象,其中的template会被编译成render函数。
然后在我们new Vue(options)的时候传入组件配置项对象,组件内部进行一系列的初始化操作,并在相应的时机执行生命周期钩子函数。有了组件实例之后就可以对数据进行很好的管理。然后就是$mount过程,会把相应的模板映射的dom转化成真实的dom,首先执行template编译生成render函数生成vnode(这也是sveltevue最大的不同,svelte直接编程成真实的dom操作,而vue则是通过vnode做了一层适应多平台的抽象,并在后续更新时进行diff优化),然后对vnode一层层patch成真实的dom,借用黄奕大佬的一张图

svelte组件渲染过程

svelte组件初始化渲染过程

首先我们看一下svelte文件会被编程成什么

  • component: 当前类的实例
  • options: 实例化组件的配置,详见https://svelte.dev/docs#Creating_a_component
  • instance:svelte文件中script标签中的js代码块编译成的instance函数,返回用于模板标签中的动态数据(为什么上图示例编译的instance是null,因为生产环境编译时如果js代码块中没有函数修改定义的变量会将这些函数和变量编译为全局的,省略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函数,生成作用于模板中的动态数据,挂载到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;
            })
            : [];
        ...
        //生成create,mount,update,destory操作dom的钩子对象
        $$.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代码块中编译生成的函数

写了个简单的例子,看下编译之后的结果

有了数据就可以创建dom元素。

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组件更新过程

但如果我们后续触发一个函数,改变动态数据的时候,会触发invalidate函数,并把$$.ctx数组中储存该数据的索引和新值传入
看下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转化成相应的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匹配对应需要更新的节点

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节点,类似于vue的render函数,只不过vue会生成抽象层vnodepatch成真实的dom,而svelte直接把模板编译成操作真实dom的语句。同时,也会把script中的代码块编译成instance函数,并把操作动态数据的地方用invalidate函数包裹,用于更新组件。

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

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。