子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。
Vue提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。
只能通过 $emit
派发一个自定义事件,父组件接收到后,由父组件修改。
(1)代码层面的优化
(2)Webpack 层面的优化
(3)基础的 Web 技术的优化
简而言之,就是先转化成AST树,再得到的render函数返回VNode(Vue的虚拟DOM节点),详细步骤如下:
首先,通过compile编译器把template编译成AST语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile是createCompiler的返回值,createCompiler是用以创建编译器的。另外compile还负责合并option。
然后,AST会经过generate(将AST语法树转化成render funtion字符串的过程)得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,里面有(标签名、子节点、文本等等)
从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
虚拟DOM是对DOM的抽象,这个对象是更加轻量级的对 DOM的描述。它设计的最初目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟DOM,因为虚拟DOM本身是js对象。 在代码渲染到页面之前,vue会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实DOM结构,最终渲染到页面。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM进行比较。在vue内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。
vue的模版编译过程主要如下:template -> ast -> render函数
vue 在模版编译版本的码中会执行 compileToFunctions 将template转化为render函数:
// 将模板编译为render函数const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)
CompileToFunctions中的主要逻辑如下∶ (1)调用parse方法将template转化为ast(抽象语法树)
constast = parse(template.trim(), options)
AST元素节点总共三种类型:type为1表示普通元素、2为表达式、3为纯文本
(2)对静态节点做优化
optimize(ast,options)
这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化
深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的DOM永远不会改变,这对运行时模板更新起到了极大的优化作用。
(3)生成代码
const code = generate(ast, options)
generate将ast抽象语法树编译成 render字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(`` render``)
生成render函数。
分析
路由保护在应用开发过程中非常重要,几乎每个应用都要做各种路由权限管理,因此相当考察使用者基本功。
体验
全局守卫:
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)
)
参考 前端进阶面试题详细解答
Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。
$el
属性。this
仍能获取到实例。另外还有 keep-alive
独有的生命周期,分别为 activated
和 deactivated
。用 keep-alive
包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated
钩子函数,命中缓存渲染后会执行 activated
钩子函数。
key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)
diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.
更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。
更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)
Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组
push();
pop();
shift();
unshift();
splice();
sort();
reverse();
由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。 Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
keep-alive可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性
include/exclude,2个生命周期
activated,
deactivated
1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
...其他模式欢迎补充
ES6
的import ... from ...
语法或CommonJS
的require()
方法引入插件Vue.use( plugin )
使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })
导航守卫
router.beforeEach
全局前置守卫
to: Route
: 即将要进入的目标(路由对象)from: Route
: 当前导航正要离开的路由next: Function
: 一定要调用该方法来 resolve
这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。next(false)
:取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => {
next();
});
router.beforeResolve((to, from, next) => {
next();
});
router.afterEach((to, from) => {
console.log('afterEach 全局后置钩子');
});
路由独享的守卫 你可以在路由配置上直接定义
beforeEnter
守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
组件内的守卫你可以在路由组件内直接定义以下路由导航守卫
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用,我们用它来禁止用户离开
// 可以访问组件实例 `this`
// 比如还未保存草稿,或者在用户离开前,
将setInterval销毁,防止离开之后,定时器还在调用。
}
}
路由钩子的执行流程, 钩子函数种类有:
全局守卫
、路由守卫
、组件守卫
beforeRouteLeave
守卫。beforeEach
守卫。beforeRouteUpdate
守卫 (2.2+
)。beforeEnter
。beforeRouteEnter
。beforeResolve
守卫 (2.5+
)。afterEach
钩子。DOM
更新。beforeRouteEnter
守卫中传给 next
的回调函数,创建好的组件实例会作为回调函数的参数传入Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步
第一步是将 模板字符串 转换成 element ASTs(解析器)
第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
声明式导航
<router-link to="/about">Go to About</router-link>
编程式导航
// literal string path
router.push('/users/1')
// object with path
router.push({ path: '/users/1' })
// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'test' } })
回答范例
vue-router
导航有两种方式:声明式导航和编程方式导航router-link
组件,添加to
属性导航;编程方式导航更加灵活,可传递调用router.push()
,并传递path
字符串或者RouteLocationRaw
对象,指定path
、name
、params
等信息router-link
最快捷,会渲染一个a标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航简单说,Vue的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
首先解析模版,生成AST语法树
(一种用JavaScript对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为Object.defineProperty()限制,监听不到变化。
解决方式:
this.$set(this.arr, 0, "OBKoro1"); // 改变数组this.$set(this.obj, "c", "OBKoro1"); // 改变对象
splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
vue源码里缓存了array的原型链,然后重写了这几个方法,触发这几个方法的时候会observer数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使用splice方法会比较好自定义,因为splice可以在数组的任何位置进行删除/添加操作
vm.$set
的实现原理是:
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
事件所监听原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。