Vue 的前世今生
- 2013 尤雨溪个人项目
- 2014.2 0.1 版本发布
- 2015.10 1.0 版本发布
- 2016.9 2.0 版本发布
- 2019.10 3.0 alpha 发布
- 性能
- 架构
- 按需引入
- Composition API
- Proxy observer
- AOT 优化
Vue 1 响应式原理
构建响应式对象流程
- walk 函数遍历 data 对象中的属性,调用 defineReactive 将其变成响应式对象
- 对于对象属性进行递归调用 walk,以保证 data 整个对象树中的属性都是响应式对象。
- defineReactive 中使用 watchers 数组储存 watcher,使用 Object.defineProperty 的 get 函数收集 watcher 和返回值,set 函数用来设置值和对 watchers 中的 watcher 进行视图更新。
Walk 函数实现
function walk(data) { Object.keys(data).foreach((key) => { defineReactive(data, key, data[key]); walk(data[key]); }); }
|
defineReactive 函数实现
function defineReactive(obj, key, value) { let oldValue = value; const watchers = []; Object.defineProperty(obj, key, { get() { watchers.push(currentWatcher); return oldValue; },
set() { if (newValue === oldValue) return; oldValue = newValue; watchers.forEach((watcher) => wathcer.update()); }, }); }
|
看了这么久 Watcher 到底是什么?

- Watcher 用于获取数据和更新视图,并实现 vue 指令
- watcher 从 data 中 get 数据 render 视图,同时 data 中的响应式对象劫持当前 watcher 并“储存”起来
- data 更新数据会触发响应式对象的 set 函数,把 get 数据时“储存”的 watchers 取出遍历,“通知”其更新视图。
- watcher“接到 data 中的数据更新通知”,重新 render 视图。
- 视图发生变化会触发 data 的中响应式对象的 set 函数,循环形成数据流。
- 例:
Watcher(vm, el, 'text', () => { currentWatcher = this; el.textContext = eval('vm.data.text'); currentWatcher = null; });
Watcher(vm, el, 'text', () => { currentWatcher = this; el.style.display = eval('Boolean(vm.data.text)') ? 'block' : 'none'; currentWatcher = null; });
|
Vue 1 中存在的几个明显问题
- 启动时拦截所有组件的状态,进行递归响应式代理影响首次渲染速度
- 内存占用率高,一个“指令”,“computed 计算属性”,“handlebar 表达式”等等均需要创建一个 watcher,watcher 数量过多导致内存占用率高。
- 模板经过编译后直接操作 dom,无法跨端。
Vue 中的优化
- 新的渲染引擎 - vdom
- Watcher 依赖力度调整
- 其他
- API、语法糖重新设计与定义
- 生命周期调整
- 双向数据流 -> 单向数据流
- 支持了 jsx 语法
- 等等…
新的渲染引擎 - vdom
<template> <div v-if="text"> {{text}} </div> </template>
render(){ return this.text ? h( 'div', null, h(this.text, null,[]) ) : vm.createEmptyVNode() }
|
Watcher 依赖力度调整
watcher 不再与单个 dom 节点、指令关联,一个 component 对应一个 watcher,极大减少了 vue 1 中 watcher 数量过多导致的内存问题。同时以来 vdom diff 在渲染时能以最小的代价来更新 dom。
Watch(compoent, vm, 'text', () => { const newVnode = component.render(); const oldVnode = component.render(); const patches = vm.diff(newVnode, oldVnode); vm.patch(component, patches); });
|
vdom 带来的优势
- 框架屏蔽具体渲染细节,抽象了渲染层,组建的抽象能力得以提升,不再依赖浏览器运行,进而可以跨段,如 SSR、同构渲染一姐小程序、weex、uni-app 等框架。
- 通过静态分析进行更多的 AOT(Ahead Of Time)编译优化。
- 附加能力:大量组件更新时以最小的代价去更新 dom。
- vdom 对比直接操作 dom 要慢,大部分情况下效率比 vue 1 差,虽然牺牲了一点性能,但是使得 vue 获得更多特性及优化空间。
AOT 编译优化
Cache static element
<div> <span class="foo"> Static </span> <span> {{dynmic}} </span> </div>
const __static1=h('span',{ class:'foo' }, 'static')
render(){ return h('div', [ __static1, h('span', this.dynamic) ]) }
|
Component fast path
- 编译后 直接判断是组件、原生标签还是文本节点,避免不必要的分支判断,提升性能。
- 提高 vdom diff 时的效率
Vue2 优化前
<Comp></Comp> <div></div> <span></span>
render(){ return createFragment([ h(Comp, null, null), h('div', null, [ h('span', null, null) ]) ]) }
function h(type, attrs, children){ if(isComponent(type)){ return createComponentVNode(type, attrs, children) } if(isDomElement(type)){ return createElementVNode(type, attrs, children) } return createStringVNode(type, attrs, children) }
|
Vue3 优化后
- 编译后直接调用不同的 createVNode 方法
render(){ return createFragment([ createComponentVNode(Comp, null, null), createElmentVNode('div',null, [ createElmentVNode('span', null, null) ]) ]) }
|
SSR optimize
<template> <div> <p class="foo"> {{msg}} </p> <comp/ </div> </template>
render(){ return h('div', [ this.ssrString( `<p class="foo">` + this.msg + '</p>' ), h(comp) ]) }
|
Inline handler
- 缓存 dom 上的 event handler,避免重复创建。
<div @click="count++"></div>
import {getBoundMethod} from 'vue'
function __fn1(){ this.count++ }
render(){ return h('div',{ onClick:getBoundMethod(__fn1,this) }) }
|
Vue3 变更
Proxy Reactive State
- Vue3 改用 Proxy 去生成响应式对象
- Vue1/2 中遍历和递归所有 data 中的属性去生成响应式对象
- Vue3 中改为仅在 get 获取这个属性的时候才去生成响应式对象,延迟了响应式对象生成,加快了首屏渲染速度。
function walk(data) { Object.keys(data).foreach((key) => { defineReactive(data, key, data[key]); walk(data[key]); }); }
function reactive(target) { let observerd = new Proxy(target, { get(target, key, receiver) { let result = Reflect.get(target, key, receiver); reactive(result); return result; }, set(target, key, value, receiver) { let oldValue = target[key]; if (value == oldValue) return; let result = Reflect.set(target, key, value, receiver); return result; }, }); return observerd; }
|
Composition API
- Vue2 中,代码根据数据、方法、计算属性等进行分块,导致可能同一个业务功能的代码需要反复上下地跳着去看。
- 虽然有 Mixin,但业务和业务之间的关系,包括命名空间都会出现一定问题。
- Vue3 中引入 Composition API 使得开发者可以根据业务将代码分块,按需引入响应式对象、watch、生命周期钩子等各种属性,使用方法类似 React Hooks,使得开发者更灵活地开发。
Vue2
export default { data() { return { counter: 0, }; }, watch: { counter(newValue, oldValue) { console.log('The new counter value is: ' + this.counter); }, }, };
|
Vue3
import { ref, watch } from 'vue';
const counter = ref(0); watch(counter, (newValue, oldValue) => { console.log('The new counter value is: ' + counter.value); });
|
Why use Composition API
- mixin、hoc、composition api 都是为了解决代码复用的问题。但是 mixin、hoc 过于灵活没有规范,导致开发人员容易写出零散、难以维护的逻辑。
- Compostion API 规避了 mixin、hoc 存在的缺陷,提供固定的编程模式->函数组合,对各模块解耦使得更优雅、更容易地去组合复用。
- 以组件状态为例,传统写法所有 state 都在一个 component,杂糅在一起,语义化不强,compostion api 使得 state 按照不同的逻辑分离出来,抽象出状态层组件。
const Foo = { template: '#modal', mixins: [Mixin1, Mixin2], methods: { click() { this.sendLog(); }, }, components: { appChild: Child, }, };
|
- 看完以上代码会发现以下问题
- sendLog 到底来自哪个 Mixin
- mixin1,mixin2 之间有没有逻辑关系
- mixin1,mixin2 如果都注入了 sendLog 使用哪个
- 如果使用 hoc 的方式,hoc 增加了两个组件实例消耗,多了两次 diff。
- 再来多几个 mixin,这个组件更难维护。
- 明显地体现了 Composition API 的好处
Time Slicing
- Vue3 最开始实现了这个特性,不过后面移除了
- 原因总结为以下两条
- 基于响应式原理及 AOT 编译优化,相比 react 而言 vue vdom diff 具有很高的效率
- Time Slicing 只在一些极端情况下有明显作用,引入会降低 vdom diff 效率,阻塞 UI 渲染,收益不大。
按需引入、支持 treeshaking
- Vue 各模块(响应式、SSR、runtime 等)的解耦,可按需引入。
Vue vs React
相同点
- 基于 MVVM 思想:响应式数据驱动试图更新
- 提供组件化的解决方案
- 跨端:基于 vdom 的渲染引擎
核心差异
- 定位
- React 是一个 Library,只专注于 state 到 view 的映射,状态、路由、动画等解决方案均来自于社区。
- Vue 是一个渐进式 Framework,设计之初考虑开发者可能面临的问题,官方提供路由、状态管理、动画、插件等比较齐全的解决方案,不强制使用,譬如模块机制、依赖注入,可以通过插件机制很好和社区方案集成。
- Library,职责范围小,开发效率低,需借助外力,但是易于扩展。对维护团队而言,保持版本间兼容成本较低。更容易集中精力专注于核心变更。
- Framework,职责范围大,开发效率高,内置一套解决方案,扩展程度低。对维护团队而言,保持版本间兼容成本较高。
- 渲染引擎
- Vue 进行数据拦截/代理,它对侦测数据的变化更准确,改变了多少数据,就触发多少更新多少。
- React setState 触发局部整体刷新,没有追踪数据变更,做到精确更新,所以提供给开发者 shouldComponentUpdate 去除一些不必要的更新。
- 基于这个响应式设计,间接影响了核心架构的 Composition API、React Hooks 的实现。
- 模板 DSL
- Vue template 语法更接近 html,静态表达能力很强,基于声明式的能力,更方便做 AOT 编译优化。
- JSX 语法可以认为是 JS 基础上又增加了对 html 的支持,本质还是命令式变成。静态表达能力偏弱,导致优化信息不足,无法很好地做静态编译。
掘金:前端 LeBron
知乎:前端 LeBron
持续分享技术博文,关注微信公众号 👇🏻
