Vue3.0 为什么要用 proxy?
在 Vue2 中,0bject.defineProperty 会扭转原始数据,而 Proxy 是创建对象的虚构示意,并提供 set、get 和 deleteProperty 等处理器,这些处理器可在拜访或批改原始对象上的属性时进行拦挡,有以下特点∶
- 不需用应用
Vue.$set
或Vue.$delete
触发响应式。 - 全方位的数组变化检测,打消了 Vue2 有效的边界状况。
- 反对 Map,Set,WeakMap 和 WeakSet。
Proxy 实现的响应式原理与 Vue2 的实现原理雷同,实现形式大同小异∶
- get 收集依赖
- Set、delete 等触发依赖
- 对于汇合类型,就是对汇合对象的办法做一层包装:原办法执行后执行依赖相干的收集或触发逻辑。
说说你对 slot 的了解?slot 应用场景有哪些
一、slot 是什么
在 HTML 中 slot
元素,作为 Web Components
技术套件的一部分,是 Web 组件内的一个占位符
该占位符能够在前期应用本人的标记语言填充
举个栗子
<template id="element-details-template">
<slot name="element-name">Slot template</slot>
</template>
<element-details>
<span slot="element-name">1</span>
</element-details>
<element-details>
<span slot="element-name">2</span>
</element-details>
template
不会展现到页面中,须要用先获取它的援用,而后增加到 DOM
中,
customElements.define('element-details',
class extends HTMLElement {constructor() {super();
const template = document
.getElementById('element-details-template')
.content;
const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(template.cloneNode(true));
}
})
在 Vue
中的概念也是如此
Slot
艺名插槽,花名“占坑”,咱们能够了解为 solt
在组件模板中占好了地位,当应用该组件标签时候,组件标签外面的内容就会主动填坑(替换组件模板中 slot
地位),作为承载散发内容的进口
二、应用场景
通过插槽能够让用户能够拓展组件,去更好地复用组件和对其做定制化解决
如果父组件在应用到一个复用组件的时候,获取这个组件在不同的中央有大量的更改,如果去重写组件是一件不明智的事件
通过 slot
插槽向组件外部指定地位传递内容,实现这个复用组件在不同场景的利用
比方布局组件、表格列、下拉选、弹框显示内容等
应用 vue 渲染大量数据时应该怎么优化?说下你的思路!
剖析
企业级我的项目中渲染大量数据的状况比拟常见,因而这是一道十分好的综合实际题目。
答复
- 在大型企业级我的项目中常常须要渲染大量数据,此时很容易呈现卡顿的状况。比方大数据量的表格、树
- 解决时要依据状况做不同解决:
- 能够采取分页的形式获取,防止渲染大量数据
- vue-virtual-scroller (opens new window)等虚构滚动计划,只渲染视口范畴内的数据
- 如果不须要更新,能够应用 v -once 形式只渲染一次
- 通过 v -memo (opens new window)能够缓存后果,联合
v-for
应用,防止数据变动时不必要的VNode
创立 - 能够采纳懒加载形式,在用户须要的时候再加载数据,比方
tree
组件子树的懒加载 - 还是要看具体需要,首先从设计上防止大数据获取和渲染;切实须要这样做能够采纳虚表的形式优化渲染;最初优化更新,如果不须要更新能够
v-once
解决,须要更新能够v-memo
进一步优化大数据更新性能。其余能够采纳的是交互方式优化,无线滚动、懒加载等计划
scoped 款式穿透
scoped
尽管防止了组件间款式净化,然而很多时候咱们须要批改组件中的某个款式,然而又不想去除scoped
属性
- 应用
/deep/
<!-- Parent -->
<template>
<div class="wrap">
<Child />
</div>
</template>
<style lang="scss" scoped>
.wrap /deep/ .box{background: red;}
</style>
<!-- Child -->
<template>
<div class="box"></div>
</template>
- 应用两个
style
标签
<!-- Parent -->
<template>
<div class="wrap">
<Child />
</div>
</template>
<style lang="scss" scoped>
/* 其余款式 */
</style>
<style lang="scss">
.wrap .box{background: red;}
</style>
<!-- Child -->
<template>
<div class="box"></div>
</template>
Vue 中 v -html 会导致哪些问题
- 可能会导致
xss
攻打 v-html
会替换掉标签外部的子元素
let template = require('vue-template-compiler');
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`)
// with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})}
console.log(r.render);
// _c 定义在 core/instance/render.js
// _s 定义在 core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {if (vnode.children) vnode.children.length = 0
if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property
if (elm.childNodes.length === 1) {elm.removeChild(elm.childNodes[0])
}
}
如果让你从零开始写一个 vuex,说说你的思路
思路剖析
这个题目很有难度,首先思考 vuex
解决的问题:存储用户全局状态并提供治理状态 API。
vuex
需要剖析- 如何实现这些需要
答复范例
- 官网说
vuex
是一个状态管理模式和库,并确保这些状态以可预期的形式变更。可见要实现一个vuex
- 要实现一个
Store
存储全局状态 - 要提供批改状态所需 API:
commit(type, payload), dispatch(type, payload)
- 实现
Store
时,能够定义Store
类,构造函数接管选项options
,设置属性state
对外裸露状态,提供commit
和dispatch
批改属性state
。这里须要设置state
为响应式对象,同时将Store
定义为一个Vue
插件 commit(type, payload)
办法中能够获取用户传入mutations
并执行它,这样能够按用户提供的办法批改状态。dispatch(type, payload)
相似,但须要留神它可能是异步的,须要返回一个Promise
给用户以解决异步后果
实际
Store
的实现:
class Store {constructor(options) {this.state = reactive(options.state)
this.options = options
}
commit(type, payload) {this.options.mutations[type].call(this, this.state, payload)
}
}
vuex 简易版
/**
* 1 实现插件,挂载 $store
* 2 实现 store
*/
let Vue;
class Store {constructor(options) {
// state 响应式解决
// 内部拜访:this.$store.state.***
// 第一种写法
// this.state = new Vue({
// data: options.state
// })
// 第二种写法:避免外界间接接触外部 vue 实例,避免内部强行变更
this._vm = new Vue({
data: {$$state: options.state}
})
this._mutations = options.mutations
this._actions = options.actions
this.getters = {}
options.getters && this.handleGetters(options.getters)
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}
get state () {return this._vm._data.$$state}
set state (val) {return new Error('Please use replaceState to reset state')
}
handleGetters (getters) {Object.keys(getters).map(key => {
Object.defineProperty(this.getters, key, {get: () => getters[key](this.state)
})
})
}
commit (type, payload) {let entry = this._mutations[type]
if (!entry) {return new Error(`${type} is not defined`)
}
entry(this.state, payload)
}
dispatch (type, payload) {let entry = this._actions[type]
if (!entry) {return new Error(`${type} is not defined`)
}
entry(this, payload)
}
}
const install = (_Vue) => {
Vue = _Vue
Vue.mixin({beforeCreate () {if (this.$options.store) {Vue.prototype.$store = this.$options.store}
},
})
}
export default {Store, install}
验证形式
import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)
export default new Vuex.Store({
state: {counter: 0},
mutations: {
// state 从哪里来的
add (state) {state.counter++}
},
getters: {doubleCounter (state) {return state.counter * 2}
},
actions: {add ({ commit}) {setTimeout(() => {commit('add')
}, 1000)
}
},
modules: {}})
参考 前端进阶面试题具体解答
Vue 与 Angular 以及 React 的区别?
Vue 与 AngularJS 的区别
Angular
采纳TypeScript
开发, 而Vue
能够应用javascript
也能够应用TypeScript
AngularJS
依赖对数据做脏查看,所以Watcher
越多越慢;Vue.js
应用基于依赖追踪的察看并且应用异步队列更新,所有的数据都是独立触发的。AngularJS
社区欠缺,Vue
的学习老本较小
Vue 与 React 的区别
相同点:
Virtual DOM
。其中最大的一个相似之处就是都应用了Virtual DOM
。(当然Vue
是在Vue2.x
才援用的)也就是能让咱们通过操作数据的形式来扭转实在的DOM
状态。因为其实Virtual DOM
的实质就是一个JS
对象,它保留了对实在DOM
的所有形容,是实在DOM
的一个映射,所以当咱们在进行频繁更新元素的时候,扭转这个JS
对象的开销远比间接扭转实在DOM
要小得多。- 组件化的开发思维。第二点来说就是它们都提倡这种组件化的开发思维,也就是倡议将利用分拆成一个个性能明确的模块,再将这些模块整合在一起以满足咱们的业务需要。
Props
。Vue
和React
中都有props
的概念,容许父组件向子组件传递数据。- 构建工具、Chrome 插件、配套框架。还有就是它们的构建工具以及 Chrome 插件、配套框架都很欠缺。比方构建工具,
React
中能够应用CRA
,Vue
中能够应用对应的脚手架vue-cli
。对于配套框架Vue
中有vuex、vue-router
,React
中有react-router、redux
。
不同点
- 模版的编写。最大的不同就是模版的编写,
Vue
激励你去写近似惯例HTML
的模板,React
举荐你应用JSX
去书写。 - 状态治理与对象属性。在
React
中,利用的状态是比拟要害的概念,也就是state
对象,它容许你应用setState
去更新状态。然而在Vue
中,state
对象并不是必须的,数据是由data
属性在Vue
对象中进行治理。 - 虚构
DOM
的解决形式不同。Vue
中的虚构DOM
管制了颗粒度,组件层面走watcher
告诉,而组件外部走vdom
做diff
,这样,既不会有太多watcher
,也不会让vdom
的规模过大。而React
走了相似于CPU
调度的逻辑,把vdom
这棵树,宏观上变成了链表,而后利用浏览器的闲暇工夫来做diff
Vue 我的项目中你是如何解决跨域的呢
一、跨域是什么
跨域实质是浏览器基于 同源策略 的一种平安伎俩
同源策略(Sameoriginpolicy),是一种约定,它是浏览器最外围也最根本的平安性能
所谓同源(即指在同一个域)具备以下三个相同点
- 协定雷同(protocol)
- 主机雷同(host)
- 端口雷同(port)
反之非同源申请,也就是协定、端口、主机其中一项不雷同的时候,这时候就会产生跨域
肯定要留神跨域是浏览器的限度,你用抓包工具抓取接口数据,是能够看到接口曾经把数据返回回来了,只是浏览器的限度,你获取不到数据。用 postman 申请接口可能申请到数据。这些再次印证了跨域是浏览器的限度。
Class 与 Style 如何动静绑定
Class
能够通过对象语法和数组语法进行动静绑定
对象语法:
<div v-bind:class="{active: isActive,'text-danger': hasError}"></div>
data: {
isActive: true,
hasError: false
}
数组语法:
<div v-bind:class="[isActive ? activeClass :'', errorClass]"></div>
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
Style
也能够通过对象语法和数组语法进行动静绑定
对象语法:
<div v-bind:style="{color: activeColor, fontSize: fontSize +'px'}"></div>
data: {
activeColor: 'red',
fontSize: 30
}
数组语法:
<div v-bind:style="[styleColor, styleSize]"></div>
data: {
styleColor: {color: 'red'},
styleSize:{fontSize:'23px'}
}
理解 history 有哪些办法吗?说下它们的区别
history
这个对象在html5
的时候新退出两个api
history.pushState()
和history.repalceState()
这两个API
能够在不进行刷新的状况下,操作浏览器的历史纪录。惟一不同的是,前者是新增一个历史记录,后者是间接替换以后的历史记录。
从参数上来说:
window.history.pushState(state,title,url)
//state:须要保留的数据,这个数据在触发 popstate 事件时,能够在 event.state 里获取
//title:题目,根本没用,个别传 null
//url:设定新的历史纪录的 url。新的 url 与以后 url 的 origin 必须是一样的,否则会抛出谬误。url 能够时绝对路径,也能够是相对路径。// 如 以后 url 是 https://www.baidu.com/a/, 执行 history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,// 执行 history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/
window.history.replaceState(state,title,url)
// 与 pushState 基本相同,但她是批改以后历史纪录,而 pushState 是创立新的历史纪录
另外还有:
window.history.back()
后退window.history.forward()
后退window.history.go(1)
后退或者后退几步
从触发事件的监听上来说:
pushState()
和replaceState()
不能被popstate
事件所监听- 而前面三者能够,且用户点击浏览器后退后退键时也能够
在 Vue 中应用插件的步骤
- 采纳
ES6
的import ... from ...
语法或CommonJS
的require()
办法引入插件 - 应用全局办法
Vue.use(plugin)
应用插件, 能够传入一个选项对象Vue.use(MyPlugin, { someOption: true})
$route
和 $router
的区别
$route
是“路由信息对象”,包含path
,params
,hash
,query
,fullPath
,matched
,name
等路由信息参数。- 而
$router
是“路由实例”对象包含了路由的跳转办法,钩子函数等
为什么要应用异步组件
- 节俭打包出的后果,异步组件离开打包,采纳
jsonp
的形式进行加载,无效解决文件过大的问题。 - 外围就是包组件定义变成一个函数,依赖
import()
语法,能够实现文件的宰割加载。
components:{AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([])
}
原理
export function (Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string): VNode | Array<VNode> | void {
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend
// 第二次渲染时 Ctor 不为 undefined
if (Ctor === undefined) {
return createAsyncPlaceholder( // 渲染占位符 空虚构节点
asyncFactory,
data,
context,
children,
tag
)
}
}
}
function resolveAsyncComponent (factory: Function, baseCtor: Class<Component>): Class<Component> | void {if (isDef(factory.resolved)) {
// 3. 在次渲染时能够拿到获取的最新组件
return factory.resolved
}
const resolve = once((res: Object | Class<Component>) => {factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {forceRender(true) //2. 强制更新视图从新渲染
} else {owners.length = 0}
})
const reject = once(reason => {if (isDef(factory.errorComp)) {factory.error = true forceRender(true)
}
})
const res = factory(resolve, reject)// 1. 将 resolve 办法和 reject 办法传入,用户调用 resolve 办法后
sync = false
return factory.resolved
}
函数式组件劣势和原理
函数组件的特点
- 函数式组件须要在申明组件是指定
functional:true
- 不须要实例化,所以没有
this
,this
通过render
函数的第二个参数context
来代替 - 没有生命周期钩子函数,不能应用计算属性,
watch
- 不能通过
$emit
对外裸露事件,调用事件只能通过context.listeners.click
的形式调用内部传入的事件 - 因为函数式组件是没有实例化的,所以在内部通过
ref
去援用组件时,理论援用的是HTMLElement
- 函数式组件的
props
能够不必显示申明,所以没有在props
外面申明的属性都会被主动隐式解析为prop
, 而一般组件所有未声明的属性都解析到$attrs
外面,并主动挂载到组件根元素下面 (能够通过inheritAttrs
属性禁止)
长处
- 因为函数式组件不须要实例化,无状态,没有生命周期,所以渲染性能要好于一般组件
- 函数式组件构造比较简单,代码构造更清晰
应用场景:
- 一个简略的展现组件,作为容器组件应用 比方
router-view
就是一个函数式组件 - “高阶组件”——用于接管一个组件作为参数,返回一个被包装过的组件
例子
Vue.component('functional',{ // 构造函数产生虚构节点的
functional:true, // 函数式组件 // data={attrs:{}}
render(h){return h('div','test')
}
})
const vm = new Vue({el: '#app'})
源码相干
// functional component
if (isTrue(Ctor.options.functional)) { // 带有 functional 的属性的就是函数式组件
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on // 处理事件
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn // 解决原生事件
// install component management hooks onto the placeholder node
installComponentHooks(data) // 装置组件相干钩子(函数式组件没有调用此办法,从而性能高于一般组件)
Vue.set 的实现原理
- 给对应和数组自身都减少了
dep
属性 - 当给对象新增不存在的属性则触发对象依赖的
watcher
去更新 - 当批改数组索引时,咱们调用数组自身的
splice
去更新数组(数组的响应式原理就是从新了splice
等办法,调用splice
就会触发视图更新)
根本应用
以下办法调用会扭转原始数组:
push()
,pop()
,shift()
,unshift()
,splice()
,sort()
,reverse()
,Vue.set(target, key, value)
-
调用办法:
Vue.set(target, key, value)
target
:要更改的数据源(能够是对象或者数组)key
:要更改的具体数据value
:从新赋的值
<div id="app">{{user.name}} {{user.age}}</div>
<div id="app"></div>
<script>
// 1. 依赖收集的特点:给每个属性都减少一个 dep 属性,dep 属性会进行收集,收集的是 watcher
// 2. vue 会给每个对象也减少一个 dep 属性
const vm = new Vue({
el: '#app',
data: { // vm._data
user: {name:'poetry'}
}
});
// 对象的话:调用 defineReactive 在 user 对象上定义一个 age 属性,减少到响应式数据中,触发对象自身的 watcher,ob.dep.notify()更新
// 如果是数组 通过调用 splice 办法,触发视图更新
vm.$set(vm.user, 'age', 20); // 不能给根属性增加,因为给根增加属性 性能耗费太大,须要做很多解决
// 批改必定是同步的 -> 更新都是一步的 queuewatcher
</script>
相干源码
// src/core/observer/index.js 44
export class Observer {// new Observer(value)
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep() // 给所有对象类型减少 dep 属性}
}
// src/core/observer/index.js 201
export function set (target: Array<any> | Object, key: any, val: any): any {
// 1. 是开发环境 target 没定义或者是根底类型则报错
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 2. 如果是数组 Vue.set(array,1,100); 调用咱们重写的 splice 办法 (这样能够更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {target.length = Math.max(target.length, key)
// 利用数组的 splice 变异办法触发响应式
target.splice(key, 1, val)
return val
}
// 3. 如果是对象自身的属性,则间接增加即可
if (key in target && !(key in Object.prototype)) {target[key] = val // 间接批改属性值
return val
}
// 4. 如果是 Vue 实例 或 根数据 data 时 报错,(更新_data 无意义)const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 5. 如果不是响应式的也不须要将其定义成响应式属性
if (!ob) {target[key] = val
return val
}
// 6. 将属性定义成响应式的
defineReactive(ob.value, key, val)
// 告诉视图更新
ob.dep.notify()
return val
}
咱们浏览以上源码可知,vm.$set 的实现原理是:
- 如果指标是数组,间接应用数组的
splice
办法触发相应式; - 如果指标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式解决,则是通过调用
defineReactive
办法进行响应式解决(defineReactive
办法就是Vue
在初始化对象时,给对象属性采纳Object.defineProperty
动静增加getter
和setter
的性能所调用的办法)
Vue 为什么没有相似于 React 中 shouldComponentUpdate 的生命周期
- 考点:
Vue
的变动侦测原理 - 前置常识: 依赖收集、虚构
DOM
、响应式零碎
根本原因是
Vue
与React
的变动侦测形式有所不同
- 当 React 晓得发生变化后,会应用
Virtual Dom Diff
进行差别检测,然而很多组件实际上是必定不会发生变化的,这个时候须要shouldComponentUpdate
进行手动操作来缩小diff
,从而进步程序整体的性能 Vue
在一开始就晓得那个组件产生了变动,不须要手动管制diff
,而组件外部采纳的diff
形式实际上是能够引入相似于shouldComponentUpdate
相干生命周期的,然而通常正当大小的组件不会有适量的 diff,手动优化的价值无限,因而目前Vue
并没有思考引入shouldComponentUpdate
这种手动优化的生命周期
vue-router 中如何爱护路由
剖析
路由爱护在利用开发过程中十分重要,简直每个利用都要做各种路由权限治理,因而相当考查使用者基本功。
体验
全局守卫:
const router = createRouter({...})
router.beforeEach((to, from) => {
// ...
// 返回 false 以勾销导航
return false
})
路由独享守卫:
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
组件内的守卫:
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {// 在渲染该组件的对应路由被验证前调用},
beforeRouteUpdate(to, from) {// 在以后路由扭转,然而该组件被复用时调用},
beforeRouteLeave(to, from) {// 在导航来到渲染该组件的对应路由时调用},
}
答复
vue-router
中爱护路由的办法叫做路由守卫,次要用来通过跳转或勾销的形式守卫导航。- 路由守卫有三个级别:
全局
、路由独享
、组件级
。影响范畴由大到小,例如全局的router.beforeEach()
,能够注册一个全局前置守卫,每次路由导航都会通过这个守卫,因而在其外部能够退出管制逻辑决定用户是否能够导航到指标路由;在路由注册的时候能够退出单路由独享的守卫,例如beforeEnter
,守卫只在进入路由时触发,因而只会影响这个路由,管制更准确;咱们还能够为路由组件增加守卫配置,例如beforeRouteEnter
,会在渲染该组件的对应路由被验证前调用,管制的范畴更准确了。 - 用户的任何导航行为都会走
navigate
办法,外部有个guards
队列按程序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会勾销原有的导航。
原理
runGuardQueue(guards)
链式的执行用户在各级别注册的守卫钩子函数,通过则持续下一个级别的守卫,不通过进入 catch
流程勾销本来导航
// 源码
runGuardQueue(guards)
.then(() => {
// check global guards beforeEach
guards = []
for (const guard of beforeGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// check in components beforeRouteUpdate
guards = extractComponentsGuards(
updatingRecords,
'beforeRouteUpdate',
to,
from
)
for (const record of updatingRecords) {
record.updateGuards.forEach(guard => {guards.push(guardToPromiseFn(guard, to, from))
})
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check the route beforeEnter
guards = []
for (const record of to.matched) {
// do not trigger beforeEnter on reused views
if (record.beforeEnter && !from.matched.includes(record)) {if (isArray(record.beforeEnter)) {for (const beforeEnter of record.beforeEnter)
guards.push(guardToPromiseFn(beforeEnter, to, from))
} else {guards.push(guardToPromiseFn(record.beforeEnter, to, from))
}
}
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
// clear existing enterCallbacks, these are added by extractComponentsGuards
to.matched.forEach(record => (record.enterCallbacks = {}))
// check in-component beforeRouteEnter
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from
)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check global guards beforeResolve
guards = []
for (const guard of beforeResolveGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
// catch any navigation canceled
.catch(err =>
isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
? err
: Promise.reject(err)
)
源码地位(opens new window)
Vue-router 路由钩子在生命周期的体现
一、Vue-Router 导航守卫
有的时候,须要通过路由来进行一些操作,比方最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就勾销跳转,并跳到登录页面让其登录。
为此有很多种办法能够植入路由的导航过程:全局的,单个路由独享的,或者组件级的
- 全局路由钩子
vue-router 全局有三个路由钩子;
- router.beforeEach 全局前置守卫 进入路由之前
- router.beforeResolve 全局解析守卫(2.5.0+)在 beforeRouteEnter 调用之后调用
- router.afterEach 全局后置钩子 进入路由之后
具体应用∶
- beforeEach(判断是否登录了,没登录就跳转到登录页)
router.beforeEach((to, from, next) => {let ifInfo = Vue.prototype.$common.getSession('userData'); // 判断是否登录的存储信息
if (!ifInfo) {
// sessionStorage 里没有贮存 user 信息
if (to.path == '/') {// 如果是登录页面门路,就间接 next()
next();} else {
// 不然就跳转到登录
Message.warning("请从新登录!");
window.location.href = Vue.prototype.$loginUrl;
}
} else {return next();
}
})
- afterEach(跳转之后滚动条回到顶部)
router.afterEach((to, from) => {
// 跳转之后滚动条回到顶部
window.scrollTo(0,0);
});
- 单个路由独享钩子
beforeEnter 如果不想全局配置守卫的话,能够为某些路由独自配置守卫,有三个参数∶ to、from、next
export default [
{
path: '/',
name: 'login',
component: login,
beforeEnter: (to, from, next) => {console.log('行将进入登录页面')
next()}
}
]
- 组件内钩子
beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave
这三个钩子都有三个参数∶to、from、next
- beforeRouteEnter∶ 进入组件前触发
- beforeRouteUpdate∶ 以后地址扭转并且改选件被复用时触发,举例来说,带有动静参数的门路 foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,因为会渲染同样的 foa 组件,这个钩子在这种状况下就会被调用
- beforeRouteLeave∶ 来到组件被调用
留神点,beforeRouteEnter 组件内还拜访不到 this,因为该守卫执行前组件实例还没有被创立,须要传一个回调给 next 来拜访,例如:
beforeRouteEnter(to, from, next) {
next(target => {if (from.path == '/classProcess') {target.isFromProcess = true}
})
}
二、Vue 路由钩子在生命周期函数的体现
- 残缺的路由导航解析流程(不包含其余生命周期)
- 触发进入其余路由。
- 调用要来到路由的组件守卫 beforeRouteLeave
- 调用局前置守卫∶ beforeEach
- 在重用的组件里调用 beforeRouteUpdate
- 调用路由独享守卫 beforeEnter。
- 解析异步路由组件。
- 在将要进入的路由组件中调用 beforeRouteEnter
- 调用全局解析守卫 beforeResolve
- 导航被确认。
- 调用全局后置钩子的 afterEach 钩子。
- 触发 DOM 更新(mounted)。
- 执行 beforeRouteEnter 守卫中传给 next 的回调函数
- 触发钩子的残缺程序
路由导航、keep-alive、和组件生命周期钩子联合起来的,触发程序,假如是从 a 组件来到,第一次进入 b 组件∶
- beforeRouteLeave:路由组件的组件来到路由前钩子,可勾销路由来到。
- beforeEach:路由全局前置守卫,可用于登录验证、全局路由 loading 等。
- beforeEnter:路由独享守卫
- beforeRouteEnter:路由组件的组件进入路由前钩子。
- beforeResolve:路由全局解析守卫
- afterEach:路由全局后置钩子
- beforeCreate:组件生命周期,不能拜访 tAis。
- created; 组件生命周期,能够拜访 tAis,不能拜访 dom。
- beforeMount:组件生命周期
- deactivated:来到缓存组件 a,或者触发 a 的 beforeDestroy 和 destroyed 组件销毁钩子。
- mounted:拜访 / 操作 dom。
- activated:进入缓存组件,进入 a 的嵌套子组件(如果有的话)。
- 执行 beforeRouteEnter 回调函数 next。
- 导航行为被触发到导航实现的整个过程
- 导航行为被触发,此时导航未被确认。
- 在失活的组件里调用来到守卫 beforeRouteLeave。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
- 在路由配置里调用 beforeEnteY。
- 解析异步路由组件(如果有)。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫(2.5+),标示解析阶段实现。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 非重用组件,开始组件实例的生命周期:beforeCreate&created、beforeMount&mounted
- 触发 DOM 更新。
- 用创立好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
- 导航实现
Vue-router 导航守卫有哪些
- 全局前置 / 钩子:beforeEach、beforeResolve、afterEach
- 路由独享的守卫:beforeEnter
- 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
Vue 的 diff 算法详细分析
1. 是什么
diff
算法是一种通过同层的树节点进行比拟的高效算法
其有两个特点:
- 比拟只会在同层级进行, 不会跨层级比拟
- 在 diff 比拟的过程中,循环从两边向两头比拟
diff
算法在很多场景下都有利用,在 vue
中,作用于虚构 dom
渲染成实在 dom
的新旧 VNode
节点比拟
2. 比拟形式
diff
整体策略为:深度优先,同层比拟
- 比拟只会在同层级进行, 不会跨层级比拟
- 比拟的过程中,循环从两边向两头收拢
上面举个 vue
通过 diff
算法更新的例子:
新旧 VNode
节点如下图所示:
第一次循环后,发现旧节点 D 与新节点 D 雷同,间接复用旧节点 D 作为 diff
后的第一个实在节点,同时旧节点 endIndex
挪动到 C,新节点的 startIndex
挪动到了 C
第二次循环后,同样是旧节点的开端和新节点的结尾 (都是 C) 雷同,同理,diff
后创立了 C 的实在节点插入到第一次创立的 D 节点前面。同时旧节点的 endIndex
挪动到了 B,新节点的 startIndex
挪动到了 E
第三次循环中,发现 E 没有找到,这时候只能间接创立新的实在节点 E,插入到第二次创立的 C 节点之后。同时新节点的 startIndex
挪动到了 A。旧节点的 startIndex
和 endIndex
都放弃不动
第四次循环中,发现了新旧节点的结尾 (都是 A) 雷同,于是 diff
后创立了 A 的实在节点,插入到前一次创立的 E 节点前面。同时旧节点的 startIndex
挪动到了 B,新节点的startIndex
挪动到了 B
第五次循环中,情景同第四次循环一样,因而 diff
后创立了 B 实在节点 插入到前一次创立的 A 节点前面。同时旧节点的 startIndex
挪动到了 C,新节点的 startIndex 挪动到了 F
新节点的 startIndex
曾经大于 endIndex
了,须要创立 newStartIdx
和 newEndIdx
之间的所有节点,也就是节点 F,间接创立 F 节点对应的实在节点放到 B 节点前面
3. 原理剖析
当数据产生扭转时,set
办法会调用 Dep.notify
告诉所有订阅者 Watcher
,订阅者就会调用patch
给实在的 DOM
打补丁,更新相应的视图
源码地位:src/core/vdom/patch.js
function patch(oldVnode, vnode, hydrating, removeOnly) {if (isUndef(vnode)) { // 没有新节点,间接执行 destory 钩子函数
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) // 没有旧节点,间接用新节点生成 dom 元素
} else {const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 判断旧节点和新节点本身一样,统一执行 patchVnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 否则间接销毁及旧节点,依据新节点生成 dom 元素
if (isRealElement) {if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
oldVnode = emptyNodeAt(oldVnode)
}
return vnode.elm
}
}
}
patch
函数前两个参数位为oldVnode
和 Vnode
,别离代表新的节点和之前的旧节点,次要做了四个判断:
- 没有新节点,间接触发旧节点的
destory
钩子 - 没有旧节点,阐明是页面刚开始初始化的时候,此时,基本不须要比拟了,间接全是新建,所以只调用
createElm
- 旧节点和新节点本身一样,通过
sameVnode
判断节点是否一样,一样时,间接调用patchVnode
去解决这两个节点 - 旧节点和新节点本身不一样,当两个节点不一样的时候,间接创立新节点,删除旧节点
上面次要讲的是 patchVnode
局部
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新旧节点统一,什么都不做
if (oldVnode === vnode) {return}
// 让 vnode.el 援用到当初的实在 dom,当 el 批改时,vnode.el 会同步变动
const elm = vnode.elm = oldVnode.elm
// 异步占位符
if (isTrue(oldVnode.isAsyncPlaceholder)) {if (isDef(vnode.asyncFactory.resolved)) {hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {vnode.isAsyncPlaceholder = true}
return
}
// 如果新旧都是动态节点,并且具备雷同的 key
// 当 vnode 是克隆节点或是 v -once 指令管制的节点时,只须要把 oldVnode.elm 和 oldVnode.child 都复制到 vnode 上
// 也不必再有其余操作
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果 vnode 不是文本节点或者正文节点
if (isUndef(vnode.text)) {
// 并且都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 并且子节点不完全一致,则调用 updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// 如果只有新的 vnode 有子节点
} else if (isDef(ch)) {if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// elm 曾经援用了老的 dom 节点,在老的 dom 节点上增加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 如果新 vnode 没有子节点,而 vnode 有子节点,间接删除老的 oldCh
} else if (isDef(oldCh)) {removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果老节点是文本节点
} else if (isDef(oldVnode.text)) {nodeOps.setTextContent(elm, '')
}
// 如果新 vnode 和老 vnode 是文本节点或正文节点
// 然而 vnode.text != oldVnode.text 时,只须要更新 vnode.elm 的文本内容就能够
} else if (oldVnode.text !== vnode.text) {nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
patchVnode
次要做了几个判断:
- 新节点是否是文本节点,如果是,则间接更新
dom
的文本内容为新节点的文本内容 - 新节点和旧节点如果都有子节点,则解决比拟更新子节点
- 只有新节点有子节点,旧节点没有,那么不必比拟了,所有节点都是全新的,所以间接全副新建就好了,新建是指创立出所有新
DOM
,并且增加进父节点 - 只有旧节点有子节点而新节点没有,阐明更新后的页面,旧节点全副都不见了,那么要做的,就是把所有的旧节点删除,也就是间接把
DOM
删除
子节点不完全一致,则调用updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧头索引
let newStartIdx = 0 // 新头索引
let oldEndIdx = oldCh.length - 1 // 旧尾索引
let newEndIdx = newCh.length - 1 // 新尾索引
let oldStartVnode = oldCh[0] // oldVnode 的第一个 child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode 的最初一个 child
let newStartVnode = newCh[0] // newVnode 的第一个 child
let newEndVnode = newCh[newEndIdx] // newVnode 的最初一个 child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 如果 oldStartVnode 和 oldEndVnode 重合,并且新的也都重合了,证实 diff 完了,循环完结
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果 oldVnode 的第一个 child 不存在
if (isUndef(oldStartVnode)) {
// oldStart 索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果 oldVnode 的最初一个 child 不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd 索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode 和 newStartVnode 是同一个节点
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode 和 newStartVnode,索引左移,持续循环
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode 和 newEndVnode 是同一个节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode 和 newEndVnode,索引右移,持续循环
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode 和 newEndVnode 是同一个节点
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode 和 newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果 removeOnly 是 false,则将 oldStartVnode.eml 挪动到 oldEndVnode.elm 之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart 索引右移,newEnd 索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果 oldEndVnode 和 newStartVnode 是同一个节点
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode 和 newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果 removeOnly 是 false,则将 oldEndVnode.elm 挪动到 oldStartVnode.elm 之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd 索引左移,newStart 索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 尝试在 oldChildren 中寻找和 newStartVnode 的具备雷同的 key 的 Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,阐明 newStartVnode 是一个新的节点
if (isUndef(idxInOld)) { // New element
// 创立一个新 Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和 newStartVnodej 具备雷同的 key 的 Vnode,叫 vnodeToMove
} else {vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error.' +
'Make sure each v-for item has a unique key.'
)
}
// 比拟两个具备雷同的 key 的新节点是否是同一个节点
// 不设 key,newCh 和 oldCh 只会进行头尾两端的互相比拟,设 key 后,除了头尾两端的比拟外,还会从用 key 生成的对象 oldKeyToIdx 中查找匹配的节点,所以为节点设置 key 能够更高效的利用 dom。if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove 和 newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 革除
oldCh[idxInOld] = undefined
// 如果 removeOnly 是 false,则将找到的和 newStartVnodej 具备雷同的 key 的 Vnode,叫 vnodeToMove.elm
// 挪动到 oldStartVnode.elm 之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果 key 雷同,然而节点不雷同,则创立一个新的节点
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
while
循环次要解决了以下五种情景:
- 当新老
VNode
节点的start
雷同时,间接patchVnode
,同时新老VNode
节点的开始索引都加 1 - 当新老
VNode
节点的end
雷同时,同样间接patchVnode
,同时新老VNode
节点的完结索引都减 1 - 当老
VNode
节点的start
和新VNode
节点的end
雷同时,这时候在patchVnode
后,还须要将以后实在dom
节点挪动到oldEndVnode
的前面,同时老VNode
节点开始索引加 1,新VNode
节点的完结索引减 1 - 当老
VNode
节点的end
和新VNode
节点的start
雷同时,这时候在patchVnode
后,还须要将以后实在dom
节点挪动到oldStartVnode
的后面,同时老VNode
节点完结索引减 1,新VNode
节点的开始索引加 1 -
如果都不满足以上四种情景,那阐明没有雷同的节点能够复用,则会分为以下两种状况:
- 从旧的
VNode
为key
值,对应index
序列为value
值的哈希表中找到与newStartVnode
统一key
的旧的VNode
节点,再进行patchVnode
,同时将这个实在dom
挪动到oldStartVnode
对应的实在dom
的后面 - 调用
createElm
创立一个新的dom
节点放到以后newStartIdx
的地位
- 从旧的
小结
- 当数据产生扭转时,订阅者
watcher
就会调用patch
给实在的DOM
打补丁 - 通过
isSameVnode
进行判断,雷同则调用patchVnode
办法 -
patchVnode
做了以下操作:- 找到对应的实在
dom
,称为el
- 如果都有都有文本节点且不相等,将
el
文本节点设置为Vnode
的文本节点 - 如果
oldVnode
有子节点而VNode
没有,则删除el
子节点 - 如果
oldVnode
没有子节点而VNode
有,则将VNode
的子节点实在化后增加到el
- 如果两者都有子节点,则执行
updateChildren
函数比拟子节点
- 找到对应的实在
-
updateChildren
次要做了以下操作:- 设置新旧
VNode
的头尾指针 - 新旧头尾指针进行比拟,循环向两头聚拢,依据状况调用
patchVnode
进行patch
反复流程、调用createElem
创立一个新节点,从哈希表寻找key
统一的VNode
节点再分状况操作
- 设置新旧