2023.04.11 更新前端面试问题总结(11道题) 您所在的位置:网站首页 前端面试题canvas 2023.04.11 更新前端面试问题总结(11道题)

2023.04.11 更新前端面试问题总结(11道题)

#2023.04.11 更新前端面试问题总结(11道题)| 来源: 网络整理| 查看: 265

2023.04.09 - 2023.04.11 更新前端面试问题总结(11道题)获取更多面试问题可以访问github 地址: https://github.com/pro-collection/interview-question/issuesgitee 地址: https://gitee.com/yanleweb/interview-question/issues

目录:

中级开发者相关问题【共计 5 道题】273.ts 中 type 和 interface的区别【TypeScript】【出题公司: 腾讯】274.实现一个类似关键字new功能的函数【JavaScript】275.枚举和常量枚举的区别【TypeScript】【出题公司: 腾讯】276.const 和 readonly 的区别【TypeScript】【出题公司: 腾讯】277.[Vue] watch 和 computed 的区别和理解【web框架】【出题公司: 腾讯】

高级开发者相关问题【共计 4 道题】268.[Vue] 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?【web框架】269.前端路由的实现原理【web框架】【出题公司: 腾讯】271.[Vue] Vue3 的响应式原理?【web框架】【出题公司: 腾讯】278.大文件上传了解多少【web应用场景】【出题公司: 百度】

资深开发者相关问题【共计 2 道题】270.小程序的大概原理?【工程化】【出题公司: 腾讯】272.[Vue] vue3 的响应式库是独立出来的,它单独使用的时候是什么效果【web框架】【出题公司: 腾讯】

中级开发者相关问题【共计 5 道题】273.ts 中 type 和 interface的区别【TypeScript】【出题公司: 腾讯】相同点都可以描述一个对象或者函数interfaceinterface User { name: string age: number } interface SetUser { (name: string, age: number): void; }tstype User = { name: string age: number }; type SetUser = (name: string, age: number) => void;都允许拓展(extends) interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说 interface 可以 extends type, type 也可以 extends interface 。差异点typetype 可以声明基本类型别名,联合类型,元组等类型type 语句中还可以使用 typeof 获取实例的 类型进行赋值其他骚操作

type StringOrNumber = string | number; type Text = string | { text: string }; type NameLookup = Dictionary; type Callback = (data: T) => void; type Pair = [T, T]; type Coordinates = Pair; type Tree = T | { left: Tree, right: Tree };interfaceinterface 能够声明合并

interface User { name: string age: number } interface User { sex: string } /* User 接口为 { name: string age: number sex: string } */

一般来说,如果不清楚什么时候用interface/type,能用 interface 实现,就用 interface , 如果不能就用 type 。

274.实现一个类似关键字new功能的函数【JavaScript】

在js中new关键字主要做了:首先创建一个空对象,这个对象会作为执行new构造函数之后返回的对象实例,将创建的空对象原型(__proto__)指向构造函数的prototype属性,同时将这个空对象赋值给构造函数内部的this,并执行构造函数逻辑,根据构造函数的执行逻辑,返回初始创建的对象或构造函数的显式返回值。

function newFn(...args) { const constructor = args.shift(); const obj = Object.create(constructor.prototype); const result = constructor.apply(obj, args); return typeof result === "object" && result !== null ? result : obj; } function Person(name) { this.name = name; } const p = newFn(Person, "Jerome"); console.log("p.name :>> ", p.name); // p.name :>> Jerome275.枚举和常量枚举的区别【TypeScript】【出题公司: 腾讯】

枚举和常量枚举(const枚举):使用枚举可以清晰地表达意图或创建一组有区别的用例

// 枚举 enum Color { Red, Green, Blue } // 常量枚举 const enum Color { Red, Green, Blue }

区别:

枚举会被编译时会编译成一个对象,可以被当作对象使用// 枚举 enum Color { Red, Green, Blue } var sisterAn = Color.Red // 会被编译成 JavaScript 中的 var sisterAn = Color.Red // 即在运行执行时,它将会查找变量 Color 和 Color.Redconst 枚举会在 typescript 编译期间被删除,const 枚举成员在使用的地方会被内联进来,避免额外的性能开销// 常量枚举 const enum Color { Red, Green, Blue } var sisterAn = Color.Red // 会被编译成 JavaScript 中的 var sisterAn = 0 // 在运行时已经没有 Color 变量

由此可见,使用 常量枚举 会有更好的性能。

定义的枚举,在经过编译器编译后是一个对象,这个对象我们可以在程序运行时使用,前面有说到。但有时定义枚举可能只是为了让程序可读性更好,而不需要编译后的代码,即不需要编译成对象。typescript中考虑到这种情况,所以加入了 const enum (完全嵌入的枚举)。

276.const 和 readonly 的区别【TypeScript】【出题公司: 腾讯】

TypeScript 中不可变量的实现方法有两种:

使用 ES6 的 const 关键字声明的值类型 被 readonly 修饰的属性 2、TypeScript 中 readonly:

TypeScript 中的只读修饰符,可以声明更加严谨的可读属性。通常在 interface 、 Class 、 type 以及 array 和 tuple 类型中使用它,也可以用来定义一个函数的参数。

3、两者区别:

(1)const 用于变量, readonly 用于属性

(2)const 在运行时检查, readonly 在编译时检查

(3)const 声明的变量不得改变值,这意味着,const 一旦声明变量,就必须立即初始化,不能留到以后赋值;readonly 修饰的属性能确保自身不能修改属性,但是当你把这个属性交给其它并没有这种保证的使用者(允许出于类型兼容性的原因),他们能改变。

const foo: { readonly bar: number; } = { bar: 123 }; function iMutateFoo(foo: { bar: number }) { foo.bar = 456; } iMutateFoo(foo); console.log(foo.bar); // 456

(4)const 保证的不是变量的值不得改动,而是变量指向的那个内存地址不得改动,例如使用 const 变量保存的数组,可以使用 push , pop 等方法。 但是如果使用 ReadonlyArray 声明的数组不能使用 push , pop 等方法。

277.[Vue] watch 和 computed 的区别和理解【web框架】【出题公司: 腾讯】

计算属性computed :

支持缓存,只有依赖数据发生改变,才会重新进行计算,计算属性可用于快速计算视图(View)中显示的属性。这些计算将被缓存,并且只在需要时更新。computed是计算属性的; 它会根据所依赖的数据动态显示新的计算结果, 该计算结果会被缓存起来。computed的值在getter执行后是会被缓存的。如果所依赖的数据发生改变时候, 就会重新调用getter来计算最新的结果。不支持异步,当computed内有异步操作时无效,无法监听数据的变化computed 属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data中声明过或者父组件传递的props中的数据通过计算得到的值如果一个属性是由其他属性计算而来的,这个属性依赖其他属性,是一个多对一或者一对一,一般用computed如果computed属性属性值是函数,那么默认会走get方法;函数的返回值就是属性的属性值;在computed中的,属性都有一个get和一个set方法,当数据变化时,调用set方法。适用于一些重复使用数据或复杂及费时的运算。我们可以把它放入computed中进行计算, 然后会在computed中缓存起来, 下次就可以直接获取了。如果我们需要的数据依赖于其他的数据的话, 我们可以把该数据设计为computed中。computed 是基于响应性依赖来进行缓存的。只有在响应式依赖发生改变时它们才会重新求值, 也就是说, 当msg属性值没有发生改变时, 多次访问 reversedMsg 计算属性会立即返回之前缓存的计算结果, 而不会再次执行computed中的函数。但是methods方法中是每次调用, 都会执行函数的, methods它不是响应式的。computed中的成员可以只定义一个函数作为只读属性, 也可以定义成 get/set变成可读写属性, 但是methods中的成员没有这样的。

侦听属性watch:

1.watch它是一个对data的数据监听回调, 当依赖的data的数据变化时, 会执行回调。在回调中会传入newVal和oldVal两个参数。Vue实列将会在实例化时调用$watch(), 他会遍历watch对象的每一个属性。watch的使用场景是:当在data中的某个数据发生变化时, 我们需要做一些操作, 或者当需要在数据变化时执行异步或开销较大的操作时. 我们就可以使用watch来进行监听。watch普通监听和深度监听不支持缓存,数据变,直接会触发相应的操作;

2.watch里面有一个属性为deep,含义是:是否深度监听某个对象的值, 该值默认为false。watch支持异步;

3.监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;

4.当一个属性发生变化时,需要执行对应的操作;一对多;

5.监听数据必须是data中声明过或者父组件传递过来的props中的数据,当数据变化时,触发其他操作,函数有两个参数,

watch 和 computed的区别是:

相同点:他们两者都是观察页面数据变化的。

不同点:computed只有当依赖的数据变化时才会计算, 当数据没有变化时, 它会读取缓存数据。 watch每次都需要执行函数。watch更适用于数据变化时的异步操作。

当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。这是和computed最大的区别,请勿滥用。

高级开发者相关问题【共计 4 道题】268.[Vue] 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?【web框架】Vue 并没有在源码中做代理

vue 并没有在源码中做代理, 至少是 2.x 是没有做事件代理的。但是理论上来说使用事件代理性能会更好一点。

阅读 vue 源码的过程中,并没有发现 vue 会自动做事件代理,但是一般给 v-for 绑定事件时,都会让节点指向同一个事件处理程序(第二种情况可以运行,但是 eslint 会警告),一定程度上比每生成一个节点都绑定一个不同的事件处理程序性能好,但是监听器的数量仍不会变,所以使用事件代理会更好一点。

react 是委托到 document 上, 然后自己生成了合成事件, 冒泡到 document 的时候进入合成事件, 然后他通过 getParent() 获取该事件源的所有合成事件, 触发完毕之后继续冒泡。但是一些特殊的比如focus这种必须放在input这些dom上。

为何事件代理会让性能好一些

说一下我个人理解,先说结论,可以使用

事件代理作用主要是 2 个

将事件处理程序代理到父节点,减少内存占用率动态生成子节点时能自动绑定事件处理程序到父节点

这里我生成了十万个 span 节点,通过 performance monitor 来监控内存占用率和事件监听器的数量,对比以下 3 种情况

不使用事件代理,每个 span 节点绑定一个 click 事件,并指向同一个事件处理程序 {{item}} 不使用事件代理,每个 span 节点绑定一个 click 事件,并指向不同的事件处理程序 {{item}} 使用事件代理 {{item}}

可以通过 chrome devtools performance monitor 查看内存使用情况

可以看到使用事件代理无论是监听器数量和内存占用率都比前两者要少

为什么 Vue 不适用事件委托

首先我们需要知道事件代理主要有什么作用?

事件代理能够避免我们逐个的去给元素新增和删除事件事件代理比每一个元素都绑定一个事件性能要更好

从vue的角度上来看上面两点

在v-for中,我们直接用一个for循环就能在模板中将每个元素都绑定上事件,并且当组件销毁时,vue也会自动给我们将所有的事件处理器都移除掉。所以事件代理能做到的第一点vue已经给我们做到了在v-for中,给元素绑定的都是相同的事件,所以除非上千行的元素需要加上事件,其实和使用事件代理的性能差别不大,所以也没必要用事件代理269.前端路由的实现原理【web框架】【出题公司: 腾讯】

在使用Vue、React等前端框架时,我们都会发现项目中只有一个HTML文件,并且在该HTML中都存在一个根标签,起到了类似于容器的作用。容器内部的内容就由我们后续编写的每个视图决定,页面的切换就是容器中视图的切换。

前端路由的实现原理简单来说,就是在不跳转或者刷新页面的前提下,为SPA应用中的每个视图匹配一个特殊的URL,之后的刷新、前进、后退等操作均通过这个特殊的URL实现。为实现上述要求,需要满足:

改变URL且不会向服务器发起请求;

可以监听到URL的变化,并渲染与之匹配的视图。

主要有Hash路由和History路由两种实现方式。下文对两者的基本原理进行简单介绍,并分别实现了一个简易的路由Demo。

Hash路由

原理就是通过键值对的形式保存路由及对应要执行的回调函数,当监听到页面hash发生改变时,根据最新的hash值调用注册好的回调函数,即改变页面。

创建路由class Routers{ constructor(){ // 保存路由信息 this.routes = {}; this.currentUrl = ''; window.addEventListener('load', this.refresh, false); window.addEventListener('hashchange', this.refresh, false); } // 用于注册路由的函数 route = (path, callback) => { this.routes[path] = callback || function(){}; } // 监听事件的回调,负责当页面hash改变时执行对应hash值的回调函数 refresh = () => { this.currentUrl = location.hash.slice(1) || '/'; this.routes[this.currentUrl](); } } window.Router = new Routers();注册路由

使用route方法添加对应的路由及其回调函数即可。以下代码实现了一个根据不同hash改变页面颜色的路由,模拟了页面的切换,在实际的SPA应用中,对应的就是页面内容的变化了。

var content = document.querySelector('body'); function changeBgColor(color){ content.style.background = color; } // 添加路由 Router.route('/', () => { changeBgColor('yellow'); }); Router.route('/red', () => { changeBgColor('red'); }); Router.route('/green', () => { changeBgColor('green'); }); Router.route('/blue', () => { changeBgColor('blue'); });History路由

在H5之前,浏览器的history仅支持页面之前的跳转,包括前进和后退等功能。

在HTML5中,新增以下API:

history.pushState(); // 添加新状态到历史状态栈 history.replaceState(); // 用新状态代替当前状态 history.state; // 获取当前状态对象

history.pushState()和history.replaceState()均接收三个参数:

state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址

由于history.pushState()和 history.replaceState()都具有在改变页面URL的同时,不刷新页面的能力,因此也可以用来实现前端路由。

创建路由类class Routers{ constructor(){ this.routes = {}; window.addEventListener('popstate', e => { const path = e.state && e.state.path; this.routes[path] && this.routes[path](); }) } init(path){ history.replaceState({path: path}, null, path); this.routes[path] && this.routes[path](); } route(path, callback){ this.routes[path] = callback || function(){}; } go(path){ history.pushState({path: path}, null, path); this.routes[path] && this.routes[path](); } } window.Router = new Routers();注册路由function changeBgColor(color){ content.style.background = color; } Router.route(location.pathname, () => { changeBgColor('yellow'); }); Router.route('/red', () => { changeBgColor('red'); }); Router.route('/green', () => { changeBgColor('green'); }); Router.route('/blue', () => { changeBgColor('blue'); }); const content = document.querySelector('body'); Router.init(location.pathname);触发事件

在使用hash实现的路由中,我们通过hashchange事件来监听hash的变化,但是上述代码中history的改变本身不会触发任何事件,因此无法直接监听history的改变来改变页面。因此,对于不同的情况,我们选择不同的解决方案:

点击浏览器的前进或者后退按钮:监听popstate事件,获取相应路径并执行回调函数点击a标签:阻止其默认行为,获取其href属性,手动调用history.pushState(),并执行相应回调。const ul = document.querySelector('ul'); ul.addEventListener('click', e => { if(e.target.tagName === 'A'){ e.preventDefault(); Router.go(e.target.getAttribute('href')); } })对比

基于hash的路由:

缺点:

看起来比较丑会导致锚点功能失效

优点:

兼容性更好无需服务器配合271.[Vue] Vue3 的响应式原理?【web框架】【出题公司: 腾讯】该话题涉及的相关内容原理:Proxy、track、trigger新增属性遍历后新增遍历后删除或者清空获取 keys删除对象属性判断属性是否存在性能

推荐阅读文档: https://juejin.cn/post/6844904122479542285

响应式仓库

Vue3 不同于 Vue2 也体现在源码结构上,Vue3 把耦合性比较低的包分散在 packages 目录下单独发布成 npm 包。 这也是目前很流行的一种大型项目管理方式 Monorepo。

其中负责响应式部分的仓库就是 @vue/reactivity,它不涉及 Vue 的其他的任何部分,是非常非常 「正交」 的一种实现方式。

甚至可以轻松的集成进 React https://juejin.cn/post/6844904095594381325

区别

Proxy 和 Object.defineProperty 的使用方法看似很相似,其实 Proxy 是在 「更高维度」 上去拦截属性的修改的,怎么理解呢?

Vue2 中,对于给定的 data,如 { count: 1 },是需要根据具体的 key 也就是 count,去对「修改 data.count 」 和 「读取 data.count」进行拦截,也就是

Object.defineProperty(data, 'count', { get() {}, set() {}, })

必须预先知道要拦截的 key 是什么,这也就是为什么 Vue2 里对于对象上的新增属性无能为力。

而 Vue3 所使用的 Proxy,则是这样拦截的:

new Proxy(data, { get(key) { }, set(key, value) { }, })

可以看到,根本不需要关心具体的 key,它去拦截的是 「修改 data 上的任意 key」 和 「读取 data 上的任意 key」。

所以,不管是已有的 key 还是新增的 key,都逃不过它的魔爪。

但是 Proxy 更加强大的地方还在于 Proxy 除了 get 和 set,还可以拦截更多的操作符。

简单的例子

先写一个 Vue3 响应式的最小案例,本文的相关案例都只会用 reactive 和 effect 这两个 api。如果你了解过 React 中的 useEffect,相信你会对这个概念秒懂,Vue3 的 effect 不过就是去掉了手动声明依赖的「进化版」的 useEffect。

React 中手动声明 [data.count] 这个依赖的步骤被 Vue3 内部直接做掉了,在 effect 函数内部读取到 data.count 的时候,它就已经被收集作为依赖了。

Vue3:

// 响应式数据 const data = reactive({ count: 1 }) // 观测变化 effect(() => console.log('count changed', data.count)) // 触发 console.log('count changed', data.count) 重新执行 data.count = 2

React:

// 数据 const [data, setData] = useState({ count: 1 }) // 观测变化 需要手动声明依赖 useEffect(() => { console.log('count changed', data.count) }, [data.count]) // 触发 console.log('count changed', data.count) 重新执行 setData({ count: 2 })

也可以把 effect 中的回调函数联想到视图的重新渲染、 watch 的回调函数等等…… 它们是同样基于这套响应式机制的。

而本文的核心目的,就是探究这个基于 Proxy 的 reactive api,到底能强大到什么程度,能监听到用户对于什么程度的修改。

讲讲原理

先最小化的讲解一下响应式的原理,其实就是在 Proxy 第二个参数 handler 也就是陷阱操作符中,拦截各种取值、赋值操作,依托 track 和 trigger 两个函数进行依赖收集和派发更新。

track 用来在读取时收集依赖。

trigger 用来在更新时触发依赖。

trackfunction track(target: object, type: TrackOpTypes, key: unknown) { const depsMap = targetMap.get(target); // 收集依赖时 通过 key 建立一个 set let dep = new Set() targetMap.set(ITERATE_KEY, dep) // 这个 effect 可以先理解为更新函数 存放在 dep 里 dep.add(effect) }

target 是原对象。

type 是本次收集的类型,也就是收集依赖的时候用来标识是什么类型的操作,比如上文依赖中的类型就是 get,这个后续会详细讲解。

key 是指本次访问的是数据中的哪个 key,比如上文例子中收集依赖的 key 就是 count

首先全局会存在一个 targetMap,它用来建立 数据 -> 依赖 的映射,它是一个 WeakMap 数据结构。

而 targetMap 通过数据 target,可以获取到 depsMap,它用来存放这个数据对应的所有响应式依赖。

depsMap 的每一项则是一个 Set 数据结构,而这个 Set 就存放着对应 key 的更新函数。

是不是有点绕?我们用一个具体的例子来举例吧。

const target = { count: 1} const data = reactive(target) const effection = effect(() => { console.log(data.count) })

对于这个例子的依赖关系,

全局的 targetMap 是:targetMap: { { count: 1 }: dep }dep 则是dep: { count: Set { effection } }

这样一层层的下去,就可以通过 target 找到 count 对应的更新函数 effection 了。

trigger

这里是最小化的实现,仅仅为了便于理解原理,实际上要复杂很多,

其实 type 的作用很关键,先记住,后面会详细讲。

export function trigger( target: object, type: TriggerOpTypes, key?: unknown, ) { // 简化来说 就是通过 key 找到所有更新函数 依次执行 const dep = targetMap.get(target) dep.get(key).forEach(effect => effect()) }278.大文件上传了解多少【web应用场景】【出题公司: 百度】大文件分片上传

如果太大的文件,比如一个视频1g 2g那么大,直接采用上面的栗子中的方法上传可能会出链接现超时的情况,而且也会超过服务端允许上传文件的大小限制,所以解决这个问题我们可以将文件进行分片上传,每次只上传很小的一部分 比如2M。

Blob 它表示原始数据, 也就是二进制数据,同时提供了对数据截取的方法 slice,而 File 继承了 Blob 的功能,所以可以直接使用此方法对数据进行分段截图。

过程如下:

把大文件进行分段 比如2M,发送到服务器携带一个标志,暂时用当前的时间戳,用于标识一个完整的文件服务端保存各段文件浏览器端所有分片上传完成,发送给服务端一个合并文件的请求服务端根据文件标识、类型、各分片顺序进行文件合并删除分片文件

客户端 JS 代码实现如下

function submitUpload() { var chunkSize = 2 * 1024 * 1024;//分片大小 2M var file = document.getElementById('f1').files[0]; var chunks = [], //保存分片数据 token = (+new Date()),//时间戳 name = file.name, chunkCount = 0, sendChunkCount = 0; //拆分文件 像操作字符串一样 if (file.size > chunkSize) { //拆分文件 var start = 0, end = 0; while (true) { end += chunkSize; var blob = file.slice(start, end); start += chunkSize; //截取的数据为空 则结束 if (!blob.size) { //拆分结束 break; } chunks.push(blob);//保存分段数据 } } else { chunks.push(file.slice(0)); } chunkCount = chunks.length;//分片的个数 //没有做并发限制,较大文件导致并发过多,tcp 链接被占光 ,需要做下并发控制,比如只有4个在请求在发送 for (var i = 0; i < chunkCount; i++) { var fd = new FormData(); //构造FormData对象 fd.append('token', token); fd.append('f1', chunks[i]); fd.append('index', i); xhrSend(fd, function() { sendChunkCount += 1; if (sendChunkCount === chunkCount) {//上传完成,发送合并请求 console.log('上传完成,发送合并请求'); var formD = new FormData(); formD.append('type', 'merge'); formD.append('token', token); formD.append('chunkCount', chunkCount); formD.append('filename', name); xhrSend(formD); } }); } } function xhrSend(fd, cb) { var xhr = new XMLHttpRequest(); //创建对象 xhr.open('POST', 'http://localhost:8100/', true); xhr.onreadystatechange = function() { console.log('state change', xhr.readyState); if (xhr.readyState == 4) { console.log(xhr.responseText); cb && cb(); } } xhr.send(fd);//发送 } //绑定提交事件 document.getElementById('btn-submit').addEventListener('click', submitUpload);

服务端 node 实现代码如下: 合并文件这里使用 stream pipe 实现,这样更节省内存,边读边写入,占用内存更小,效率更高,代码见fnMergeFile方法。

//二次处理文件,修改名称 app.use((ctx) => { var body = ctx.request.body; var files = ctx.request.files ? ctx.request.files.f1 : [];//得到上传文件的数组 var result = []; var fileToken = ctx.request.body.token;// 文件标识 var fileIndex = ctx.request.body.index;//文件顺序 if (files && !Array.isArray(files)) {//单文件上传容错 files = [files]; } files && files.forEach(item => { var path = item.path; var fname = item.name;//原文件名称 var nextPath = path.slice(0, path.lastIndexOf('/') + 1) + fileIndex + '-' + fileToken; if (item.size > 0 && path) { //得到扩展名 var extArr = fname.split('.'); var ext = extArr[extArr.length - 1]; //var nextPath = path + '.' + ext; //重命名文件 fs.renameSync(path, nextPath); result.push(uploadHost + nextPath.slice(nextPath.lastIndexOf('/') + 1)); } }); if (body.type === 'merge') {//合并分片文件 var filename = body.filename, chunkCount = body.chunkCount, folder = path.resolve(__dirname, '../static/uploads') + '/'; var writeStream = fs.createWriteStream(`${folder}${filename}`); var cindex = 0; //合并文件 function fnMergeFile() { var fname = `${folder}${cindex}-${fileToken}`; var readStream = fs.createReadStream(fname); readStream.pipe(writeStream, { end: false }); readStream.on("end", function() { fs.unlink(fname, function(err) { if (err) { throw err; } }); if (cindex + 1 < chunkCount) { cindex += 1; fnMergeFile(); } }); } fnMergeFile(); ctx.body = 'merge ok 200'; } });大文件上传断点续传

在上面我们实现了文件分片上传和最终的合并,现在要做的就是如何检测这些分片,不再重新上传即可。 这里我们可以在本地进行保存已上传成功的分片,重新上传的时候使用spark-md5来生成文件 hash,区分此文件是否已上传。

为每个分段生成 hash 值,使用 spark-md5 库将上传成功的分段信息保存到本地重新上传时,进行和本地分段 hash 值的对比,如果相同的话则跳过,继续下一个分段的上传

方案一: 保存在本地 indexDB/localStorage 等地方, 推荐使用 localForage 这个库npm install localforage

客户端 JS 代码

//获得本地缓存的数据 function getUploadedFromStorage() { return JSON.parse(localforage.getItem(saveChunkKey) || "{}"); } //写入缓存 function setUploadedToStorage(index) { var obj = getUploadedFromStorage(); obj[index] = true; localforage.setItem(saveChunkKey, JSON.stringify(obj)); } //分段对比 var uploadedInfo = getUploadedFromStorage();//获得已上传的分段信息 for (var i = 0; i < chunkCount; i++) { console.log('index', i, uploadedInfo[i] ? '已上传过' : '未上传'); if (uploadedInfo[i]) {//对比分段 sendChunkCount = i + 1;//记录已上传的索引 continue;//如果已上传则跳过 } var fd = new FormData(); //构造FormData对象 fd.append('token', token); fd.append('f1', chunks[i]); fd.append('index', i); (function(index) { xhrSend(fd, function() { sendChunkCount += 1; //将成功信息保存到本地 setUploadedToStorage(index); if (sendChunkCount === chunkCount) { console.log('上传完成,发送合并请求'); var formD = new FormData(); formD.append('type', 'merge'); formD.append('token', token); formD.append('chunkCount', chunkCount); formD.append('filename', name); xhrSend(formD); } }); })(i); }

方案2:服务端用于保存分片坐标信息, 返回给前端

需要服务端添加一个接口只是服务端需要增加一个接口。 基于上面一个栗子进行改进,服务端已保存了部分片段,客户端上传前需要从服务端获取已上传的分片信息(上面是保存在了本地浏览器),本地对比每个分片的 hash 值,跳过已上传的部分,只传未上传的分片。

方法1是从本地获取分片信息,这里只需要将此方法的能力改为从服务端获取分片信息就行了。

资深开发者相关问题【共计 2 道题】270.小程序的大概原理?【工程化】【出题公司: 腾讯】

具体流程可以看下面这个图:

要了解小程序架构原理, 要从以下几个方面入手探索

宿主环境执行环境小程序整体架构运行机制更新机制数据通信机制登录机制性能方向问题JSCore

具体内容可以参考文档:

https://juejin.cn/post/6976805521407868958272.[Vue] vue3 的响应式库是独立出来的,它单独使用的时候是什么效果【web框架】【出题公司: 腾讯】

vue3 的响应式库是独立出来的,它可以很方便的集成进 React, 作为 React 的状态管理库使用!

使用示范

定义 store

// store.ts import { reactive, computed, effect } from '@vue/reactivity'; export const state = reactive({ count: 0, }); const plusOne = computed(() => state.count + 1); effect(() => { console.log('plusOne changed: ', plusOne); }); const add = () => (state.count += 1); export const mutations = { // mutation add, }; export const store = { state, computed: { plusOne, }, }; export type Store = typeof store;

消费使用

// Index.tsx import { Provider, useStore } from 'rxv' import { mutations, store, Store } from './store.ts' function Count() { const countState = useStore((store: Store) => { const { state, computed } = store; const { count } = state; const { plusOne } = computed; return { count, plusOne, }; }); return ( 计数器 store中的count现在是 {countState.count} computed值中的plusOne现在是 {countState.plusOne.value} add ); } export default () => { return ( ); };

可以看出,store的定义只用到了@vue/reactivity,而rxv只是在组件中做了一层桥接,连通了Vue3和React,正如它名字的含义:React x Vue。

如何实现

只要effect能接入到React系统中,那么其他的api都没什么问题,因为它们只是去收集effect的依赖,去通知effect触发更新。

effect接受的是一个函数,而且effect还支持通过传入schedule参数来自定义依赖更新的时候需要触发什么函数,

而rxv的核心api: useStore接受的也是一个函数selector,它会让用户自己选择在组件中需要访问的数据。

把selector包装在effect中执行,去收集依赖。

指定依赖发生更新时,需要调用的函数是当前正在使用useStore的这个组件的forceUpdate强制渲染函数。

简单的看一下核心实现

share.ts

export const useForceUpdate = () => { const [, forceUpdate] = useReducer(s => s + 1, 0); return forceUpdate; }; export const useEffection = (...effectArgs: Parameters) => { // 用一个ref存储effection // effect函数只需要初始化执行一遍 const effectionRef = useRef(); if (!effectionRef.current) { effectionRef.current = effect(...effectArgs); } // 卸载组件后取消effect const stopEffect = () => { stop(effectionRef.current!); }; useEffect(() => stopEffect, []); return effectionRef.current };

核心逻辑在此

import React, { useContext } from 'react'; import { useForceUpdate, useEffection } from './share'; type Selector = (store: T) => S; const StoreContext = React.createContext(null); const useStoreContext = () => { const contextValue = useContext(StoreContext); if (!contextValue) { throw new Error( 'could not find store context value; please ensure the component is wrapped in a ', ); } return contextValue; }; /** * 在组件中读取全局状态 * 需要通过传入的函数收集依赖 */ export const useStore = (selector: Selector): S => { const forceUpdate = useForceUpdate(); const store = useStoreContext(); const effection = useEffection(() => selector(store), { scheduler: job => { if (job() === undefined) return; forceUpdate(); }, lazy: true, }); const value = effection(); return value; }; export const Provider = StoreContext.Provider;

参考文档:

https://github.com/sl1673495/react-composition-apihttps://juejin.cn/post/6844904054192078855


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有