MVC 与 MVVM
- vue框架中MVVM的
M 就是后端的数据
,V 就是节点树
,VM 就是new出来的那个Vue({})对象
MVVM的优势
1、mvc和mvvm都是一种设计思想。 主要就是mvc中Controller演变成mvvm中的viewModel
。 mvvm主要解决了mvc中大量DOM操作使页面渲染性能降低,加载速度变慢的问题 。
2、MVVM与MVC最大的区别就是:它实现了View和Model的自动同步
:当Model的属性改变时,我们不用再自己手动操作Dom元素
来改变View的显示,它会自动变化。
3、整体看来,MVVM比MVC精简很多,我们不用再用选择器频繁地操作DOM。
- MVVM并不是用VM完全取代了C,ViewModel存在目的在于
抽离Controller中 |展示| 的业务逻辑
,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。
v-model原理
双向绑定原理
let obj = {}
// 劫持对象get set操作
Object.defineProperty(obj,'username',{
set(val){
//TODO 操作视图
},
get(){
}
})
data使用return
- 闭包:每个组件都有自己的私有作用域,确保各组件数据不会相互干扰
- 纯对象:相互干扰
v-show与v-if
- v-if 不会挂载到DOM树 => 触发回流(重排)和重绘
- v-show 会挂载到DOM树,改变display属性 => 只触发重绘
虚拟DOM
- 它是一个object对象模型,用来模拟真实的dom
- 在vue中,每个组件都有一个
render函数
,每一个render函数都会返回一个虚拟dom树
,这也就意味着每个组件都对应一棵虚拟DOM树
。 创建组件、响应式数据的变化都会重新渲染视图调用render函数
,如果直接使用真实DOM会消耗性能,导致渲染效率降低。首次渲染效率比直接操作真实DOM低
,因为在创建真实DOM之前要创建一次虚拟DOM,但是后续如果有数据更新就会体现优势
是什么
- vue2.x才有虚拟DOM
- 本质是js对象 => 跨平台(nodejs)
在vue中做了什么
- 更新后的虚拟DOM树和旧的DOM树比较,找到最小更新量,更新必要的真实DOM节点
diff中的patch()算法
-
diff算法是用JavaScript来表示一个dom树的结构
-
然后用这个dom去构建一个真实的dom 插入到文档中
-
当状态变更的时候 重新构造一个dom树 比较新旧dom树 记录两个dom树的差异 并且通知视图开始更新
-
diff算法就 用来比较vdom结构的
// 1.初始化 patch(container,vnode)
// 2.更新 update(vnode,newVnode)
// 虚拟DOM转为真实DOM 初始化渲染
function patch(vnode){
// 虚拟DOM生成的3个要素
let tag = vnode.tag // 目标元素
let attrs = vnode.attrs || {} //属性
let children = vnode.children || [] // 子节点
if(!tag){
return
}
// 1. 创建对应的DOM
let el = document.createElement(tag)
let attrsName
// 2. 给DOM添加属性
for(attrsName in attrs){
if(attrs.hasOwnpProperty(attrsName)){
el.setAttribute(attrsName,attrs[attrsName])
}
}
//3. 添加子节点
children.forEach(node =>{
el.appendChild(createElement(node))
})
return el
}
// 更新节点 更新子节点
function update(vnode,newVnode){
let children = vnode.children || [] // 现有节点
let newChildren = newVnode.children || [] // 新节点
children.forEach((childVNode,index) =>{
// 循环拿到新节点的每一项, (与旧节点对应的一项,对比是否有变化)
let newChildVnode = newChildren[index]
// 第一层没变化 (包括对比attrs,任何变化)
if(childVNode.tag === newChildVnode.tag){
// 递归判断下一层
update(childVNode,newChildVnode)
}else{
// 有变化直接替换
replaceNode(childVNode,newChildVnode)
}
})
}
发布订阅+数据劫持defineProperty,触发更新视图,实现响应式
数据发生变化,通知模板,为什么能通知==>对数据进行了订阅,在set时不直接操作DOM,不具备通用性
// 响应式
// 1. 数据联动(双向绑定)
// 2. 需要捕获到修改
// 订阅器模型
let Dep = {
chilentList:{},// 容器
//添加订阅 添加数据劫持阶段
listen:function(key,fn){
if(!this.chilentList[key]){
this.chilentList[key]=[]
}
this.chilentList[key].push(fn)
},
// 发布 数据set 执行绑定的事件
trigger:function(){
let key = Array.prototype.shift.call(arguments)
let fns = this.chilentList[key];
if (!fns || fns.length === 0){
return false
}
for(fn of fns){
console.log(fn)
// 绑定到订阅对象
fn.apply(this,arguments)
}
}
}
// 数据劫持
let getData = function ({data,tag,datakey,selector}){
let value = '',
el = document.querySelector(selector)
Object.defineProperty(data,datakey,{
//取值
get:function(){
console.log('取值')
return value
},
set:function(val){
console.log('设置值')
value = val
// 此处使用发布模式,能通用
// document.getElementById('name').innerHTML = val
// 发布
Dep.trigger(tag,val)
}
})
// 订阅
Dep.listen(tag,function(text){
// 需要更改的属性值
console.log(text)
el.innerHTML = text
})
}
<body>
订阅视图-1: <span class="box1"></span>
订阅视图-2: <span class="box2"></span>
<script src="/vue订阅发布.js"></script>
<script>
// 监听的数据对象
let obj = {}
getData({
data:obj,
tag:'view-1',
datakey:'one',
selector:'.box1'
})
getData({
data:obj,
tag:'view-2',
datakey:'two',
selector:'.box2'
})
//初始赋值
obj.one = '视图-1'
obj.two = '视图-2'
</script>
</body>
应用层
$nextTick()
Vue更新DOM是异步的,在下一次DOM 更新循环结束之后执行延迟回调
vue 执行dom 更新是异步的, 只要是观察到数据变化, vue将会开启一个队列, 并缓冲在同一事件循环中发生的所有数据改变。 如果同一个watcher 被多次触发,只会被推如到队列中一次。 在这种缓冲时去除重复数据
对于避免不必要的计算, 和DOM 操作非常重要。 然后下一个事件循环 “tick”中, vue 刷新队列执行实际(去重)工作。 在Vue 内部尝试对异步队列使用原生的promise.then 和 MessageChange, 如果执行环境不支持, 会采用setTime(()=> {} , 0)代替。
this.$nextTick(() => {
this.twodiv = this.$refs.onediv.innerHTML; // 通过原生$ref获取到第一个div改变后的值,然后赋给第二给div
console.log(this.twodiv);
});
单页与多页的区别及优缺点
单页应用(SPA):只有一个主页面应用
- 组件 ==> 页面片段
- 跳转 ==> 刷新局部资源
- 场景 ==> PC端
优点:
- 体验好,快
- 改动内容,不用加载整个页面
- 前后端分离
缺点:
- 不利于SEO
- 首屏加载速度慢
- 页面复杂的高
多页应用
整页面刷新
v-for与v-if
v-for优先级高
vue-router与location.href有什么区别
location.href
:简单方便,刷新页面vue-router
:实现了按需加载,减少的dom消耗; 封装原生js的history
Vue原理之侦听器实现
如何让数据变得可观测
class Obesrver{
// value 观测数据
constructor(value){
this.value = value
// 判断类型
if (Array.isArray(value)){
// 数组逻辑
this.observeArr(value)
}else{
// 对象逻辑
this.observeObj(value)
}
}
// 数组逻辑
observeArr(value){
// 对数组变动检测:找到改变原数组的方法,然后对这些方法进行劫持处理。 不能直接修改其原型
// 中间桥梁
let newArr = function(){}
const changeArr = [
"splice",
"shift",
"unshift",
"push",
"pop",
"reverse",
"sort"
]
for(let fn of changeArr){
newArr.prototype[fn] = function (...args){
console.log('数组变动')
Array.prototype[fn].call(this,...args)
}
}
value.__proto__ = new newArr() // 这样调用数组方法会__
}
// 观测对象
observeObj(value){
// 获取属性值数组
let keys = Object.keys(value)
for(let key of keys){
if(typeof value[key] === 'object'){
// 递归
new Obesrver(value[key])
}else{
//为每个属性添加数据劫持
this.defineData(value,key)
}
}
}
//数据劫持
defineData(obj,key){
let value = obj[key]
Object.defineProperty(obj,key,{
// 可枚举
enumerable:true,
//可修改
configurable:true,
//存取描述,不可与数据描述同时存在
//value :该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
//writable :当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false。
get(){
console.log(`读取了${key}属性值:${value}`)
return value
},
set(val){
value = val
console.log(`设置了${key}属性值:${value}`)
}
})
}
}
let obj = {
a:1,
b:2,
c:{
f:222,
g:333
},
h:[3,4,5]
}
new Obesrver(obj)
// obj.c.f //读取了f属性值:222
// obj.b //读取了b属性值:2
// obj.a = 3 //设置了a属性值:3
// obj.c.f = 666 //设置了f属性值:666
obj.h.push(1)
console.log(obj.h)
// 监听了每个属性值的变化
key作用,以及不用下标去设置
- 提供给虚拟DOM唯一的值,提高更新效率,当数据发生改变时,Vue会根据新数据生成新的虚拟DOM,随后通过
比较新旧虚拟DOM的tag和key值是否相同
,进行DOM元素的就地复用或更新
- 如果对数组进行增删操作,从修改项开始,与key的绑定关系就会发送变化,触发重新渲染,会影响性能,也会产生一些bug