# vue编译原理

1、@vue/compiler-core解析

2、前沿发展方向:fizz流式渲染、svelte编译框架

3、编译原理入门:the super tiny compiler (opens new window)

4、正则表达式可视化工具:可视化工具 (opens new window)

5、 vue3模版转译工具:vue3 template explorer (opens new window)

# 1、思考点

  1. 哪些是vue2.x的性能提升点?
  • diff:diff优化如何更快,算法已很难优化
    • 将diff算法拆出,放在worker执行,另起线程(preact)
    • 重编译,轻运行时,参考svelte框架,编译优化处理动静结合页面可获取最大收益
  • proxy:使用proxy比object.defineProperty好
    • API功能优势可代理数组和对象
    • 代理返回对象,使用函数式编程,挂载至this的属性减少,优化性能
  • ssr更多的利用静态字符串拼接减少动态处理,来优化性能

# 2、编译原理

  • 常见场景有两种:纯静态页面和动静结合页面,vue2.0全部需走diff。
  • vue3的优化地方:业务中绝大部分为动静结合的页面,对该类型页面进行编译优化处理可以获取最大的收益。

总体思想:分治思想-分而治之,将大的,复杂的问题拆解成若干类似的小问题

<!-- 纯静态页面 -->
<div>
  <h1>hello world!</h1>
</div>
<!-- 动静结合页面 -->
<div>
  <h1>hello world!</h1>
  <p>{{text}}</p>
</div>
1
2
3
4
5
6
7
8
9

# 1、动静分离处理

  • 使用patchFlag进行Vnode标记,记录具体动态属性,实现靶向更新。
  • 添加dynamicChildren数组,提取动态节点存储,使用block tree进行优化
export const enum PatchFlags {
  TEXT = 1,// 动态的文本节点
  CLASS = 1 << 1,  // 2 动态的 class
  STYLE = 1 << 2,  // 4 动态的 style
  PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
  FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
  HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
  KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 512
  DYNAMIC_SLOTS = 1 << 10,  // 动态 solt
  HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
  BAIL = -2 // 一个特殊的标志,指代差异算法
}

// like fiber!
// Block - Block Tree
{
  tag: 'div',
  children: [
    { tag: 'h1', children: 'hello world!' },
    { tag: 'p', children: ctx.text, patchFlag: 1 /* TEXT */ },
  ],
  dynamicChildren: [
    // 所有子元素中动态的部分哦
    { tag: 'p', children: ctx.text, patchFlag: 1 /* TEXT */ },
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

问题1:若只提取动态节点,与原dom节点不对等,diff存在问题,例如v-if、v-for

解决方法:使用block tree解决,尽量保持一样的结构

// demo1
<div>
  <h1>hello world!</h1>
  <div v-if="someCondition">
    <p>{{ text }}</p>
  </div>
  <section v-else>
    <p>{{ text }}</p>
  </section>
</div>

// demo2
<div>
  <p v-for="i in list">{{ i }}</p>
</div>

//尽量保持一样的结构,使用block tree解决
{
  tag: 'div',
  children: [
    { tag: 'h1', children: 'hello world!' },
    // ...
  ],
  dynamicChildren: [
    // v-if 也作为一个 Block
    // 现在,我们有了 Block Tree!
    { tag: 'div', dynamicChildren: []},
    { tag: 'section', dynamicChildren: []},
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

问题2:并不能彻底解决可能的边界情况,采用兜底方案,使用原有diff算法。

资料:vue3渲染playground (opens new window)

// 新的数据结构block tree,对应新的渲染函数
// disableTracking用于判断是否回退diff
function openBlock(disableTracking = false) {
  blockStack.push((currentBlock = disableTracking ? null : []));
}

function createBlock(type, props, children, patchFlag, dynamicProps) {
  const vnode = createVNode(type, props, children, patchFlag, dynamicProps, true);
  // 构造 `Block Tree`
  vnode.dynamicChildren = currentBlock || EMPTY_ARR;
  closeBlock();
  // 注意是需要条件的
  if (someCondition === true) {
      currentBlock.push(vnode);
  }
  return vnode;
}

// 不是所有的都可以是 Block 的
if (
  isBlockTreeEnabled > 0 &&
  // avoid a block node from tracking itself
  !isBlockNode &&
  // has current parent block
  currentBlock &&
  // presence of a patch flag indicates this node needs patching on updates.
  // component nodes also should always be patched, because even if the
  // component doesn't need to update, it needs to persist the instance on to
  // the next vnode so that it can be properly unmounted later.
  (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
  // the EVENTS flag is only for hydration and if it is the only flag, the
  // vnode should not be considered dynamic due to handler caching.
  patchFlag !== PatchFlags.HYDRATE_EVENTS
) {
  currentBlock.push(vnode)
}

<div>
  <h1>hello world!</h1>
</div>

const render = ()
  => (openBlock(), createBlock('div', null, [createVNode('h1', null, 'hello world!')]))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 2、缓存提升

  • 动态节点提升

    哪些不可被提升:动态props、ref、自定义指令等

// hoists,虚拟dom缓存提升,非动态,只会执行一次
let __h1__ = createVNode('h1', null, 'hello world!')
const render = ()
  => (openBlock(), createBlock('div', null, [__h1__]))
1
2
3
4
  • 动态节点属性提升

    提升局部属性,动态节点中不变的数据进行属性提升

<div>
  <h1 class='foo'>{{ text }}</h1>
</div>

// props hoists
let __cls__ = { class: 'foo' }
const render = () => (openBlock(), createBlock('div', null, [
  createVNode('p', __cls__, ctx.text, PatchFlags.TEXT)
]))
1
2
3
4
5
6
7
8
9
  • 预先字符串化

    将多个节点进行字符串拼接,统一append至节点中,减少dom操作

// 预先字符串化!!! 
export function createStaticVNode(
  content: string,
  numberOfNodes: number
): VNode {
  // A static vnode can contain multiple stringified elements, and the number
  // of elements is necessary for hydration.
  const vnode = createVNode(Static, null, content)
  vnode.staticCount = numberOfNodes
  return vnode
}

// 当然是满足一定量的情况,比如超过 20 个满足条件的节点,才能被预先字符串化
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 事件提升

    将模版中的事件统一提升至变量中,避免每次编译时绑定事件都为新事件需要重新渲染

import React from 'react'
import A from './components/A'

function App() {
  return (//每次编译都需render,每次onclick都为新函数,可对事件进行缓存避免重复渲染
      <A onClick={() => handleSomeClick()} />
  )
}

const handler = () => handleSomeClick()
(openBlock(), createBlock(A, { onClick: handler }))
1
2
3
4
5
6
7
8
9
10
11
  • 代码库拆分

    原编译库拆分成多个 compile-* 的文件夹,用于整体模块解耦合,例如react拆分为react和react-dom

DETAILS

面试题:vue3是如何变的更快?(底层、源码)

  • diff方法优化:动静分离处理,使用patchFlag进行动态属性标记,实现靶向更新

    • vue2.0中虚拟dom是进行全量的对比
    • vue3.0中新增了静态标记(patchFlag),再与上次的虚拟节点进行对比时,只对比带有patchFlag的节点,且在具体节点对比时可根据patchFlag的值得知当前节点要对比的具体内容,实现靶向更新
  • hositStatic静态提升

    • vue2.0无论元素是否参与更新,每次都会重新创建
    • Vue3.0对于不参与更新的元素,只会被创建一次,之后会在每次渲染时进行复用
  • cacheHandlers事件监听器缓存:默认情况下onclick等事件会视为动态绑定,所以每次都会追逐它的变化,但是因为是同一个函数,直接进行缓存复用,无需追踪变化。

Last Updated: 8/21/2021, 10:45:45 PM