Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

svelte 源码阅读 #15

Open
Jarweb opened this issue Jun 17, 2020 · 0 comments
Open

svelte 源码阅读 #15

Jarweb opened this issue Jun 17, 2020 · 0 comments

Comments

@Jarweb
Copy link
Owner

Jarweb commented Jun 17, 2020

分析流程

// 项目代码
// app.svelte
<script>
	import { onMount } from 'svelte'
	import Demo from './Demo.svelte'
	export let name;
	let count = 0

	const handle = () => {
		count += 1
	}
  
  // 可以写多个
  onMount(() => console.log('mount'))
	onMount(() => console.log('mount'))
</script>
<style>
	h1 {
		color: purple;
	}
</style>

<h1>Helloza {name}! {count}</h1>
<button on:click={handle}>click</button>
<Demo msg={count} />

// demo.svelte
<script>
  // export 表示,接收外表的 props
	export let msg;
</script>

<h1>Hello {msg}!</h1>



// 编译阶段
// code is input,svelt-loader 进行对 svelte 文件编译
// 每个 svelte 文件,执行一次这个方法
// source 是文件的源码字符串
preprocess(source, ...).then(processed => {
  // ...
  compile(processed.toString(), compileoptions)
})

// 主要作用是根据 option 对 source 做预处理
async function preprocess(source, preprocessor, options) {
  // 如果有 preprocessor, 根据 preprocessor 对 source 进行处理
  // 返回一个对象
  return {
    code: source,
    dependencies: ...,
    toString() {
      return source
    }
  }
}

function compile(source, options = {}) {
  // 构建一个 stat 实例,主要是保存编译速度之类
  // 对 source 进行 parse 为 ast
  const ast = parse$2(source, options)
  // 进行组件实例化,组件类里面处理了大量逻辑
  component = new Component(ast, source, ...
  // 对组件进行浏览器端代码生成,区分 ssr/dom
  // 通过 dom 函数处理,得到的是一个 function create_fragment(ctx){...} 函数的声明字符串
  // 也就是说 js 是一段字符串,最后会将这段字符串与项目源码一起输出到 thunk 里
  js = ssr ? ssr(component, options) : dom(component, options)
  // 返回一个对象
  res = component.generate(js)
  return res
}

function parse$2(template, options = {}) {
  // parser 实例化
  const parser = new Parser$2(template, options)
  // parser 对象结构为:
  {
    stack:[
      {start, end, type, children}
    ],
    css: [
      {start,end,attributes,children, content}
    ],
    js: [
      {start, end, context, content}
    ],
    html: {start, end, type, children}
    meta_tags,
    template,
    filename,
    customeElement,
  }
  // 返回一个对象
  return {
    html: parser.html,
    css: parser.css[0],
    instance: js context is default [0],
    module: js context is module [0]
  }
}

class Parser$2 {
  // 对模版进行 ast 编译,分离 css/js/html 等。
}

// 很复杂
class Component {}

// 生成运行时代码片段
function dom(component, options) {
  // 构造一个 renderer 实例
  const renderer = new Renderer(component, options)
  // 构造一个 builder 实例,之后完这个 builder 里添加各种代码片段
  const builder = new CodeBuilder() 
  // 各种处理,往 builder 里插入代码字符串片段
  ...
  // 返回这个 builder
  return builder.toString()
}

//////////// dom 生成的函数片段 start /////////
// 根据实际情况,生产的片段可能不一样
function create_fragment(ctx) {
  var h1, t0, t1, t2, t3, current;

  var demo = new Demo({ props: { msg: "aa" } });

  return {
    c() {
      h1 = element("h1");
      t0 = text("Helloza ");
      t1 = text([✂129 - 133]);
      t2 = text("!");
      t3 = space();
      demo.$$.fragment.c();
      attr(h1, "class", "svelte-i7qo5m");
      dispose = listen(button, "click", [✂348-354])
    },

    m(target, anchor) {
      insert(target, h1, anchor);
      append(h1, t0);
      append(h1, t1);
      append(h1, t2);
      insert(target, t3, anchor);
      mount_component(demo, target, anchor);
      current = true;
    },

    p(changed, ctx) {
      if (!current || changed.name) {
        set_data(t1, [✂129 - 133]);
      }
    },

    i(local) {
      if (current) return;
      transition_in(demo.$$.fragment, local);

      current = true;
    },

    o(local) {
      transition_out(demo.$$.fragment, local);
      current = false;
    },

    d(detaching) {
      if (detaching) {
        detach(h1);
        detach(t3);
      }

      destroy_component(demo, detaching);
    }
  };
}

function instance($$self, $$props, $$invalidate) {
  [✂10 - 60] // 是一个占位符???

  $$self.$set = $$props => {
    if ('name' in $$props) $$invalidate('name', name = $$props.name);
  };

  return { name };
}

class App extends SvelteComponent {
  constructor(options) {
    super();
    // 执行 init 方法
    init(this, options, instance, create_fragment, safe_not_equal, ["name"]);
  }
}
//////////// dom 生成的函数片段 end /////////


res = component.generate(js)
// 的到的对象结构是:
{
  js: {
    code // 这里包含着我们自己也的代码,以及 dom 函数生成的运行时代码,就是上面的代码片段。最后这些代码经过 babal 编译后,输出到文件 zhunk 中
    map // soucemap 实例
  }
  css:{
    code // css 代码
    map // soucemap 实例
  }
  ast, // parse 生成的 ast 对象
  warnings: [],
  // 代码里所有的声明
  // eg. in demo.svelte file
  // export let name
  // <div>{name}</div>
  vars: [ 
    {
      name: 'name',
      injected: false,
      module: false,
      mutated: false,
      reassigned: false,
      referenced: true,
      writable: false
    }
  ]
  stat // 编译的统计
}



// 运行时流程
// 入口是
const app = new App({
  target: document.body,
  props: {name: 'hhha'}
})

// 编译后的 App 组件
new App extends SvelteComponent {
  constructor(options) {
    super();
    // 执行 init 方法
    init(this, options, instance, create_fragment, safe_not_equal, ["name"]);
  }
}

// svelte 没有虚拟 dom,首次渲染时,从父一直往子插入到 html 文档中。更新阶段,直接更新 dom
function init(component, options, instance, create_fragment, not_equal$$1, prop_names) {
  // 保存当前组件的实例
  // 初始化 component.$$ 属性,往这个属性上挂载一些属性:
  const $$ = component.$$ = {
    fragment,
    ctx,
    props,
    update,
    bound,
    on_mount: [],
    on_destroy: [],
    before_render: [],
    after_render: [],
    context,
    callbacks,
    dirty
  }
	
	// 执行 instance 函数,得到当前组件需要的 props, 挂到 ctx 上。这里需要注意 cb 参数,这个函数会将所有的反应数据进行包装。更新数据操作会执行这个 cb 函数。
	$$.ctx = instance(component,prosp,cb) || props
  // 执行 所有的 $$.before_render 数组里存的函数,首次 $$.before_render 数组 为空
  // 执行 create_fragment 函数,拿到一个对象,可以看上面的例子
  $$.fragment = create_fragment($$.ctx)
	// 执行 $$.fragment.c(),该函数会创建当前组件的所有 dom 元素,挂载 html 属性到 dom 上,绑定组件的事件。同时执行子组件的 $$.fragment.c() 函数。子组件的实例有了吗?子组件的实例化在父组件执行 create_fragment 函数的时候,就进行实例创建了。这里其实是从父到子,一直执行各自的 create_fragment 函数。生成各自的 dom 和属性挂载。但还没有插入到 html 文档上。
  // 执行顺序是:父创建dom -》子创建dom ... -〉挂载子属性 -》子事件绑定到 dom -》挂载父属性 -〉 父事件绑定到 dom
  // 执行 mount_component 函数, 该函数会从父一直递归到子组件,把前面生成好的 dom + 属性 插入到父中。
	// 流程是:父插入到 root -》子插入到父 ... 一直到完成为止。这时候所有的 dom 都插入到html 文档了。首次渲染相当于结束
  mount_component(component, options.target, options.anchor)
	// 更新操作,首次渲染基本可以忽略这个函数
	flush()
}

// 看一下 init 函数的 instance 参数
// 编译后,有这个东西 [✂10-249✂],是原来代码的展位符??
function instance($$self, $$props, $$invalidate) {
  let {name} = $$props
  let count = 0
  // 可以看出,事件触发 state 更新时,会先执行 $$invalidate 函数, 该函数会执行 make_dirty 方法,从而触发更新
  const handle = () => {
    $$invalidate('count', count += 1)
  }
  setTimeout(() => {
    $$invalidate('count', count += 1)
  }, 3000)
  // 组件实例的 $set 方法, 某些方式的更新数据会用到这个方法。例如:调用子的 $set 方法,来更新子的 props
  $$self.$set = $$props => {
    if ('name' in $$props) $$invalidate('name', name = $$props.name)
  }
  return {name, count, handle}
}

// 首次渲染阶段,不断递归这个函数
function mount_component(component, target, anchor) {
  // 执行 $$.fragment.m 函数。该函数会将之前创建好的 dom 插入到 html 文档上,然后递归执行 mount_component 函数。从父到子一直插入到 html 文档上
  // 递归完后,执行 after_render 函数,该函数执行时,会执行该组件的 onMount 生命函数,svelte 的生命函数一个组件可以写多个。同时收集该组件的 onMount 函数返回值,等组件卸载时执行。可以用来卸载时清除副作用。
  // onMount 函数执行顺序: 子组件 -》 父组件
}

// 首次渲染生命函数:
 beforeUpdate -》子 beforeUpdate -》父 afterUpdate -  afterUpdate -  onMount -》子 onMount
// 更新阶段生命函数
 beforeUpdate -》子 beforeUpdate -》子 afterUpdate -》子 onDestroy -  afterUpdate -》父 onDestroy

// 更新阶段
// 每个反应式的属性都会被 $$invalidate 函数包裹:事件、ajax、异步 等
// $$invalidate 执行时,触发 make_dirty
function make_dirty(component, key) {
  // 缓存需要更新的组件实例 push in dirty_components
  // 执行 schedule_update
}

function schedule_update() {
  // 异步执行 flush,在本次事件循环后紧跟的微任务队列中执行
}

function flush() {
  // 如果有 dirty_components.length,循环拿到当前组件,循环执行 update 函数
  // 如果有 binding_callbacks ,执行 binding_callbacks
  // 如果有 render_callbacks,执行 render_callbacks
  // 如果有 flush_callbacks,执行 flush_callbacks
}

function update($$) {
  // 执行生命函数 before_render
  // 执行 $$.fragment.p 函数,更新所有的的 state
  $$.fragment.p($$.dirty, $$.ctx)
  // 收集 after_render
}

// 每次更新所有的反应式数据
// 同时更新子组件的 props 
p(changed, ctx) {
    if (!current || changed.name) {
      // 更新数据
       set_data(t1, [✂280-284]);
    }

    if (!current || changed.count) {
       set_data(t3, [✂288-293]);
    }

    if (!current || changed.hi) {
       set_data(t5, [✂296-298]);
    }

    var demo_changes = {};
  	// 更新子的 props
    if (changed.count) demo_changes.msg = [✂357-362];
    demo.$set(demo_changes);
  
 		// 当有显示隐藏某些片段时,会执行该片段的 p/c/m 函数
    if ([✂706-719]) {
      if (if_block) {
        // 执行片段的 p,进行数据更新或片段更新,这是一个递归操作。不断对片段里的片段或数据进行更新
        if_block.p(changed, ctx);
        transition_in(if_block, 1);
      } else {
        // create_if_block 这个函数很重要。当有 if/else 的模版逻辑时。都会编译生成这个函数代码。所以编译后的代码量会很大。这个函数的逻辑与 create_fragment 函数的逻辑相似,该很熟生成子组件的实例。然后返回一个包含操作函数的对象。{c,m,o,i,o,d}
        if_block = create_if_block(ctx);
        // 执行片段的 c,进行 dom 元素生成和属性挂载
        if_block.c();
        transition_in(if_block, 1);
        // 执行片段的 m,进行 dom 插入
        if_block.m(if_block_anchor.parentNode, if_block_anchor);
      }
    } else if (if_block) {
      group_outros();
      transition_out(if_block, 1, () => {
        if_block = null;
      });
      check_outros();
    }
}

// $set 其实也是调用 $$invalidate
$$self.$set = $$props => {
  if ('name' in $$props) $$invalidate('name', name = $$props.name);
};

总结

  • 没有 Virtual DOM 。享受不到 Virtual DOM 带来的诸多好处,比如基于 render function 的组件的强大抽象能力,基于 vdom 做测试,服务端/原生渲染亲和性等等。
  • 核心思想在于『通过静态编译减少框架运行时的代码量』
  • 首次渲染阶段,从父开始一层层插入当前的父中。父插入 root -》 子插入父
  • 更新阶段,收集需要更新的组件,更新该组件的数据。当组件节点变化时(删除/变更),该组件的父组件会对该子组件的 dom 进行卸载,然后重新生成新的 dom 并插入到当前父中。
  • 简单的 demo 生成的代码量非常小,当模版中大量判断渲染显示隐藏的逻辑时,生成的代码量很大。包含的运行时代码也越多。
  • 性能上也和 vanilla JS 相差无几。内存占用比 vdom 类框架少。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant