我理解的Vue3优点

634 阅读11分钟

Vue3产生原因

更快

1. 响应式系统的改进

  • 创建实例时采用Proxy对于一个大规模数据的监听减少性能开销,虽然对于深层级对象,仍然需要递归实现,但是它是在proxy的getter操作中赋予响应式。意味着只有访问到这个层级的属性才会建立响应;而不是像Vue2一样直接给整个对象都加上响应,因为需要预先知道要拦截的key是什么,所以并不能检测对象属性的添加和删除。
// 创建响应式对象
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 当访问属性时,进行依赖收集
      track(target, key);
      // 使用 Reflect.get 确保操作的一致性
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      // 当设置属性时,触发更新
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return result;
    }
  });
}
关键点
  • 延迟响应式连接:Vue 3 的 Proxy 实现只在实际访问属性时建立响应式连接,而不是在初始化时对整个对象进行处理。这减少了不必要的性能开销。
  • 细粒度的依赖收集:通过 Proxy,Vue 3 能够更细粒度地追踪依赖,只对访问过的属性进行响应式处理。
  • 动态属性支持:与 Object.defineProperty 不同,Proxy 能够拦截对象属性的添加和删除操作,使得 Vue 3 的响应式系统更加灵活。

2. 虚拟 DOM 的优化

Vue 3 对虚拟 DOM 算法进行了重构,引入了静态树提升和动态块的概念。这意味着 Vue 3 可以更高效地处理静态和动态内容,减少不必要的渲染操作。

  • 虚拟DOM算法重构,模版分析对静态节点生成优化提示,编译时跳过不需要编译部分;
<template>
  <div>
    <h1>这是一个静态标题</h1>
    <p>{{ dynamicContent }}</p>
    <button @click="handleClick">点击我</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      dynamicContent: '这是动态内容'
    };
  },
  methods: {
    handleClick() {
      this.dynamicContent = '内容已更新';
    }
  }
};
</script>

编译后的渲染函数

import { createVNode, render, toDisplayString } from 'vue';

export default {
  setup() {
    const dynamicContent = ref('这是动态内容');

    // 静态节点被提升到渲染函数外部,并标记为 `-1`(`HOISTED`)。这意味着这些节点在首次渲染时会被创建一次,并在后续的更新中直接复用,不会参与后续的 `diff` 操作。
    const staticH1 = createVNode('h1', null, '这是一个静态标题', -1 /* HOISTED */);
    // 被缓存到 `staticButton` 的 `onClick` 属性中。这样在后续的渲染中,事件处理函数不会被重新创建,而是直接复用。
    const staticButton = createVNode('button', {
      onClick: handleClick
    }, '点击我', -1 /* HOISTED */);

    return () => {
      // 动态内容的 VNode 被包装在一个 `VNode` 中,并标记为 `1`(`TEXT_CHILDREN`)。这意味着这个节点是动态的,需要在每次更新时重新渲染。
      const dynamicP = createVNode('p', null, toDisplayString(dynamicContent.value), 1 /* TEXT_CHILDREN */);

      // 创建根节点的 VNode
      const vnode = createVNode('div', null, [staticH1, dynamicP, staticButton]);

      return vnode;
    };
  }
};
关键点
  • 渲染函数的优化
    • 静态节点被提升到渲染函数外部,只在组件首次渲染时创建一次。
    • 动态节点在每次组件更新时重新生成,但仅限于动态部分。
问题:setup函数只会初始化调用?组件更新时只会调用setup函数返回的函数?
setup 函数的执行机制
  • setup 函数只在组件初始化时调用一次

    • setup 是 Vue 3 中组合式 API 的入口函数,它只会在组件初始化时被调用一次。它的主要作用是初始化组件的状态、方法和响应式逻辑。
    • setup 函数中定义的响应式状态(如 refreactive)和方法(如 computedwatch)会被返回,并在组件的渲染函数中使用。
  • 组件更新时,会调用 setup 函数返回的渲染函数

    • setup 函数返回的是一个渲染函数(或一个对象,其中包含 render 函数)。这个渲染函数会在组件的响应式状态发生变化时被调用,用于重新渲染组件。
    • 渲染函数负责生成虚拟 DOM(VNode),并将其传递给 Vue 的渲染器,渲染器会根据新的 VNode 更新实际的 DOM。

3. 细粒度更新

  • 在 Vue 3 中,PatchFlags 是一个关键的优化机制,它通过标记虚拟 DOM 节点的动态特性,使得渲染器在更新时能够更高效地处理变化。这种机制将虚拟 DOM 的更新性能与动态内容的数量相关联,而不是模板的整体大小。

  • 将vdom更新性能由与模板整体大小相关提升为与动态内容的数量相关,这个区块的大小,甚至可以小到class的名字;新增PatchFlag标记,只比较有PatchFlag节点,PatchFlag有枚举值(TEXT/动态class/动态style/动态属性);

PatchFlags 的实现原理
  1. PatchFlags 是一个枚举类型
export enum PatchFlags {
  TEXT = 1,         // 动态文本节点
  CLASS = 1 << 1,   // 动态类名
  STYLE = 1 << 2,   // 动态样式
  PROPS = 1 << 3,   // 动态属性
  // 其他标志...
}
  1. 编译阶段的优化 在编译阶段,Vue 3 会分析模板中的动态和静态内容,并为动态节点生成相应的 PatchFlags。编译后,动态节点会被标记为 PatchFlags.TEXTPatchFlags.CLASS,而静态节点则会被提升到渲染函数外部。
<template>
  <div>
    <p>{{ msg }}</p> <!-- 动态文本 -->
    <span :class="classObj"></span> <!-- 动态类名 -->
  </div>
</template>
  1. 运行时的优化 在运行时,Vue 3 的渲染器会根据 PatchFlags 来决定如何更新节点。例如,当检测到 PatchFlags.TEXT 时,渲染器会直接更新文本内容,而不会重新渲染整个节点。这种细粒度的更新机制显著减少了不必要的 DOM 操作。
const patchElement = (n1, n2) => {
  const el = (n2.el = n1.el);
  const { patchFlag } = n2;

  if (patchFlag & PatchFlags.TEXT) {
    if (n1.children !== n2.children) {
      hostSetElementText(el, n2.children);
    }
  }

  if (patchFlag & PatchFlags.CLASS) {
    if (n1.props.class !== n2.props.class) {
      hostPatchProp(el, 'class', null, n2.props.class);
    }
  }

  // 其他动态特性的处理...
};
  • render阶段对不参与更新的vnode做静态提升,只创建一次,后面数据变化reRender直接复用;同理,事件监听也缓存;
1. 静态节点提升(Static Node Hoisting)

将模板中的静态部分(不会改变的部分)提前提升到渲染函数外部。这样在组件更新时,这些静态节点可以直接复用,而无需重新创建。

<template>
  <div>
    <span>静态文本</span>
    <p>{{ dynamicContent }}</p>
  </div>
</template>

编译后,静态节点会被提升到渲染函数外部:

const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "静态文本", -1 /* HOISTED */); // 静态节点,被提升到渲染函数外部,只在首次渲染时创建一次。-   静态节点的 `PatchFlag` 被标记为 `-1`,表示该节点是静态的,不会参与后续的 `diff` 操作。

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1,
    _createVNode("p", null, _toDisplayString(_ctx.dynamicContent), 1 /* TEXT */)
  ]));
}
2. 事件监听缓存(Event Listener Caching)

通过缓存事件处理函数,避免在每次渲染时重新创建事件监听器。

<template>
  <button @click="onClick">点击我</button>
</template>

编译后,事件处理函数会被缓存:

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("button", {
    // `_cache[1]` 是缓存的事件处理函数,首次渲染时会被创建,并在后续渲染时复用。
    onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
  }, "点击我"));
}
  • vue3把所有的slot统一编译一个函数,而调用这个函数,是子组件的行为,只需要重新渲染子组件,而不必再去渲染父组件了;这个函数在子组件中被调用时,会根据父组件传递的插槽内容生成对应的虚拟 DOM。
插槽函数在子组件中的工作原理

关键点:

  1. 插槽内容的编译

    • 在编译阶段,Vue 3 会将父组件中定义的插槽内容编译为一个函数。这个函数在运行时会被子组件调用,以生成对应的虚拟 DOM。
  2. 插槽函数的传递

    • 父组件将编译后的插槽函数传递给子组件。这些函数被存储在子组件的 slots 对象中。
  3. 子组件中的插槽渲染

    • 子组件在渲染时,会调用这些插槽函数来生成插槽内容的虚拟 DOM,并将其插入到子组件的渲染树中。
  4. 性能优化

    • 由于插槽内容的生成是通过函数动态完成的,只有当插槽内容发生变化时,子组件才会重新渲染插槽部分,而不会影响整个父组件的渲染。
// 父组件
<template>
  <ChildComponent>
    <template #header>
      <h1>这是头部</h1>
    </template>
    <p>这是默认插槽的内容</p>
    <template #footer>
      <footer>这是尾部</footer>
    </template>
  </ChildComponent>
</template>

// 子组件
<template>
  <div>
    <slot name="header"></slot>
    <div>子组件的内容</div>
    <slot></slot>
    <slot name="footer"></slot>
  </div>
</template>

编译后的插槽函数

// 编译后的父组件渲染函数
import { createVNode, render } from 'vue';

export default {
  setup() {
    return () => {
      const headerSlot = () => createVNode('h1', null, '这是头部');
      const defaultSlot = () => createVNode('p', null, '这是默认插槽的内容');
      const footerSlot = () => createVNode('footer', null, '这是尾部');

      return createVNode(ChildComponent, null, {
        header: headerSlot,
        default: defaultSlot,
        footer: footerSlot
      });
    };
  }
};

// 编译后的子组件渲染函数
export default {
  setup() {
    return () => {
      const slots = {
        header: this.$slots.header,
        default: this.$slots.default,
        footer: this.$slots.footer
      };

      return createVNode('div', null, [
        slots.header ? slots.header() : null,
        createVNode('div', null, '子组件的内容'),
        slots.default ? slots.default() : null,
        slots.footer ? slots.footer() : null
      ]);
    };
  }
};
插槽内容变化时的更新流程
  1. 响应式数据变化

    • 在父组件中,headerContentdefaultContentfooterContent 是响应式数据。当这些数据发生变化时,Vue 的响应式系统会触发更新。
  2. 父组件的渲染函数重新执行

    • 父组件的渲染函数会重新执行,生成新的虚拟 DOM。
    • 在渲染函数中,插槽内容被编译为函数(如 headerSlotdefaultSlotfooterSlot),这些函数会根据最新的响应式数据生成新的虚拟 DOM 节点。
  3. 子组件的插槽函数重新调用

    • 子组件接收到新的插槽函数后,会调用这些函数来生成新的插槽内容的虚拟 DOM。
    • 子组件的渲染函数会将新的插槽内容插入到子组件的渲染树中。
  4. 渲染器的 diff 算法

    • Vue 的渲染器会比较新旧虚拟 DOM 的差异,并只更新发生变化的部分。
    • 例如,如果只有 headerContent 发生了变化,渲染器只会更新 <h1> 标签的内容,而不会影响其他部分。

更小

1. 按需引入

  • Vue3.0中,采取了ES module imports按需引入的方式。如果我们没有用到一些内置组件时,在编译时就不会去加载这些内容。而之前Vue的整个代码都是直接将整个Vue对象放进来,所以一些没用到的东西,也无法通过tree-shaking将其扔掉。
ES Module 按需引入的核心机制
  1. ES Module 的静态结构

    • ES Module 的 importexport 语法是静态的,这意味着它们在代码中是固定的,不能动态改变。这种静态结构使得工具(如 Webpack 和 Vite)能够在编译时分析代码,找出未被使用的模块。
  2. Tree-Shaking

    • Tree-shaking 是一种代码优化技术,它通过静态分析移除未使用的代码。Vue 3 的模块化设计使得 Tree-shaking 更加有效。当使用 ES Module 时,未被引用的模块不会被打包,从而优化了最终产物的大小。

    • 通过以下步骤实现 Tree-shaking:

      • 静态分析:在编译阶段,构建工具会分析项目中的所有模块及其依赖关系,构建一个依赖图(Dependency Graph),明确每个模块的输入和输出。
      • 死代码消除:通过静态分析,识别出哪些模块或变量未被使用或引用,然后将这些未使用的模块或变量从最终的输出文件中移除。
    • 为了确保 Tree-shaking 的效果,开发者需要避免在模块中使用副作用(如全局变量、定时器等)。副作用可能会影响 Tree-shaking 的效果,因为构建工具可能无法确定这些代码是否可以安全移除。为了避免副作用影响 Tree-shaking,可以采取以下措施:

      1. 避免使用全局变量,尽量使用模块作用域变量。
      2. 将代码封装在函数中,避免在模块加载时执行。
      3. 避免使用 import 语句执行副作用。
      // 避免
      // side-effect.js
      console.log('副作用代码');
      // main.js
      import './side-effect.js';
      
      // 推荐
      // side-effect.js
      export function log() {
        console.log('副作用代码');
      }
      // main.js
      import { log } from './side-effect.js';
      log();
      
      1. 避免使用 evalnew Function。使用函数封装动态代码
      // 使用函数封装动态代码
      function dynamicLog() {
        console.log('动态代码');
      }
      dynamicLog();
      
      1. 避免在模块中使用定时器。将定时器封装在函数中
      2. 避免修改全局状态。尽量将状态封装在模块内部或通过 Vue 的响应式系统管理。
      3. 使用 sideEffects 字段显式声明模块是否包含副作用。
      {
        "name": "my-library",
        "sideEffects": ["./src/side-effect.js"]
      }
      
  3. 动态导入

    • Vue 3 支持动态导入(import()),这允许开发者在运行时按需加载模块。这种方式不仅减少了初始加载时间,还提升了用户体验。

2. 移除冷门特性

  • 移除冷门feature,如filter和inline-template

更易于维护

1. TypeScript 支持

  • 采用TS开发,在编码期间做类型检查,也可定义接口类型,利于IDE对变量类型推导;Flow对复杂场景类型检查支持不好,如组件更新props时,没有正确推导出vm.$options.props;

2. 单元化开发

  • 更好的代码管理方式monorepo,根据功能将不同模块拆分到packages目录下不同子目录,每个package有各自API、类型定义和测试,拆封更细化,职责和依赖关系更明确
  • 一些package,如reactivity响应式库可独立于Vue使用,减少引用包体积,而Vue2是做不到这一点的

3. 组合式 API

  • 组合式API在复用、类型推导更友好;
import { ref, computed } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const doubleCount = computed(() => count.value * 2);

    function increment() {
      count.value++;
    }

    return { count, doubleCount, increment };
  }
};
  • 编译器重构-插件化设计、带位置信息的parser(source maps)。Vue 3 的编译器通过 @vue/compiler-core@vue/compiler-dom 等模块实现了插件化设计。开发者可以通过自定义插件来扩展编译器的功能,例如添加新的指令、优化代码生成等。Vue 3 的编译器在解析模板时,会为每个节点生成位置信息(如行号、列号等)。这些信息被存储在抽象语法树(AST)中,可以在后续的代码生成和调试中使用。
如何通过插件扩展编译器的功能
import { CompilerOptions, extendCompilerOptions } from '@vue/compiler-core';

// 自定义插件
function myCustomPlugin(options: CompilerOptions) {
  // 扩展编译器选项
  extendCompilerOptions(options, {
    onNodeTransform: (node) => {
      // 自定义节点转换逻辑
      if (node.type === 'Element' && node.tag === 'div') {
        node.tag = 'span'; // 将 <div> 转换为 <span>
      }
    }
  });
}

// 使用插件
import { compile, parse, generate } from '@vue/compiler-core';

const template = `
  <div>
    <h1>Hello World</h1>
    <p>{{ message }}</p>
  </div>
`;

//编译模板:
const code = compile(template, { plugins: [myCustomPlugin] });
// 解析模板:将模板字符串解析为抽象语法树(AST)。
const ast = parse(template, {
  outputSourceRange: true // 启用位置信息
});

// 转换节点:对 AST 中的节点进行转换,例如处理指令、动态绑定等。
transform(ast, {
  nodeTransforms: [
    (node) => {
      if (node.type === 'Element' && node.tag === 'h1') {
        node.tag = 'span'; // 将 <h1> 转换为 <span>
      }
    }
  ]
});

// 生成代码:根据转换后的 AST 生成渲染函数代码。
const code = generate(ast);

console.log(code); // 输出编译后的代码
console.log(ast); // 输出 AST,包含位置信息

更好的多端渲染支持

  • 引入一个Custom Render API,更好的多端渲染支持;

1. Custom Renderer API 的使用

允许开发者定义自己的渲染逻辑。通过实现特定的接口方法,开发者可以控制如何创建元素、设置属性、挂载子节点等。

import { createApp, h } from 'vue';

// 定义一个简单的自定义渲染器
function customRenderer(vNode) {
  // 将 vNode 转化为文本描述
  const description = `Component: ${vNode.type.name}, Props: ${JSON.stringify(vNode.props)}`;
  console.log(description); // 打印文本描述
}

// 创建一个简单的 Vue 组件
const MyComponent = {
  name: 'MyComponent',
  props: {
    message: String
  },
  render() {
    return h('div', null, this.message);
  }
};

// 使用自定义渲染器创建 Vue 应用
const app = createApp({
  render() {
    return h(MyComponent, { message: 'Hello, Custom Renderer!' });
  }
});

// 运行应用,传入自定义渲染器
app.mount({ render: customRenderer });

多端渲染支持: Vue 组件渲染到 Canvas

使用了 Pixi.js 来创建一个 Canvas 渲染器。我们定义了 createElementpatchPropinsert 方法,这些方法控制了如何创建元素、设置属性和挂载子节点。

import { createRenderer } from 'vue';
import { Application, Graphics } from 'pixi.js';

// 创建一个自定义渲染器
const renderer = createRenderer({
  createElement(type) {
    let element;
    switch (type) {
      case 'rect':
        element = new Graphics();
        element.beginFill(0xff0000);
        element.drawRect(0, 0, 500, 500);
        element.endFill();
        break;
      case 'circle':
        element = new Graphics();
        element.beginFill(0xffff00);
        element.drawCircle(0, 0, 50);
        element.endFill();
        break;
    }
    return element;
  },
  patchProp(el, key, prevValue, nextValue) {
    switch (key) {
      case 'x':
        el.x = nextValue;
        break;
      case 'y':
        el.y = nextValue;
        break;
    }
  },
  insert(el, parent) {
    parent.addChild(el);
  }
});

// 创建一个 Vue 应用
export function createApp(rootComponent) {
  return renderer.createApp(rootComponent);
}

// 初始化 Canvas 容器
const game = new Application({ width: 750, height: 750 });
document.body.append(game.view);

// 使用自定义渲染器渲染 Vue 组件
import App from './App.vue';
createApp(App).mount(game.stage);

新功能

1. renderTriggered API

  • Vue 3 提供了 renderTriggered API,可以在浏览器中直接查看触发更新的具体代码位置。这使得开发者能够更直观地调试和优化性能问题。
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="updateMessage">更新消息</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello Vue 3!'
    };
  },
  methods: {
    updateMessage() {
      // 组件中,数据的变化会通过响应式系统通知渲染器进行更新。渲染器会生成一个新的虚拟 DOM,并通过 `diff` 算法比较新旧虚拟 DOM 的差异,然后更新真实 DOM。
      this.message = 'Message updated!';
    }
  },
  // 生命周期钩子,它在组件的渲染函数被触发时被调用。-   在开发模式下,Vue 会在触发渲染时收集相关信息(如触发原因、虚拟节点等),并通过 `renderTriggered` 钩子暴露给开发者。
  // 在生产模式下,Vue 3 的构建工具会自动移除这些调试代码,确保不会影响性能。因此,你不需要手动关闭 `renderTriggered` 钩子。
  renderTriggered(event) {
    console.log('renderTriggered:', event);
    console.log('更新触发的组件:', event.vnode);
    console.log('触发更新的原因:', event.reason);
  }
};
</script>

2. 跨组件状态共享

  • Vue 3 通过 observableeffect 实现了跨组件的状态共享。这种机制使得状态管理更加灵活,适用于复杂的应用场景。
// 祖先组件
import { ref, provide } from 'vue';

export default {
  setup() {
    const sharedState = ref('Hello from App Component');
    provide('sharedState', sharedState);
    return {};
  }
};

// 后代组件
import { inject } from 'vue';

export default {
  setup() {
    const sharedState = inject('sharedState');
    return { sharedState };
  }
};

问题

尽管 Vue 3 带来了诸多改进,但它也面临一些挑战。例如,Vue 3 不再支持 IE9 等较旧的浏览器。