✨「前端进阶」深入浅出Vue2.x

282 阅读6分钟

重新认识 Vue

一、引言

写在前面:(以下内容主要针对Vue2版本展开)

​ 提起Vue,人们往往会说出MVVM模式,数据双向绑定,数据劫持等等。但你们是否真的了解Vue?尝试了解以下的问题可以更好地帮我们理解Vue:

  1. 对于变化侦测的实现,Vue与React,Angular比起来有何不同?
  2. Vue“推模式”地侦测变化有哪些缺点,它又是如何改进的?
  3. 追踪变化中,Vue2中的对Object、Array的数据劫持有哪些不足?
  4. 数据的Set、Get在data、watcher、Dep怎样流转?
  5. Vue是如何Patch新旧节点的?
  6. Vue的生命周期包括哪几个阶段,各阶段做了什么?
  7. new Vue调用发生了什么?
  8. Vue框架在设计时,怎么处理“渐进式”?或代码结构的思想

如果以上问题,你都有一定地理解,那么恭喜你,你对Vue的了解算是很深入的。如果不了解 也没关系,让我们一起来探讨,一起深入浅出Vue吧。

二、理解

1. 对于变化侦测的实现,Vue与React,Angular比起来有何不同?

​ 从状态生成DOM再到用户界面的流程叫做渲染,响应式系统赋予框架重新渲染的能力,应用在运行中不断地进行重新渲染。那什么时候需要重新渲染呢?当数据变化的,也就是状态变化的时候。而变化侦测就是做这项工作,它主要分为两种类型,一种是“拉”,一种是“推”。angular和react的变化侦测都属于“拉”类型,就是说当状态发生变化时,它不知道哪个状态变了,于是框架内部进行暴力对比哪些节点需要重新渲染。在angular是脏检测机制,而在react是虚拟DOM对比。 拉的粒度算是最粗的,因为它不知道哪些节点发生了变化,但相对应的内存开销也比较小。

​ 而Vue的变化侦测属于“推”,当状态发生变化时,Vue在一定程度上知道哪些状态发生了变化,它知道的信息更多,自然可以更细粒度地更新。

2. Vue“推模式”地侦测变化有哪些缺点,它又是如何改进的?

​ Vue虽然能更细粒度地更新,但因为状态绑定的依赖越多,追踪起来相应的内存开销会越来越大。因此从Vue2.0开始,状态绑定的依赖的细粒度从具体的DOM节点变成组件,从通知具体节点到通知组件,并引入虚拟DOM,在组件内部通过虚拟DOM的对比,找到变化的节点。这大大降低依赖的数量,从而减少依赖追踪的内存开销。

3. 追踪变化中,Vue2中的对Object、Array的数据劫持有哪些不足?

​ 对于Object,Object.defineProperty是对对象添加属性操作,通过对对象添加set、get方法达到数据劫持的效果,以用来追踪变化。但它只能追踪目前对象现有的属性,所以它无法追踪到对象未来的属性变化(即对象的增删操作)。当然针对这一情况,Vue也是提供相应的setset、delete来解决,而Vue3.0则是使用Proxy代理对象来取代了这个API。

​ 对于Array,Vue重写了可能改变数组数据的方法来达到数据劫持,它同样无法侦测到通过数据下标的赋值操作,以及array.length=0清空数据的操作。Object.defineProperty可以检测到这一变化,但Vue并没有使用,出于性能问题的考虑,无法对每个数组元素进行劫持。

4. 数据的Set、Get在data、watcher、Dep怎样流转?

Get 当外界读取数据时触发getter,将watcher加入依赖项的Dep中,收集依赖。

外界 -> watcher ->data

Set 当数据发生变化,触发setter重新赋值,从依赖的Dep中的watcher发生通知。

data -> Dep.notify -> watcher ->外界

其中Dep与watcher为多对多的关系,watcher中记录着自己订阅了谁,Dep中也记录订阅的watcher。

附:调用watch.teardown方法可以消除观察数据,其实现就是将watcher实例从其订阅依赖的列表中移除自己。

5. Vue在Diff中如何Patch新旧节点的?

Vue在patch节点时使用的是同层级比较,大体分为3种情况:

  1. 旧节点不存在: 直接挂载新旧点
  2. 新旧节点都存在,但不同类型: 删除旧节点,挂载新节点
  3. 新旧节点都存在,且同类型: Patch节点,达到最大程度的复用

其中在Patch节点时,为了提高对比效率,Vue2采用的是双端比较查找,在双端对比还未遍历完节点时,则采用暴力地对比。在节点的移动也有一定的方法:必须移动到所有未处理节点的最前面新增节点也必须插入未处理节点的前面,如果插入到已处理节点的后面会造成一定的patch冲突,避免重复。(具体可看源码或相关书籍理解,附diff思维导图)

屏幕截图 2021-08-09 213628.png

6. Vue的生命周期包括哪几个阶段,各阶段做了什么?

Vue的生命周期主要包括:初始化阶段、模板编译阶段、挂载阶段、卸载阶段

  1. 初始化阶段:初始化生命周期、事件、渲染函数,state等,通过传入Vue实例挂载一系列option。(beforeCreated、created钩子)

  2. 模板编译阶段:将模板 -> AST 抽象语法树 ->渲染函数 将模板编译成渲染函数到浏览器中执行,同时处理静态节点的优化。(runtimeonly版本不存在)

  3. 挂载阶段:渲染函数 -> Vnode ->视图,将实例挂载到传入的el中。(beforeMount、mounted钩子)

  4. 卸载阶段:当vm.$destory被调用时,组件进入卸载阶段,清除相关实例(beforeDestroy、destroyed钩子)

Vue生命周期.png

7. new Vue调用发生了什么?

​ 当new Vue()被调用时,首先会调用原型上的_init方法进行一些初始化操作,主要是data、event等的初始化,初始化后则进入挂载阶段。这边值得一提的是初始化injections需要在初始化data之前,因为在data中得范围的到注入的值,所以先于data初始化。下图为Vue的 _init方法

Vue初始化函数.png

8. Vue框架在设计时,怎么处理“渐进式”?或代码结构的思想

​ 渐进式框架,它是一种理念,就是用你想用的的功能特性,不要求一次性接收全部的功能属性。我们可以使用Vue做一些表单渲染操作而不引进VueRouter路由,Vuex状态管理等,可以自底向上逐层构建应用。(相比angular、react都有一定的侵入性,angular强制使用模板机制和依赖注入,react必须理解函数式编程和副作用处理等)。

​ Vue框架的设计从构造函数初始化开始,到原型prototype上挂载方法,再到全局API的声明,逐层往上。通过解耦的Node options来实现跨平台。

9. 关于NextTick(补充)

总所周知,Vue的数据更新是异步更新,如果要使用数据更新后的DOM则需要使用nextTick回调,其中包含了宏任务与微任务、事件轮询等(如果不了解这些概念,请自行百度)。那为什么nextTick能拿到数据更新后的DOM呢?(其实准确说,nextTick回调的代码也必须放在数据更新后面)因为更新DOM的回调也是使用vm.$nextTick来注册到微任务的,在更新DOM代码后使用nextTick回调自然也就能拿到数据更新后的DOM。那还有个问题:多次调用nextTick会向任务队列添加多个任务吗? 答案是不会,多次调用也只会向任务队列添加一个任务。因为被添加到任务队列中的任务只需执行一次,就可将本轮事件循环中使用nextTick方法注册的回调都依次执行一遍。

三、最后

​ 以上的一些总结,是我在观看了深入浅出Vue这本书后结合自己的理解总结的一些要点,如有不足之处,还望指出交流。如果你觉得本文章对你有帮助,麻烦留下你的小心心~~,就是对我最大的支持。