MVVM
视图模型双向绑定,是Model-View-ViewModel的缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。以前是操作DOM结构更新视图,现在是数据驱动视图。
M:模型(Model) :data中的数据
V:视图(View) :模板代码
VM:视图模型(ViewModel):Vue实例
数据代理
data的初始化:data的数据会保存到vm._data中。在vm(vue实例)上设置一个代理,使vm.x可以访问到vm._data.x。然后通过observer类让data转换成响应式数据。
Object的监听
通过Object.defineProperty()将属性设置成getter/setter的形式实现了数据劫持,每次读取数据触发getter,修改数据触发setter
为每一个数据设置了Dep来收集依赖。
Watcher就是依赖,指哪些地方使用了数据,一个组件实例、计算属性、watch都是watcher的实例。
Data 通过Observer类 把数据转换成getter/setter的形式 实现数据劫持
外界通过Watcher读数据的时候,会触发getter将Watcher加入Dep中。
数据发生变化时,会触发setter,向Dep里的Watcher发通知。
Watcher接到通知后,通知外界,引发视图更新/调用回调等。
由于使用的是Object.defineProperty() 只能追踪属性是否被修改 无法追踪对象新增和删除属性,所以才有vm.$set()和vm.$delete()
Array的监听
创建了一个拦截器来覆盖数组原型,拦截器继承自Array.prototype,但新加了处理过的push pop shift unshift splice reverse sort方法,调用这些方法的时候,实际调用的是处理过的这些方法,可以监听到变化
对数组操作时只有使用这些方法才能监听 使用数组下标的形式不能监听到
单页应用和多页应用的区别
单页应用 single page application(SPA):指只有一个主页面的应用,只需要加载一次公共资源(js css等)。页面中按功能模块划分为不同组件,切换页面时只要切换相应组件即可,即局部刷新。
多页应用multi page application(MPA):多个页面,每次切换页面都要跳转到新的页面,重新加载资源,整个刷新。
单页应用的适用场景和不适用的场景
单页应用
优点:切换页面流畅
缺点:1.不利于SEO(搜索引擎优化)页面内容是用js渲染出来的 不利于检索 要用SSR(服务器端渲染)优化 2.首屏加载耗时多
适用于对流畅度体验高的应用
客户端渲染:使用 JavaScript 框架进行页面渲染
服务端渲染:服务端将HTML文本组装好,并返回给浏览器,这个HTML文本被浏览器解析之后,不需要经过 JavaScript 脚本的执行,即可直接构建出希望的 DOM 树并展示到页面中。
多页应用
优点:1.利于搜索引擎优化 2.首屏加载快
缺点:1.切换页面较慢 2.重复加载资源
适用于对SEO要求高的如官网首页
Vue和原生js的对比
vue基于MVVM模式,数据驱动修改视图,不用对dom进行直接操作。
vue按功能抽离出组件进行开发,使代码可以尽可能的复用,避免了大量重复代码,增加了可读性。
vue有各种页面之间传值的方法以及状态管理工具vuex,原生开发的页面传值方法很复杂。
vue是单页应用,且vue有虚拟dom和diff算法可以做到局部刷新,dom尽可能复用,提升了用户的切换页面的体验。
Vue2 Vue3的对比
1、响应式原理
Vue2对于对象类型,通过Object.defineProperty给data添加getter和setter实现数据拦截,每个data都有dep属性来收集依赖watcher对象。对于数组类型,通过重写数组原型对象上的七大方法来进行数据拦截。
Vue2的缺点是,只能监听到对对象属性的修改,如果新增或删除对象属性,必须要用Vue.set和Vue.delete方法。
Vue3通过给data创建代理proxy来拦截对数据属性的增删改,通过反射reflect完成对数据属性的操作。
Vue3实现了对数据的完全响应,对数据属性的新增和删除也可以响应到。
2、v-if和v-for的优先级
vue2里v-for的优先级高于v-if,vue3里v-if的优先级高于v-for。实际使用的时候不要把v-for和v-if放在同一节点,推荐用template包裹v-if,内部在用v-for遍历。
3、新增组合式API
Vue2中是选项式API,按data、watch、computed放在一起。而Vue3新增了组合式API,按照逻辑来写。
4、生命周期的改变
Vue3删除了beforeCreate和Created两个钩子,用hooks函数的形式引用,对Vue2中的生命周期名字进行了修改,比如mounted改为onMounted。。。
data为什么是函数
1.因为对象是引用数据类型 如果把data单纯的写成对象形式 组件复用时 所有data都指向同一个对象 修改时会相互影响
2.data写成函数形式 每次组件被复用的时候 都会返回一个新的对象 让每个组件维护自己的data
v-for和v-if的优先级
vue2中v-for高于v-if vue3中v-if高于v-for
不建议同时使用v-for和v-if 每次循环之后都要v-if判断 造成性能浪费 可以用template绑v-if然后在里面用v-for遍历
虚拟DOM和diff算法
虚拟DOM
直接操作真实DOM:把旧DOM全删了,然后根据状态生成新的DOM,造成性能浪费。
虚拟DOM:是一个树形结构的JS对象,对应真实DOM。
组件首次渲染时,将模板编译进render函数,调用render函数会生成虚拟dom,再生成对应的真实dom,并挂载到页面中的指定位置。
组件的数据更新时,setter触发dep.notify通知watcher进行patch,组件内通过diff算法比较新旧虚拟DOM,渲染不同的部分,其他部分直接复用。
(Vue1是细粒度侦测,每个节点是一个watcher,状态改变的时候直接通知该节点。Vue2是中等粒度,每个组件是一个watcher,状态改变时通知组件,然后组件内部用虚拟DOM和diff算法重新渲染。)
VNode:是一个类,可以实例成不同的VNode实例,对应真实的DOM节点。所有的VNode组成一个VDom树。
和真实DOM一样有文本节点 注释节点 元素节点等。如元素节点VNode包括tag(标签) data(attribute class等) children(子节点) context(当前组件的vue实例)属性
diff算法 (patch)
概念:对比新旧虚拟dom的差异,只对变化了的虚拟dom更新对应的真实dom,其他没变化的直接对真实dom复用,从而提高效率。
真实DOM操作远不如JavaScript的运算速度快 虚拟DOM实质就是js对象
对比方式:深度优先,同层比较
主要的几个函数
- render函数
- patch函数
- patchVnode函数
- updateChildren函数
1、从根节点开始遍历,对节点执行patch(oldVnode,newVnode)函数,比较两个根节点是否是相同节点 (key相同且sel选择器相同)。如果不同,直接替换(删除旧的真实DOM,用newVnode生成新的真实DOM)
2、如果是相同节点,对两个根节点执行patchVnode(oldVnode, newVnode),比较属性、文本、子节点。
属性、文本直接用newVNode的去替换真实DOM的属性和文本。
都存在子节点时,执行updateChildren函数,去进一步比较他们的子节点。
3、新旧虚拟节点的子节点对比,执行updateChildren函数,(Vue2的双端diff算法)
双端diff算法的思想是对新旧虚拟DOM的两端进行对比,并做相应的移动操作
对于两组子节点,分别有四个指针,指向新前、新后、旧前、旧后,每一轮循环依次做四个对比
进入循环内的节点都是未处理过的节点
while(oldStart<=oldEnd && newStart<=newEnd)
1 oldStart === newStart找到,执行patchVnode进一步比较,新旧指针往中间移(oldStart++ newStart++)
2 oldEnd === newEnd找到,执行patchVnode进一步比较,新旧指针往中间移
3 oldEnd === newStart找到,执行patchVnode进一步比较,指针中间移,且把oldEnd对应的真实DOM移动到oldStart对应的(没处理过的DOM的开头)真实DOM的最前面,oldEnd对应旧节点置undifined标记已移动,指针往中间移
4 oldStart === newEnd 同3,比较+移动真实DOM
5 没有找到 则newStartVnode去oldChildren里找是否有可复用的节点(找相同的key)
如果找到的话,把该oldNode放到oldStart对应的真实DOM的最前面,对应旧节点置undifined标记已移动,newStart++
如果没有,说明是新节点,新增直接放到真实DOM的头部,newStart ++
出循环
oldStart>oldEnd && newStart <=newEnd 说明 [newStart,newEnd]之间的都是新节点,逐一挂载
oldStart<=oldEnd && newStart >newEnd 说明[oldStart,oldEnd]之间的都是旧节点,都删除
key的作用
key主要是方便复用的 用来判断新旧节点是否是相同节点(key相同且选择器相同)
1.如果在列表最前面新加节点 假设他们只有text文本不同 不设key的话
那么新旧dom进行对比的时候 他们都会被认为是相同的节点 然后进行patchNode又发现它们的文本不一样 要进行文本的删除修改 又对DOM做了操作 没有做到真正的复用
2.如果出现双端算法找不到的情况 又没有key 那这个新节点就会创建一个新的节点 不能做到复用
生命周期
参考:
生命周期: 从一个vue实例从创建到销毁的过程,就是这个vue实例的生命周期。
生命周期钩子/生命周期函数: 在生命周期中相应阶段时自动调用的函数
整个生命周期过程:
一、初始化阶段:(new Vue()到created)
1.new Vue()实例化一个vue实例,初始化event事件、lifecycle生命周期
2.执行beforeCreate函数
3.初始化状态,顺序是props methods data computed watch ,完成数据代理
4.执行created函数
二、模板编译阶段(created到beforeMount)
5.把el的outerHTML或template模板编译成可执行的render函数
6.调用beforeMount函数
三、挂载阶段(beforeMount到Mount)
7.执行render函数,生成虚拟DOM,渲染为相应真实DOM,挂载到页面指定的位置
8.调用mounted函数。
四、更新阶段
9.数据发生变化,完成挂载后,当数据发生改变时,watcher会通知虚拟dom重新渲染
10.调用beforeUpdate函数 (此时vm的_isMounted属性为true,_isDestroyed为false)
11.执行render函数,生成新的虚拟DOM,用diff算法进行比较后,更新真实DOM。
12.调用updated函数
五、销毁阶段
13.调用beforeDestroy函数
14.销毁实例
15.调用destroyed函数
父子组件生命周期执行顺序
挂载阶段:父组件beforeCreate->父组件created->父组件beforeMount->子组件beforeCreate->子组件created-子组件beforeMount->子组件mounted-> -> 父组件 mounted
更新阶段:父组件 beforeUpdate -> 子组件 beforeUpdate -> 子组件 updated -> 父组件 updated
销毁阶段:父组件 beforeDestroy -> 子组件 beforeDestroy -> 子组件 destroyed -> 父组件 destroyed
Vuex
vuex是怎么实现的(vuex的本质)
vuex的state怎么判断是外部修改的还是内部修改的
vuex 中唯一更改状态的方法就是 mutation
底层修改state是通过设置_committing 标志变量为 true,然后才能修改 state,修改完毕还需要还原_committing 变量。外部修改虽然能够直接修改 state,但是并没有修改_committing 标志位,所以只要 watch 一下 state,state change 时判断是否_committing 值为 true,即可判断修改的合法性。
this.$store._committing
vm.$nextTick()
要获取到数据更新之后的最新dom时使用
数据更新后虚拟dom重新渲染的任务是异步的,当发生数据变化时,相应的Watcher会推入一个队列里,然后在该轮事件循环中,修改多次数据会合并成最新的一次,在下一轮事件循环中触发
$nextTick()接收一个回调,将回调包装成一个异步任务
vue2提供了几种包装方式包括promise.then mutationobserver(微任务)/ setimmediate settimeout(宏任务)
vue3直接是包装成promise.then
优先promise.then因为它是微任务 优先级更高 也就是nexttick返回一个promise juejin.cn/post/684490…
对dom的修改也是一个微任务,和nexttick属于同级,执行先后看推入队列的先后顺序,所以在修改数据之后写this.nextTick就可以获取最新的dom。
组件间通信
数据在哪里 操作数据的方法就在哪里
props 父-子
单向数据流
父传子:props
自定义事件+ref 子-父
双向数据流
父里给子组件绑定自定义事件指定回调
1.@myEvent="callback"
2.this.$refs获取子组件后用.$on('myEvent',callback)
子里emit触发自定义事件并传参给父
this.$emit('myEvent',args)
解绑:this.$off('myEvent')
或者父传一个函数给子 子里传参
全局事件总线 任意
new Vue
在beforeCreate()里初始化的时候Vue.prototype.$bus = this
在要接受数据的地方绑事件和写回调
组件中通过this.$bus.$on、this.$bus.$off、this.$bus.$emit 使用 --使用方法类似组件的自定义事件
1.bus所有的组件都能看到 注意命名
2.注意在绑了事件的组件在destroy前解绑事件
vuex
全局状态管理
插槽
父-》子插入相应的html结构
1.匿名(默认)插槽
<!-- 父组件中-->
<child-component>
会被插入子组件的内容
</child-component>
<!-- 子组件中-->
<slot></slot>
2.具名插槽 父组件通过template和v-slot 子组件通过name 插入不同的插槽
<!-- 父组件中-->
<child-component>
<template v-slot:slotName1>
slotName1
<template/>
<--简写方式-->
<template #slotName1>
slotName2
<template/>
</child-component>
<!-- 子组件中-->
<slot name="slotName1></slot>
<slot name="slotName2></slot>
3.作用域插槽 把子组件的数据传到父组件中方便插槽使用
父组件中 <child-component>{child.prop} <child-component>是无效的 父组件的模板无法访问子组件的数据
使用作用域插槽 将子要传的数据作为属性绑定上去 父组件声明v-slot:slotName=""来接收
<!-- 父组件中 默认插槽-->
<child-component>
<template v-slot:default="slotProps">
{{slotProps.childProp}}
<template/>
</child-component>
也可以直接解构赋值
<!-- 父组件中 默认插槽-->
<child-component>
<template v-slot:default="{childProp}">
{{childProp}}
<template/>
</child-component>
<!-- 子组件中-->
<slot :childProp="childProp"></slot>
inject和provide-祖先和子孙组件之间传值
inject在data/props之前初始化,provide在data/props之后初始化,注入内容时,是将内容注入到当前vc的__provide中
inject配置key先在当前组件读取内容(__provide),读取不到则取它的父组件读取,找到最终内容保存到当前实例(vc)中,这样可以直接通过this读取到inject注入的内容。
// 父组件
export default {
provide: {
name: "父组件数据",
say() {
console.log("say say say");
},
},
// 当需要用到this的时候需要使用函数形式
provide() {
return {
todoLength: this.todos.length
}
},
}
// 子组件
<template>
<div>
<div>provide inject传递过来的数据: {{ name }}</div>
<div>provide inject传递过来的方法<button @click="say">say</button></div>
</div>
</template>
<script>
export default {
inject: ['name', 'say'],
},
}
</script>
父组件怎么获取子组件的方法和数据
用$refs获取子组件 然后直接访问
watch和computed
computed:计算属性
计算属性,一个数据依赖于其他数据
有缓存: 当依赖数据不变时,即使页面重新渲染,计算属性会立即返回之前的计算结果,而不必再次执行函数
只能处理同步数据:因为computed是通过return来返回计算属性的 return是同步执行的
watch:监听属性
监听一个数据,在它发生变化时执行回调
无缓存,页面重新渲染,值不变也会执行
可以写异步代码
执行顺序
1.如果watch没有设置immediate
初始化时 beforeMount -> computed -> mounted
修改watch和computed共同的依赖属性 watch->beforeUpdate->computed->updated
2.如果watch设置了immediate
初始化时 watch->created->beforeMount->computed->mounted
如果监听了计算属性的值,那么watch监听的计算属性会先执行
修改watch和computed共同的依赖属性 methods(如果触发了)->watch->beforeUpdate->computed->updated
编译
编译阶段(created->beforeMount) 把template编译为可执行的render函数
将template模板字符串解析成AST抽象语法树(parse解析)
优化AST(optimize优化)
将优化后的AST生成代码字符串(generate代码生成)
转换成render函数
render:h=>h(app) h是createElement的简写,render函数接收第一个参数是createElement函数,生成虚拟dom VNode