Fork me on GitHub

Vue2和Vue3原理区别

Vue2

响应式原理

用数据劫持结合发布者-订阅者模式实现:通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

创建订阅者dep和观察者watcher进行依赖收集与派发更新:在getter中添加对应的Dep,在setter中通知相关Watcher进行更新。

对数组:通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持

  1. Observer:监听器,监听数据变化,对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
  2. Compile:解析指令,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
  3. Watcher:订阅者,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图(Vue的核心功能强调的是状态到界面的映射)
1
2
3
4
5
6
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () { ... },
set: function reactiveSetter (newVal) { ... }
})

MVVM

MVVM,是Model-View-ViewModel的简写。MVVM 的ViewModel将视图 UI 和业务逻辑分开,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。

MVVM采用双向数据绑定,view中数据变化将自动反映到viewModel上,反之,model中数据变化也将会自动展示在页面上。把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。

MVVM核心思想,是关注model的变化,让MVVM框架利用自己的机制自动更新DOM,也就是所谓的数据-视图分离。

优点:

  • Controller简洁清晰
  • 开发解耦、方便测试

虚拟DOM

虚拟DOM就是一个用来表示真实DOM的对象, 为了解决浏览器性能问题。它通过js的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM节点 dom。diff 则是通过JS层面的计算,返回一个patch对象,即补丁对象,在通过特定的操作解析patch对象,完成页面的重新渲染。

1.用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中 –> VNode

2.当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异 –> diff

3.把所记录的差异应用到所构建的真正的DOM树上,视图就更新了 –> patch

优点:

  • 不会进行回流和重绘;
  • 对于频繁操作,只进行一次对比差异并修改真实 DOM,减少了真实 DOM 中多次回流重绘引起的性能损耗;
  • 有效降低大面积的重绘与排版,只更新差异部分,进行渲染局部。

Diff算法

Diff算法是一种用来对比新旧虚拟DOM的算法,通过对比找出改变的虚拟DOM,然后单独更新这个虚拟DOM对应的真实节点,提高性能。

新旧虚拟DOM对比的时候,Diff算法比较只会在同层级进行, 不会跨层级比较。 所以Diff算法是:广度优先算法。 时间复杂度:O(n)。

优化时间复杂度:

  • 只比较同层级,不会跨层级比较
  • tag不同,直接删除重建,不深度比较
  • tag和key都相同,则认为是相同节点,不深度比较

diff遵循以下原则:

  • 在旧的虚拟DOM中找到与新的虚拟DOM相同的key
    • tag没有发生变化,就直接使用原来的真实DOM
    • 内容发生改变,就替换掉之前旧的虚拟DOM,生成新的真实DOM
  • 在旧的虚拟DOM中未找到与新的虚拟DOM相同的key
    • 直接生成新的真实DOM

vue的diff算法优化(双端 diff 算法)

diff 算法的目的是根据 key 复用 dom 节点,通过移动节点而不是创建新节点来减少 dom 操作。

当数据改变时,会触发setter,并且通过Dep.notify去通知所有订阅者Watcher,订阅者们就会调用patch方法,给真实DOM打补丁,更新相应的视图。

整个过程是逐步找到更新前后vdom的差异,然后将差异反应到DOM树上(也就是patch),特别要提一下Vue的patch是即时的,并不是打包所有修改最后一起操作DOM(React则是将更新放入队列后集中处理)

优先处理特殊场景、“原地复用”(Vue会尽可能复用DOM,尽可能不发生DOM的移动)

双端 diff 算法

diff算法从两边向中间比较,需要四个指针,分别指向新旧两个 vnode 数组的头尾。头和尾的指针向中间移动,直到 oldStartIdx <= oldEndIdx 并且 newStartIdx <= newEndIdx,说明就处理完了全部的节点。

双端 diff 是头尾指针向中间移动的同时,对比头头、尾尾、头尾、尾头是否可以复用,如果可以的话就移动对应的 dom 节点。

如果头尾没找到可复用节点就遍历 vnode 数组来查找,然后移动对应下标的节点到头部。

最后还剩下旧的 vnode 就批量删除,剩下新的 vnode 就批量新增。

Vue中diff算法的实现

  1. 创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
  2. 删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。
  3. 更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode。

相关方法:

  • 通过函数sameVnode判断两个vnode是否相同
  • patch: 当数据发生改变时,defineProperty => get会调用Depnotify方法调用Watcher进行更新,当每次走get调用_update的时候,都会走patch函数,更新真实DOM
  • patchVnode: 该函数是递归调用updateChildren的入口,除了比对子节点以外,还会将老节点上的东西更新到新节点中
  • updateChildren:更新节点调用的方法

vue3优化diff算法(快速 diff 算法)

与Angular区别

与Aangular双向数据绑定不同,Vue组件不能检测到实例化后data属性的添加、删除,因为Vue组件在实例化时才会对属性执行getter/setter处理,所以data对象上的属性必须在实例化之前存在,Vue才能够正确的进行转换。因而,Vue提供的并非真正意义上的双向绑定,更准确的描述应该是单向绑定,响应式更新,而Angular即可以通过$scope影响view上的数据绑定,也可以通过视图层操作$scope上的对象属性,属于真正意义上的视图与模型的双向绑定。

vue2原理存在的问题

  • 初始化时(Object.defineProperty)需要递归遍历对象所有 key,如果对象层次较深,性能不好
  • 通知更新过程需要维护大量 dep 实例和 watcher 实例,额外占用内存较多
  • Object.defineProperty 无法监听到数组元素及数组长度的变化,只能通过劫持重写会改变原数组的方法
  • 动态新增,删除对象属性无法拦截,只能用特定 Vue.set()/delete 解决
  • 不支持 Map、Set、WeakMap 等数据结构

Vue3

Proxy 代替 Object.defineProperty 重构了响应式系统。
通过Proxy(代理): 拦截对对象属性的操作, 包括属性值的增删改查。

通过 Reflect(反射): 对被代理对象的相应属性进行特定的操作。

1
2
3
4
new Proxy(target, {  // target 为组件的 data 返回的对象
get(target, key) {},
set(target, key, value) {}
})

使用Proxy代理的缺点:

  • 原始值的响应式系统的实现(proxy 的使用本身就是对于 对象的拦截,导致必须将他包装为一个对象, 通过.value的方式访问
  • ES6 解构,不能随意使用。会破坏他的响应式特性
  • 不兼容IE

vue3相对vue2的优点

代码层面:

  • 更完善的响应式系统(初始化时间和内存占用都得到改善)
  • 更好的Ts支持
  • Composition API
  • 支持多根节点组件

编译:

  • diff算法的优化:

    vue2中的虚拟dom是全量的对比(每个节点不论写死的还是动态的都会一层一层比较,这就浪费了大部分时间在对比静态节点上)。

    vue3新增了静态标记(patchflag)与上次虚拟节点对比时,只对比带有patch flag的节点(动态数据所在的节点);可通过flag信息得知当前节点要对比的具体内容。

  • hoistStatic 静态提升:

    vue2无论元素是否参与更新,每次都会重新创建然后再渲染。

    vue3对于不参与更新的元素(静态元素),会做静态提升(放在render函数外面),只会被创建一次,在渲染时直接复用即可。

  • cacheHandlers 事件侦听器缓存:

    vue2.x中,绑定事件每次触发都要重新生成全新的function去更新,cacheHandlers 是Vue3中提供的事件缓存对象,当 cacheHandlers 开启,会自动生成一个内联函数,同时生成一个静态节点。当事件再次触发时,只需从缓存中调用即可,无需再次更新。

打包:更好的支持tree-sharking,打包的体积更小

依赖收集

Vue2 中是通过 Observer,Dep,Watcher 这三个类来实现依赖收集。

Vue3 中是通过 track 收集依赖,通过 trigger 触发更新,本质上就是用 WeakMap,Map,Set 来实现。

defineProperty 和 Proxy

  • Object.defineProperty 是 Es5 的方法,Proxy 是 Es6 的方法
  • defineProperty 不能监听到数组下标变化和对象新增属性,Proxy 可以
  • defineProperty 是劫持对象属性,Proxy 是代理整个对象
  • defineProperty 局限性大,只能针对单属性监听,所以在一开始就要全部递归监听。Proxy 对象嵌套属性运行时递归,用到才代理,也不需要维护特别多的依赖关系,性能提升很大,且首次渲染更快
  • defineProperty 会污染原对象,修改时是修改原对象,Proxy 是对原对象进行代理并会返回一个新的代理对象,修改的是代理对象
  • defineProperty 不兼容 IE8,Proxy 不兼容 IE11

Vue3和Vue2的区别

  • 响应式原理
  • 生命周期钩子名称
  • 自定义指令钩子名称
  • 新的内置组件
  • diff 算法
  • Composition API

参考

剖析Vue原理&实现双向绑定MVVM

深入理解vue

深入Vue2.x的虚拟DOM diff原理

20分钟吃透Diff算法核心原理

vue3,对比 vue2 有什么优点?

-------------完结撒花 -------------