理解Vue的MVVM
MVVM和MVC的区别
MVC(Model-View-Controller)
- Model:真正的逻辑处理,操作DOM,例如jslib库中的form、popup、drag等功能组件都属于model模块
- View:只管页面的显示和样式展示
- Control:一些逻辑控制,进行页面节点事件的注册和控制,以及页面加载性能的实现
MVVM(Model-View-ViewModel)
- Model: 所有的数据
- View:视图
- ViewModel: View和Model的桥梁
Vue双向绑定原理
实现mvvm的双向绑定,是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
数据监听器Observer,主要是给每个vue的属性用Object.defineProperty()
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key]);
})
}
function defineReactive (obj, key, val) {
var dep = new Dep(); // 为每个属性创建一个订阅者集合
Object.defineProperty(obj, key, { // 数据劫持
get: function() {
dep.addSub(Dep.target); // 添加订阅者watcher到订阅者集合dep
},
set: function (newVal) {
dep.notify();// 作为发布者发出通知,通知后dep会循环调用各自的update方法更新视图
}
})
}
指令解析器Compile,解析各种指令成真正的html
function Compile(node, vm) {
}
Compile.prototype.compileElement = function(node, vm) {
if(node.nodeType === 1) { // input元素
var attr = node.attributes;
// 解析属性
for(var i = 0; i < attr.length; i++ ) {
if(attr[i].nodeName == 'v-model') {//遍历属性节点找到v-model的属性
var name = attr[i].nodeValue; // 获取v-model绑定的属性名
//监听input事件
node.addEventListener('input', function(e) {
// 给相应的data属性赋值,进而触发该属性的set方法,在set方法中发出通知
vm[name]= e.target.value;
});
// 创建新的watcher,会触发该属性的get方法,在get方法中向对应属性的订阅者集合dep添加订阅者watcher,
new Watcher(vm, node, name, 'value');
}
};
}
}
首先我们为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,{{}}也会,v-bind也会,只要用到该属性的指令理论上都会,接着为input会添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。
限制:无法监听属性的添加和删除、数组索引和长度的变更
参考网址:vue双向绑定原理分析;vue依赖收集代码分析
vue怎么操作DOM
渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们。
我们先根据真实DOM生成一棵virtual DOM,当virtual DOM某个节点的数据改变后会生成一个新的Vnode,然后Vnode和oldVnode作对比,发现有不一样的地方就直接修改在真实的DOM上,然后使oldVnode的值为Vnode。
diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。
参考网址:https://www.cnblogs.com/wind-lanyan/p/9061684.html
虚拟DOM(virtual DOM)
参考网址:https://www.cnblogs.com/wind-lanyan/p/9061684.html
diff算法
updateChildren方法做了什么:
四个指针:oldStart,oldEnd,newStart,newEnd
每次对比都会按照以下步骤依次判断:
- oldStart和newStart指向同类节点:更新DOM,oldStart++,newStart++
- oldEnd和newEnd指向同类节点:更新DOM,oldEnd–,newEnd–
- oldStart和newEnd指向同类节点:oldStart移到oldEnd指向的节点后面,oldStart++,newEnd–
- oldEnd和newStart指向同类节点:oldEnd移到oldStart指向的节点前面,oldEnd–,newStart++
- 新增节点:在oldVNode中找不到newStart指向节点,说明该节点是新增的,直接插入到oldStart指向的节点前面
- 更新的节点:在oldVNode中能找到newStart指向节点,说明该节点的位置移动了,直接移到oldStart指向的节点前面
- 如果oldStart>oldEnd或者newStart>newEnd,说明对比完了
(如果newStart和newEnd先相遇了,说明有需要删除的节点,直接删除oldStart和oldEnd之间的节点就可以了;如果oldStart和oldEnd先相遇了,说明有新增的节点,直接插入到对应位置即可)
参考网址:详解vue的diff算法;深入 Vue2.x 的虚拟 DOM diff 原理;VUE更新使用的PATCH算法(DIFF算法);图文详解 vue diff 核心方法 updateChildren
key的作用
key 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
个人理解:
不设置key:updateChildren里每次判断的时候都认为他们是不用的节点,直接创建新的节点并插入,没有复用DOM
设置key:updateChildren判断的时候能找到相同的节点,只操作新增、修改、或删除的节点即可,实现了DOM的最大程度复用,减少了回流
因为插入节点以后,index会改变,会导致同一个节点前后index不一样而影响判断,所以将key设置为index是没用的
参考:关于Vue v-for中的:key作用
为什么data在组件中必须是一个函数
Vue每复用一次组件都返回一个新的对象,data是定义在组件的原型对象上的,如果data是对象的话,各个组件对象修改data会相互影响, 如果是用函数的话,每次返回的data都是新的,拥有独立的作用域
参考网址:https://blog.csdn.net/lareinalove/article/details/94019594
$nextTick
API:Vue.nextTick( [callback, context] )
用法:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。(当数据更新了,在dom中渲染后,自动执行该函数)
// 传入回调函数
this.$nextTick(() => {
this.msg2 = this.$refs.msgDiv.innerHTML
})
// 不传回调函数返回的是一个promise
this.nextTick()
.then(function () {
// DOM 更新了
})
源码解析:
1.生成异步函数timerFunc,在函数中调用flushCallbacks
2.调用nextTick:将监听事件加入到callbacks数组中;判断是否pending中,是的话等待本轮操作完毕,不是的话将pending更新为true,然后执行异步函数timerFunc
3.flushCallbacks中执行callbacks数组中得回调函数
原理:
1.观察到数据变化
2.开启一个队列,把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。
3.在下一个事件循环中,Vue会清空队列,并进行必要的DOM更新
参考网址:https://juejin.im/post/6844904120625659911
https://segmentfault.com/a/1190000012861862
https://blog.csdn.net/zhouzuoluo/article/details/84752280
Vue 不能检测数组和对象的变化
其实说白了就是vue的监听机制都是通过defineproperty来实现的,但是不能检测对象新添加的属性,对象可以初始化的时候就设置好属性,不加新属性,数组也是对象,然而如果要求数组初始化的时候就设置好所有属性(索引),不能新增索性(改变长度),显然是不可能的,那么要数组也就没用了。
v-for与v-if不能同时使用
原因:
vue 处理指令时,v-for 比 v-if 具有更高的优先级 ,因此哪怕我们只渲染出一小部分用户的元素,也得在每次重新渲染的时候遍历整个列表,不论活跃用户是否发生了变化
解决办法:
将遍历的对象替换为一个计算属性
参考网址:https://blog.csdn.net/betterliumm/article/details/93472616
vue-router原理分析
参考链接:
https://www.cnblogs.com/yanze/p/7644631.html
https://www.cnblogs.com/hity-tt/p/7059192.html