我的阿里面经(上)
前言
有啥前言啊,突如其来的两个电面,打出了我最真实的水平。废话不多说了。。。
整体内容偏向于前端基础,广度感人,面试官非常nice,并没有到了中途感觉自己是个废物,然后直接挂掉电话去角落哭。整个过程三十分钟,我把我记下来的问题,和我当时的心路历程和回答抛出来,希望能够记录当时的自己有多傻。当然了,总结经验和教训才是这篇博客的主旨,问题驱动的学习看起来功利,多少也是收获。
Q1:聊聊你对于Redux和Vuex的使用经验及看法
我的回答:redux和vuex的功能都是提供统一的状态管理,用法上都是针对组件进行需要的数据注入,vuex的mapState,redux的connect。vuex的状态树核心还是一个vue实例,state对应到data,getter对应到computed。redux的话没有再深入了解。
写在这里:我是一个主要使用vue的同学,vuex的源码也看过一些,反正知道的都说了。react算是自己想了解,redux也只是停留在了使用的层面。所以说用不用在生产上可能不重要,有没有自己写出来的项目也不那么重要,重要的是了解一些原理,谁知道啥时候就用上了。
A1:对比分析法
先允许我摘抄一段尤大在知乎上的回答:
Vuex 其实是一个针对 Vue 特化的 Flux,主要是为了配合 Vue 本身的响应式机制。当然吸取了一些 Redux 的特点,比如单状态树和便于测试和热重载的 API,但是也选择性的放弃了一些在 Vue 的场景下并不契合的特性,比如强制的 immutability(在保证了每一次状态变化都能追踪的情况下强制的 immutability 带来的收益就很有限了)、为了同构而设计得较为繁琐的 API、必须依赖第三方库才能相对高效率地获得状态树的局部状态等等(相比之下 Vuex 直接用 Vue 本身的计算属性就可以)
所以 Vue + Vuex 会更简洁,也不需要考虑性能问题,代价就是 Vuex 只能和 Vue 配合。Vue + Redux 也不是不可以,但是 Redux 作为一个泛用的实现和 Vue 的契合度肯定不如 Vuex。
从这一段我们可以抽取出一些比较高级别的观点:
- Vuex相对于Redux的共性:单一状态树,便于测试和热重载的API。
- Vuex相对于Redux的差异:没有强制的immutability,没有复杂的API,不依赖第三方库就可以获取局部状态(这里应该说的是Redux只提供了一个getState方法获取整个状态树,而Vuex通过内涵的Vue实例的computed就可以实现)。
- Vuex只能配合Vue,而“Redux is a predictable state container for JavaScript apps.”
从两者的工作流程上来对比:
- Vuex:视图层dispatch-actions或commit-mutations,mutate-state,state-render
- Redux:视图层dispatch-actions,reducer消费actions并返回新的store-render
后续的任务肯定是再深入的了解Redux乃至Flux的工作原理。Vuex的源码倒是读过一些,整理好了也会发出来。
Q2:Vue是如何实现响应式的,收集依赖是怎么做的,computed是怎么实现
我的回答:Vue的响应式核心是Object.defineProperty,通过对响应式数据设置getter和setter,并在编译模板和处理computed属性时收集依赖,作为数据的订阅者,setter方法被触发时,能够主动通知到这些依赖使其产生响应。编译模板时,对于v-model指令指明的数据以来关系,则会产生一个数据与虚拟DOM之间的依赖,数据变更时,会通过patch的过程,将响应产生变化的虚拟DOM渲染到页面上。computed答得烂七八杂我也不记得。
A2:先熟悉官方文档中“深入响应式原理”一章
首先,我们在编写组件时,统一导出的是一个普通的对象,解析.vue文件时,编译模板,基于这个对象创建Vue实例。对于data属性,通常是个函数,返回实例所需的状态集合,遍历这个集合使用Object.defineProperty把这些状态转化为getter/setter。官方文档也提示了,Object.defineProperty不可以被转化到ES5,所以IE8无法支持。
对组件内的状态,也就是data赋值会触发setter方法,而读取则会触发getter方法,这个过程是隐含的。而响应式则通过watcher实例实现,组件渲染过程中肯定会访问data,这就会触发getter,从而触发watcher收集到一个依赖关系,而后当被依赖的数据被重新赋值,触发setter,就会通知watcher,按照之前收集的依赖关系进行通知,触发组件的render函数。我们编写的模板部分,就相当于render函数。
由于创建getter/setter是在创建Vue实例的阶段执行的,如果对实例追加属性,无法使其正确的产生getter/setter,也不会让watcher感知到它与其他数据的关联,可以使用Vue.set,或者Vue.prototype.$set来实现。
异步更新队列可能也会被问到,同一个EventLoop,或者说某一段不包含一部代码的逻辑,可能会有多个数据更新,多次触发watcher,这些更改会被推出一个队列,这个队列中的更改会进行去重,并尝试使用Promise.then、MutationObserver、setImmediate甚至setTimeout(fn,0)来代替。注意这些方法有些是宏任务,有些是微任务,但是都是为了实现Vue渲染的一个“tick”。所以Vue提供了nextTick函数来将修改推入下一个更新队列,防止更新被合并。
注意:不可被监听的变化包括1.数组中原始类型元素的值的变化。2.数组的length变化。这里解释一下为什么,defineProperty这个API,看名字就知道是给某个对象定义一个属性,而属性其实是包含元属性的,就是描述这个属性的属性。就用数组的length属性,这个属性具有什么特性呢--writable:true可写、enumerable:false不可被枚举、configurable:false不可被配置。对于[[Configurable]]的描述如下:
If
false
, the property can't be deleted, can't be changed to an accessor property and attributes other than [[Value]] and [[Writable]] can't be changed.
不能被delete操作符删除,不能被变更为访问器属性,[[Value]]和[[Writable]]的值也不能修改。所以length不能转换成getter/setter,也就没法收集依赖和通知watcher。
至于computed属性,通常我们只会用到getter,其实也可以设置它的setter,比如:
{
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: {
get(){
return this.firstName + ' ' + this.lastName
},
set(newValue): {
const [firstName, lastName] = newValue.split(" ");
this.firstName = firstName;
this.lastName = lastName;
}
}
}
}
这样看起来更像是一个看得到getter/setter的可响应的一个属性,而相比data中的属性,computed的初始值往往不是确定的,而是依赖关系得到的,而这个初始值,或者说变化后的值,都是延迟求值,用到时才会调用get来更新一下自己。且具有缓存性。
这里先简单聊一下computed的创建与工作方式,稍后我们再来通过源码来详细聊聊。
Vue的响应式离不开Watcher、Observe和Dep。初始化computed时,会相应的对每一个computed字段创建一个内部watcher,即_computedWatchers集合。然后通过defineComputed函数,将computed字段转换成访问器属性挂载到Vue实例上。而defineComputed定义的get访问器,会从_computedWatchers中拿出对应的watcher,如果有依赖则调用evaluate方法进行懒求值,然后返回计算结果。更多的细节,稍后会更新一篇Vue响应式原理源码分析的文章,我相信会在Watcher的部分看到端倪。
Q3:Vue3相比Vue2做出了哪些改变,Proxy有什么优势
我的回答:根据我看的新闻和技术分享,Vue3中最主要的变化就是使用Proxy取代Object.defineProperty作为响应式的底层技术实现。其他的我也不熟悉,怕说了被深究,就说了这一点。Proxy的优势能够监听更多的变化,具体有哪些我记不得了。真是拉胯,知道Proxy至少证明我还是看过技术新闻的。
A3:回顾尤大的发布会PPT
Vue3就目前公开的消息来看,最明显的还是Proxy取代Object.defineProperty。除此之外,通过回顾发布会的ppt,我们还应当知道以下特性:
- 全局API,内置组件和功能支持tree-shaking,也就是说可以部分引用,而不用担心将整个Vue引入
- 基于Proxy的变动侦测,性能更优
- 代码采用monorepo,简单来说就是像乐高积木的一样的项目
- TypeScript
- 开放更多底层功能,如自定义渲染函数
- 模板静态分析,生成更优化的VDOM树,静态节点和动态节点分离,更新时直接便利动态节点
- Function-based API,类似React的函数组件,逻辑复用更容易,相比mixin更明了,更好的类型支持,更好的tree-shaking。mixin使用的属性来源不清晰,被mixin的组件也容易知道如何配合;高阶组件的props来源不明确;composition functions就是简单的函数,让组件以功能层面得到拆解,而不是生命周期。
除了上述内容,更多详情请看:github.com/vuejs/rfcs
下面再来说说Proxy。Proxy是一个完全不支持IE的特性,作用是基于target目标对象,使用handler作为代理行为,创建一个代理实例。使用代理实例访问相应的属性,行为将被handler截获并处理。关于Proxy的详细内容稍后也可以写一篇文章,这里先简单讨论一下。
Proxy的handler可以设置以下属性:
handler.getPrototypeOf()
:拦截Object.getPrototypeOf操作handler.setPrototypeOf()
:拦截Object.setPrototypeOf操作handler.isExtensible()
:拦截Object.isExtensible操作handler.preventExtensions()
:拦截Object.preventExtensions操作handler.getOwnPropertyDescriptor()
:拦截Object.getOwnPropertyDescriptor操作handler.defineProperty()
:拦截Object.defineProperty操作handler.has()
:拦截in操作符handler.get()
:拦截访问操作handler.set()
:拦截赋值操作,这里可以参见Reflect.set,对于数组中的元素赋值也可以被拦截,这里是普通setter做不到的。而且数组的length属性,也在set范围内。handler.deleteProperty()
:拦截delete操作符handler.ownKeys()
:拦截Object.getOwnPropertyNames和Object.getOwnPropertySymbolshandler.apply()
:拦截函数的调用handler.construct()
:拦截new操作符
小结
现总结了前三个问题,可以发现对于Vue的响应式原理仍然只是一知半解,没有对于源码进行深入的观察和分析,只能得到诸如Object.defineProperty设置访问器属性、收集依赖、通知变化等零零散散的内容。事实上这里还应该注重一些概念的细化,比如观察者模式和订阅发布模式的区别,Vue的watcher用的是哪一种;Proxy元编程能力到底如何,具体哪些算作set操作,借助其他的拦截功能是不是可以实现更多功能。
除了这些细节和具体的知识点要更进一步的深入了解外,应用层的功能和工作方式的对比,也是学习的一种基本技能。Vuex和Redux都提供状态管理功能,使用上能够根据经验说个一二三点,但是差异性往往蕴含更多内容。Vuex只能结合Vue使用,因为核心的响应式状态树是依赖Vue实例实现的;Redux虽然知道action和reducer,但是对局部状态树的获取,与组件的链接关系还不甚明确。
这些问题都是和两大主流视图渲染库息息相关的问题,同时也生发出了一些基础知识,如设计模式和Proxy等知识点。在使用工具的过程中,多去探求3W--是什么(Proxy是什么)&为什么(为什么要使用状态管理)&如何(视图如何根据data进行变化),除了广度也要有深度,这是我在面试和学习中得到的一条真理。同时面试官也对我提出了很中肯的建议,不管是内建还是外显知识体系,都要有一个清晰的思路,每个问题说明白,有条理,这样才不会错过什么细节。