Vue 3.0 (上)- 04

201 阅读4分钟

重编译,轻运行

思考1. 哪些是 vue2.x 的性能提升点?

  • diff 如果你想到了 diff,那怎么更快呢?

diff 放在 worker 里执行,不会阻塞当前

  • proxy 当让比 Object.defineProperty 好,为什么?

vue2 总是挂载在实例this 上,项目大的话都使用Object.defineProperty 开销大;proxy 返回对象,挂载在this 上面就会少,内存上会有提升。

  • SSR 更多的利用静态字符串拼接

vue2 项目复杂度高的话,服务端压力大 ...

思考2. 纯静态

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

纯静态的问题:

不论页面渲染多少次,都不会边,但是每一次重新渲染都会走一遍 diff,只是不会patch,开销diff

思考3. 动静结合

<div>
  <h1>hello world!</h1>
  <p>{{ text }}</p>
</div>

如果都把静态的部分提取出来,只处理动态部分(节点)Vue3 标记静态节点

业务中绝大部分情况都是动静结合的,所以在处理这种类型的页面上做优化,可以获取到最大的收益

思考4. 分治思想

分而治之,将大的,复杂的问题拆解成若干类似的小问题

思考5. 分!

  1. 先怎么分?是不是把静态的和动态的要先分开?怎么体现?

分开动静,某个动态无法继续的时候还可以退回,走全diff; 各自声明两个数据结构存储

  1. 存在哪儿?

还是存在寻你 dom 中,多一个dynamicChildren

  1. 还可以继续分吗?怎么甄别呢?

可以通过字符串匹配得知动态,静态 - {{}} : 之类 通过 patchFlag 给每个节点做标记,在虚拟 dom 中挂一个标记,判断什么具体什么类型

image.png

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 */ },
  ]
}

思考7. 导致问题的原因找到了吗?如何解决?

dynamicChildren可能会和原始的有冲突

p 节点相同,但是外层一个是div 另一个是 section;正常 diff 先走 div,然后再section 完全不同的节点,直接把 div 干掉重新添加;但是现在不会再重新渲染

// 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>

思考7. 导致问题的原因找到了吗?如何解决?

原因只是把动态的节点提出来,但是没有考虑外层结构

// 尽量保持一样的结构
{
  tag: 'div',
  children: [
    { tag: 'h1', children: 'hello world!' },
    // ...
  ],
  dynamicChildren: [
    // v-if 也作为一个 Block
    // 现在,我们有了 Block Tree!
    { tag: 'div', dynamicChildren: []},
    { tag: 'section', dynamicChildren: []},
  ]
}

整个v-if 形成一个block,新增了block tree, block - 新的数据结构,额外挂了几个属性

思考8. 并不能彻底解决可能的边界情况,怎么兜底?

<p v-for='i in 100'>{{ i }}</p> // i 只可能是静态的
<p v-for='i in list'>{{ i }}</p> // i 是动态的,也可能是动态的

分治的好处,随时可以退一步海阔天空; i in list 这种情况,就会使用传统的 diff

思考9. 新的数据结构一定对应着新的渲染函数

// 感受一下

// openBlock
// disableTracking ????
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)
}

openBlock 和 createBlock 成对出现

现在可以这么写渲染函数了

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

const render = ()
  => (openBlock(), createBlock('div', null, [createVNode('h1', null, 'hello world!')]))

思考10. 上面的渲染函数可以优化吗?

// hoists
let __h1__ = createVNode('h1', null, 'hello world!')

const render = ()
  => (openBlock(), createBlock('div', null, [__h1__]))

将函数的结果提在外面,返回就是一个结果,相当于做了一个缓存

思考11. 节点提升的有效范围?

可以理解为,思考哪些是不可以被提升的

  • 动态 props
  • ref
  • 自定义指令
  • ...

思考12. 节点不能提升了,我们就眼巴巴望着?

节点虽然是动态的,有些地方可以提升,比如属性

<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)
]))

最大程度提升静态内容

思考13. 还可以继续?

// 预先字符串化!!!
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 个满足条件的节点,才能被预先字符串化

思考14. 还有啥可以优化的?

还知道 React 的一个坑点吗?

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

function App() {

  return (
    <>
      <A onClick={() => handleSomeClick()} />
    </>
  )
}

优化:缓存事件

const handler = () => handleSomeClick()
(openBlock(), createBlock(A, { onClick: handler }))

// v-once

此外还有很多优化和值得思考的地方,比如代码层面

编译原理

编译流程:词法&语法分析 -> Parsing -> Transformation 词法分析(Tokenlize) - 打成一个个的字符串

For the following syntax:
 *
 *   (add 2 (subtract 4 2))
 *
 * Tokens might look something like this:
 *
 *   [
 *     { type: 'paren',  value: '('        },
 *     { type: 'name',   value: 'add'      },
 *     { type: 'number', value: '2'        },
 *     { type: 'paren',  value: '('        },
 *     { type: 'name',   value: 'subtract' },
 *     { type: 'number', value: '4'        },
 *     { type: 'number', value: '2'        },
 *     { type: 'paren',  value: ')'        },
 *     { type: 'paren',  value: ')'        },
 *   ]
 *

Parsing - 将token 合成 AST

 * And an Abstract Syntax Tree (AST) might look like this:
 *
 *   {
 *     type: 'Program',
 *     body: [{
 *       type: 'CallExpression',
 *       name: 'add',
 *       params: [{
 *         type: 'NumberLiteral',
 *         value: '2',
 *       }, {
 *         type: 'CallExpression',
 *         name: 'subtract',
 *         params: [{
 *           type: 'NumberLiteral',
 *           value: '4',
 *         }, {
 *           type: 'NumberLiteral',
 *           value: '2',
 *         }]
 *       }]
 *     }]
 *   }

Transformation - 增删改 AST 对象,变成新的 AST

编译器:

function compiler(input) {
  let tokens = tokenizer(input);
  let ast    = parser(tokens);
  let newAst = transformer(ast);
  let output = codeGenerator(newAst);

  // and simply return the output!
  return output;
}

[super-tiny-compiler]github.com/jamiebuilds…