Vue:
- vue 数据双向绑定原理;
- vue computed 原理、watch 和 methods 的区别;
- vue 编译器结构图、生命周期、vue 组件通信;
- mvc 模式、mvp 模式、mvvm 模式;
- vue dom diff、vuex、vue-router
数据双向绑定:
代码语言:javascript复制理解:**vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的.**
原理:是观察者`observer`通过`Object.defineProperty()`来劫持到各个属性的`getter` `setter`,在数据变动的时候,会被`observer`观察到,会通过`Dep`通知数据的订阅者watcher,之后进行相应的视图上面的变化。
https://juejin.im/entry/6844903479044112391
computed 原理、computed 和 watch 的区别
computed
定义:是一个计算属性,类似于过滤器,对绑定到 view 的数据进行处理
适用于重新计算比较费时不用重复数据计算的环境。所有 getter 和 setter 的 this 上下文自动地绑定为 Vue 实例。如果一个数据依赖于其他数据,那么把这个数据设计为 computed。
computed 的常规使用
代码语言:javascript复制 data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
//默认使用方式,读取并返回当前的属性值(无法去修改读取到的值)
fullNameone: function () {
return this.firstName ' ' this.lastName
},
//使用get 和 set 方去修改获取到的属性值
fullNameSed:{
get(){//回调函数 当需要读取当前属性值是执行,根据相关数据计算并返回当前属性的值
return this.firstName ' ' this.lastName
},
set(val){ //监视当前属性值的变化,当属性值发生变化时执行,更新相关的属性数据
//val就是fullName的最新属性值
console.log(val)
const names = val.split(' ');
console.log(names)
this.firstName = names[0];
this.lastName = names[1];
}
}
},
vue 中 computed 的传参:利用闭包:
代码语言:javascript复制computed: {
text() {
return function (index) {
return this.Ratedata[index].currentRate.toFixed(0) '%';
}
}
}
watch
定义:watch 是一个观察的动作
1.watch 监听数据变化
1>监听数据(单一数据)
代码语言:javascript复制data(){
return {
firstname:''
}
}
watch: {
firstname(newVal,oldVal){
this.fullname = newVal '-' this.lastname
}
}
2>监听对象的属性变化
1.直接通过 watch 的深度监听去监听对象的属性变化(deep 开启深度监听)
**deep
的意思就是深入观察,监听器会一层层的往下遍历,给对象的所有属性都加上这个监听器,但是这样性能开销就会非常大了,任何修改obj
里面任何一个属性都会触发这个监听器里的 handler。
new Vue({
data: {
formData: {
channel: "音乐",
style: "活泼",
},
},
watch: {
formData: {
handler(newValue, oldValue) {
//监听对象的所有属性(性能开销就会非常大)
console.log(newValue.channel); //返回的当前对象
},
deep: true,
},
},
});
优化,使用字符串形式监听,一层一层解析下去,直到遇到属性**a**
,然后才给**a**
设置监听函数
new Vue({
data: {
formData: {
channel: "音乐",
style: "活泼",
},
},
watch: {
"formData.channel": {
handler(newValue, oldValue) {
console.log(newValue); //返回的为当前对象的属性channel的值
},
},
},
});
3>初始化时就监听,由于初始化时 watch 不执行。
代码语言:javascript复制 watch: {
'defaultVal': {
immediate: true, // immediate选项可以开启首次赋值监听
handler (newVal, oldVal) {
if (newVal) {
console.log(newVal,oldVal)
this.selectVal = newVal
}
}
}
2.利用 computed 的属性(将对象的属性提取出来进行监听)
代码语言:javascript复制new Vue({
data: {
formData: {
channel: "音乐",
style: "活泼",
},
},
computed: {
channel() {
return this.formData.channel;
},
},
watch: {
channel(newValue, oldValue) {
console.log(newval);
},
},
});
2.watch 监听路由变化
代码语言:javascript复制watch: {
$route( to , from ){ // to要跳转到的路由的地址 , from 表示从哪跳转
console.log( to , from )
}
}
methods
- 可以使用 methods 来代替 computed,实际上效果是一样;
- 其中 methods【有括号()】,computed 不带括号;
- computed 是基于它的依赖缓存,只有相关依赖发生改变时才会重新取值;
- methods 在重新渲染的时候,函数总会重新调用执行;
- 使用 computed 会比 methods 方法性能更好。
总结:
1.如果一个数据依赖于其他数据,那么把这个数据设计为 computed 的 。 2.如果你需要在某个数据变化时做一些事情,使用 watch 来观察这个数据变化 3.如果需要实时调用,使用 methods 比 computed 更合适
computed 和 watch 的区别:
1.computed 是计算属性,watch 是监听,即观察属性。 2.comPuted 具有缓存性,页面重新渲染值不变化,计算属性会立即返回之前的计算结果,而不必再次执行函数。watch 每次都会去执行函数。 3.computed 是需要返回值的,且一般是对源数据进行过滤,为同步。watch 可监听异步请求返回的值。
相关文章:https://www.yuque.com/docs/share/e153d391-2487-4dc7-a442-6a1ef00e6f20?# 《vue 的 data,props,computed,watch 详解》
vue 编译器结构图、生命周期
生命周期详解:
- beforeCreate(初始化界面前)组件未创建(加载动画)
- created(初始化界面后)组件被创建了(获取数据)
- beforeMount(渲染 dom 前)组件挂载前
- mounted(渲染 dom 后)组件挂载(页面显示完成)
- beforeUpdate(更新数据前)组件更新前调用的函数
- updated(更新数据后)组件更新完毕
- beforeDestroy(卸载组件前)组件销毁之前
- destroyed(卸载组件后)组件销毁之后
异步请求数据时应该位于生命周期:
一般在 created
里面就可以,如果涉及到需要页面加载完成之后的话就用 mounted
。
nextTick
定义:nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 nextTick,则可以在回调中获取更新后的 DOM.使用场景:在 Vue 生命周期的created()钩子函数进行的 DOM 操作一定要放在Vue.nextTick()的回调函数中 。
vue 组件通信
父传子:
子传父:
同步异步父子组件生命周期顺序
在单一组件中,钩子的执行顺序是 beforeCreate-> created -> mounted->… ->destroyed,但当父子组件嵌套时,父组件和子组件各拥有各自独立的钩子函数,这些父子组件的这些钩子是如何交融执行,且执行顺序又是怎样的呢?
同步父子组件:
代码语言:javascript复制//同步组件引入方式
import Page from "@/components/page";
加载渲染过程
代码语言:javascript复制父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
更新过程
代码语言:javascript复制父beforeUpdate->子beforeUpdate->子updated->父updated
销毁过程
代码语言:javascript复制父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
常用钩子简易版
代码语言:javascript复制父create->子created->子mounted->父mounted
总结: > 同步父子组件渲染,更新,销毁的生命周期都是在当前父组件的生命周期中执行 父 beforeMount, 父 beforeUpdate,beforeDestory 后,中间插入子组件的生命周期,最后才使用 父 Mounted,父 updated,父 destroyed。
异步父子组件:
代码语言:javascript复制//异步组件引入
const Page = () => import("@/components/page");
const Page = (resolve) => require(["@/components/page"], page);
加载渲染过程
代码语言:javascript复制父beforeCreate->父created->父beforeMount->父mounted->父beforeUpdate->子beforeCreate->子created->子beforeMount->子mounted->父updated
更新过程(异步和同步相同)
代码语言:javascript复制父beforeUpdate->子beforeUpdate->子updated->父updated
销毁过程(异步和同步相同)
代码语言:javascript复制父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
总结: 异步父子组件的渲染,更新,销毁,更新和销毁的生命周期和同步组件相同,但是渲染与同步的不同, 父组件的
beforeCreate、created、beforeMount、mounted、``beforeUpdate (其中多了一步 update)
–>所有子组件的mounted
—-> 父组件的Updated
总结:
两种引入方式的不同之处在于:
同步引入时生命周期顺序为:父组件的
beforeCreate、created、beforeMount
–> 所有子组件的beforeCreate、created、beforeMount
–> 所有子组件的mounted
–> 父组件的mounted
异步引入时生命周期顺序:父组件的
beforeCreate、created、beforeMount、mounted
–> 子组件的beforeCreate、created、beforeMount、mounted
mvc、mvp、mvvm
MVC,MVP 和 MVVM 都是常见的软件架构设计模式(Architectural Pattern),它通过分离关注点来改进代码的组织方式。不同于设计模式(Design Pattern),只是为了解决一类问题而总结出的抽象方法,一种架构模式往往使用了多种设计模式。 要了解 MVC、MVP 和 MVVM,就要知道它们的相同点和不同点。不同部分是 C(Controller)、P(Presenter)、VM(View-Model),而相同的部分则是 MV(Model-View)。
Model(数据层):Model 层用于封装和应用程序的业务逻辑相关的数据以及对数据的处理方法。 View(视图层):主要负责数据的展示
MVC
MVC 模式的意思是,软件可以分成三个部分。
- 视图(View):用户界面。
- 控制器(Controller):业务逻辑
- 模型(Model):数据保存
各部分之间的通信方式如下
- MVC 是单向通信。也就是 View 跟 Model,必须通过 Controller 来承上启下。
- 实例化 View 并向对应的 Model 实例注册,当 Model 发生变化时就去通知 View 做更新,这里用到了观察者模式。
- Controller 非常薄,只起到路由的作用,而 View 非常厚,业务逻辑都部署在 View。
- 接受用户指令时,MVC 可以分成两种方式:
- 通过 View 接受指令,传递给 Controller。
- 直接通过 controller 接受指令。
MVP
定义:Model-View-Presenter, MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。
- 各部分之间的通信,都是双向的。
- View 与 Model 不发生联系,都通过 Presenter 传递。
- View 非常薄,不部署任何业务逻辑,称为”被动视图”(Passive View),即没有任何主动性,而 Presenter 非常厚,所有逻辑都部署在 Presenter。
MVVM:
定义:指的是 Model - View -ViewModel 的简写,即模型-视图-视图模型 MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。唯一的区别是,它采用双向绑定(data-binding):View 的变动,自动反映在 ViewModel,反之亦然。
优点:
1、 提高可维护性 。解决了 MVP 大量的手动 View 和 Model 同步的问题,提供双向绑定机制。2、 简化测试。 因为同步逻辑是交由 Binder 做的,View 跟着 Model 同时变更,所以只需要保证 Model 的正确性,View 就正确。大大减少了对 View 同步更新的测试。
缺点:
1、过于简单的图形界面不适用,或说牛刀杀鸡。2、对于大型的图形应用程序,视图状态较多,ViewModel 的构建和维护的成本都会比较高。3、数据绑定的声明是指令式地写在 View 的模版当中的,这些内容是没办法去打断点 debug 的。
MVVM 模式及与 MVP 和 MVC 的区别
MVC 和 MVP 的关系
- 相同点:MVP 是从经典的模式 MVC 演变而来,它们的基本思想有相通的地方:Controller/Presenter 负责逻辑的处理,Model 提供数 据,View 负责显示。
- 不同点:在 MVP 中 View 并不直接使用 Model,它们之间的通信是通过 Presenter (MVC 中的 Controller)来进行的,所有的交互都发生在 Presenter 内部,而在 MVC 中 View 会直接从 Model 中读取数据而不是通过 Controller。MVC 全部是单项通信,而 MVP 是双向通信。
MVVM 和 MVP 的关系
- 相同点:MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。 唯一的区别是,
- 不同点:MVVM 采用双向绑定(data-binding):View 的变动,自动反映在 ViewModel,反之亦然。这样 开发者就不用处理接收事件和 View 更新的工作。MVVM 把 View 和 Model 的同步逻辑自动化了。以前 Presenter 负责的 View 和 Model 同步不再手动地进行操作,而是交给框架所提供的数据绑定功能进行负责,只需要告诉它 View 显示的数据对应的是 Model 哪一部分即可。简化了业务与界面的依赖,还解决了数据频繁更新的问题(以前用 jQuery 操作 DOM 很繁琐)。
vue 数据双向绑定原理
在 Vue 中,使用了双向绑定技术(Two-Way-Data-Binding),就是 View 的变化能实时让 Model 发生变化,而 Model 的变化也能实时更新到 View。
不同的 MVVM 框架中,实现双向数据绑定的技术有所不同。目前一些主流的前端框架实现数据绑定的方式大致有以下几种:
- 数据劫持 (Vue)
- 发布-订阅模式 (Knockout、Backbone)
- 脏值检查 (Angular)
我们这里主要讲讲Vue 的双向绑定:
Vue 采用数据劫持&发布-订阅模式的方式,通过 ES5 提供的 Object.defineProperty()
方法来劫持(监控)各属性的 getter
、setter
,并在数据(对象)发生变动时通知订阅者,触发相应的监听回调。并且,由于是在不同的数据上触发同步,可以精确的将变更发送给绑定的视图,而不是对所有的数据都执行一次检测。 要实现 Vue 中的双向数据绑定,大致可以划分三个模块:Observer、Compile、Watcher,如图:
- Observer 数据监听器 负责对数据对象的所有属性进行监听(数据劫持),监听到数据发生变化后通知订阅者。
- Compiler 指令解析器 扫描模板,并对指令进行解析,然后绑定指定事件。
- Watcher 订阅者 关联 Observer 和 Compile,能够订阅并收到属性变动的通知,执行指令绑定的相应操作,更新视图。Update()是它自身的一个方法,用于执行 Compile 中绑定的回调,更新视图。
数据劫持
一般对数据的劫持都是通过 Object.defineProperty 方法进行的,Vue 中对应的函数为 defineReactive
,其普通对象的劫持的精简版代码如下:
从 MVC –> MVP –> MVVM
vue 的单项数据流:
• Vue 是单向数据流,不是双向绑定 • Vue 的双向绑定不过是语法糖 • Object.definePropert 是劫持属性的 get 和 set 用来做响应式更新的。
vue 还是单向数据流,v-model 只不过是语法糖,它是:value=”sth”和@change=”val => sth = val”的简写形式。
Vue 是怎么实现数据响应式更新的:
面试官期望听到的回答是:通过 Object.defineProperty()的 get 和 set 方法来实现响应式更新。
Vue 的虚拟 Dom
浏览器解析一个 html 大致分为五步: 创建 DOM tree –> 创建 Style Rules -> 构建 Render tree -> 布局 Layout –> 绘制 Painting。
每次对真实 dom 进行操作的时候,浏览器都会从构建 dom 树开始从头到尾执行一遍流程。真实的 dom 操作代价昂贵,操作频繁还会引起页面卡顿影响用户体验,虚拟 dom 就是为了解决这个浏览器性能问题才被创造出来 虚拟 dom 在执行 dom 的更新操作后,虚拟 dom 不会直接操作真实 dom,而是将更新的 diff 内容保存到本地 js 对象中,然后一次性 attach 到 dom 树上,通知浏览器进行 dom 绘制避免大量无谓的计算。
简单总结:虚拟 DOM 是将真实的 DOM 节点用 JavaScript 模拟出来,将 DOM 变化的对比,放到 Js 层来做。
你的知道浏览器的虚拟 DOM 与真实 DOM 的区别
(注意:需不需要虚拟 DOM,其实与框架的 DOM 操作机制有关):
- 虚拟 DOM 不会进行排版与重绘操作
- 虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分(注意!),最后并在真实 DOM 中进行排版与重绘,减少过多 DOM 节点排版与重绘损耗
- 真实 DOM 频繁排版与重绘的效率是相当低的
- 虚拟 DOM 有效降低大面积(真实 DOM 节点)的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部(同 2)
使用虚拟 DOM 的损耗计算:
代码语言:javascript复制总损耗 = 虚拟DOM增删改 (与Diff算法效率有关)真实DOM差异增删改 (较少的节点)排版与重绘
直接使用真实 DOM 的损耗计算:
代码语言:javascript复制总损耗 = 真实DOM完全增删改 (可能较多的节点)排版与重绘
总之,一切为了减弱频繁的大面积重绘引发的性能问题,不同框架不一定需要虚拟 DOM,关键看框架是否频繁会引发大面积的 DOM 操作
vue 的 diff 是算法
在采取 diff 算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。Vue 的 diff 算法是仅在同级的 vnode 间做 diff,递归地进行同级 vnode 的 diff,最终实现整个 DOM 树的更新。因为跨层级的操作是非常少的,忽略不计,这样时间复杂度就从 O(n3)变成 O(n)。
JavaScript:回流(重排)与重绘
回流(元素的布局位置,规模尺寸,隐藏等改变)
回流当 render tree 中的一部分或全部因为元素的规模尺寸、布局、隐藏等改变时,浏览器重新渲染部分 DOM 或全部 DOM 的过程。回流也被称为重排,其实从字面上来看,重排更容易让人形象易懂(即重新排版整个页面)。
重绘(不改变元素的位置,只改变元素的样式)
当页面元素样式改变不影响元素在文档流中的位置时(如 background-color,border-color,visibility),浏览器只会将新样式赋予元素并进行重新绘制操作。
回流必将引起重绘,而重绘不一定会引起回流。
如何减少回流、重绘
CSS 中避免回流、重绘
1.尽可能在 DOM 树的最末端改变 class 2.避免设置多层内联样式 3.动画效果应用到 position 属性为 absolute 或 fixed 的元素上 4.避免使用 table 布局 5.使用 css3 硬件加速,可以让 transform、opacity、filters 等动画效果不会引起回流重绘
JS 操作避免回流、重绘
1.避免使用 JS 一个样式修改完接着改下一个样式,最好一次性更改 CSS 样式,或者将样式列表定义为 class 的名称 2.避免频繁操作 DOM,使用文档片段创建一个子树,然后再拷贝到文档中 3.先隐藏元素,进行修改后再显示该元素,因为 display:none 上的 DOM 操作不会引发回流和重绘 4.避免循环读取 offsetLeft 等属性,在循环之前把它们存起来 5.对于复杂动画效果,使用绝对定位让其脱离文档流,否则会引起父元素及后续元素大量的回流
总结:
回流在浏览器中属于一种用户主导的操作,所以知道如何去改进回流时间以及知道各种文档属性(DOM 节点深度,css 的渲染效率,各种各样的样式改变)对回流时间的影响对于前端开发来讲是很有帮助的。有时即便是回流一个单一的元素,也可能要求它的父元素以及任何跟随它的元素也产生回流。例如需要改变某个元素的背景,这就不涉及该元素的属性,所以只发生重绘。
vuex
定义:vue.js 应用程序开发的状态管理模式
vuex 五大核心属性:state,getter,mutation,action,module
- state:存储数据,存储状态;在根实例中注册了 store 后,用 this.$store.state 访问数据。
- getters: store 的计算属性,它的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算,多用于过滤 state 的数据,用 this.$store.getter 访问数据。
- mutation:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。用 this.$store.commit(‘add’, { amount: 10 })来提交数据。
- action:包含任意异步操作,通过提交 mutation 方法,通过 mutation 来改变 state。通过 this.$store.dispatch(‘addCountAction’)发起异步。
- module:将 store 分割成模块,每个模块都具有 state、mutation、action、getter、甚至是嵌套子模块。
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
├── state.js # 根级别的 state
└── modules
├── module1.js # 模块1的state树
└── module2.js # 模块2的state树
module1.js
代码语言:javascript复制import { WIDTH_ADD } from "@/store/mutation-types.js"; // 引入事件类型
export default {
namespaced: true,
state: {
width: 100,
height: 50,
},
getters: {
modulegGetWidth(state, getters, rootState) {
return state.width;
},
modulegetHeight(state, getters, rootState) {
return state.height;
},
},
mutations: {
[WIDTH_ADD](state) {
// 使用常量替代 Mutation 事件类型
return state.width ;
},
addHeight(state) {
// 不使用常量
return state.height ;
},
},
actions: {},
};
index.js
代码语言:javascript复制import state from "./state";
import getters from "./getters";
import mutations from "./mutations";
import actions from "./actions";
import module1 from "./modules/module1.js"; // 引入module1
export default {
state,
getters,
mutations,
actions,
modules: {
module1, // 注册完成
},
};
vue-router
定义:简单来说路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源(请求不同的页面是路由的其中一种功能)。
异步交互体验的更高级版本就是 SPA —— 单页应用。单页应用不仅仅是在页面交互是无刷新的,连页面跳转都是无刷新的,为了实现单页应用,所以就有了前端路由。
前端路由
1. hash 模式
改变 url 的情况下,保证页面的不刷新。后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。另外每次 hash 值的变化,还会触发hashchange
这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。然后我们便可以监听hashchange
来实现更新页面部分内容的操作:
hash 模式背后的原理是**onhashchange**
事件,可以在**window**
对象上监听这个事件。
//www.xxx.com/#/login
http: function matchAndUpdate() {
// todo 匹配 hash 做 dom 更新操作
}
window.addEventListener("hashchange", matchAndUpdate);
//onhashChange
window.onhashchange = function (event) {
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
document.body.style.color = hash;
};
2. history 模式
因为 HTML5 标准发布,多了两个 API,pushState()
和 replaceState()。
通过这两个 API (1)可以改变 url 地址且不会发送请求,(2)不仅可以读取历史记录栈,还可以对浏览器历史记录栈进行修改。
同时还有popstate
事件。通过这些就能用另一种方式来实现前端路由了,但原理都是跟 hash 实现相同的。
function matchAndUpdate() {
// todo 匹配路径 做 dom 更新操作
}
window.addEventListener("popstate", matchAndUpdate);
window.history.pushState(stateObject, title, URL);
window.history.replaceState(stateObject, title, URL);
用了 HTML5 的实现,单页路由的 url 就不会多出一个#,变得更加美观。但因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。
区别:
- 前面的 hashchange,你只能改变#后面的 url 片段。而 pushState 设置的新 URL 可以是与当前 URL 同源的任意 URL。
- history 模式则会将 URL 修改得就和正常请求后端的 URL 一样,如后端没有配置对应/user/id 的路由处理,则会返回 404 错误
当用户刷新页面之类的操作时,浏览器会给服务器发送请求,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。
vue 路由传参方式
1.params 传参(刷新参数会丢失):
name 与 params 结合使用 相当于 post。 传参方式:this.$router.push({name:’路由命名’,params:{参数名:参数值,参数名:参数值}}) url 展现方式:/detail
代码语言:javascript复制//父组件传参
this.$router.push({
name: "Describe",
params: {
id: id,
},
});
//子组件接收
this.$route.params.id;
2.query 传参:(刷新不会丢失参数)
path 与 query 结合使用,相当于 get,参数会显示在地址栏里 传参方式:this.$router.push({path:’路由命名’, query:{ 参数名:参数值 } }) url 展现方式:/detail?id=1&user=123&identity=1
代码语言:javascript复制//父组件传值
this.$router.push({
path: "/describe",
query: {
id: id,
},
});
//子组件
this.$route.query.id;
3.params 动态路由传参(刷新不会丢失参数):
参数会显示在地址栏 **this.$router.push({path:’路由命名’, params:{ 参数名:参数值 } })url 展现方式:/detail/3/1
父组件:
代码语言:javascript复制 //父组件
this.$router.push({
path: '/describe',
params: {
id: id
}
})
//路由写法
{
path: '/describe/:id',
name: 'Describe',
component: Describe
}
//子组件
this.$route.params.id
4.router-link 传参
1.路径:http://localhost:8081/#/test?name=1(jquery 和 params 的结合)
代码语言:javascript复制//(id是参数)
<router-link :to="{path:'/test',query: {name: id}, params:{id:id}}">跳转</router-link>
//使用
this.$route.query.id
this.$route.params.id
2.路径:http://localhost:8081/#/test/1(相当一动态路由传参)
代码语言:javascript复制//(id是参数)
<router-link :to="'/test/' id">跳转</router-link>
//路由
{
path:'/test/:id/',
name:'Test',
component:require('./components/Test.vue')
}
//使用
this.$route.params.id // (这个id给上图路由的配置有关)
注意:router-link 中链接如果是‘/’开始就是从根路由开始,如果开始不带‘/’,则从当前路由开始
5.vuex 传参
页面刷新 store.state 中的数据消失是不可避免的,那么使用 localStorage 或者 sessionStorage 来避免这个问题。如果是一个一个数据添加实在是太繁琐了。那么就需要一个全局的方法来,将 store 的数据存储在 localStorage 里。 App.vue
代码语言:javascript复制created: {
//在页面刷新时将vuex里的信息保存到localStorage里
window.addEventListener("beforeunload", () => {
localStorage.setItem("messageStore", JSON.stringify(this.$store.state));
});
//在页面加载时读取localStorage里的状态信息
localStorage.getItem("messageStore") &&
this.$store.replaceState(
Object.assign(
this.$store.state,
JSON.parse(localStorage.getItem("messageStore"))
)
);
}
$router 和 $route 区别
1>router 和 route 的区别
- router:router 为 VueRouter 实例(路由实例),是路由的操作对象,对象包括了路由的跳转方法,钩子函数等。
- route:route 为当前 router 跳转对象, 包含路由信息对象, 只读对象, 包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。
//1、手写完整的 path:
this.$router.push({ path: `/user/${userId}` });
this.$route.params.userId;
//2、用 params 传递(url 不带参数,http:localhost:8080/#/user):
this.$router.push({ name: "user", params: { userId: "123" } });
this.$route.params.userId;
//3、用 query 传递(url 带参数:http:localhost:8080/#/user?userId=123):
this.$router.push({ path: "/user", query: { userId: "123" } });
this.$route.query.userId;
//
this.$router.back();
vue 的首屏优化
一、减小 chunk-vendors.js 体积
解决方式: 1、路由懒加载
代码语言:javascript复制const Home = () => import(/* webpackChunkName: "home" */ '@/views/home');
const Design = () => import(/* webpackChunkName: "design" */ '@/views/design');
const HotZone = () => import(/* webpackChunkName: "hot-zone" */ '@/views/hot-zone');
2、lodash 按需加载
代码语言:javascript复制import _ from 'lodash'; // 或 import lodash from 'lodash'
// 改成
import flow from 'lodash/flow';
3、gd-components 组件按需引入
安装babel-plugin-import
插件,修改babel.config.js
文件
module.exports = {
presets: ['@vue/app'],
plugins: [
['import', {
"libraryName": '@gaoding/gd-components',
"libraryDirectory": 'lib/components'
}]
]
};
4.对 css 和 js 进行压缩
二、DNS
预解析
代码语言:javascript复制<meta http-equiv="x-dns-prefetch-control" content="on" />
<link rel="dns-prefetch" href="//retcode.alicdn.com">
<link rel="dns-prefetch" href="//st-gdx.dancf.com">
<link rel="dns-prefetch" href="//alicdn.huanleguang.com">
关于这点,底下有详细的文章介绍,有兴趣的朋友可以点进去了解,这边不多介绍。
三、图片懒加载
四、减少 dom 数量和请求数
由图片 lazy load,我们自然就考虑到了 dom 也可以 lazy load,所以把之前图片懒加载的方式延伸一下,改成 dom lazy load,同时对应的请求也放到后面再执行。
优化前的节点数和请求数:
可以看到请求数和 dom 节点数都已经大幅度的减少。
具体实现的原理很简单,也是利用IntersectionObserver api
, 当 dom 刚刚进入视口再开始请求和渲染,对应的代码实现。
lazyLoadTemplate() {
this.intersectionObservers = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.intersectionRatio < this.ratio) return;
const id = e.target.dataset.id;
this.loaded[id] = true;
const funcName = `load${firstUpperCase(id)}`;
this[funcName] && this[funcName]();
this.intersectionObservers.unobserve(e.target);
});
}, {
threshold: [0, 0.1, 0.25, 1]
});
const sections = this.$el.querySelectorAll('.home-sections');
[...sections].forEach(item => {
this.intersectionObservers.observe(item);
});
},
五、 SSR
与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:
- 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
- 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。通常可以产生更好的用户体验,并且对于那些「内容到达时间(time-to-content) 与转化率直接相关」的应用程序而言,服务器端渲染 (SSR) 至关重要。
服务端渲染实现原理机制:在服务端拿数据进行解析渲染,直接生成 html 片段返回给前端。然后前端可以通过解析后端返回的 html 片段到前端页面,大致有以下两种形式: 1、服务器通过模版引擎直接渲染整个页面,例如 java 后端的 vm 模版引擎,php 后端的 smarty 模版引擎。 2、服务渲染生成 html 代码块, 前端通过 AJAX 获取然后使用 js 动态添加。
作者:wqzwh 链接:https://juejin.im/post/6844903545448169479 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
六.预渲染(prerender-spa-plugin)
我们知道 SPA 有很多优点,不过一个缺点就是对(不是 Google 的)愚蠢的搜索引擎的 SEO 不友好,为了照顾这些引擎,目前主要有两个方案:服务端渲染(Server Side Rendering)、预渲染(Prerending)。
如果你只需要改善少数页面(例如 /
, /about
, /contact
等)的 SEO
,那么你可能需要预渲染。无需使用 web 服务器实时动态编译 HTML (服务端渲染, SSR),而是使用预渲染方式,在构建时(build time)简单地生成针对特定路由的静态 HTML 文件。它主要使用 prerender-spa-plugin 插件,其与 SSR 一样都可以加快页面的加载速度,并且侵入性更小,在已上线的项目稍加改动也可以轻松引入预渲染机制,而 SSR 方案则需要将整个项目结构推翻;
访问预渲染出来的页面在访问时与SSR
一样快,并且它将服务端编译 HTML 的时机提前到了构建时,因此也降低了服务端的压力,如果你的服务器跟我的一样买的 1M1G1 核 的小水管服务器 ( 穷 ),那么预渲染可能更适合你。不过 SSR 和预渲染的使用场景还是有较明显的区别的。预渲染的使用场景更多是简单的静态页面。服务端渲染适用于复杂、较大型、与服务端交互频繁的功能型网站,比如电商网站。
最后如果希望进一步优化生成出来页面的 SEO,可以配合 vue-meta-info 这个网上有很多文章,就不赘述了
作者:SHERlocked93 链接:https://juejin.im/post/6844903668488093704 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
七.服务端开启 gzip 压缩
8.app.js 分包
9.首页加 loading 或 骨架屏 (仅仅是优化体验)
webpack 模块
webpack 的优点:
- 代码转换:
typeScript
编译成javaScript
、scss,less
编译成css
. - 文件优化:压缩
javaScript
、css
、html
代码,压缩合并图片。 - 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
- 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
- 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。
- 扩展性强,插件机制完善。
webpack 打包过程:
1.利用 babel 完成代码转换,并生成单个文件的依赖 2.从入口开始递归分析,并生成依赖图谱 3.将各个引用模块打包为一个立即执行函数 4.将最终的 bundle 文件写入 bundle.js 中
Webpack 的四大核心:
- entry:js 入口源文件
- output:生成文件
- loader:进行文件处理
- plugins:插件,比 loader 更强大,能使用更多 webpack 的 api
Entry
webpack 应该使用哪个模块做为入口文件,来作为构建其内部依赖图的开始。进去入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的,每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。 单⼊⼝:entry 是⼀个字符串
代码语言:javascript复制module.exports = {
entry: './src/index.js'
}
多⼊⼝:entry 是⼀个对象
代码语言:javascript复制module.exports = {
entry: {
index: './src/index.js',
manager: './src/manager.js'
}
}
Output
告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,这些都可以在 webpack 的配置文件中指定。 单⼊⼝配置
代码语言:javascript复制module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js’,
path: __dirname '/dist'
}
};
多⼊⼝配置
代码语言:javascript复制module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name].js',
path: __dirname '/dist'
}
}
通过[name]占位符确保⽂件名称的唯⼀
Loader
loader
让 webpack
能够去处理那些非 javaScript
文件(**webpack**
** 自身只理解 `javaScript)。
loader` 可以将所有类型的文件转换为 `webpack` 能够处理的有效模块**,然后你就可以利用 webpack
的打包能力,对它们进行处理。
loader 的特点
- 一个 Loader 的职责是单一的,只需要完成一种转换
- 一个 Loader 其实就是一个 Node.js 模块,这个模块需要导出一个函数
- loader 总是从右到左地被调用。
常用的 loader
处理样式
**css-loader**
: 加载.css 文件,**style-loader**
:使用 style 标签将 **`css-loader` 内部样式注入到我们的 html 页面****less-loader, sass-loader**
: 解析 css 预处理器
处理 js
- 让你能使用最新的 js 代码(ES6,ES7…)
- 让你能使用基于 js 进行了拓展的语言,比如 React 的 JSX;
处理文件
处理图片资源时,我们常用的两种 loader 是file-loader
或者url-loader
,两者的主要差异在于。url-loader
可以设置图片大小限制,当图片超过限制时,其表现行为等同于file-loader
,而当图片不超过限制时,则会将图片以base64
的形式打包进 css 文件,以减少请求次数
处理.vue 文件
vue-loader
是 webpack
的加载器模块,它使我们可以用 .vue
文件格式编写单文件组件。单文件组件文件有三个部分,即模板、脚本和样式。 vue-loader
模块允许 webpack
使用单独的加载器模块(例如 sass 或 scss 加载器
)提取和处理每个部分。该设置使我们可以使用 .vue
文件无缝编写程序。
开发一个 loader
需求:手写一个 loader
,将 'kobe'
转换成 'Black Mamba'
。当然大家可以根据自己的需求进行设计。这里只是讲解方法。
1、编写 loader
在根目录下,新建目录 kobe-loader
作为我们编写 loader
的名称,执行 npm init -y
命令,新建一个模块化项目,然后新建 index.js
文件,相关源码如下:
module.exports = function(content) {
return content && content.replace(/kobe/gi, 'Black Mamba')
}
2、注册模块
正常我们安装的 loader
是从 npm
下载安装,但是我们可以在 kobe-loader
目录底下使用 npm link
做到在不发布模块的情况下,将本地的一个正在开发的模块的源码链接到项目的 node_modules
目录下,让项目可以直接使用本地的 npm
模块。
npm link
然后在项目根目录执行以下命令,将注册到全局的本地 npm
模块链接到项目的 node_modules
下
$ npm link kobe-loader
注册成功后,我们可以在 node_modules
目录下能查找到对应的 loader
。
3、在 webpack 中配置 loader
在 webpack.base.conf.js
加上如下配置
{
test:/.js/,
loader: 'kobe-loader'
}
此时,我们在所有 js 文件下书写的 'kobe'
就全部替换成 'Black Mamba'
了。
4、配置参数
上面我们是写死的替换文案,假如我想通过配置项来改变,可以在 loader 中做以下调整
代码语言:javascript复制// custom-loader/index.js
var utils = require('loader-utils')
module.exports = function (content) {
const options = utils.getOptions(this)
return content && content.replace(/kobe/gi, options.name)
}
// webpack.base.conf.js
{
test:/.js/,
use: {
loader: 'kobe-loader',
options: {
name: 'kobe',
}
}
}
Plugin
专注处理 webpack 在编译过程中的某个特定的任务的功能模块,可以称为插件。
Plugin 的特点
- 是一个独立的模块
- 模块对外暴露一个 js 函数
- 函数的原型
(prototype)
上定义了一个注入compiler
对象的apply
方法apply
函数中需要有通过compiler
对象挂载的webpack
事件钩子,钩子的回调中能拿到当前编译的compilation
对象,如果是异步编译插件的话可以拿到回调callback
- 完成自定义子编译流程并处理
complition
对象的内部数据 - 如果异步编译插件的话,数据处理完成后执行
callback
回调。
常用 Plugin
HotModuleReplacementPlugin
代码热替换。因为Hot-Module-Replacement
的热更新是依赖于webpack-dev-server
,后者是在打包文件改变时更新打包文件或者 reload 刷新整个页面,HRM
是只更新修改的部分。HtmlWebpackPlugin
, 生成 html 文件。将 webpack 中entry
配置的相关入口 chunk 和extract-text-webpack-plugin
抽取的 css 样式 插入到该插件提供的template
或者templateContent
配置项指定的内容基础上生成一个 html 文件,具体插入方式是将样式link
插入到head
元素中,script
插入到head
或者body
中。ExtractTextPlugin
, 将 css 成生文件,而非内联 。该插件的主要是为了抽离 css 样式,防止将样式打包在 js 中引起页面样式加载错乱的现象。NoErrorsPlugin
报错但不退出 webpack 进程UglifyJsPlugin
,代码丑化,开发过程中不建议打开。uglifyJsPlugin
用来对 js 文件进行压缩,从而减小 js 文件的大小,加速 load 速度。uglifyJsPlugin
会拖慢 webpack 的编译速度,所有建议在开发简单将其关闭,部署的时候再将其打开。多个 html 共用一个 js 文件(chunk),可用CommonsChunkPlugin
purifycss-webpack
。打包编译时,可剔除页面和 js 中未被使用的 css,这样使用第三方的类库时,只加载被使用的类,大大减小 css 体积optimize-css-assets-webpack-plugin
压缩 css,优化 css 结构,利于网页加载和渲染webpack-parallel-uglify-plugin
可以并行运行 UglifyJS 插件,这可以有效减少构建时间
开发一个 plugin
- Webpack 在编译过程中,会广播很多事件,例如 run、compile、done、fail 等等,可以查看官网;
- Webpack 的事件流机制应用了观察者模式,我们编写的插件可以监听 Webpack 事件来触发对应的处理逻辑;
- 插件中可以使用很多 Webpack 提供的 API,例如读取输出资源、代码块、模块及依赖等;
1、编写插件
在根目录下,新建目录 my-plugin 作为我们编写插件的名称,执行 npm init -y 命令,新建一个模块化项目,然后新建 index.js 文件,相关源码如下:
代码语言:javascript复制class MyPlugin {
constructor(doneCallback, failCallback) {
// 保存在创建插件实例时传入的回调函数
this.doneCallback = doneCallback
this.failCallback = failCallback
}
apply(compiler) {
// 成功完成一次完整的编译和输出流程时,会触发 done 事件
compiler.plugin('done', stats => {
this.doneCallback(stats)
})
// 在编译和输出的流程中遇到异常时,会触发 failed 事件
compiler.plugin('failed', err => {
this.failCallback(err)
})
}
}
module.exports = MyPlugin
2、注册模块
按照以上的方法,我们在 my-plugin 目录底下使用 npm link 做到在不发布模块的情况下,将本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 npm 模块。
代码语言:javascript复制npm link
然后在项目根目录执行以下命令,将注册到全局的本地 npm 模块链接到项目的 node_modules 下
代码语言:javascript复制$ npm link my-plugin
注册成功后,我们可以在 node_modules 目录下能查找到对应的插件了。
3、配置插件
在 webpack.base.conf.js 加上如下配置
代码语言:javascript复制plugins: [
new MyPlugin(
stats => {
console.info('编译成功!')
},
err => {
console.error('编译失败!')
}
)
]
执行运行 or 编译命令,就能看到我们的 plugin 起作用了。
loader 是一个编译器,
loader
让webpack
能够去处理那些非javaScript
文件(**webpack**
** 自身只理解 `javaScript)。
loader` 可以将所有类型的文件转换为 `webpack` 能够处理的有效模块**,loader,它是一个转换器,将 A 文件进行编译成 B 文件,比如:将 A.less 转换为 A.css,单纯的文件转换过程。
plugin 是一个扩展器,它丰富了 webpack 本身,针对是 loader 结束后,webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务
文章引用: https://juejin.im/post/6844904113914773518 https://juejin.im/post/6844903480126078989