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要定义或修改的属性名称或Symboldescriptor要定义或修改的属性描述符(配置对象)
第三个参数也就是 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 插入到对应的位置
- 然后就将起始位置后移,直接循环结束 经过上面的比较之后,剩下的
- 新节点剩余,则表示为新增的节点,那直接循环遍历剩余的数据,分别创建节点并插入到就末尾索引 节点之前
- 旧节点剩余,则表示为已经移除的节点,直接从节点数据 中移除