前言 金三银四快过去了,抓紧这段时间再复习下vue,为了在面试官前突出自己,在回答的时候能带上源码的实现和理解往往更容易成功。这里整理了vue常见的面试题和相应的源码及解读,希望对大家有所帮助。篇幅较长,分为vue2篇和vue3篇。
自己整理手写的,有哪里不对或者说的不准确的欢迎指出来,我会虚心接收指教。
vue2 1.vue响应式原理 回答这个问题,首先要搞清楚什么叫响应式。通常vue中所说的响应式是指数据响应式,数据变化可以被检测并对这种变化做出响应的机制。而在Vue这种MVVM框架中,最重要的核心就是实现数据层和视图层的连接,通过数据驱动应用,数据变化,视图更新。Vue中的方案是数据劫持+发布订阅模式。
vue在初始化的时候会进行劫持,包括props,data,methods,computed,watcher,并根据数据类型来做不同处理.
如果是对象则采用Object.defineProperty()的方式定义数据拦截:
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
return val
},
set(v) {
val = v
notify()
}
})
}
如果是数组,则覆盖数组的7个变更方法实现变更通知:
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
['push','pop','shift','unshift','splice','sort','reverse']
.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this , args)
notify()
return result
})
})
这是数据劫持的部分,接下来说下视图更新的机制:
由于 Vue 执行一个组件的 render 函数是由 Watcher 去代理执行的,Watcher 在执行前会把 Watcher 自身先赋值给 Dep.target 这个全局变量,等待响应式属性去收集它。 在组件执行render函数时访问了响应式属性,响应式属性就会精确的收集到当前全局存在的 Dep.target 作为自身的依赖。 在响应式属性发生更新时通知 Watcher 去重新调用
进行组件的视图更新,视图更新的时候会通过diff算法对比新老vnode差异,通过patch即时更新DOM。 2.v-if和v-for哪个优先级高 答案是v-for解析的优先级高,可以在源码的compiler/codegen/index.js 里的genElement函数找到答案
function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
const children = el.inlineTemplate ? null : genChildren(el, state, true )
code = _c('${el.tag}'${
data ? ,${data} : '' // data
}${
children ? ,${children} : '' // children
})
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
vue中的内置指令都有相应的解析函数,执行顺序是通过简单的if else-if语法来确定的。在genFor的函数里,最后会return一个自运行函数,再次调用genElement。
虽然v-for和v-if可以放一起,但我们要避免这种写法,在官网中也有明确指出,这会造成性能浪费。
3.key的作用 作用:用来判断虚拟DOM的某个节点是否为相同节点,用于优化patch性能,patch就是计算diff的函数。
先看下patch函数:
只提取了本次要分析的关键代码
function patch (oldVnode, vnode) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null , null , removeOnly)
} else {
// some code
}
}
return vnode
}
patch函数接收oldVnode和vnode,也就是要比较的新旧节点对象。
首先会用isUndef函数判断传入的两个vnode是否为空对象再做相应处理。当两个都为节点对象时,再用sameVnode来判断是否为同一节点,再判断本次操作是新增、修改、还是移除。
function sameVnode (a, b) {
return (
a.key === b.key // key值
&&
(
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
isDef(a.data) === isDef(b.data) && // 是否都定义了data,data包含一些具体信息,例如onclick , style
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
)
}
sameVnode通过判断key、标签名、是否为注释、data等是否相等,来判断是否需要进行比较。
值得比较则执行patchVnode,不值得比较则用Vnode替换oldVnode,再渲染真实dom。
patchVnode会对oldVnode和vnode进行对比,然后进行DOM更新。这个会在diff算法里再进行说明。
v-for通常都是生成一样的标签,所以key会是patch判断是否相同节点的唯一标识,如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,就会去做pathVnode pdateChildren的更新操作,这造成了大量的dom更新操作,所以设置唯一的key是必要的。
4.双向绑定原理 vue中双向绑定是一个指令v-model,可以绑定一个动态值到视图,同时视图中变化能改变该值。v-model是语法糖,默认情况下相当于:value和@input。
通常在表单元素可以直接使用v-model,这是vue解析的时候对这些表单元素进行了处理,根据控件类型自动选取正确的方法来更新元素。
v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
text 和 textarea 元素使用 value property 和 input 事件; checkbox 和 radio 使用 checked property 和 change 事件; select 字段将 value 作为 prop 并将 change 作为事件。 如果是自定义组件的话要使用它需要在组件内绑定props value并在数据更新数据的时候用$emit('input'),也可以在组件里定义modal属性来自定义绑定的属性名和事件名称。
model: {
prop: 'checked',
event: 'change'
}
5.nextTick原理 先看下官方文档的说明:
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。
nextTick就是将回调函数放到队列里去,保证在异步更新DOM的watcher后面,从而获取到更新后的DOM。
结合src/core/util/next-tick源码再进行分析。
首先是定义执行任务队列方法
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
按照推入callbacks队列的顺序执行回调函数。
然后定义timerFunc函数,根据当前环境支持什么方法来确定调用哪个异步方法
判断的顺序是:
Promise > MutationObserver > setImmediate > setTimeout
最后是定义nextTick方法:
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
其实nextTick就是一个把回调函数推入任务队列的方法。
6.data为什么是函数 如果组件里 data 直接写了一个对象的话,那么在模板中多次声明这个组件,组件中的 data 会指向同一个引用。
此时对 data 进行修改,会导致其他组件里的 data 也被修改。使用函数每次都重新声明一个对象,这样每个组件的data都有自己的引用,就不会出现相互污染的情况了。
7.组件通信方式 1. props和
、
适合父子组件的通信,通过props传递响应式数据,父组件通过
监听事件、子组件通过
发送事件。
on和emit是在组件实例初始化的时候通过
初始化事件,在组件实例vm._events赋值一个空的事件对象,通过这个对象实现事件的发布订阅。下面是事件注册的几个关键函数:
// 组件初始化event对象,收集要监听的事件和对应的回调函数
function initEvents (vm: Component) {
vm._events = Object.create(null )
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
...
// 注册组件监听的事件
function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
2.
、
、
,还有
ref: 在普通DOM元素上声明就是DOM元素的引用,组件就是指向组件实例。 $parent:访问组件的父组件实例 $children:访问所有的子组件集合(数组) $root: 指向root实例 3. Event Bus
通常是创建一个
,实现任何组件在这个实例上的事件触发与监听。原理就是一个发布订阅的模式,跟
一样,在实例化一个组件的事件通过initEvents初始化一个空的event对象,再通过实例化后的这个bus(vue实例)手动的
、
添加监听和触发的事件,代码在
:
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
// 传入的事件如果是数组,就循环监听每个事件
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// 如果已经有这个事件,就push新的回调函数进去,没有则先赋值空数组再push
(vm._events[event] || (vm._events[event] = [])).push(fn)
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
...
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
...
let cbs = vm._events[event]
// 循环调用要触发的事件的回调函数数组
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
4. attrs、listeners
: 包含了父作用域
绑定的数据,组件可以通过
继续传给子组件 : 包含了父作用域中的
(不含 .native 修饰器的) 监听事件,可以通过
传入内部组件 5. provide、inject
父组件通过provide注入一个依赖,其所有的子孙组件可以通过inject来接收。要注意的是官网有这一段话:
提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
所以Vue不会对provide中的变量进行响应式处理。要想 inject 接受的变量是响应式的,provide 提供的变量本身就需要是响应式的。实际上在很多高级组件中都可以看到组件会将this通过provide传递给子孙组件,包括element-ui、ant-design-vue等。
6. vuex 状态管理实现通信
vuex是专为vue设计的状态管理模式。每个组件实例都有共同的store实例,并且store.state是响应式的,改变state唯一的办法就是通过在这个store实例上commit一个mutation,方便跟踪每一个状态的变化,实现原理在下面的vuex原理里有讲。
8.computed、watch、method有什么区别 computed:有缓存,有对应的watcher,watcher有个lazy为true的属性,表示只有在模板里去读取它的值后才会计算,并且这watcher在初始化的时候会赋值dirty为true,watcher只有dirty为true的时候才会重新求值,重新求值后会将dirty置为false,false会直接返回watcher的value,只有下次watcher的响应式依赖有更新的时候,会将watcher的dirty再置为false,这时候才会重新求值,这样就实现了computed的缓存。
watch:watcher的对象每次更新都会执行函数。watch 更适用于数据变化时的异步操作。如果需要在某个数据变化时做一些事情,使用watch。
method: 将方法在模板里使用,每次视图有更新都会重新执行函数,性能消耗较大。
9.生命周期 官网对生命周期的说明:
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
生命周期就是每个Vue实例完成初始化、运行、销毁的一系列动作的钩子。
基本上可以说8 个阶段创建前/后,载入前/后,更新前/后,销毁前/后。
创建前/后: 在 beforeCreate 阶段,vue 实例的挂载元素 el 还没有。 载入前/后:在 beforeMount 阶段,vue 实例的$el 和 data 都初始化了,但还是挂载之前为虚拟的 dom 节点,data.message 还未替换。在 mounted 阶段,vue 实例挂载完成,data.message 成功渲染。 更新前/后:当 data 变化时,会触发 beforeUpdate 和 updated 方法。 销毁前/后:在执行 destroy 方法后,对 data 的改变不会再触发周期函数,说明此时 vue 实例已经解除了事件监听以及和 dom 的绑定,但是 dom 结构依然存在 结合源码再理解,在源码中生命周期钩子是用callHook函数调用的。看下callHook函数:
function callHook (vm: Component, hook: string) {
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null , vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
接收一个vm组件实例的参数和hook,取组件实例的$options传入的hook属性值,有的话会循环调用这个钩子的回调函数。在调用生命钩子的回调函数之前会临时pushTarget一个null值,也就是将Dep.target置为空来禁止在执行生命钩子的时候进行依赖收集。
vm.$emit('hook:' + hook)则是用来给父组件监听该组件的回调事件。
接下来看每个生命钩子具体调用的时机。
1. beforeCreate、created: Vue.prototype._init = function (options?: Object) {
...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
在执行beforeCreate之前调用了
initLifecycle、initEvents、initRender
函数,所以beforeCreate是在初始化生命周期、事件、渲染函数之后的生命周期。
在执行created之前调用了initInjections、initState、initProvide,这时候created初始化了data、props、watcher、provide、inject等,所以这时候就可以访问到data、props等属性。
2. beforeMount、mounted 3. beforeUpdate、updated 这两个钩子函数是在数据更新的时候进行回调的函数。在
src/core/instance/lifecycle.js
找到beforeUpdate调用的代码:
...
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */ )
...
_isMounted为ture的话(DOM已经被挂载)会调用callHook(vm, 'beforeUpdate')方法,然后会对虚拟DOM进行重新渲染。然后在/src/core/observer/scheduler.js下的flushSchedulerQueue()函数中渲染DOM,flushSchedulerQueue会刷新watcher队列并执行,执行完所有watcher的run方法之后(run方法就是watcher进行dom diff并更新DOM的方法),再调用callHook(vm, 'updated'),代码如下:
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
...
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
watcher.run()
}
...
callUpdatedHooks(updatedQueue)
...
}
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
4. beforeDestroy、destroyed 这两个钩子是vue实例销毁的钩子,定义在Vue.prototype.$destroy中:
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null )
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
在销毁之前执行callHook(vm, 'beforeDestroy'),然后销毁的时候做了几件事:
如果有父元素,将父元素的$children中把该组件实例移除。 移除watchers,并在依赖订阅者中移除自己。 删除数据引用 5. activated、deactivated 剩下的还有
activated、deactivated、errorCaptured
三个钩子函数。
activated、deactivated这两个钩子函数分别是在keep-alive 组件激活和停用之后的回调。
errorCaptured捕获到当子孙组件错误时会被调用,在源码中可以经常看到try catch中catch会调用handleError函数,handleError会向组件所有的父级组件抛出异常,
function handleError (err: Error, vm: any, info: string) {
pushTarget()
try {
if (vm) {
let cur = vm
while ((cur = cur.$parent)) {
const hooks = cur.$options.errorCaptured
if (hooks) {
for (let i = 0; i < hooks.length; i++) {
try {
const capture = hooks[i].call(cur, err, vm, info) === false
if (capture) return
} catch (e) {
globalHandleError(e, cur, 'errorCaptured hook')
}
}
}
}
}
globalHandleError(err, vm, info)
} finally {
popTarget()
}
}
分析完源码再一下官网图示,会更清楚:
10.keep-aliva原理 keep-alive是Vue.js的一个内置组件。它能够将不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。
include与exclude两个属性,允许组件有条件地进行缓存,max属性确定最多缓存多少组件实例。
keep-alive是一个组件,跟其他组件一样有生命周期和render函数,keep-alive包裹的分析keep-alive就是分析一个组件。
源码再
src/core/components/keep-alive
,created声明了要缓存的组件对象,和存储的组件keys,keep-alive销毁的时候会用pruneCacheEntry将缓存的所有组件实例销毁,也就是调用组件实例的destroy方法。在挂载完成后监听include和exclude,动态地销毁已经不满足include的组件和满足exclude的组件实例:
created () {
this.cache = Object.create(null ) // 存储需要缓存的组件
this.keys = [] // 存储每个需要缓存的组件的key,即对应this.cache对象中的键值
},
// 销毁keep-alive组件的时候,对缓存中的每个组件执行销毁
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.$watch('include', val => {
pruneCache(this , name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this , name => !matches(val, name))
})
},
接下来是render函数:
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
// 如果vnode存在就取vnode的选项
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
//获取第一个有效组件的name
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode// 说明不用缓存,直接返回这个组件进行渲染
}
// 匹配到了,开始缓存操作
const { cache, keys } = this // keep-alive组件的缓存组件和缓存组件对应的key
// 获取第一个有效组件的key
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
// 这个组件的实例用缓存中的组件实例替换
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
// 更新当前key在keys中的位置
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
// 如果缓存中的组件个数超过传入的max,销毁缓存中的LRU组件
// LRU: least recently used 最近最少用,缓存淘汰策略
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
// 若第一个有效的组件存在,但其componentOptions不存在,就返回这个组件进行渲染
// 或若也不存在有效的第一个组件,但keep-alive组件的默认插槽存在,就返回默认插槽的第一个组件进行渲染
return vnode || (slot && slot[0])
}
代码做了详细的注释,这里再分析下render做了什么。
通过this.$slots.default拿到插槽组件,也就是keep-alive包裹的组件,getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag)。接下来会将这个name通过include与exclude属性进行匹配,匹配不成功(说明不需要进行缓存)则不进行任何操作直接返回vnode
(vnode节点描述对象,vue通过vnode创建真实的DOM)
。
匹配到了就开始缓存,根据key在this.cache中查找,如果存在则说明之前已经缓存过了,直接将缓存的vnode的componentInstance(组件实例)覆盖到目前的vnode上面。否则将vnode存储在cache中。并且通过remove(keys, key),将当前的key从keys中删除再重新keys.push(key),这样就改变了当前key在keys中的位置。这个是为了实现max的功能,并且遵循缓存淘汰策略。
如果没匹配到,说明没缓存过,这时候需要进行缓存,并且判断当前缓存的个数是否超过max指定的个数,如果超过,则销毁keys里的最后一个组件,并从keys中移除,这个就是LRU(
Least Recently Used :最近最少使用
)缓存淘汰算法。
最后返回vnode或者默认插槽的第一个组件进行DOM渲染。
12.虚拟dom和diff算法 虚拟DOM是对DOM的描述,用对象属性来描述节点,本质上是JavaScript对象。它有几个意义:
具备跨平台的优势 由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器、小程序、Node、原生应用、服务端渲染等等。
提升渲染性能 频繁变动DOM会造成浏览器的回流或者重回,而通过将大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,可以减少真实DOM的操作次数,从而提高性能。
代码可维护性更高 通过虚拟 DOM 的抽象能力,可以用声明式写 UI 的方式,大大提高了我们的工作效率。
在vue中template最终会转成render函数,而render函数最终是执行的createElement,生成vnode,vnode正是 vue中用来表示虚拟DOM的类,看下vnode:
class VNode {
tag: string | void ;
data: VNodeData | void ;
children: ?Array<VNode>;
text: string | void ;
elm: Node | void ;
ns: string | void ;
context: Component | void ; // rendered in this component's scope
key: string | number | void ;
componentOptions: VNodeComponentOptions | void ;
componentInstance: Component | void ; // component instance
parent: VNode | void ; // component placeholder node
// strictly internal
raw: boolean ; // contains raw HTML? (server only)
isStatic: boolean ; // hoisted static node
isRootInsert: boolean ; // necessary for enter transition check
isComment: boolean ; // empty comment placeholder?
isCloned: boolean ; // is a cloned node?
isOnce: boolean ; // is a v-once node?
asyncFactory: Function | void ; // async component factory function
asyncMeta: Object | void ;
isAsyncPlaceholder: boolean ;
ssrContext: Object | void ;
fnContext: Component | void ; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
看下其中关键的几个属性:
tag: 当前节点的标签名 data: 表示节点上的class,attribute,style以及绑定的事件 children: 当前节点的子节点,是一个数组 text: 当前节点的文本 elm: 当前虚拟节点对应的真实dom节点 key: 节点的key属性,被当作节点的标志,用以优化 componentOptions: 组件的option选项 componentInstance: 当前节点对应的组件的实例 parent: 当前节点的父节点 isStatic: 是否为静态节点 children和parent是指当前的vnode的子节点和父节点,这样一个个vnode就形成了DOM树。
diff算法发生在
的时候,也就是数据更新的时候,
diff算法会将新旧虚拟DOM作对比,将变化的地方转换为DOM
。
当某个数据被修改的时候,依赖对应的watcher会通知更新,执行渲染函数会生成新的vnode,vnode再去与旧的vnode进行对比更新,这就是vue中的虚拟dom diff算法触发的流程。
看下组件更新的_update方法:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean ) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */ )
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
...
vm.$el = vm._patch(),这个就是最终渲染的DOM元素,patch就是vue中diff算法的函数,在key的作用章节有提过。patch将新旧虚拟DOM节点比较后,最终返回真实的DOM节点。
patch 看下patch代码(部分):
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
/*vnode不存在则直接调用销毁钩子*/
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
/*标记旧的VNode是否有nodeType*/
/*Github:https://github.com/answershuto*/
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
/*是同一个节点的时候直接修改现有的节点*/
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
...
return vnode.elm
首先是判断是否有新的vnode,没有代表是要销毁旧的vnode,调用销毁组件的钩子。
然后判断是否有旧的vnode,没有代表是新增,也就是新建root节点。
接下来判断旧的vnode是否是真实的元素,而不是组件,如果是组件并且用someVnode判断新旧节点是否是相同的节点(sameVnode在key的作用章节有做解析),是进行patchVnode,这时候进行真正的新老节点的diff。
patchVnode function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 两个vnode相同,说明不需要diff,直接返回
if (oldVnode === vnode) {
return
}
// 如果传入了ownerArray和index,可以进行重用vnode,updateChildren里用来替换位置
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
// 如果oldVnode的isAsyncPlaceholder属性为true时,跳过检查异步组件,return
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
/*
如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),
并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),
那么只需要替换elm以及componentInstance即可。
*/
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节点没有text文本时*/
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 两个vnode都定义了子节点,并且不相同,就对子节点进行diff
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 如果只有新的vnode定义了子节点,则进行添加子节点的操作
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null , ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果只有旧的vnode定义了子节点,则进行删除子节点的操作
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(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分为多种情况,分析下子节点的diff过程
(oldCh 为 oldVnode的子节点,ch 为 Vnode的子节点)
oldCh、ch都定义了调用updateChildren再进行diff 若 oldCh不存在,ch 存在,首先清空 oldVnode 的文本节点,同时调用 addVnodes 方法将 ch 添加到elm真实 dom 节点当中 若 oldCh存在,ch不存在,则删除 elm 真实节点下的 oldCh 子节点 若 oldVnode 有文本节点,而 vnode 没有,那么就清空这个文本节点 是子节点diff的函数,也是最重要的环节。
updateChildren function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 声明oldCh和newCh的头尾索引和头尾的vnode,
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
// 判断两边的头是不是相同节点
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// 判断尾部是不是相同节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// 判断旧节点头部是不是与新节点的尾部相同,相同则把头部往右移
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 判断旧节点尾部是不是与新节点的头部相同,相同则把头部往左移
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
/*
生成一个key与旧VNode的key对应的哈希表
*/
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false , newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false , newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
// oldCh或者newCh遍历完,说明剩下的节点不是新增就是删除
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
首先给startIndex和endIndex来作为遍历的索引,在遍历的时候会先判断头尾节点是否相同,没有找到相同节点后再按照通用方式遍历查找;查找结束再按情况处理剩下的节点;借助key通常可以非常精确找到相同节点。
当oldCh 或者 newCh 遍历完后(遍历完的条件就是 oldCh 或者 newCh 的 startIndex >= endIndex ),说明剩下的节点为新增或者删除,这时候停止oldCh 和 newCh 的 diff。
13.Vuex原理 vuex是什么,先看下官方的原话:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
这段话可以得出几个结论:
,而像redux与react是解耦的,然后vuex是状态管理模式,所有的状态以一种可预测的方式发生变化。
设计思想:
Vuex的设计思想,借鉴了Flux、Redux,将数据存放到全局的store,再将store挂载到每个vue实例组件中,利用Vue.js的细粒度数据响应机制来进行高效的状态更新。
原理可以从使用方式开始分析。
Vue.use(Vuex); // 1\. vue的插件机制,安装vuex
let store = new Vuex.Store({ // 2.实例化store,调用install方法
state,
getters,
modules,
mutations,
actions,
plugins
});
new Vue({ // 3.注入store, 挂载vue实例
store,
render: h=>h(app)
}).$mount('#app');
Vue.use是vue中的插件机制,内部会调用插件的install方法,vuex的install方法:
export function install (_Vue) {
if (Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
/*保存Vue,同时用于检测是否重复安装*/
Vue = _Vue
/*将vuexInit混淆进Vue的beforeCreate(Vue2.0)或_init方法(Vue1.0)*/
applyMixin(Vue)
}
vuex是个全局的状态管理,全局有且只能有一个store实例,所以在install的时候会判断是否已经安装过了,这个就是单例模式,确保一个类只有一个实例。在第一次install的时候会applyMixin,applyMixin是
导入的方法:
function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this , options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
先是判断下vue的版本,这边分析vue2的逻辑。利用Vue.mixin混入的机制,在组件实例的beforeCreate调用vuexInit方法,首先判断options是否有store,没有代表是root节点,这时候要进行store初始化,没有的话就取父组件的$store赋值,这样就实现了全局共用唯一的store实例。
store实现的源码在
,其中最核心的是响应式的实现,通过resetStoreVM(this, state)调用,看下这个方法:
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// bind store public getters
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null )
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure environment.
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
resetStoreVM首先会遍历wrappedGetters,使用Object.defineProperty方法对store.getters的每一个getter定义get方法,这样访问this.$store.getter.test就等同于访问store._vm.test。
state是通过new一个Vue对象来实现数据的“响应式化”,运用Vue的data属性来实现数据与视图的同步更新,computed实现getters的计算属性。最终访问store.state也就是访问store._vm.state。
最后 在面试前花了三个月时间刷了很多大厂面试题,最近做了一个整理并分类,主要内容包括html,css,JavaScript,ES6,计算机网络,浏览器,工程化,模块化,Node.js,框架,数据结构,性能优化,项目等等。
包含了腾讯、字节跳动、小米、阿里、滴滴、美团、58、拼多多、360、新浪、搜狐等一线互联网公司面试被问到的题目,涵盖了初中级前端技术点。
无偿分享给大家,算是一个感恩回馈吧,详细内容可以在文末自行获取哈!
HTML HTML5新特性,语义化 浏览器的标准模式和怪异模式 xhtml和html的区别 使用data-的好处 meta标签 canvas HTML废弃的标签 IE6 bug,和一些定位写法 css js放置位置和原因 什么是渐进式渲染 html模板语言 meta viewport原理 CSS 盒模型,box-sizing CSS3新特性,伪类,伪元素,锚伪类 CSS实现隐藏页面的方式 如何实现水平居中和垂直居中。 说说position,display 请解释*{box-sizing:border-box;}的作用,并说明使用它的好处 浮动元素引起的问题和解决办法?绝对定位和相对定位,元素浮动后的display值 link和@import引入css的区别 解释一下css3的flexbox,以及适用场景 inline和inline-block的区别 哪些是块级元素那些是行级元素,各有什么特点 grid布局 table布局的作用 实现两栏布局有哪些方法? css dpi 你知道attribute和property的区别么 css布局问题?css实现三列布局怎么做?如果中间是自适应又怎么做? 流式布局如何实现,响应式布局如何实现 移动端布局方案 实现三栏布局(圣杯布局,双飞翼布局,flex布局) 清除浮动的原理 overflow:hidden有什么缺点? padding百分比是相对于父级宽度还是自身的宽度 css3动画,transition和animation的区别,animation的属性,加速度,重力的模拟实现 CSS 3 如何实现旋转图片(transform: rotate) sass less 对移动端开发了解多少?(响应式设计、Zepto;@media、viewport、JavaScript 正则表达式判断平台。) 什么是bfc,如何创建bfc?解决什么问题? CSS中的长度单位(px,pt,rem,em,ex,vw,vh,vh,vmin,vmax) CSS 选择器的优先级是怎样的? 雪碧图 svg 媒体查询的原理是什么? CSS 的加载是异步的吗?表现在什么地方? 常遇到的浏览器兼容性问题有哪些?常用的hack的技巧 外边距合并 解释一下“::before”和“:after”中的双冒号和单冒号的区别 JS js的基本类型有哪些?引用类型有哪些?null和undefined的区别。 如何判断一个变量是Array类型?如何判断一个变量是Number类型?(都不止一种) Object是引用类型嘛?引用类型和基本类型有什么区别?哪个是存在堆哪一个是存在栈上面的? JS常见的dom操作api 解释一下事件冒泡和事件捕获 事件委托(手写例子),事件冒泡和捕获,如何阻止冒泡?如何组织默认事件? 对闭包的理解?什么时候构成闭包?闭包的实现方法?闭包的优缺点? this有哪些使用场景?跟C,Java中的this有什么区别?如何改变this的值? call,apply,bind 显示原型和隐式原型,手绘原型链,原型链是什么?为什么要有原型链 创建对象的多种方式 实现继承的多种方式和优缺点 new 一个对象具体做了什么 手写Ajax,XMLHttpRequest 变量提升 举例说明一个匿名函数的典型用例 指出JS的宿主对象和原生对象的区别,为什么扩展JS内置对象不是好的做法?有哪些内置对象和内置函数? attribute和property的区别 document load和document DOMContentLoaded两个事件的区别 === 和 == , [] === [], undefined === undefined,[] == [], undefined == undefined typeof能够得到哪些值 什么是“use strict”,好处和坏处 函数的作用域是什么?js 的作用域有几种? JS如何实现重载和多态 常用的数组api,字符串api 原生事件绑定(跨浏览器),dom0和dom2的区别? 给定一个元素获取它相对于视图窗口的坐标 如何实现图片滚动懒加载 js 的字符串类型有哪些方法? 正则表达式的函数怎么使用? 深拷贝 编写一个通用的事件监听函数 web端cookie的设置和获取 setTimeout和promise的执行顺序 JavaScript 的事件流模型都有什么? navigator对象,location和history js的垃圾回收机制 内存泄漏的原因和场景 DOM事件的绑定的几种方式 DOM事件中target和currentTarget的区别 typeof 和 instanceof 区别,instanceof原理 js动画和css3动画比较 JavaScript 倒计时(setTimeout) js处理异常 js的设计模式知道那些 轮播图的实现,以及轮播图组件开发,轮播10000张图片过程 websocket的工作原理和机制。 手指点击可以触控的屏幕时,是什么事件? 什么是函数柯里化?以及说一下JS的API有哪些应用到了函数柯里化的实现?(函数柯里化一些了解,以及在* 函数式编程的应用,最后说了一下JS中bind函数和数组的reduce方法用到了函数柯里化。) JS代码调试
框架 使用过哪些框架? zepto 和 jquery 是什么关系,有什么联系么? jquery源码如何实现选择器的,为什么$取得的对象要设计成数组的形式,这样设计的目的是什么 jquery如何绑定事件,有几种类型和区别 什么是MVVM,MVC,MVP Vue和Angular的双向数据绑定原理 Vue,Angular组件间通信以及路由原理 react和vue的生命周期 react和vue的虚拟dom以及diff算法 vue的observer,watcher,compile react和angular分别用在什么样的业务吗?性能方面和MVC层面上的区别 jQuery对象和JS的Element有什么区别 jQuery对象是怎么实现的 jQuery除了它封装了一些方法外,还有什么值得我们去学习和使用的? jQuery的$(‘xxx’)做了什么事情 介绍一下bootstrap的栅格系统是如何实现的 浏览器相关 跨域,为什么JS会对跨域做出限制 前端安全:xss,csrf... 浏览器怎么加载页面的?script脚本阻塞有什么解决方法?defer和async的区别? 浏览器强缓存和协商缓存 浏览器的全局变量有哪些 浏览器同一时间能够从一个域名下载多少资源 按需加载,不同页面的元素判断标准 web存储、cookies、localstroge等的使用和区别 浏览器的内核 如何实现缓存机制?(从200缓存,到cache到etag再到) 说一下200和304的理解和区别 什么是预加载、懒加载 一个 XMLHttpRequest 实例有多少种状态? dns解析原理,输入网址后如何查找服务器 服务器如何知道你? 浏览器渲染过程 ie的某些兼容性问题 session 拖拽实现 拆解url的各部分 ES6 谈一谈 promise 所有的 ES6 特性你都知道吗?如果遇到一个东西不知道是 ES6 还是 ES5, 你该怎么区分它 es6的继承和es5的继承有什么区别 promise封装ajax let const的优点 es6 generator 是什么,async/await 实现原理 ES6和node的commonjs模块化规范区别 箭头函数,以及它的this 计算机网络 HTTP协议头含有哪些重要的部分,HTTP状态码 网络url输入到输出怎么做? 性能优化为什么要减少 HTTP 访问次数? Http请求的过程与原理 https(对是https)有几次握手和挥手?https的原理。 http有几次挥手和握手?TLS的中文名?TLS在哪一网络层? TCP连接的特点,TCP连接如何保证安全可靠的? 为什么TCP连接需要三次握手,两次不可以吗,为什么 为什么tcp要三次握手四次挥手? tcp的三次握手和四次挥手画图(当场画写ack 和 seq的值)? tcp与udp的区别 get和post的区别?什么情况下用到? http2 与http1 的区别? websocket 什么是tcp流,什么是http流 babel是如何将es6代码编译成es5的 http2的持久连接和管线化 域名解析时是tcp还是udp 域名发散和域名收敛 Post一个file的时候file放在哪的? HTTP Response的Header里面都有些啥? 工程化 对webpack,gulp,grunt等有没有了解?对比。 webpack的入口文件怎么配置,多个入口怎么分割。 webpack的loader和plugins的区别 gulp的具体使用。 前端工程化的理解、如何自己实现一个文件打包,比如一个JS文件里同时又ES5 和ES6写的代码,如何编译兼容他们 模块化 对AMD,CMD,CommonJS有没有了解? 为什么要模块化?不用的时候和用RequireJs的时候代码大概怎么写? 说说有哪些模块化的库,有了解过模块化的发展的历史吗? 分别说说同步和异步模块化的应用场景,说下AMD异步模块化实现的原理? 如何将项目里面的所有的require的模块语法换成import的ES6的语法? 使用模块化加载时,模块加载的顺序是怎样的,如果不知道,根据已有的知识,你觉得顺序应该是怎么样的? Nodejs 对nodejs有没有了解 Express 和 koa 有什么关系,有什么区别? nodejs适合做什么样的业务? nodejs与php,java有什么区别 Nodejs中的Stream和Buffer有什么区别? node的异步问题是如何解决的? node是如何实现高并发的? 说一下 Nodejs 的 event loop 的原理 数据结构 基本数据结构:(数组、队列、链表、堆、二叉树、哈希表等等) 8种排序算法,原理,以及适用场景和复杂度 说出越多越好的费波拉切数列的实现方法? 性能优化 cdn的用法是什么?什么时候用到? 浏览器的页面优化? 如何优化 DOM 操作的性能 单页面应用有什么SEO方案? 单页面应用首屏显示比较慢,原因是什么?有什么解决方案? 其他 正则表达式 前端渲染和后端渲染的优缺点 数据库的四大特性,什么是原子性,表的关系 你觉得前端体系应该是怎样的? 一个静态资源要上线,里面有各种资源依赖,你如何平稳上线 如果要你去实现一个前端模板引擎,你会怎么做 知道流媒体查询吗? SEO mysql 和 mongoDB 有什么区别? restful的method解释 数据库知识、操作系统知识 click在ios上有300ms延迟,原因及如何解决 移动端的适配,rem+媒体查询/meta头设置 移动端的手势和事件; unicode,utf8,gbk编码的了解,乱码的解决 ----------------------------
原文链接:https://www.jianshu.com/p/05c1c230595c
程序猿的技术大观园:www.javathinker.net
[这个贴子最后由 flybird 在 2021-06-24 13:16:18 重新编辑]
网站系统异常
系统异常信息
Request URL:
http://www.javathinker.net/WEB-INF/lybbs/jsp/topic.jsp?postID=3870
java.lang.NullPointerException
如果你不知道错误发生的原因,请把上面完整的信息提交给本站管理人员 。