Vue 2
这里介绍老版本 vue 相关的知识点
Vue 2 原理相关
Vue 优点
- 轻量级, 只关注视图层,是一个构建数据的视图集合,大小很小;
- 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
- 双向数据绑定:保留了 angular 的特点,在数据操作方面更为简单;
- 组件化:保留了 react 的优点,实现了 html 的封装和重用,在构建单页面应用方面有着独特的优势;
- 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
- 虚拟 DOM:dom 操作是非常耗费性能的,不再使用原生的 dom 操作节点,极大解放 dom 操作,但具体操作的还是 dom 不过是换了另一种方式;
- 运行速度更快:相比较于 react 而言,同样是操作虚拟 dom,就性能而言, vue 存在很大的优势。
Vue 2 原理
讲到 vue , 那就必须联想到 MVVM (Model -> View -> ViewModel) MVVM 指的是 Model、View 和 ViewModel,它把每个 HTML 页面都拆分成了这三个部分
- Model 表示当前页面渲染时所依赖的数据源。
- View 表示当前页面所渲染的 DOM 结构。
- ViewModel 表示 vue 的实例,它是 MVVM 的核心。
总之, 更加方便的操作 data 中的数据 基本原理:
- 通过 Object.defineProperty() (vue3.0 使用 proxy)把 data 对象中所有属性添加到 [VM] 上。
- 为每一个添加到 [VM] 上的属性,都指定一个 getter/setter
- 在 getter/setter 内部去操作(读/写)data 中对应的属性
总结一下就是: 当一个 Vue 实例创建时,Vue 会遍历 data 中的属性,用 Object.defineProperty(vue3.0 使用 proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
响应式
Vue 中最核心的也就是它的响应式,所谓响应式就是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
主要步骤:
-
对劫持的数据对象
Observe
进行遍历, 包括子属性对象的属性,都加上setter
和getter
这样的属性,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了其数据的变化 -
Vue 的编译器
Compile
解析模板指令, 将模板变量替换成数据, 然后初始化页面, 并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者, 一旦数据有变动, 收到通知, 更新视图。 -
订阅者
Watcher
是 Observer 和 Compile 之间通信的桥梁,主要做的事情是: ① 在自身实例化时往属性订阅器(Dep)里面添加自己 ② 自身必须有一个 update()方法 ③ 待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。Dep 指用于收集 Watcher 订阅者们
-
MVVM 作为数据绑 定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。
MVVM 优缺点
优点:
- ⾃动更新 dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动 dom 中解放
- 分离视图(View)和模型(Model),降低代码耦合,提⾼视图或者逻辑的重⽤性: ⽐如视图(View)可以独⽴于 Model 变化和修改,⼀个 ViewModel 可以绑定不同的"View"上,当 View 变化的时候 Model 不可以不变,当 Model 变化的时候 View 也可以不变。你可以把⼀些视图逻辑放在⼀个 ViewModel ⾥⾯,让很多 view 重⽤这段视图逻辑
- 提⾼可测试性: ViewModel 的存在可以帮助开发者更好地编写测试代码
缺点:
- 对于⼤型的图形应⽤程序,视图状态较多,ViewModel 的构建和维护的成本都会⽐较⾼
- ⼀个⼤的模块中 model 也会很⼤,虽然使⽤⽅便了也很容易保证了数据的⼀致性,当时⻓期持有,不释放内存就造成了花费更多的内存
- Bug 很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得⼀个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地⽅就变得不那么容易了。另外,数据绑定的声明是指令式地写在 View 的模版当中的,这些内容是没办法去打断点 debug 的
数据劫持 Object.defineProperty()
Object.defineProperty()
有三个参数, 分别是:obj
, prop
, descriptor
obj
要定义的对象,prop
要定义或修改的属性名称或Symbol
descriptor
要定义或修改的属性描述符(配置对象)
第三个参数也就是 options
, 可以设置的参数有:
- value:给 target[key]设置初始值
- get:调用 target[key]时触发
- set:设置 target[key]时触发
- writable:规定 target[key]是否可被重写,默认 false
- enumerable:规定了 key 是否会出现在 target 的枚举属性中,默认为 false
- configurable:规定了能否改变 options,以及删除 key 属性,默认 false
虚拟 DOM
react
和 vue
中都用到了虚拟 dom,所谓虚拟 dom,是一个用于表示真实 DOM
结构和属性的 JavaScript
对象,这个对象用于对比虚拟 DOM
和当前真实 DOM
的差异化,然后进行局部渲染从而实现性能上的优化。
两者在使用 JS 实现模拟真是 Dom 的部分几乎是一致的。在 Diff 部分, 两者的算法也是类似的, 均有 delete, replace, insert, 但是两者的 diff 策略是不一致的;
- Diff 算法借助元素的 Key 判断元素是新增、删除、修改,从而减少不必要的 元素重渲染。
- react 中的 diff 策略是, 自顶向下全 diff
- vue 中的 diff 策略是, 跟踪每一个组件的依赖关系,不需要重新渲染整个组件树, 也就是 数据劫持, 然后对每个数据添加 getter/setter, 同时 watcher 实例对象会在组件渲染时,将属性记录为 dep, 当 dep 项中的 setter 被调用时,通知 watch 重新计算,使得关联组件更新。
虚拟 Dom 的数据结构, 主要包括:tag
, data
, children
- tag:必选。就是标签。也可以是组件,或者函数
- props:非必选。就是这个标签上的属性和方法
- children:非必选。就是这个标签的内容或者子节点,如果是文本节点就是字符串,如果有子节点就是数组。换句话说 如果判断 children 是字符串的话,就表示一定是文本节点,这个节点肯定没有子元素
diff 算法
在新老虚拟 DOM 对比时:
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行 patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的 children 没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行 updateChildren,判断如何对这些新老节点的子节点进行操作(diff 核心)。 匹配时,找到相同的子节点,递归比较子节点
在 diff 中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从 O(n3)降低值 O(n),也就是说,只有当新旧 children 都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。
vue 2 中使用的是双端 diff,字面意思就是从两端开始分别向中间进行遍历比较的算法
所谓双端 diff,主要有五种比较:
- 新旧头相等
- 新旧尾相等
- 旧头等于新尾
- 旧尾等于新头
- 四者互不相等
其中前四种很好理解,
- 当旧的头和新的头对比 key相同,那么新旧节点的头对应的指针向后移一位,
- 同理 新旧尾相同,那么新旧尾节点指针向前挪一位
- 当旧头和新尾节点key 一样,旧头向后挪一位,新尾节点指针向前挪一位, 这里就会需要节点的移动,将 旧的头节点 重新插入到 当前 旧的尾结点之后
- 同理,旧尾和新头的key 一样,那新头向后挪一位,旧尾向前挪一位, 这里是需要移动的,旧节点数组的末尾索引对应的 vnode 插入到旧节点数组 起始索引对应的 vnode 之前
最复杂的就是四者都不一样时, 至此,双端对比就结束了,这时候,剩下的新旧节点,需要重新生成 key(vnode.key 作为键),
然后,就需要拿着没找到的节点 key 找到旧节点中对应的 key的节点,这时候就意味着,有两种情况: - 旧的列表中有找到, 这是后就需要移动节点,将旧节点中对应的节点插入到 旧头位置之前,然后将旧节点位置中的元素设置为 undefined,
- 旧的列表压根不存在,属于新添加的节点,就直接将 新的 vnode 插入到对应的位置
- 然后就将起始位置后移,直接循环结束 经过上面的比较之后,剩下的
- 新节点剩余,则表示为新增的节点,那直接循环遍历剩余的数据,分别创建节点并插入到就末尾索引 节点之前
- 旧节点剩余,则表示为已经移除的节点,直接从节点数据 中移除