目录
#前言@[TOC](文章目录)
#一、React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?@[TOC](文章目录)
#二、Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的@[TOC](文章目录)
参数
#三、在 Vue 中,子组件为何不可以修改父组件传递的 Prop?如果修改了,Vue 是如何监控到属性的修改并给出警告的?@[TOC](文章目录)
#四、双向绑定和 vuex 是否冲突?@[TOC](文章目录)
#五、Vue 的响应式原理中 Object.defineProperty 有什么缺陷?为什么在 Vue3.0 采用了 Proxy,抛弃Object.defineProperty?@[TOC](文章目录)
#六、Vue 的父组件和子组件生命周期钩子执行顺序是什么?@[TOC](文章目录)
#七、vue 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?@[TOC](文章目录)
#八、vue 渲染大量数据时应该怎么优化?@[TOC](文章目录)
#九、vue 如何优化首页的加载速度?vue 首页白屏是什么问题引起的?如何解决呢?@[TOC](文章目录)
#十、Vue 中的 computed 是如何实现的?(腾讯、平安)@[TOC](文章目录)
#总结@[TOC](文章目录)
前端面试问题,可能问题不是很全面,但基本上是常见的且自我补充的一个过程,相信从中可以完善自己。另外部分见解答案解析来自github博客等资料整理过程中如有错误欢迎指出!
key是给每一个vnode的唯一id,可以依靠key,更准确, 更快的拿到oldVnode中对应的vnode节点,带key就不是就地复用了,在sameNode函数 a.key === b.key对比中可以避免就地复用的情况,所以会更加准确;利用key的唯一性生成map对象来获取对应节点,比遍历方式更快,而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。
核心是利用ES5的Object.defineProperty,这也是Vue.js为什么不能兼容IE8及以下浏览器的原因。
Object.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象.(实现的数据劫持)
Object.defineProperty(obj, prop, descriptor)
参数
obj
要定义属性的对象。
prop
要定义或修改的属性的名称或
Symbol
。
descriptor
要定义或修改的属性描述符。
observe的功能就是用来监测数据的变化。实现方式是给非VNode的对象类型数据添加一个Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个Observer对象实例Observer是一个类,它的作用是给对象属性添加getter和setter,用于 收集依赖 和 派发更新
收集依赖
const dep = new Dep() // 实例化一个Dep实例
在get函数中通过dep.depend()做依赖收集
收集过程:当我们实例化一个渲染watcher的时候,首先进入watcher的构造函数逻辑,然后执行他的this.get()
方法,进入get函数把Dep.target赋值为当前渲染watcher并压栈(为了恢复用)。接着执行vm._render()
方法,生成渲染VNode,并且在这个过程对vm上的数据访问,这个时候就触发数据对象的getter(在此期间执行Dep.target.addDep(this)
方法,将watcher订阅到这个数据持有的dep的subs中,为后续数据变化时通知到哪些subs做准备)。然后递归遍历添加所有子项的getter。
Watcher在构造函数中初始化两个Dep实例数组。newDeps代表新添加的Dep实例数组,deps代表上一次添加的Dep实例数组。
依赖清空:在执行清空依赖(cleanupDeps)函数时,会首先遍历deps,移除对dep的订阅,然后把newDepsIds和depIds交换,newDeps和deps交换,并把newDepIds和newDeps清空。考虑场景,在条件渲染时,及时对不需要渲染数据的订阅移除,减少性能浪费。
考虑到Vue是数据驱动的,所以每次数据变化都会重写Render,那么vm._render()
方法会再次执行,并再次触发数据。
收集依赖的目的是为了当这些响应式数据发生变化,触发它们的setter的时候,能知道应该通知哪些订阅者去做相应的逻辑处理派发更新
派发更新
childOb = !shallow && observe(newVal) // 如果shallow为false的情况,会对新设置的值变成一个响应式对象
dep.notify() // 通知所有订阅者
派发过程:当我们组件中对响应的数据做了修改,就会触发setter的逻辑,最后调用dep.notify()
方法,它是Dep的一个实例方法。具体做法是遍历依赖收集中建立的subs,也就是Watcher的实例数组【subs数组在依赖收集getter中被添加,期间通过一些逻辑处理判断保证同一数据不会被添加多次】,然后调用每一个watcher的update方法。
update函数中有个queueWatcher(this)
方法引入了队列的概念,是vue在做派发更新时优化的一个点,它并不会每次数据改变都会触发watcher回调,而是把这些watcher先添加到一个队列中,然后在nextTick后执行watcher的run函数。
run函数:先通过this.get()
得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep模式任何一个条件,则执行watcher的回调,注意回调函数执行的时候会把第一个参数和第二个参数传入新值value和旧值oldValue,这就是当我们自己添加watcher时候可以在参数中取到新旧值的来源。对应渲染watcher而言,在执行this.get()
方法求值的时候,会执行getter方法。因此在我们修改组件相关数据时候,会触发组件重新渲染,接着重新执行patch的过程。
简单概括:vue通过Object.defineProperty 劫持传进来的数据, 然后在数据getter的时候订阅重新编译模板的消息,然后通过监听元素的事件,监听输入框值变化,将新的值重新赋值给被劫持的data,这样就会触发setter函数,再setter函数中就会去发布重新编译模板的消息;
if (process.env.NODE_ENV !== 'production') {
var hyphenatedKey = hyphenate(key);
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
vm
);
}
defineReactive$$1(props, key, value, function () {
if (!isRoot && !isUpdatingChildComponent) {
warn(
"Avoid mutating a prop directly since the value will be " +
"overwritten whenever the parent component re-renders. " +
"Instead, use a data or computed property based on the prop's " +
"value. Prop being mutated: \"" + key + "\"",
vm
);
}
});
}
在initProps的时候,在defineReactive时通过判断是否在开发环境,如果是开发环境,会在触发set的时候判断是否此key是否处于updatingChildren中被修改,如果不是,说明此修改来自子组件,触发warn提示。
简单来说就是:一个父组件下不只有你一个子组件。同样,使用这份 prop 数据的也不只有你一个子组件。如果每个子组件都能修改 prop 的话,将会导致修改数据的源头不止一处。所以我们需要将修改数据的源头统一为父组件,子组件像要改 prop 只能委托父组件帮它。从而保证数据修改源唯一。
严格模式"use strict" 指令在 JavaScript 1.8.5 (ECMAScript5) 中新增。
它不是一条语句,但是是一个字面量表达式,在 JavaScript 旧版本中会被忽略。
"use strict" 的目的是指定代码在严格条件下执行。
严格模式下你不能使用未声明的变量。
在严格模式中使用Vuex,当用户输入时,v-model会试图直接修改属性值,但这个修改不是在mutation中修改的,所以会抛出一个错误。当需要在组件中使用vuex中的state时,有2种解决方案:
1、在input中绑定value(vuex中的state),然后监听input的change或者input事件,在事件回调中调用mutation修改state的值
2、使用带有setter的双向绑定计算属性。见以下例子(来自官方文档):
computed:{
message:{
get (){
return this.$store.state.obj.message
},
set (value) { this.$store.commit('updateMessage', value)
}
}
}
VueX规定了单向数据流,把把VueX的State放到v-model双向绑定报错,本来就是代码问题。和冲突没关系。而且VueX的双向绑定就是利用了new Vue实现的。为了单项数据流设置了Flag作为标记。不应该是VueX和双向绑定的冲突。是coder的问题。 (来自github)
Object.defineProperty
的第一个缺陷,无法监听数组变化。 然而Vue的文档提到了Vue是可以检测到数组变化的,但是只有以下八种方法,vm.items[indexOfItem] = newValue
这种是无法检测的。为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
由于只针对了以上八种方法进行了hack处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
而要取代它的Proxy有以下两个优点;
可以劫持整个对象,并返回一个新对象
Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是
Object.defineProperty
不具备的。
Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty
只能遍历对象属性直接修改。
当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写
vue的生命周期:
beforeCreate
created
beforeMount
mounted
beforeDestory
destoryed
beforeUpdate
updated
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
父beforeUpdate->子beforeUpdate->子updated->父updated
父beforeUpdate->父updated
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
总结:从外到内,再从内到外总结:从外到内,再从内到外
首先我们需要知道事件代理主要有什么作用?
vue没有自动做事件代理,只有在非常非常多的节点中,使用事件代理会提高一点性能,否则绑定在每个节点中几乎没有差别
1.添加加载动画,优化用户体验
2.利用服务器渲染SSR,在服务端渲染组件
3.避免浏览器处理大量的dom,比如懒加载,异步渲染组件,使用分页,尽量不要再用vue的双向数据绑定了 或者只用部分页面中处理的数据
4.对于固定的非响应式的数据,使用Object.freeze冻结
首页白屏的原因:
vue是单页面应用, html 是靠 js 生成,因为首屏需要加载很大的js文件(app.js
vendor.js
),需要将所有需要的资源都下载到浏览器端并解析。
考虑解决办法:
computed本身是通过代理的方式代理到组件实例上的,所以读取计算属性的时候,执行的是一个内部的getter,而不是用户定义的方法。
computed内部实现了一个惰性的watcher,在实例化的时候不会去求值,其内部通过dirty属性标记计算属性是否需要重新求值。当computed依赖的任一状态(不一定是return中的)发生变化,都会通知这个惰性watcher,让它把dirty属性设置为true。所以,当再次读取这个计算属性的时候,就会重新去求值。
惰性watcher/计算属性在创建时是不会去求值的,是在使用的时候去求值的。
如果你觉得这面试题对你有帮助,我想请你帮我个小忙:来一个点赞收藏三连击;
其实做这个专栏我也有私心,就是希望借助每天写一篇面试题,督促自己学习,以免在吹水群甚至都没有谈资!
对了,如果你的朋友也在准备面试前端开发
,请将这个系列扔给他,
好了,今天就到这里,学废了的同学,记得在评论区留言:打卡
。给同学们以激励。
再次说明整理问题不易,如有内容的需要改进的请指出,以便及时修改!感谢!
学习过程中,祝大家早日取得合适offer!