数据驱动:ViewModel,保证数据和视图的一致性
组件系统:应用类UI可以看作全部是由组件树构成的
初次渲染:
- initState ->进行双向绑定
- $mount->将template编译成render函数
- 执行渲染 触发属性getter函数,将渲染watcher 收集到dep(依赖收集器)中
- 调用render 函数 生成 虚拟节点对象 vnode
- 打补丁patch(elm,vnode)
更新
- 修改数据 触发属性set
- 然后dep.notify() ->watch.update派发更新
- 触发render watcher 的render回调
- 生成新的vnode
- patch(oldVnode,newVnode) ,diff算法更新视图。
Vue的MVVM实现是通过数据劫持配合发布者-订阅者模式完成的。首先是new Vue实例化一个Vue对象作为入口,new Vue的时候会创建Observer对象和Compile对象。
Observer负责数据劫持,观察数据变化,创建依赖收集器Dep对象。当数据发生变化的时候,通知Dep,然后Dep会通知所有相关的订阅者Watcher调用更新视图的回调函数,更新视图。
Compile负责解析指令和双大括号的插值表达式,将对应的数据渲染到页面相应的地方,同时创建订阅者Watcher,并添加到依赖收集器Dep中,绑定数据变化时,更新视图的回调函数。
以上流程的对应实现都可以在Vue源码中的src/core/observer和src/compiler目录中找到。
参考MVVM实现流程
虚拟DOM: 用一个JS对象来描述一个DOM节点
虚拟DOM实现原理:
- 通过js建立节点描述对象
- diff算法比较分析新旧两个虚拟DOM差异
- 将差异patch到真实dom上实现更新
虚拟DOM概述:虚拟 DOM 中最为核心的部分是 patch() 方法,通过该方法,Vue 可以将最新的 vnode 渲染到页面上,实现组件的重新渲染。
patch() 方法:在重新渲染组件的时候,并不会使用暴力覆盖的方法,而是细心的比较新老 vnode 之间的差异,只对有差异的地方进行真实 DOM 的更新操作。这样,就可以极大的减少操作真实 DOM 的次数,提高性能。patch() 方法中使用的算法就是广为人知的 diff 算法。
diff 算法 主要有四块内容 :创建节点、删除节点、更新节点、删除子节点
组件页面渲染时,将render返回的新vnode(新节点)和组件实例保存的vnode(旧节点)作为参数,调用patch方法,更新DOM
提供一个在页面上以存在的DOM元素作为Vue实例的挂载目标,可以是CSS选择器,也可以是一个HTMLElement实例
nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
**主要思路:**采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法。
原理:
Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
所以为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。
使用场景
在你更新完数据后,需要及时操作渲染好的 DOM时
在一个组件实例中,只有在data里初始化的数据才是响应的,Vue不能检测到对象属性的添加/删除,没有在data里声明的属性不是响应的,所以数据改变了但是不会在页面渲染;
原理:
- 为对象添加一个新的响应式数据:调用 defineReactive 方法为对象增加响应式数据,然后执行 dep.notify 进行依赖通知,更新视图
- 为数组添加一个新的响应式数据:通过 splice 方法实现
解决数据层级结构太深的问题
在开发业务时,经常会岀现异步获取数据的情况,有时数据层次比较深,如以下代码: ,
可以使用vm. s e t 手动定义一层数据 : v m . set手动定义一层数据: vm. set手动定义一层数据:vm.set(“demo”,a.b.c.d)
什么样的数组变更不能被检测到呢?
- 通过索引修改数组的值
- 通过修改长度改变数组
- 调用Array.prototype上的方法
会: push() pop() shift() unshift() splice() sort() reverse()
这些方法都可改变原数组,并且能够被检测到,进行页面更新,这7个方法是vue包装之后方法
不会:filter() concat() slice() map()
新数组替换旧数组,不会改变原数组,页面不更新,不会被拦截
解决: Vue.set / this.$set 强制更新
1、使用函数劫持的方式,重写了数组的方法(push、pop、shift、unshift、sort、reverse、splice)
2、Vue 将 data 中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组api时,可以通知依赖更新.如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。
- 处理组件配置项;初始化根组件时进行了选项合并操作,将全局配置合并到根组件的局部配置上;初始化每个子组件时做了一些性能优化,将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率;
- 初始化组件实例的关系属性,比如 p a r e n t 、 parent、parent、children、r o o t 、 root、root、refs 等
- 处理自定义事件
- 调用 beforeCreate 钩子函数
- 初始化组件的 inject 配置项,得到 ret[key] = val 形式的配置对象,然后对该配置对象进行响应式处理,并代理每个 key 到 vm 实例上
- 数据响应式,处理 props、methods、data、computed、watch 等选项
- 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
- 调用 created 钩子函数
- 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount 方法,反之,没提供 el 选项则必须调用 $mount
- 接下来则进入挂载阶段
因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染;所以为了性能考虑,Vue会在本轮数据更新后,再去异步更新视图。
原理:
- 调用 notify() 方法,通知watcher 进行更新操作
- 依次调用watcher 的 update 方法
- 对watcher 进行去重操作(通过id),放到队列里
- 执行完后异步清空这个队列, nextTick(flushSchedulerQueue) 进行批量更新操作
可以同名,methods的方法名会被data的属性覆盖;调试台也会出现报错信息,但是不影响执行;
原因:源码定义的initState函数内部执行的顺序:props>methods>data>computed>watch
vue 把 data、props、store 等数据做成响应式,也就是会对这些响应式数据做深度监听,给每一个object类型的key(包括嵌套object)添加observer(vue3使用proxy)。所以如果我们不需要数据是响应式的,可以在.vue 文件头部直接使用 let、const 定义变量,在组件销毁的时候将该这些变量设为null。
在vue中,使用watch来监听数据的变化;
1.监听的数据后面可以写成对象形式,包含handler方法,immediate和deep。
2.immediate表示在watch中首次绑定的时候,是否执行handler,值为true则表示在watch中声明的时候,就立即执行handler方法,值为false,则和一般使用watch一样,在数据发生变化的时候才执行handler。
3.当需要监听一个对象的改变时,普通的watch方法无法监听到对象内部属性的改变,只有data中的数据才能够监听到变化,此时就需要deep属性对对象进行深度监听。
router:为VueRouter的实例,相当于一个全局的路由器对象,里面含有很多属性和子对象,例如history对象…经常用的跳转链接就可以用this.$router.push,和router-link跳转一样。
route:相当于当前正在跳转的路由对象。。可以从里面获取name,path,params,query等
① hash:
使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
带#,如:http://localhost:8080/#/pageA。改变hash,浏览器本身不会有任何请求服务器动作的,但是页面状态和url已经关联起来了
② history :
依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;
不带#, 如:http://localhost:8080/ 正常的而路径,并没有#。基于HTML5的 pushState、replaceState实现:(如果后台没有做相应配置,history页面会在再次刷新的时候,报404错误; )
③ abstract :
支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.
全局的路由钩子函数:beforeEach、afterEach、 beforeResolve (一般用于全局进行权限跳转)
单个的路由钩子函数:beforeEnter、beforeLeave(路由内部钩子,一般在路由表里)
组件内的路由钩子函数:beforeRouteEnter、beforeRouteLeave、beforeRouteUpdate、
- beforeEach:( 全局前置守卫 )每一次路由该变的之后页面加载之前执行,三个参数(to 将要进入的路由对象、from 即将离开的路由对象、next 跳转方法),next 必须调用
- afterEach:( 全局后置守卫 )每一次路由该变的之后页面加载之后执行;两个参数(to 将要进入的路由对象、from 即将离开的路由对象)
- beforeEnter:( 单个路由守卫 )进入指定路由跳转时需要执行的逻辑
- beforeLeave:离开指定路由跳转时需要执行的逻辑
- beforeRouteEnter、beforeRouteLeave、 beforeRouteUpdate都是写在组件里面,也有三个参数(to、from、next)
【1】全局守卫:是指路由实例上直接操作的钩子函数,特点是所有路由配置的组件都会触发,直白点就是触发路由就会触发这些钩子函数
- beforeEach(to,from, next)
- beforeResolve(to,from, next)
- afterEach(to,from)
【2】路由守卫: 是指在单个路由配置的时候也可以设置的钩子函数
- beforeEnter(to,from, next)
【3】组件守卫:是指在组件内执行的钩子函数,类似于组件内的生命周期,相当于为配置路由的组件添加的生命周期钩子函数。
- beforeRouteEnter(to,from, next) 无法获取组件 this
- beforeRouteUpdadte(to,from, next) 当前路由改变,但组件被复用时调用;例:foo/1 => foo/2
- beforeRouteLeave(to,from, next) 离开后,禁止用户在未保存修改前离开
用JavaScript对象模拟真实DOM树,对真实DOM进行抽象
diff算法:比较两棵虚拟树的差异
pach算法:将两个虚拟DOM对象的差异应用到真实的DOM树
vue2.x版本中,当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级;
vue3.x版本中,当 v-if 与 v-for 一起使用时,v-if 具有比 v-for 更高的优先级。
官网明确指出:避免 v-if 和 v-for 一起使用,永远不要在一个元素上同时使用 v-if 和 v-for。可以先对数据在计算数据中进行过滤,然后再进行遍历渲染;
操作和实现起来都没有什么问题,页面也会正常展示。但是会带来不必要的性能消耗;
- 加载渲染过程: 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
- 子组件更新过程:父beforeUpdate->子beforeUpdate->子updated->父updated
- 父组件更新过程:父beforeUpdate->父updated
- 销毁过程: 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
- 可以通过名字找到对应的组件( 递归组件:组件自身调用自身 )
- 可以通过 name 属性实现缓存功能 (keep-alive)
- 可以通过 name 来识别组件(跨级组件通信时非常重要)
- 使用 vue-devtools 调试工具里显示的组见名称是由 vue 中组件 name 决定的
⽤ timeline ⼯具。 通过 timeline 来查看每个函数的调⽤时常,定位出哪个函数的问题,从⽽能判断哪个组件出了问题
1. 编码阶段
尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
v-if 和v-for区分使用场景,v-if和v-for不要连用;
v-for遍历必须为item添加key,key保证唯一性,且避免同时使用vif
computed和method区分使用场景
路由懒加载、图片懒加载、长列表动态加载
第三方模块按需导入
对于短时间的大量操作(缩放、滚动)使用防抖、节流函数
事件的销毁
SPA 页面采用keep-alive缓存组件
组件的延迟加载,可以把页面资源划分为多份,用到的时候才会按需加载,这样减少第一次加载的消耗。
代码精简,去除 console ,可复用的方法、组件提取出来
不要写行内样式,避免dom重绘
2. SEO优化
- 服务端渲染SSR
- 预渲染
3. 打包优化
- 压缩代码
- Tree Shaking/Scope Hoisting
- 使用cdn加载第三方模块
- 多线程打包happypack
- splitChunks抽离公共文件
- sourceMap优化
- 把不常改变的库放到index.html中,通过cdn引入
- vue路由的懒加载
- vue组件尽量不要全局引入
- 使用轻量级的工具库
首屏时间
指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容;
加载慢的原因:
常见的几种SPA首屏优化方式
减小入口文件体积:路由懒加载
静态资源本地缓存: 前端合理利用localStorage
UI框架按需加载
图片资源的压缩: 在线字体图标、雪碧图
组件重复打包: 配置CommonsChunkPlugin
// 假设moment.js文件是一个常用的库,现在很多文件都使用了该文件,这就造成了重复加载
// 解决方案: 在webpack的config文件中,修改CommonsChunkPlugin的配置。
minChunks: 3 //把使用3次及以上的包抽离出来 放进公告依赖文件,避免了重复加载
开启GZip压缩:compression-webpack-plugin插件
nmp i compression-webpack-plugin -D //安装 compression-webpack-plugin
// 并在vue.config.js 中引入并修改webpack配置 在服务器上我们也要做相应的配置。
const CompressionPlugin = require('compression-webpack-plugin')
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置...
config.mode = 'production'
return {
plugins: [new CompressionPlugin({
test: /\.js$|\.html$|\.css/, //匹配文件名
threshold: 10240, //对超过10k的数据进行压缩
deleteOriginalAssets: false //是否删除原文件
})]
}
}
}
使用SSR( 服务端渲染):组件或页面通过服务器生成html字符串,再发送到浏览器(Vue建议使用nuxt.js)实现服务端渲染。
Server-Side Rendering 我们称其为SSR,意为服务端渲染指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程;
解决了以下两个问题:
- seo:搜索引擎优先爬取页面HTML结构,使用ssr时,服务端已经生成了和业务想关联的HTML,有利于seo
- 首屏呈现渲染:用户无需等待页面所有js加载完成就可以看到页面视图(压力来到了服务器,所以需要权衡哪些用服务端渲染,哪些交给客户端
优点:
SSR 有着更好的 SEO(搜索引擎优化)、并且首屏加载速度更快。
缺点:
开发条件会受限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。
服务器会有更大的负载需求。
axios是通过promise实现对ajax技术的一种封装,就像jQuery实现ajax封装一样。
简单来说: ajax技术实现了网页的局部数据刷新,axios实现了对ajax的封装。 axios是ajax, ajax不止axios。
定义在一个函数内部的函数。其中一个内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
用途:1.模仿块级作用域,2.存储变量,3.封装私有变量
- 箭头函数没有原型,原型是undefined
- 箭头函数this指向全局对象,而函数指向引用对象
- call,apply,bind方法改变不了箭头函数的指向
同步:向服务器发送请求,必须等请求到内容,才能刷新页面,用户才能看到新内容
异步:向服务器发送请求,这时可以做其他事情,内容请求到时,用户不用刷新页面,也可以看到新内容
- cookie数据存放在客户的浏览器上,session存放在服务器
- cookie不是很安全,别人可以分析存放在本地的COOKIE进行COOKIE欺骗,考虑安全应该使用seesion
- session会在一定事件内保存在服务器上,当访问增多,会比较占用你的服务器的性能,考虑到减轻服务器性能方面,应当使用cookie
- 单个cookie保存的数据不能超过4k,很多浏览器都限制一个站点最多保存20个cookie