0%

原理:

将被KeepAlive的组件从原容器搬运到另一个隐藏的容器中,实现“假卸载“,当被搬运到隐藏容器中的组件需要再次被挂载时,我们也不能执行真正的挂载逻辑,而是把组件从隐藏容器中再搬运到原容器,这个过程对应到组件的生命周期,就是activated和deactivated

一个简单的KeepAlive组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const KeepAlive = {
//keepAlive组件独有的属性,用作标识
__isKeepAlive: true,
setup(props, { slots }) {
//创建一个缓存对象
//key:vnode.type
//value:vnode
const cache = new Map();
//当前keepAlive组件的实例
const instance = currentInstance;
//对于KeepAlive组件来说,它的实例上有存在特殊的keepAliveCtx对象,该对象由渲染器注入
//该对象会暴露渲染器得到一些内部方法,其中move函数用来将一段DOM移动到另一个容器中
const { move, createElement } = instance.KeepAliveCtx;

//创建隐藏容器
const storageContainer = createElement("div");

//keepAlive组件实例上会被添加两个内部函数,分别是_deActivate和_activate
//这两个函数会在渲染器中被调用
instance._deActivate = (vnode) => {
move(vnode, storageContainer);
};
instance._activate = (vnode, container, anchor) => {
move(vnode, container, anchor);
};
return () => {
//keepAlive的默认插槽就是要被keepAlive的组件
let rawVNode = slots.default();
//如果不是组件,直接渲染,因为非组件的虚拟结点无法被keepAlive
if (typeof rawVNode.type !== "object") {
return rawVNode;
}
//在挂载时先获取缓存的组件vnode
const cachedVNode = cache.get(rawVNode.type);
if (cachedVNode) {
//如果有缓存的内容,说明不应该执行挂载,应该执行激活
//继承组件实例
rawVNode.component = cachedVNode.component;
//在vnode上添加keptAlive属性,标记为true,避免渲染器重新挂载
rawVNode.keptAlive = true;
} else {
//如果没有缓存,则将其添加到缓存中,这样下次激活组件时不会执行新的挂载动作
cache.set(rawVNode.type, rawVNode);
}
//在组件vnode上添加shouldKeepAlive属性,并标记为true,避免渲染器将组件卸载
rawVNode.shouldKeepAlive = true;
//将keepAlive组件的实例也添加到vNode上,以便在渲染器中访问
rawVNode.KeepAliveInstance = instance;
//渲染组件 vnode
return rawVNode;
};
},
};

KeepAlive组件会对内部组件进行操作,主要是在内部组件的vnode对象上添加一些标记属性,以便渲染器能够执行特定的逻辑,这些标记属性包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function unmount(vnode) {
if(vnode.type === Fragment) {
vnode.children.forEach(c=>unmount(c))
return
}else if(typeof vnode.type === 'object') {
//vnode.shouldAlive是一个布尔值,用来标识该组件是否应该被KeepAlive
if(vnode.shouldKeepAlive) {
//对于需要被keppAlive的组件,我们不应该直接卸载,而应该调用该组件的父组件,即keepAlive的父组件_deActive函数使其失活
vnode.KeepAliveInstance._deActivate(vnode)
}else {
unmount(vnode.component.subTree)
}
return
}
const parent = vnode.el.parentNode
if(parent) {
parent.removeChild(vnode.el)
}
}

shouldKeepAlive:该属性会被添加到内部组件的vnode对象,这样当渲染器卸载内部组件时,可以通过检查该属性得知,内部组件需要被KeepAlive,于是渲染器不会真的卸载内部组件,而是会调用_deActivate函数完成搬运工作

keptAlive:内部组件如果已经被缓存,则还会为其添加一个keptAlive标记,这里当内部之间需要重新渲染时,渲染器并不会重新挂载它,而会将其激活

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function patch(n1,n2,container,anchor) {
if(n1 && n1.type !== n2.type) {
unmount(n1)
n1=null
}
const {type} = n2
if(typeof type === 'string'){

}else if(type === Text){

}else if(type === Fragment){

}else if(typeof type === 'object' || typeof type === 'function') {
if(!n1) {
//如果该组件已经被keepAlive,则不会重新挂载它,而是会调用_active来激活它
if(n2.keptAlive){
n2.keepAliveInstance._activate(n2,container,anchor)
}else {
mountComponent(n2,container,anchor)
}
}else {
patchComponent(n1,n2,anchor)
}
}

}

失活的本质就是将组件所渲染的内容移动到隐藏容器中,激活的本质是将组件所渲染的内容搬运到原来的容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//涉及move的函数由渲染器注入的
function mountComponent(vnode,container,anchor) {
//...
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots,
mounted: [],
//只有KeepAlive组件的实例下会有keepAliveCtx属性
keepAliveCtx: null
}
//检查当前要挂载的组件是否是KeepAlive组件
const isKeepAlive = vnode.type.__isKeepAlive
if(isKeepAlive) {
//在KeepAlive组件实例上添加keepAliveCtx对象
instance.keepAliveCtx = {
//move函数用来移动一段vnode
move(vnode,container,anchor) {
//本质上是将组件渲染的内容移动到指定容器中,即隐藏容器中
insert(vnode.component.subTree.el,container,anchor)
},
createElement
}
}//...
}

用LRU算法实现缓存管理,可以为缓存设置最大容量

1
2
3
<KeepAlive :max="2">
<component :is="dynamicComp"></component>
</KeepAlive>

LRU算法的思想就是会把当前访问(或渲染)的组件作为最新一次渲染的组件,移动到队头,而如果队列容量不够,会把最久未被使用的组件即队尾组件移出队列,也可以自定义缓存实例:

1
2
3
<KeepAlive :cache="cache">
<component :is="dynamicComp"></component>
</KeepAlive>

自我介绍

1.css选择器优先级

2.visibility:none,opacity:0,display:none区别 如果设置还能进行事件绑定和监听吗

3.闭包

4.作用域链

5 深拷贝有几种实现方法

差点写错了

6.遇到过内存泄漏问题吗?怎么解决

7.浏览器垃圾回收机制

之前看过V8的垃圾回收机制,就回答了那部分

8.vue看过哪些源码?答:响应式部分

9 讲讲vue2 vue3响应式原理

10 vue2中computed有用到watcher吗?

我了解的是vue3的源码,用到了副作用函数和getter,本质上也是发布订阅模式…

11 vue3diff 算法有什么优化?

讲了Vue2和vue3的diff算法以及优化原理

12 vue3中的patchFlag了解吗 不是很了解

13 你的项目用的gulp做打包 是基于什么想法用的gulp

14 了解vite snowpack吗

不是很了解

15 计网还记得哪些?http https tcp udp

16 讲讲http和https

17 https还有什么需要缺陷

回答了http1,http2,http3的区别

18 http3还了解哪些

就讲了Quic协议解决可靠传输问题和运输层用UDP实现

问:还有吗? 了解的只有这些

19 深挖Quic协议 不是很了解

20 实习项目中做了什么

21 实习项目中遇到了哪些困难

22 怎么在组件中实现的国际化

23 写一个二叉树的前序遍历 非递归写法

反问:学习建议

回答:计网挺熟悉的 你项目中用的gulp做打包,在项目中用哪些构建工具要做一个系统的考察

加载顺序:

在new Vue()时,vue\src\core\instance\index.js里面的_init()初始化各个功能

1
2
3
4
5
6
7
8
9
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) //初始化各个功能
}

在_init()中是这样一个执行顺序,initState是在beforeCreate和created之间

1
2
3
4
5
6
7
8
9
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')

在initState()中:

1
2
3
4
5
6
7
if (opts.props) initProps(vm, opts.props)//初始化Props
if (opts.methods) initMethods(vm, opts.methods)//初始化methods
if (opts.data) {
initData(vm)} else {
observe(vm._data = {}, true /* asRootData */)}//初始化data
if (opts.computed) initComputed(vm, opts.computed)//初始化computed

所以Props,methods,data和computed的初始化都是在beforeCreated和created之间完成的。

浏览器的开发者工具的Network的Size栏出现三种情况:

  • from memory cache
  • from disk cache
  • 资源本身大小(比如13.6k)

二三级缓存原理:

1.先查找内存,如果内存中存在,从内存中加载

2.如果内存中未查找到,选择硬盘获取,如果硬盘中有,从硬盘中加载

3.如果硬盘中未查找到,就进行网络请求

4.加载到的资源缓存到硬盘和内存

http状态码及区别

  • 200 from memory cache:不访问服务器,一般已经加载过该资源且缓存在了内存中,直接从内存中读取缓存,浏览器关闭后,数据将不存在(资源被释放掉),再次打开相同的页面时,不会出现from memory cache,一般脚本,字体,图片会存在内存中
  • 200 from disk cache:不访问服务器,已经在之前的某个时间加载过该资源,直接从硬盘中读取缓存,关闭浏览器后,数据依然存在,此资源不会随着该页面的关闭而释放掉,下次打开仍然是from disk cache,一般非脚本存在其中,如css
  • 200 资源大小数值:从服务器下载最新资源
  • 304 Not Modified:访问服务器,发现数据没有更新,服务器返回此状态码,然后从缓存中获取数据

几种状态执行顺序:

例子:加载一种资源:访问->200->退出浏览器->再进来->200(from disk cache)->刷新->200(from memory cache)

组件设计原则是一些通用的指导原则,可以帮助开发人员设计出高质量、可重用和可扩展的组件。以下是一些常见的组件设计原则:

  1. 单一职责原则(Single Responsibility Principle):每个组件应该只关注一个特定的功能或任务,而不是承担过多的责任。这有助于保持组件的简洁性和可维护性。

  2. 开闭原则(Open-Closed Principle):组件应该对扩展开放,对修改关闭。这意味着,当需要增加新的功能时,应该通过扩展组件来实现,而不是修改已有的代码。这提高了组件的可重用性和可扩展性。(比如封装一个通用的弹窗组件,有兑奖成功,失败等各种样式和功能的弹窗,在弹窗上再继续扩展封装新的弹窗而不是去修改内部代码)

  3. 依赖倒置原则(Dependency Inversion Principle):组件应该依赖于抽象而不是具体的实现。这意味着,组件应该通过接口或抽象类与其他组件进行通信,而不是直接依赖于具体的实现类。这有助于降低组件之间的耦合度。

  4. 组合原则(Composition Principle):通过组合多个小的、独立的组件来构建更复杂的组件。这样可以实现组件的复用,并且易于测试和维护。

  5. 一致性原则(Consistency Principle):组件应该保持一致的设计和行为,以提供更好的用户体验。组件的命名、结构、样式等应该符合一致的规范。

  6. 可重用性原则(Reusability Principle):组件应该设计为可重用的,可以在不同的场景和项目中使用。这可以通过将组件解耦、提供合适的接口和文档、遵循设计模式等方式实现。

  7. 可测试性原则(Testability Principle):组件应该易于测试,以确保其功能的正确性和稳定性。组件的设计应该支持单元测试和集成测试,并提供适当的接口和工具。

这些原则可以帮助开发人员设计出高质量、可维护和可扩展的组件,提高开发效率并提供更好的用户体验。然而,具体的组件设计还需要根据具体的项目需求和技术栈进行调整和优化。

概念:

1.CI(Continuous integration,持续集成)

频繁地(一天多次)将代码集成到主干

2.CD(Continous delivery**,持续交付)**

持续集成的下一步,指的是,频繁将软件的新版本,交付给质量团队或者用户,以供评审,如评审通过,代码进入生产阶段,手动部署

3.CD(Continous deployment,持续部署)

持续交付的下一步,指的是,代码通过评审后,自动部署到生产环境

意义:

CI(持续集成)

(1)快速发现错误,每完成一点更新,就集成到主干,可以快速发现错误,定位错误也比较容易

(2)防止分支大幅偏离主干,如果不是经常集成,主干又在不断更新,会导致以后集成的难度变大,甚至难以集成

持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量,它的核心措施是,代码集成到主干之前,必须通过自动化测试,只要有一个测试用例失败,就不能集成

CD(持续交付)

保证不管怎么更新,软件是随时可以交付的

CD(持续部署)

保证代码在任何时刻都是可部署,可以进入生产阶段

流程

提交

开发者提交代码

测试(第一轮)

代码仓库commit操作配置钩子,只要提交代码合并进主干,就会跑自动化测试

三种测试:

单元测试:针对函数或模块的测试

集成测试:针对整体产品的某个功能的测试,又称功能测试

端对端测试:从用户界面直达数据库的全链路测试

构建:

测试通过,代码即可进入主干,就算可以交付

交付后,就先进行构建,再进入第二轮测试

所谓构建,指的就是将源码转换为可以运行的实际代码,比如安装依赖,配置各种资源(样式,JS脚本,图片)等

常用的构建工具:

·Jenkins

·Travis

·Codeship

·Strider

测试(第二轮)

全面测试,单元测试和集成测试都会跑,有条件也要做端对端测试,所有测试以自动化为主,少数无法自动化的测试用例,人工跑

部署

通过第二轮测试,当前代码就是一个可以直接部署的版本,将这个版本的所有文件打包存档,发到生产服务器,生产服务器将打包文件解包成本地的一个目录,再将允许路径的符号链接指向这个目录,然后重新启动应用

回滚

一旦当前版本发送问题,就要回滚到上一个版本的构建结果,最简单的做法就是修改符号链接,指向上一个版本目录

1.实习负责的什么项目,项目中遇到了哪些困难

2.你讲到了Vue-i18n插件,设计插件的思想:x

3.Vue中封装组件的思想

4.函数式编程的思想 x

5.vue和vue3双向绑定原理,区别

6.Proxy的优点

6.data中为什么返回函数比较好,不是对象

7.data中如果用对象,造成了数据污染,要怎么避免数据污染 x

组件复用造成了数据污染,那就只能避免组件的复用?

8.vue方法中声明了对象没有响应式,怎么实现响应式

9.vue源码中,data,comouted,methods声明的顺序的是怎样的 x

10.vue的生命周期

11.vue3的生命周期和vue2生命周期的不同点

12.讲讲keep-alive

13.keep-alive的实现原理

14.你的项目中用到了组件库,讲讲前端工程化

15.tree-traking原理,怎么实现tree-shaking,tree-shaking怎么知道没用的代码 x

16.前端中的CI/CD是怎样的流程 x

17.了解抽象语法树AST吗

18.babel-loader原理讲讲

19.http缓存

20.如果浏览器中没有设置强缓存和协商缓存,浏览器会自动缓存吗

21.from disk和from memory的区别 x

22.http和https的区别

还问了一些有些没想起来

反问:有点紧张,觉得我面试表现得怎么样

回答:看出来你有点紧张,很多东西原理了解的还不深入,但是整个前端体系比较全面,了解工程化知识,框架也熟悉

1
2
3
4
5
6
7
8
9
function myInstanceof(left,right) {
let prototype = right.prototype
left = left.__proto__
while(true){
if(!left)return false
if(left == prototype)return true
left = left.__proto__
}
}

hook就是解决函数组件没有state,生命周期,逻辑不能复用的一种解决方案

dispatcher

在真实hook中,组件mount时的hook和update时的hook来源于不同对象,这类对象在源码中称为dispatcher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// mount时的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
// ...省略
};

// update时的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
// ...省略
};

可见,mount时调用的hook和update时调用的hook是两个不同的函数,在FunctionComponent render前,会根据FunctionComponent对应fiber的一下条件区分mount和update

1
current === null || current.memoizedState === null

并把不同情况对应的dispatcher赋值给全局变量ReactCurrentDispatcher的current属性

1
2
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null ? HookDispatcherOnMount : HooksDispatcherOnUpdate

在FuntionComponent render时,会从ReactCurrentDispatcher.current(即当前dispatcher,则FunctionComponent render时调用的hook是不同函数)

Hook数据结构:

1
2
3
4
5
6
7
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
}

memoizedState:

hook与FunctionComponent fiber都存在memoizedState属性:

fiber.memoizedState:FunctionComponent对应fiber保存的Hooks链表

hook.memoizedState:Hooks链表中保存的单一hook对应的数据

不同类型hook的memoizedState保存不同类型数据:

useState:对于const [state,updateState] = useState(initialState),memoizedState保存state的值

useReducer:对于const[state,dispatch]=useReducer(reducer,[]),memoizedState保存state的值

useEffect:memoizedState保存包含useEffect回调函数,依赖项等的链表数据结构effect,effect会保存在fiber.updateQueue

useRef:对于useRef(1),memoizedState保存{current:1}

useMemo:对于useMemo(callback,[depA]),memoizedState保存[callback,depA],与useMemo的区别是,useCallback保存的是callback函数本身,而useMemo保存的是callback函数的执行结果

有些hook没有memoizedState:

如useContext

effect是用来注册副作用函数,同时它允许指定一些选项参数options,例如指定scheduler调度器控制副作用函数的执行时机和方式,先实现一个懒执行的副作用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function effect(fn,options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length-1]
}
effectFn.options = options
effectFn.deps = []
//只有非lazy才执行
if(!options.lazy) {
effectFn()
}
//将副作用函数作为返回值返回
return effectFn//新增
}

实现了让副作用函数不立即执行的功能,将副作用函数effectFn作为effect函数的返回值,这就意味着当调用effect函数时,通过其返回值能够拿到对应的副作用函数,这样我们能够手动执行副作用函数

1
2
3
4
5
const effectFn = effect(()=>{
console.log(obj.foo)
},{lazy:true})
//手动执行副作用函数
effectFn()

如果把传递给effect的函数看做一个getter,那么这个getter函数可以返回任何值

1
2
3
4
5
const effectFn = effect(
//getter返回obj.foo和obj.bar
() => obj.foo + obj.bar,
{ lazy: true}
)

为了手动执行副作用函数时就能拿到其返回值,改动effect函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function effect(fn,options = {}){
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
//将fn执行结果存储到res
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length-1]
//将res作为effectFn返回值
return res;
}
effectFn.options = options
effectFn.deps = []
if(!options.lazy){
effectFn()
}
return effectFn
}

传递给effect函数的参数fn才是真正的副作用函数,而effectFn是我们包装后的副作用函数,为了通过effectFn得到真正的副作用函数fn的执行结果,我们需要将其保存到res变量,然后将其作为effectFn函数的返回值

接下来就可以定义cmputed函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function computed(getter) {
//把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter,{
lazy: true
})
const obj = {
//当读取value时才执行effectFn
get value() {
return effectFn()
}
}
return obj
}

computed函数接收一个getter函数作为参数,我们把getter函数作为副作用函数,用它创建一个lazy的effect,computed函数的执行会返回一个对象,该对象的value属性是一个访问器属性,只有当读取value值时,才执行effectFn并将结果作为返回值返回

使用computed函数创建一个计算属性:

1
2
3
4
const data ={ foo: 1, bar: 2}
const obj = new Proxy(data, {...})
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value)//3

当多次访问sumRes.value的值,会导致effectFn进行多次计算,即使obj.foo和obj,bar的值本身没有变化,利用闭包实现对值进行缓存的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function computed(getter) {
//value用来缓存上一次计算的值
let value
//dirty标志,用来标识是否需要重新计算值,为true意味着“脏”没需要计算
let dirty = true
const effectFn = effect(getter,{
lazy:true
})
const obj = {
get value(){
//只有脏时才计算值,并将得到的值缓存到value
if(dirty){
value = effectFn()
//将dirty设置为false,下一次访问直接使用缓存到value中的值
dirty = false
}
return value
}
}
return obj
}

但是此时改变obj.foo或者obj.bar,再访问sumRes.value会发现访问到的值没有发生变化

1
2
3
4
5
6
7
8
const data = {foo: 1,bar: 2}
const obj = new Proxy(data,{...})
const sumRes = computed(() => obj.foo+obj.bar)
console.log(sumRes.value)//3
console.log(sumRes.value)//3
obj.foo++;
//再次访问得到的仍然是3
console.log(sumRes.value)//3

这是因为,第一次访问sumRes.value的值后,变量dirty被设置为false,代表不需要计算,即使我们修改obj.foo的值,但只要dirty的值为false,就不会重新计算,所有导致我们得到错误的值,因此当obj.foo或者obj.bar的值发生改变时,只要dirty的值重置为true就可以,这时就用到scheduler选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function computed(getter) {
let value
let dirty = true
cont effectFn = effect(getter, {
lazy: true,
scheduler() {
if(!dirty) {
dirty = true
//当计算属性依赖的响应式数据变化时,手动调用trigger函数触发响应
trigger(obj,'value')
}
}
})
const obj = {
get value() {
if(dirty){
value = effectFn()
dirty = false
}
//当读取value手动调用track函数进行追踪
track(obj,'value')
return value
}
}
return obj
}

当读取一个计算属性value,需要手动调用track函数,把计算属性返回的对象obj作为target,同时作为第一个参数传递给track函数,当计算属性所依赖的响应式数据变化时会立即调度函数,在调度函数内手动调用trigger函数触发响应即可。