前端基础知识学习第五节(Vue篇)
1.
Vue是否能检测到数组下标修改元素以及修改length属性的变化?源码如何实现的?
ES5规范下无法做到
Vue内部通过对data中定义的数组对象更改其__proto__指向一个内部的对象来实现对一些方法进行劫持,
这些方法有push、pop、shift、unshift、splice、sort、reverse,当调用这些方法的时候首先会将原始方法(Native)
通过apply绑定到data中定义的数组对象执行,然后因为data中定义的数组对象是可Reactive,最后会通知依赖更新触发重新渲染
2.
Vue的Virtual Dom Diff内部如何实现的?
Vue的Virtual Dom是通过JS对象结构来表示真实Dom结构,Vue的渲染过程是template -> render function -> vnode -> 真实dom,virtual dom diff过程
发生在vnode到生成真实dom阶段,比较只会发生在同层级进行比较,同层级比较完之后比较子节点,直到叶子节点。Vue通过patch方法把virtual dom转化
成真实的dom元素,patch方法是借鉴snabbdom.js这个库来实现的。
3.
Vue组件间通讯方式?
1) 父子组件之间通过prop emit,父组件通过prop传递数据给子组件,子组件通过emit传递数据给父组件
2) 多层组件之间通过对中间组件设置inheritAttrs: false,然后给子组件添加v-bind="$attr" v-on="$listeners",来实现多层组件之间相互通信
3) 通过Event Bus(new Vue()创建一个空的Vue实例)来实现跨组件通信,其内部实现原理是观察者模式
4) 通过Vuex来实现跨组件通信
4.1)mutations:
更改Vuex的store中状态的唯一方法是提交mutation,commit推送一个mutation,这么设计的目的是为了便于追踪数据流向,便于调试
4.2) actions:
action提交的是mutations,而不是直接变更状态,action可以包含任意异步操作,dispatch推送一个action
4.
Vue为什么采用异步更新DOM策略?nextTick如何实现的?
Vue内部数据更新触发DOM更新的流程是:响应式数据发生变化时,它的setter函数会通知闭包中的Dep执行notify函数,Dep会遍历它管理的所有Watcher对象,
逐个触发Watcher对象的update方法,从update源码来看其实Vue是支持同步更新渲染视图的,只是Vue默认使用了异步渲染视图策略,添加Watcher到队列,到
合适的时机(向microtask或者task推入一个function)更新DOM,添加到Watcher队列的逻辑中做了对相同id的Watcher去重处理,简单的来说在具体的开发实践
中,如果频繁多次的对响应数据进行更改,其实Vue内部只会执行一次更新DOM的操作,这也是Vue异步更新DOM为什么会高效的原因。
nextTick的使用方法是传递一个function函数作为参数,在function函数里面会有对数据变化触发DOM更新之后,进行页面最新DOM元素的访问的代码。
我们知道nextTick的用法之后就可以考虑它内部应该如何实现这个功能了,根据HTML Standard,在每个task运行完以后,UI都会重新渲染,那么我们只需要在task
结束之后执行function就能得到最新的DOM元素了,到这里我们考虑task如何实现,JS中的task有microtask macrotask,这两个任务相互协作,交替执行,
同一时刻添加到任务队列的异步任务,microtask会优先执行,但是有一个不一样的地方是同一时间添加到mircotask队列的任务一次全部执行,
常见的会添加到microtask队列的异步任务有Promise MutationObserver,会添加到macrotask队列的异步任务有setTimeout setInterval ajax DOM事件处理
I/O,Vue的nextTick内部的实现逻辑是优先使用microtask,先判断是否支持原生Promise,如果不支持则检测是否支持MutationObserver,
如果不支持microtask则降级到macrotask,具体的方法是setTimeout,主要的思路就是这样。
参考:https://www.zhihu.com/question/55364497/answer/144215284
5.
Vue的key有什么作用?内部原理是什么?
Vue中的key最常用的场景是v-for生成的元素列表中,最佳的实践是使用item的id或者其他不会重复的string|number(不可以使用对象或者数组等非简单数据类型)
属性值作为key,其作用主要是为了准确的标示出来列表中的每一个元素,key值是vnode对象的一个属性,所以会在新旧vnode进行diff阶段使用,可以快速准确的
确认哪些元素修改了,哪些元素是新增的,保证vnode diff的准确性。
内部实现原理是key作为vnode对象的一个属性,列表元素会根据元素在数组中的存储顺序逐个进行比较对比,所以如果对数组的元素进行删除或者添加时候,
使用index作为key的话就会因为index的动态变化导致key也跟着变化,不能够准确的唯一标示出来元素,在后续的vnode dff过程中就会不准确,最直接的问题
就是不能准确的处理哪些元素需要更新,这也是为什么不建议使用index作为key的原因。另外Vue中在使用相同标签名元素的过度切换时,也会使用到key属性,
其目的也是为了让Vue可以区分它们,否则Vue只会替换其内部属性而不会触发过渡效果
6.
Vue的动画如何实现的?
Vue中动画最常用的实现方案是过渡动画,用法是将要做动画的元素放到<transition></transition>元素包括。在进入/离开的过渡中,会有6个class切换:
v-enter、v-enter-active、v-enter-to、v-leave、v-leave-active、v-leave-to,Vue内部会在不同的动画阶段对这个6个class进行添加、移除切换,
所以开发者只需要定义好这6个class的样式就可以了,这种动画的实现原理被称作FLIP
参考:https://blog.csdn.net/DeepLies/article/details/88087881
7.
Vue的slot、slot-scope作用以及原理?
Vue插槽的作用是为了让开发者更好的复用组件以及对组件进行一些定制化的处理
Vue的插槽是组件的一块HTML结构,这块HTML结构的内容在父组件中定义然后传递给子组件,Vue的插槽分为默认插槽、具名插槽、作用域插槽
默认插槽:一个组件只能有一个默认插槽
具名插槽:通过slot="name",name就是插槽的名字,可以同多个具名插槽,在子组件中通过<slot name="name"></slot>来匹配
作用域插槽:默认插槽和具名插槽都是在父组件中定义了结构和样式,依赖的数据也是在父组件,作用域插槽和前两者一个很大的不同是,作用域插槽的
渲染HTML结构的数据是需要子组件传递的,也就是说数据是需要子组件提供
内部原理是插槽在父组件渲染时会被编译成一个未执行的函数,然后在子组渲染时调用这个函数(作用于插槽场景会传数据到这个函数)
V2.6版本使用方式有变化具体可以看官网文档
参考:https://juejin.im/post/5cb0564e5188251acb530087
https://juejin.im/post/5a69ece0f265da3e5a5777ed
https://cn.vuejs.org/v2/guide/components-slots.html
8.
Vue的事件绑定内部原理?
Vue中的事件类型分为原生DOM事件和自定义事件,这两种事件类型都可以被添加到DOM元素和自定义组件上,只是添加事件的方式不太一样,
为DOM元素添加原生DOM事件,比如click事件可以通过v-on:click="handler",为Vue组件添加原生DOM事件稍有不同需要添加.native修饰符,自定义
事件的添加方式是一样的v-on:customEvent="handler",Vue内部处理事件的流程是:
1) 编译模板生成AST,根据指令找出绑定的事件名和对应的回调函数名
2) 根据AST生成最终render代码(codegen阶段),绑定到VNode.data上,带native修饰符的事件会放在VNode.data.nativeOn对象上,不带native修饰符的
事件会放在VNode.data.on对象上
3) 在VNode转化成真实DOM时,会根据VNode.data.nativeOn/on绑定事件
4) 自定义事件的实现逻辑核心就是典型的观察者订阅者模式
参考链接:https://lq782655835.github.io/blogs/vue/vue-code-4.event.html
https://segmentfault.com/a/1190000009750348
9.
Vue.use内部实现?
Vue.use方法是在全局注册一个插件,使用方法是Vue.use(plugin),plugin可以是一个对象也可以是一个函数,如果是一个对象则调用它的install方法,否则会
被当做install方法调用。Vue.use内部做了避免插件被多次注册的处理,通过installedPlugins这个数组判断plugin插件是否被注册过,如果已注册则直接返回,否则
执行注册插件的逻辑,源代码如下:
Vue.use = function (plugin) { var installedPlugins = (this._installedPlugins || (this._installedPlugins = [])); // 已注册插件会保存在这个数组里 if (installedPlugins.indexOf(plugin) > -1) { return this } // additional parameters var args = toArray(arguments, 1); // 把除plugin参数之外的参数放到一个新的数组args里 args.unshift(this); // args = [vm, arg1, arg2, ...] if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args); } else if (typeof plugin === 'function') { plugin.apply(null, args); } installedPlugins.push(plugin); // 插件注册完成push到已安装插件数组 return this };
10.
Vue.mixin&mixins实现原理?
Vue.mixins是全局注册一个混入,影响注册之后所有创建的每个Vue实例,一般用于开发插件,不建议在应用代码中使用
组件的mixins接收一个混入对象的数组。这些混入对象可以像正常的实例对象一样包含实例选项,这些选项将会被合并到最终的组件选项中,使用的是和
Vue.extend一样的选项合并逻辑。也就是说,如果你的混入包含一个created钩子,而创建组件本身也有一个,那么两个函数都会被调用,mixin钩子按照
传入顺序依次调用,并在调用组件自身的钩子之前调用
参考:https://www.jianshu.com/p/f34863f2eb6d
11.
Vuex的getter和Vue实例的computed有什么区别?为什么要使用Vuex的命名空间?
答案:
Vuex中的getter在具体的每个属性实现上与computed类似,差别就是getter中的属性依赖于state,computed中的属性依赖于Vue实例中的data定义,
但是两者最重要的区别是,Vuex中的getter可以在任何Vue实例中通过mapGetter辅助函数映射到computed中需要的属性,而computed只属于当前这个
Vue的实例,两者的数据都是可以缓存的
Vuex由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,默认情况下,模块内部的action、mutation和getter是注册在全局命名空间的,
这种情况下如果模块比较多,就有可能导致命名冲突的问题,这种情况下最佳的实践就是使用Vuex的命名空间,使用方法就是在具体module里面设置
namespaced: true
参考:https://segmentfault.com/a/1190000019077663
https://vuex.vuejs.org/zh/guide/modules.html
12.
Vue的router模块中history模式实现原理,为什么刷新为404?如何解决,为什么?
答案:
Vue&React的router模块实现都有两种模式一种是hash另外一种就是history。hash模式兼容性好,缺点就是路由和带参数看起来不是很“优雅”,这里
主要记录history模式的实现原理。history模式主要使用了HTML5对history扩展的2个方法pushState、replaceState和一个事件popState事件,但是
pushState、replaceState方法的调用并不会触发popState事件,所以React在对点击路由跳转以及在URL中直接更改路由跳转是自己实现的逻辑
处理,通过一个路由渲染对应的component,其他的component隐藏来实现的,如果是浏览器后退或者前进则通过popState的事件回调里面做相应
的处理。刷新出现404是因为前端维护的路由规则只在前端资源有对应的处理,而服务器上并没有相应的请求资源(因为history模式是真正的URL
获取资源的规则),所以访问不到要请求的文件出现了404,解决的办法就是对请求不到的路由直接转向到index.html,然后交给前端代码来处理
跳转的逻辑。