前端面试知识点
2019-01-27 本文已影响368人
夜幕小草
一、作用域(全局作用域、函数作用域)
- 、全局作用域
顾名思义:也就是挂在window上下文中的属性,或者function外部的变量,成为全局变量
JS中声明全局变量主要分为显式声明或者隐式声明- 显示声明
1、var(关键字)+变量名(标识符)的方式在function外部声明,即为全局变量,否则在function声明的是局部变量。如:var test = 5; //全局变量 function a(){ var cc=3; //函数变量 alert(test); // 3 } function b(){alert(test);}
- 隐式声明
2、没有使用var,直接给标识符test赋值,这样会隐式的声明了全局变量test。即使该语句是在一个function内,当该function被执行后test变成了全局变量。
3、使用window全局对象来声明,全局对象的属性对应也是全局变量test = 5;//全局变量 function a(){ aa=3; //全局变量 alert(test); // 3 }
window.test window.test = 80
- 显示声明
- 、函数作用域
在fuction内部声明的变量就是局部变量,这个范围的上下文就是函数作用域function(){ //函数作用域 var a = 90 }
二、变量提升(变量、函数)
三、事件循环
四、面向对象
五、继承
六、类型(值类型、引用类型)
七、几个常见的api(foreach、every、some、filter、sort、map)
八、浏览器
九、性能优化
十、webpack处理
十一、vue实现原理
十一、vue知识点一
-
生命周期钩子函数
- 在
beforeCreate
钩子函数调用的时候,是获取不到props
或者data
中的数据的,因为这些数据的初始化都在initState
中。
initState
主要是对props, methods, data, computed
和watch
进行初始化 - 然后会执行
created
钩子函数,在这一步的时候已经可以访问到之前不能访问到的数据,但是这时候组件还没被挂载,所以是看不到的。 - 接下来会先执行
beforeMount
钩子函数,开始创建VDOM
,最后执行mounted
钩子,并将VDOM
渲染为真实DOM
并且渲染数据。组件中如果有子组件的话,会递归挂载子组件,只有当所有子组件全部挂载完毕,才会执行根组件的挂载钩子。 - 接下来是数据更新时会调用的钩子函数
beforeUpdate
和updated
,就是分别在数据更新前和更新后会调用。 - 另外还有
keep-alive
独有的生命周期,分别为activated
和deactivated
。用keep-alive
包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactivated
钩子函数,命中缓存渲染后会执行actived
钩子函数。 - 最后就是销毁组件的钩子函数
beforeDestroy
和destroyed
。前者适合移除事件、定时器等等,否则可能会引起内存泄露的问题。然后进行一系列的销毁操作,如果有子组件的话,也会递归销毁子组件,所有子组件都销毁完毕后才会执行根组件的destroyed
钩子函数。 附生命周期示意图:
image.png
- 在
-
组件通信
组件通信一般分为以下几种情况:- 父子组件通信
- 兄弟组件通信
- 跨多层级组件通信
- 任意组件
对于以上每种情况都有多种方式去实现,接下来就来学习下如何实现。
- 父子组件通信
父组件通过props
传递数据给子组件,子组件通过emit
发送事件传递数据给父组件,这两种方式是最常用的父子通信实现办法。是典型的单向数据流,父组件通过props
传递数据,子组件不能直接修改props
, 而是必须通过发送事件的方式告知父组件修改数据。
父组件:
<template> <div> <child :total="total" v-on:reduce="reduce"></child> <button @click="increse">增加5</button> </div> </template> <script> import Child from "./child.vue" export default { components: { Child }, data: function () { return { total: 0 }; }, methods: { increse: function () { this.total += 5; }, reduce: function (v) { this.total -= v; } } } </script>
子组件child.vue
<template> <div> <span>{{total}}</span> <button @click="reduce">减少5</button> </div> </template> <script> export default { props: { total: Number }, methods: { reduce: function(){ this.$emit("reduce", 5) } } } </script> 这种方式需要在父组件中添加一个reduce方法,并且需要在<child>标签上添加v-on:reduce监听事件,方式有点繁琐
另外这两种方式还可以使用语法糖
v-model
来直接实现,因为v-model
默认会解析成名为value
的prop
和名为input
的事件。这种语法糖的方式是典型的双向绑定,常用于UI
控件上,但是究其根本,还是通过事件的方法让父组件修改数据。
如何使用v-model来实现父子组件间的通信(其实原理还是和上面一样)<template> <div> <child v-model="total"></child> <button @click="increse">增加5</button> </div> </template> <script> import Child from "./child.vue" export default { components: { Child }, data: function () { return { total: 0 }; }, methods: { increse: function () { this.total += 5; } } } </script>
子组件child.vue:
<template> <div> <span>{{value}}</span> <button @click="reduce">减少5</button> </div> </template> <script> export default { props: { value: Number // 注意这里是value }, methods: { reduce: function(){ this.$emit("input", this.value - 5) } } } </script> 这样是不是简单许多? 如果把<input v-model="total">理解成<input v-on:input="total=arguments[0]" :value="total">就容易懂了
当然我们还可以通过访问
$parent
或者$children
对象来访问组件实例中的方法和数据。
另外如果你使用Vue 2.3
及以上版本的话还可以使用$listeners
和.sync
这两个属性。
$listeners
属性会将父组件中的 (不含.native
修饰器的)v-on
事件监听器传递给子组件,子组件可以通过访问$listeners
来自定义监听器。示例代码待贴
.sync 属性是个语法糖,可以很简单的实现子组件与父组件通信
示例代码待贴
- 兄弟组件通信
对于这种情况可以通过查找父组件中的子组件实现,也就是this.$parent.$children
,在$children
中可以通过组件name
查询到需要的组件实例,然后进行通信。
代码待贴
- 跨多层次组件通信
对于这种情况可以使用Vue 2.2
新增的API provide / inject
,虽然文档中不推荐直接使用在业务中,但是如果用得好的话还是很有用的。
假设有父组件 A,然后有一个跨多层级的子组件 B
// 父组件 A export default { provide: { data: 1 } } // 子组件 B export default { inject: ['data'], mounted() { // 无论跨几层都能获得父组件的 data 属性 console.log(this.data) // => 1 } }
- 任意组件
这种方式可以通过 Vuex 或者 Event Bus 解决,另外如果你不怕麻烦的话,可以使用这种方式解决上述所有的通信情况
- extend 能做什么
这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount 一起使用。// 创建组件构造器 let Component = Vue.extend({ template: '<div>test</div>' }) // 挂载到 #app 上 new Component().$mount('#app') // 除了上面的方式,还可以用来扩展已有的组件 let SuperComponent = Vue.extend(Component) new SuperComponent({ created() { console.log(1) } }) new SuperComponent().$mount('#app')
- mixin 和 mixins 区别
mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。
虽然文档不建议我们在应用中直接使用Vue.mixin({ beforeCreate() { // ...逻辑 // 这种方式会影响到每个组件的 beforeCreate 钩子函数 } })
mixin
,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的ajax
或者一些工具函数等等。
mixins
应该是我们最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过mixins
混入代码,比如上拉下拉加载数据这种逻辑等等。
另外需要注意的是mixins
混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。
- computed 和 watch 区别
computed
是计算属性,依赖其他属性计算值,并且computed
的值有缓存,只有当计算值变化才会返回内容。
watch
监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作
所以一般来说需要依赖别的属性来动态获得值的时候可以使用computed
,对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用watch
另外computed
和watch
还都支持对象的写法,这种方式知道的人并不多。vm.$watch('obj', { // 深度遍历 deep: true, // 立即触发 immediate: true, // 执行的函数 handler: function(val, oldVal) {} }) var vm = new Vue({ data: { a: 1 }, computed: { aPlus: { // this.aPlus 时触发 get: function () { return this.a + 1 }, // this.aPlus = 1 时触发 set: function (v) { this.a = v - 1 } } } })
- keep-alive 组件有什么作用
如果你需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用keep-alive
组件包裹需要保存的组件。
对于keep-alive
组件来说,它拥有两个独有的生命周期钩子函数,分别为activated
和deactivated
。用keep-alive
包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactivated
钩子函数,命中缓存渲染后会执行actived
钩子函数。
- v-show 与 v-if 区别
v-show
只是在display: none
和display: block
之间切换。无论初始条件是什么都会被渲染出来,后面只需要切换CSS
,DOM
还是一直保留着的。所以总的来说v-show
在初始渲染时有更高的开销,但是切换开销很小,更适合于频繁切换的场景。
v-if
的话就得说到Vue
底层的编译了。当属性初始为false
时,组件就不会被渲染,直到条件为true
,并且切换条件时会触发销毁/挂载组件,所以总的来说在切换时开销更高,更适合不经常切换的场景。
并且基于v-if
的这种惰性渲染机制,可以在必要的时候才去渲染组件,减少整个页面的初始渲染开销。
- 组件中 data 什么时候可以使用对象
组件复用时所有组件实例都会共享data
,如果data
是对象的话,就会造成一个组件修改data
以后会影响到其他所有组件,所以需要将data
写成函数,每次用到就调用一次函数获得新的数据。
当我们使用new Vue()
的方式的时候,无论我们将data
设置为对象还是函数都是可以的,因为new Vue()
的方式是生成一个根组件,该组件不会复用,也就不存在共享 data 的情况了
十二、vue知识点二
-
响应原理
Vue
内部使用了Object.defineProperty()
来实现数据响应式,通过这个函数可以监听到set
和get
的事件。
以上代码简单的实现了如何监听数据的var data = { name: 'Dashu' } observe(data) let name = data.name // -> get value data.name = 'jjj' // -> change value function observe(obj) { // 判断类型 if (!obj || typeof obj !== 'object') { return } Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) } function defineReactive(obj, key, val) { // 递归子属性 observe(val) Object.defineProperty(obj, key, { // 可枚举 enumerable: true, // 可配置 configurable: true, // 自定义函数 get: function reactiveGetter() { console.log('get value') return val }, set: function reactiveSetter(newVal) { console.log('change value') val = newVal } }) }
set
和get
的事件,但是仅仅如此是不够的,因为自定义的函数一开始是不会执行的。只有先执行了依赖收集,从能在属性更新的时候派发更新,所以接下来我们需要先触发依赖收集。
在解析如上模板代码时,遇到<div> {{name}} </div>
{{name}}
就会进行依赖收集。
接下来我们先来实现一个Dep
类,用于解耦属性的依赖收集和派发更新操作。
以上的代码实现很简单,当需要依赖收集的时候调用// 通过 Dep 解耦属性的依赖和更新操作 class Dep { constructor() { this.subs = [] } // 添加依赖 addSub(sub) { this.subs.push(sub) } // 更新 notify() { this.subs.forEach(sub => { sub.update() }) } } // 全局属性,通过该属性配置 Watcher Dep.target = null
addSub
,当需要派发更新的时候调用notify
。
接下来我们先来简单的了解下Vue
组件挂载时添加响应式的过程。在组件挂载时,会先对所有需要的属性调用Object.defineProperty()
,然后实例化Watcher
,传入组件更新的回调。在实例化过程中,会对模板中的属性进行求值,触发依赖收集。
触发依赖收集时的操作:
以上就是class Watcher { constructor(obj, key, cb) { // 将 Dep.target 指向自己 // 然后触发属性的 getter 添加监听 // 最后将 Dep.target 置空 Dep.target = this this.cb = cb this.obj = obj this.key = key this.value = obj[key] Dep.target = null } update() { // 获得新值 this.value = this.obj[this.key] // 调用 update 方法更新 Dom this.cb(this.value) } }
Watcher
的简单实现,在执行构造函数的时候将Dep.target
指向自身,从而使得收集到了对应的Watcher
,在派发更新的时候取出对应的Watcher
然后执行update
函数。
接下来,需要对defineReactive
函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码
以上所有代码实现了一个简易的数据响应式,核心思路就是手动触发一次属性的function defineReactive(obj, key, val) { // 递归子属性 observe(val) let dp = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { console.log('get value') // 将 Watcher 添加到订阅 if (Dep.target) { dp.addSub(Dep.target) } return val }, set: function reactiveSetter(newVal) { console.log('change value') val = newVal // 执行 watcher 的 update 方法 dp.notify() } }) }
getter
来实现依赖收集。var data = { name: 'yck' } observe(data) function update(value) { document.querySelector('div').innerText = value } // 模拟解析到 `{{name}}` 触发的操作 new Watcher(data, 'name', update) // update Dom innerText data.name = 'yyy'
-
Object.defineProperty 的缺陷
以上已经分析完了Vue
的响应式原理,接下来说一点Object.defineProperty
中的缺陷。
如果通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染,因为Object.defineProperty
不能拦截到这些操作,更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是Vue
内部通过重写函数的方式解决了这个问题。
对于第一个问题,Vue
提供了一个API
解决
对于数组而言,Vue 内部重写了以下函数实现派发更新export function set (target: Array<any> | Object, key: any, val: any): any { // 判断是否为数组且下标是否有效 if (Array.isArray(target) && isValidArrayIndex(key)) { // 调用 splice 函数触发派发更新 // 该函数已被重写 target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } // 判断 key 是否已经存在 if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any).__ob__ // 如果对象不是响应式对象,就赋值返回 if (!ob) { target[key] = val return val } // 进行双向绑定 defineReactive(ob.value, key, val) // 手动派发更新 ob.dep.notify() return val }
// 获得数组原型 const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) // 重写以下函数 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // 缓存原生函数 const original = arrayProto[method] // 重写函数 def(arrayMethods, method, function mutator (...args) { // 先调用原生函数获得结果 const result = original.apply(this, args) const ob = this.__ob__ let inserted // 调用以下几个函数时,监听新数据 switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // 手动派发更新 ob.dep.notify() return result }) })
-
编译过程
想必大家在使用Vue
开发的过程中,基本都是使用模板的方式。那么你有过「模板是怎么在浏览器中运行的」这种疑虑嘛?
首先直接把模板丢到浏览器中肯定是不能运行的,模板只是为了方便开发者进行开发。Vue
会通过编译器将模板通过几个阶段最终编译为render
函数,然后通过执行render
函数生成Virtual DOM
最终映射为真实DOM
接下来我们就来学习这个编译的过程,了解这个过程中大概发生了什么事情。这个过程其中又分为三个阶段,分别为:AST
(抽象语法树)
1.将模板解析为AST
2.转化为AST
3.将AST
转换为render
函数
在第一个阶段中,最主要的事情还是通过各种各样的正则表达式去匹配模板中的内容,然后将内容提取出来做各种逻辑操作,接下来会生成一个最基本的AST
对象
然后会根据这个最基本的{ // 类型 type: 1, // 标签 tag, // 属性列表 attrsList: attrs, // 属性映射 attrsMap: makeAttrsMap(attrs), // 父节点 parent, // 子节点 children: [] }
AST
对象中的属性,进一步扩展 AST。
当然在这一阶段中,还会进行其他的一些判断逻辑。比如说对比前后开闭标签是否一致,判断根组件是否只存在一个,判断是否符合 HTML5 Content Model 规范等等问题。
接下来就是优化AST
的阶段。在当前版本下,Vue
进行的优化内容其实还是不多的。只是对节点进行了静态内容提取,也就是将永远不会变动的节点提取了出来,实现复用Virtual DOM
,跳过对比算法的功能。在下一个大版本中,Vue
会在优化AST
的阶段继续发力,实现更多的优化功能,尽可能的在编译阶段压榨更多的性能,比如说提取静态的属性等等优化行为。
最后一个阶段就是通过AST
生成render
函数了。其实这一阶段虽然分支有很多,但是最主要的目的就是遍历整个AST
,根据不同的条件生成不同的代码罢了。 -
NextTick 原理分析
nextTick
可以让我们在下次DOM
更新循环结束之后执行延迟回调,用于获得更新后的DOM
。
在Vue 2.4
之前都是使用的microtasks
,但是microtasks
的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用macrotasks
又可能会出现渲染的性能问题。所以在新版本中,会默认使用microtasks
,但在特殊情况下会使用macrotasks
,比如v-on
。
对于实现macrotasks
,会先判断是否能使用setImmediate
,不能的话降级为MessageChannel
,以上都不行的话就使用setTimeout
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks) } } else if ( typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]') ) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = flushCallbacks macroTimerFunc = () => { port.postMessage(1) } } else { macroTimerFunc = () => { setTimeout(flushCallbacks, 0) } }
详细了解 Vue 技术揭秘
十二、react实现原理
十三、闭包
十四、混合开发兼容问题
十五、混合开发难点
十六、首页白屏处理(service worker)
作者不易,失业中。。。
前端面试知识点