面试小结
React vs Vue
前言:
- 技术选型没有绝对的对与错
- 技术选型要考虑的因素非常多
- 表达自己的观点,并说明理由
两者的本质区别:
- vue - 本质是
MVVM框架
,由mvc发展而来 - React - 本质是
前端组件化框架
,由后端组件化发展而来 - 但两者都能实现相同的功能
模板的区别:
-
vue - 使用模板(template)由angular提出
-
React - 使用JSX,一种编程语法,类似于html
-
模板语法上更倾向于JSX:
- vue中的指令v-需要现学,并且它们的值是放在引号中,比较困惑;
- 而JSX中,只需要知道大括号中可以写JS表达式,就OK
-
模板分离上更倾向于vue
组件化的区别:
- React本身就是组件化,没有组件化就不是React
- vue也支持组件化,不过是在MVVM上的扩张
- 对于组件化,更倾向于React,做的更彻底
两者共同点:
- 支持组件化
- 数据驱动视图
总结:
1.国内,首推vue,文档易读、易学、社区足够大
2.团队水平高,推荐使用React,组件化和jsx
React
对组件化的理解
与面向对象类似,分为: 封装、继承、多态
组件的封装
- 视图
- 数据
- 变化逻辑(数据驱动视图变化)
分装完成后使用者不用明白组件中的视图、数据、变化逻辑,只需要提供组件需要的数据即可
组件的复用
- props传递(传递不同的数据,组件产生不同的效果,但模板是一样的)
- 复用
JSX的本质
React引入的一种编程的语法,类似于html
JSX语法
JSX语法无法被浏览器直接解析,最终需要转换成JS来运行
- html(普通标签)
- 引入JS变量和表达式
- if判断
- for循环
- style 和 calssName
- 事件绑定
JSX解析(本质)
JSX是语法糖,需被解析成JS才能运行,通过React.createElement('tagName', {标签属性}, 子元素...),可以联想到vdom中的h函数(生成vnode)
- JSX其实是语法糖
- 开发环境会将JSX编译成JS代码
- JSX的写法大大降低了学习成本和编码工作量
JSX已经标准化
- JSX是React引入的,但不是React独有的
- JSX其它项目也可以使用
- React.createElement是可以自定义修改的
- 说明: 本身功能已完备,兼容和扩展性没问题
JSX和vdom的关系
为何需要vdom
- vdom是React初次推广开的,结合JSX来使用
- JSX就是模板(类似于vue模板),最终需要渲染成html
- 初次渲染+修改state后的re-render
- 正好符合vdom的应用场景
React.createElement() 和 h()函数
所有的JSX代码都会解析成JS代码来执行,JS代码执行时会返回vnode
何时patch
- 初次渲染 - ReactDOM.render(, container),会触发patch(container, vnode)
- re-render - setState时会触发patch(vnode, newVnode)进行对比
自定义组件的处理
- 'div'会直接渲染成
<div>
- Input和List,是自定义组件class,vdom默认不认识
- 因此,Input和List定义时必须声明render函数
- 并根据props初始化实例,染回执行实例的render函数
- render函数返回的是一个vnode对象
return React.createElement('div', null,
React.createElement(Input, {addTitle: this.addTitle.bind(this)}),
React.createElement(List, {data: this.state.list}),
React.createElement('p', null, 'this is demo')
)
// React.createElement(List, {data: this.state.list}), 类似以下代码
const list = new List({data: this.state.list})
const vnode = list.render()
总结
- 为何需要vdom: JSX需要渲染成html,并通过vdom数据驱动视图操作、渲染html
- React.createElement和h函数一样,都生成vnode
- 何时patch:ReactDOM.render()初次渲染和setState后续更新
- 自定义组件的解析:初始化实例,然后执行render函数返回vnode
React 生命周期
Mounting 阶段
Mounting阶段叫挂载阶段,伴随着整个虚拟DOM的生成,它里面有三个生命周期函数,分别是:
- componentWillMount: 在组件被挂载到页面之前执行
- render: 页面初始化、state和props发生变化时执行
- componentDidMount: 组件挂载完成时被执行
componentWillMount:
此函数只在组件初始化时或页面刷新时执行一次。
render:
render()是纯函数,只返回需要渲染的东西,不应该包含其它的业务逻辑,可以返回原生DOM、React组件、Fragment、字符串、数字、Boolean和null等内容。
componentDidMount:
此函数只在组件初始化时或页面刷新时执行一次。
组件装载之后调用,此时可以获取到DOM节点并操作,比如对canvas、svg的操作,服务器请求,订阅都可以写在里面,但是记得在componentWillUnmount中取消订阅
Updation 更新阶段
Updation阶段,也是组件发生改变的更新阶段,它有两个基本部分组成,一是props
属性改变,二是state
状态改变
- shouldComponentUpdate: 在组件更新之前,自动被执行
- componentWillUpdate: 在函数shouldComponentUpdate返回true后执行
- render()
- componentDidUpdate: 在组件更新之后执行,它是组件更新的最后一个环节
shouldComponentUpdate:
shouldComponentUpdate(nextProps, nextState),有两个参数nextProps和nextState,表示新的属性和变化之后的state,返回一个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true,通常利用此生命周期来优化React程序性能
render
更新阶段也会触发此函数
componentDidUpdate:
componentDidUpdate(preProps, preState, snapshot),该方法有三个参数,之前的props、state以及snapshot 组件更新之后执行,它是组件更新的最后一个环节
Unmounting 卸载阶段
- componentWillUnmount: 当我们的组件被卸载或销毁时就会调用
componentWillUnmount:
在组件被销毁之前可以在函数中执行,去除定时器,取消网络请求,清理无效的DOM元素等垃圾清理工作
生命周期改善程序性能
通过 shouldComponentUpdate 函数改善React组件的性能
// nextProps: 变化后的属性,nextState: 变化后的状态
shouldComponentUpdate(nextProps, nextState) {
if(nextProps.content !== this.props.content) {
return true
}else {
return false
}
}
Vue
vue-router 的导航钩子
全局守卫
beforeEach 全局前置守卫
接收三个参数:
- to: Route: 即将进入的目标路由对象
- from: Route: 当前导航正在离开的路由
- next: Function: 一定要调用此方法来resolve这个钩子
router.beforeEach((to, from ,next) => {
if(to.name !== 'Login' && !isAuthenticated) {
next({name: "Login"})
}else {
next()
}
})
afterEach 全局后置钩子
此钩子不会接受next函数,也不会改变导航本身
router.afterEach((to, from) => {
// ...
})
路由独享的守卫
可以在路由配置上直接定义beforeEnter守卫
const router = new Router({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from , next) => {
// ...
}
}
]
})
组件内的守卫
beforeRouteEnter
- 在渲染该组件的对应路由被confirm前调用
- 不能获取组件实例 this
- 因为在守卫执行前,组件实例还没有被创建
- 可以通过传入一个回调给next来访问组件实例
beforeRouteEnter(to, from , next) {
next(vm => {
// 通过 vm 访问组件实例
})
}
beforeRouteUpdate
- 在当前路由改变,但是该组件被复用时调用
- 对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候
- 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用
- 可以访问组件实例
this
beforeRouteUpdate(to, from , next) {
// just use this
this.name = to.params.name
next()
}
beforeRouteLeave
- 导航离开该组件的对应路由时调用
- 可以访问组件实例
this
beforeRouteLeave(to, from ,next) {
const answer = window.confirm('Do you realy want to leave?')
if(answer) {
next()
}else {
next(false)
}
}
vue-router中 hash模式和 history模式
在 vue的路由配置中有 mode选项,最直观的区别就在于 url中上,hash模式的 url带有一个 #字符,而 history模式没有;vue默认使用 hash模式
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '',
name:'',
component: ...
}
]
})
前端路由
hash: 使用 url中的 hash(#后面的参数)来作为路由,通过监听 hash变化找到对应组件中内容更新到页面中去;支持所有浏览器
history: 使用 HTML5 History API和服务器配置;参考官网中 HTML5 History模式;有兼容性,支持特定浏览器
后端路由
- 直接访问 url,向服务器请求资源
前后端路由的区别
- 后端路由向服务器发出请求
- 前端路由修改视图时,不会向服务器发出请求
hash模式和 history模式的实现及区分
从用户角度看前端路由实现了两个功能(使用 ajax更新页面状态的情况下)
- 记录当前页面的状态(保存、分享当前页的 url,再次打开该 url时,页面还是保存的分享时的状态)
- 可是使用浏览器的前进后退功能(如点击后退按钮,可以是页面回到 ajax更新页面之前的状态,url页回到之前的状态)
作为开发者,要实现这两个功能,我们要做到:
- 监测 url的变化
- 改变 url且不让浏览器向服务端发出请求
- 截获 url地址,并解析出需要的信息来匹配路由规则
说明:我们前端路由常用的 hash模式和 history模式,实际上就实现了上面的功能
hash模式
这里的 hash就是指 url尾巴后的 #号及其后面的字符;这里的 #和 CSS中的 #是一个意思;hash也称为锚点,原本是用来做页面定位的,它可以使用对应的 id元素显示在可视区域内。
由于 hash值变化不会导致浏览器向服务器发出请求,而且 hash改变会触发 hashchange事件
,浏览器的前进后退也能对其进行控制,所以人们在 HTML5的 history出现前,基本都是使用 hash来实现前端路由的
特点:
- 兼容性好,所有浏览器都支持
- hash虽然会出现在 url中,当不会被包含在 HTTP请求中,对后端完全没有影响,因此改变 hash不会重新加载页面
- hash原本是用来页面定位的,如果拿来做路由的话,原本锚点的功能就不能用了
- hash传参是基于 url的,如果要传递复杂数据,会有体积限制
核心 API:
- onhashchange事件:在当前 url的锚点部分(以 #为开始)发生改变时触发,hash变化包括以下部分
- 手动修改 url
- 手动修改 hash
- 浏览器的前进后退
vue实现 hash路由的原理:
<p>hash test</p>
<button id="btn">修改 hash</button>
<script>
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash', location.hash)
}
// 首页初次加载,获取hash
document.addEventListener('DOMContentLoaded', () => {
console.log('hash', location.hash)
})
// js修改url
document.getElementById('btn').addEventListener('click', () => {
location.href = "#/user"
})
</script>
history模式
history模式不仅仅可以在 url中放参数,还可以进数据放在一个特定的对象中。
history利用了 HTML5 History Interface中新增的 pushState()和 replaceState()方法;(需要特定浏览器的支持)history不能运用于 IE8以下
特点:
- 兼容指定浏览器
- 没有符号 #,看起来优雅很多
- pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中
核心 API:
-
history.pushState(state, title, url):HTML5新接口,按指定的名称和URL(如果提供该参数)将数据push进会话历史栈,可以改变网址(存在跨域限制)而不刷新页面
- state:状态对象state是一个JavaScript对象,通过pushState () 创建新的历史记录条目
- title:标题 — Firefox 目前忽略这个参数,但未来可能会用到
- url: 该参数定义了新的历史URL记录
-
history.replaceState(state, title, url):HTML5新接口,按指定的数据,名称和URL(如果提供该参数),修改历史栈上最新的入口,可以改变网址(存在跨域限制)而不刷新页面
- state, title, url与 pushState一致
-
window.onpopstate:是popstate事件在window对象上的事件处理程序
- 调用history.pushState()或者history.replaceState()不会触发popstate事件
- popstate事件只会在浏览器某些行为下触发, 比如点击
后退、前进按钮
(或者在JavaScript中调用history.back()、history.forward()、history.go()
方法),此外,a 标签的锚点也会触发该事件
window.onpopstate = function(event) {
alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
//绑定事件处理函数
history.pushState({page: 1}, "title 1", "?page=1");
//添加并激活一个历史记录条目 http://example.com/example.html?page=1,条目索引为1
history.pushState({page: 2}, "title 2", "?page=2");
//添加并激活一个历史记录条目 http://example.com/example.html?page=2,条目索引为2
history.replaceState({page: 3}, "title 3", "?page=3");
//修改当前激活的历史记录条目 http://ex..?page=2 变为 http://ex..?page=3,条目索引为3
history.back();
// 弹出 "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back();
// 弹出 "location: http://example.com/example.html, state: null
history.go(2);
// 弹出 "location: http://example.com/example.html?page=3, state: {"page":3}
vue实现 history路由的原理:
<p>H5 history</p>
<button id="btn">修改</button>
<script>
// 首页初次加载,获取path
document.addEventListener('DOMContentLoaded', () => {
console.log(location.pathname)
})
// 打开一个新路由
// 【注意】用pushState方式,浏览器不会刷新页面
// history.pushState() 方法向浏览器历史添加了一个状态
// pushSteate() 接受三个参数:state 对象, title (目前被忽略了),URL
document.getElementById('btn').addEventListener('click', () => {
const state = {
name: 'page1'
}
console.log('切换路由到', 'page1')
history.pushState(state, '', 'page1') // pushState ***
})
//监听浏览器前进后退 onpopstate
window.onpopstate = (event) => { //onpopstate ***
console.log('onpopstate', event.state, location.pathname)
}
</script>
总结
hash模式和 history模式都属于浏览器自身的特性,vue-Router只是利用了这两个特性(通过调用浏览器提供的接口)结合注册 Vue全局插件、注册 Vue全局组件(router-view)等技术来实现前端路由
区别:
- hash通过 window.
onhashchange
监听 hash变化 - H5 history通过 history.pushState()/replaceState()向浏览器历史添加/修改一个记录,通过 window.
onpopstate
监听浏览器前进后退 - H5 history只需在后端(Apache 或 Nginx)进行简单的路由配置,同时搭配前端路由的 404 页面支持
两者取舍:
- To B系统推荐使用hash,简单易用,对url规范不敏感
- To C系统,可以考虑H5 history,但需要服务端支持
基础
base
-
web标准
- 结构: 用于结构化的web标准化技术主要由Html、Xhtml、Xml
- 表现: 主要是CSS
- 行为: 体现行为的标准技术主要有Dom和Javascript
-
主流浏览器及内核
- Chrome - Blink
- Safari - Webkit
- Firefox - Gecko
- IE - Trident
- Opera - Blink
- 百度、世界之窗 - IE内核
- 360、猎豹浏览器 - IE+Chrome 双内核
- 搜狗、QQ浏览器 - Trident(兼容模式)+Webkit(高速模式)
HTML
DOCTYPE的作用
DOCTYPE是html5标准网页声明,且必须声明在html文档的第一行<!DOCTYPE html>
。作用是通知浏览器的解析器使用什么文档标准解析此文档
文档解析类型有:
- BackCompat: 怪异模式,默认使用此模式
- 会模拟更旧的浏览器的行为
- CSS1Compat: 标准模式,浏览器使用W3C的标准解析渲染页面
- 页面按照HTML和CSS的定义渲染
HTML、XHTML、XML的区别
- HTML(超文本标记语言): 在html4.0之前HTML现有实现再有标准,导致HTML非常混乱松散
- XML(可扩展标记语言): 主要用于存储数据和结构,可扩展,作用与JSON类似
- XHTML(可扩展超文本标记语言): 基于上面两者而来,W3C为解决HTML混乱问题而生,基于此诞生了HTML5,开头加入
<!DOCTYPE html>
,就是标准模式
HTML语义化的理解
语义化是指使用恰当的html标签,让页面具有良好的结构与含义,比如<p>
代表段落,<article>
代表正文内容等。
语义化的好处:
meta标签
meta标签由name和content两个属性来定义,来描述一个HTML网页文档的属性,如作者、时间、网页描述、关键词、页面刷新等,除了http规定的一些name外,开发者也可以自定义name
- charset:用于描述HTML文档的编码形式
<meta charset="UTF-8" >
- http-equiv:相当于http的文件头作用,如下可以设置http的缓存过期日期
<meta http-equiv="expires" content="Wed, 20 Jun 2019 22:33:00 GMT" >
- viewport:移动端中,开发人员可以控制视口的大小和比例
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" >
- apple-mobile-web-app-status-bar-style:开发过PWA应用的开发者应该很熟悉,为了自定义评估工具栏的颜色
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" >
src和href的区别
- src是指向外部资源的位置,指向的内容会嵌入到文档中当前标签所在的位置,在请求src资源时会将其指向的资源下载并应用到文档内,如JS脚本、img图片、frame等元素。当浏览器解析到该元素时,会暂停其它资源的下载和处理,直到该资源加载、编译、执行完毕,所以一般JS脚本会放在底部而不是头部
- href是指向网络资源所在位置(超链接),用来建立和当前元素、文档之间的连接,当浏览器识别到它指向的文件时,就会并行下载资源,不会停止对当前文档的处理
img中srcset的作用
srcset提供了根据屏幕条件选取图片的能力
可以设计响应式图片,我们可以使用两个新的属性srcset和sizes来提供更多额外的资源图像和提示,帮助浏览器选择一个正确的资源 srcset定义了我们允许浏览器选择的图像集,以及每个图像的大小。 sizes定义了一组媒体条件(如屏幕宽度)并且指明当某些媒体条件为真时,什么样的图片尺寸为最佳选择。
有了以上属性,浏览器会:
- 查看设备宽度
- 检查sizes列表中那个媒体条件是第一个为真
- 查看给与该媒体查询的槽大小
- 加载srcset列表中引用的最接近所选的槽大小的图像
<img src="clock-demo-thumb-200.png" alt="clock" srcset="clock-demo-thumb-200.png 200w, clock-demo-thumb-400.png 400w" sizes="(min-width:600px) 200px, 50vw" />
script标签中defer和async的区别
- defer:
<script defer>
浏览器指示脚本在文档被解析后执行,script被异步加载后并不会立即执行,而是等待文档被解析完毕后执行 - acync:
<script async>
同样是异步加载,区别是脚本加载完毕后立即执行,这导致async属性下的脚本是乱序的,对于script有先后依赖关系的情况,并不适用 - 总结: defer是异步加载,并等到文档解析完毕后才执行;async也是异步加载,并且加载完毕后立即执行,在项目中一般将script脚本放在文档最后
前端存储方式
-
cookies: 在HTML5之前本地存储的主要方式
- 优点: 兼容性好,请求头自带cookies方便
- 缺点: 大小只有4k容量小,每次请求都自带cookies整体浪费流量
-
localStorage: HTML5加入的以键值对(key:value)为标准的存储方式
- 优点: 操作方便,永久性存储(5M),兼容IE8+
-
sessionStorage: 与上边类似,区别是sessionStorage当页面关闭后会被清理,与cookies和localStorage不同,不能再所有同源窗口中共享,是会话级别的存储方式
-
IndexedDB: 被正式纳入HTML5标准的数据库存储方案,是NoSQL数据库,使用键值对进行存储,可进行快速读取操作,非常适用于web场景
HTML5新特性
新增的语义、结构化标签
标签 | 标签 |
---|---|
<article>:文档中定义文章内容 | <aside>:侧边栏 |
<header> | <footer> |
<main> | <nav>:导航 |
<section>:在文档中定义部分,与 div类似 |
一个盒子水平垂直居中的方法
// html部分(此部分内容不变,下面例子共用)
<body>
<div id='container'>
<div id='center' style="width:100px;height:100px;background-color: #0F0">center</div>
</div>
</body>
1.绝对定位 + 负边距实现(已知盒子的宽高)
- 注意:此方法需要知道被居中元素的宽高,才能计算出 margin值,兼容所有浏览器
// css
#container {
position: relative;
}
#center {
position: absolute;
top: %50;
left: %50;
margin: -50px 0 0 -50px;
}
2.绝对定位 + margin: auto;
- 此方法无需知道被垂直居中元素的宽高,不兼容低版本的浏览器
#container {
position: relative;
height: 100px; // 必须声明高度
}
#center {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
3.绝对定位 + css3(未知元素高度)
- 利用 CSS3的 transform,可以在未知元素宽高情况下实现元素的垂直居中
#container {
position: relative;
}
#center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
4.flex布局
- 利用flex布局,其中justify-content和 align-items设置子项的对齐方式, 不能兼容低版本的IE浏览器。
// 直接在父容器中设置即可
#container {
height: 100px; // 必须有高度
display: flex;
justify-content: center;
align-items: center;
}
5.flex/grid + margin: auto;(最简单写法)
- 容器元素设为 flex布局或 grid布局,子元素只要写 margin: auto; 即可,不能兼容低版本的IE浏览器。
#container {
height: 100px; // 必须有高度
display: flex/grid;
}
#center {
margin: auto;
}
CSS
CSS选择器的优先级
CSS选择器的优先级是: 内联样式>ID选择器>类选择器>标签选择器
- 内联 - 1.0.0.0
- ID选择器 - 0.1.0.0
- 类选择器 - 0.0.1.0
- 标签选择器 - 0.0.0.1
同类型的选择器会相加,但没有进位
link和@import的区别
- link属于XHTML标签,而@import是css提供的
- 页面被加载时,link会同时被加载,而@import引用的css会等到页面加载完成后再加载
- import只能在IE5中识别,而link是XHTML标签,无兼容问题
- link方式的样式权重高于@import的权重
隐藏页面元素(CSS)
- opacity: 0; 本质是将元素透明,依然占据空间且可以交互
- visibility: hidden; 隐藏元素,占据空间,不可以交互
- overflow: hidden; 只隐藏元素中内容溢出的部分,占据空间,不可交互
- display: none; 将元素从文档流中去除,不占据空间也不能交互
- z-index: -9999; 原理是将元素的层级放在最底层,被其它元素覆盖
- transform: scale(0,0); 将元素缩放为0,占据空间,不能交互
- transform: translate(-1000px); 将元素移到可视区域外
px/em/rem 的区别
- px: 绝对单位,页面按精确像素展示
- em: 相对单位,基准点为父元素字体的大小,如果自身定义了font-size按自身来计算,整个页面内的1em不是一个固定值
- rem: 相对单位,基准点永远是html中的font-size,整个页面的1rem是一个固定值,CSS3新属性
z-index 的理解
控制设置了position属性元素的垂直叠加顺序,元素默认的值为0,通过修改z-index来控制元素图层的位置
- ...
- 2层
- 1层
- 0层,默认
- -1层
- -2层
- -...
层叠上下文
定义:
是HTML元素的三维概念,是一个假想的相对于电脑屏幕或网页的Z轴上的延伸,HTML元素根据其自身属性按照优先级占用层叠上下文的空间
产生:
- z-index 的值不为auto的relative和absolute和fixed
- opacity 属性值小于1的元素
- transform 属性值不为none的元素
清除浮动的方法
- 添加额外标签-clear: both;
<div> // ... <div style="clear: both;"></div> </div>
- 父级添加overflow属性(BFC)或设置高度
<div style="overflow: hidden;"> // ... </div>
- 为类选择器清除浮动(推荐)
.box:after { content: ''; display: block; height: 0; visibility: hidden; clear: both; } <div class="box"> // ... </div>
盒模型的理解
盒模型由content、padding、border、margin组成
定义:
当对一个文档进行布局时,浏览器的渲染引擎会根据标准之一的CSS基础盒模型,将所有元素表示为一个个矩形盒子(box)。CSS决定了这些盒子的大小、位置以及属性(颜色、背景、边框等)
标准盒模型与怪异盒模型的区别
标准盒模型
在W3C标准下,我们定义的width === content的宽度值,height === content的高度值,因此,元素的宽度为:
元素宽度 = margin-left+margin-right+border-left+border+right+padding-left+padding-right+width
怪异盒模型
在IE怪异盒模型(IE8以下)width !== content的宽度,而是 width === border-left+border-right+padding-left+padding-right+width
元素宽度 = margin-left+margin-right+width
更改盒模型
box-sizing: content-box; // 标准盒模型
box-sizing: border-box; // 怪异盒模型
BFC 的理解
块级格式上下文: 它是一块独立的区域,让处于BFC内部的元素与外部的元素互相隔离
触发条件
- 根元素,即HTML元素
- position: fixed/absolute;
- float不为none
- voerflow不为visible
- display的值为 inline-block、table-cell、table-caption
应用场景
- 防止浮动导致父元素高度塌陷
<style> .container { border: 10px solid red; overflow: hidden; } .inner { float: left; background: #08BDEB; height: 100px; width: 100px; } </style> <div class="container"> <div class="inner"> // ... </div> </div>
- 避免外边距折叠
<style> .container { background-color: green; overflow: hidden; } .inner { background-color: lightblue; margin: 10px 0; } .bfc{ overflow: hidden; } </style> <div class="container"> <div class="inner">1</div> <div class="bfc"> <div class="inner">2</div> </div> <div class="inner">3</div> </div>
translate和position改变元素位置的区别
- translate是transform的一个值,改变transform或opacity不会触发浏览器的重新布局reflow或重绘repaint
- 改变绝对定位会触发浏览器的重新布局,进而触发重绘
- transform使浏览器为元素创建一个GUP图层,但改变绝对定位会使用CPU
- 使用translate更高效,可以缩短拼花动画的绘制时间
伪类和伪元素的区别
伪类: 以冒号:作为前缀,被添加到一个选择器末尾的关键字,当你希望样式在特定状态下才被呈现到指定的元素时,你可以在元素的选择器后面添加对应的伪类
- 元素:first-child {}
- 元素:nth-child(n) {}
- 元素:hover {}
- 元素:focus {}
- a:link {未访问的链接}
- a:visited {以访问的链接}
- a:active {选定的链接}
伪元素: 用于创建一些不在文档中的元素,并为其添加样式
- ::before
- ::after
区别
- 伪类是通过在元素选择器上加入伪类改变元素的状态
- 伪元素是通过对元素的操作来添加元素
Less
Less预处理器: 支持CSS语法,引入变量,混合,运算,嵌套等,简化CSS编写,降低维护成本
变量
- 语法: @变量名: 值;
@sz: 1000px; p { font-size: @sz; span { // 运算 font-size: @sz - 2; } }
嵌套
```less
.box {
background-color: #0f0;
ul {
list-style: none;
}
}
```
混合(函数)
- 定义: #函数名(){} || .函数名(){}
- 调用: #函数名 || .函数名
- 传参、默认值
// 1. 定义
#public() {
width: 200px;
height: 200px;
}
.origin() {
width: 200px;
height: 200px;
}
// 2. 调用
div {
#public()
}
h2 {
.public()
}
// 3. 传参、默认值
#base(@w:200px, @h:200px) {
width: @w;
height: @h;
}
div {
#base(600px, 600px);
}
Sass
变量
- 语法: $blue: #1875e7;
$blue: #1875e7;
div {
color: $blue;
}
- 嵌套在字符串中,必须写在
#{}
中
$side: left;
.rounded {
border-#{$side}-radius: 5px;
}
计算功能
body {
}
JS
JS基础
谈谈你对原型链的理解
此问题关键在于两点: 原型对象,原型链的形成
-
原型对象
-
大部分函数都有一个
prototype
属性,此属性用来指向原型对象,而所有被同一个函数创建的对象都可以通过自带的属性__proto__指向原型对象,因此这些对象便可以通过__proto__访问原型对象的属性 -
例如hasOwnProperty()方法存在于Object原型对象中,因此它便可以被任何对象当做自己的方法使用
-
<script>
// 一般原型中只存放构造函数的 方法 也可以存放 属性, 用于同类对象的调用,
// 相当于原型中的方法共享给同类对象, 节省内存
function Students(name, age) {
this.name = name;
this.age = age;
}
// 找到原型
var yxStudents = Students.prototype;
// 给原型 添加方法
yxStudents.sayHi = function (swing, code) {
// 方法中的 this 指向方法的调用者
console.log('我叫'+this.name+', 今年'+this.age+', 我最喜欢的事情是'+swing+', 我的专业是'+code+'程序员');
return this;
};
yxStudents.type = '学生';
var stu1 = new Students('小五', 18);
stu1.sayHi('游泳', 'Java');
console.log(stu1);
// 实例对象中 __proto__ 存放的是原型对象的地址
console.log(stu1.__proto__ === yxStudents);
// 实例中的对象是如何共享到原型中的成员的
// 实例对象先在自身内部查找, 如找不到,
// 就会通过系统分配的 __proto__中提供的原型地址找原型对象
// 如找不到, 就在原型的原型中去找
</script>
obj.hasOwnProperty(key)
// 如果对象obj包含key属性则返回true否则false
-
原型链
- 原因是每个对象都有__proto__属性,此属性指向该对象的构造函数的原型
- 对象可以通过__proto__与上游的构造函数的原型对象连接起来,而上游的原型对象也有一个__proto__,这样就形成了原型链
判断一个变量是否是数组
-
Array.isArray(arr)
if(Array.isArray(value)) { return true }
-
toString(arr)
if(!Array.isArray) { Array.isArray = function(args) { return Object.prototype.toString.call(args) === "[object Array]" } }
ES6模块与CommonJS模块的区别
- ES6 Module和CommonJS模块的不同点:
- CommonJS是对模块的浅拷贝,ES6 Module是对模块的引用,即ES6 Module是只读的,不能改变其值,就是指针指向不能变
- import的接口是只读状态,不能修改其变量值。即不能修改其变量的指针指向,但可以改变变量内部指针指向;可以对CommonJS重新赋值(改变指针指向),但对ES6 Module赋值会编译报错
- ES6 Module和CommonJS模块的共同点:
- CommonJS和ES6 Module都可以对引入的对象进行赋值,即对对象内部属性进行改变
async/await
async函数,就是Generator函数的语法糖,它建立在Promise上,并且与所有的基于Promise和API兼容
- async 声明一个异步函数
- 自动将常规函数转换成Promise,返回值是一个promise对象
- 只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数
- async函数内部可以使用await
await暂停异步的功能执行
- 放置在Promise调用之前,await强制后面的代码等待,直到Promise完成并返回结果
- 只能与Promise一起使用,不适用于回调
- 只能用于async函数内部
async/await相对于Promise的优势
- 代码类似于同步代码,Promise虽然摆脱了回调地狱,但.then的链式调用也会带来额外的阅读负担
- Promise传递中间值非常麻烦,而async/await几乎是同步写法
- 错误处理更友好,async/await使用try/catch,Promise使用.catch非常冗余
- 调试更友好,在.then使用调试器后,调试器并不能进入后续的.then代码块,因为调试器只能跟踪同步代码
JavaScript 的参数传递方式
-
基本类型传递方式
JS中基本类型数据是按照值传递
var a = 1 function test(x) { x = 10 console.log(x) } test(a) // 10 console.log(a) // 1
-
复杂类型按引用传递
function test(person) { person.age = 26; person = { name: 'ijk', age: 123 } return person } const p1 = {name: 'xyz', age: 20} const p2 = test(p1) console.log(p1) // {name: 'xyz', age: 26} console.log(p2) // {name: 'ijk', age: 123}
-
按共享传递
复杂类型之所以会产生这种特性,原因就是在传递过程中,对象
p1
先产生一个副本p1
,这个副本p1
并不是深克隆得到的副本p1
,副本p1
地址同样指向对象p1
指向的堆内存。因此,在函数中修改
person
只是修改了副本p1
,p1
对象没有变化,但是如果修改了person.age = 26
是修改了两者指向的统一个堆内存,此时p1
也会受到影响。这种特性叫做
传递引用
或按共享传递
。
BingInt 提案
JS中最大安全数字Number.MAX_SAFE_INTEGER === 2^56,即在此数范围内不会出现精度丢失(小数除外)。
但是一旦超过此范围,JS就会出现计算不准确的情况,使得在大数计算时需要依靠第三方库进行解决,因此官方提出BinInt来解决问题。
null和undefined的区别
null表示为空,代表此变量没有值,一个对象可以是null,代表是空对象,而null本身不是对象,因为000开头代表是对象,而null全为零,所以将它误判为对象
undefined表示不存在,JS是一门动态类型语言,成员除了表示存在的值外,还有可能根本就不存在(因为是否存在只有在运行期间才知道),这就是undefined的意义所在。
类型转换的规则
转字符串
- 任何数据和字符串相加都会先自动转换为String类型
console.log('1'+NaN) // 1NaN console.log('1'+1) // 11 console.log('1'+false, '1'+true) // 1false 1true console.log('1'+undefined) // 1undefined
转数字
- 任何数据在做算数运算时(除了和字符串相加)或任何数据在和数字比较时,其它数据都会先自动转换为Number类型
- 任何数据和NaN做算数运算(除了和字符串相加外),结果始终是NaN
console.log(1-'1') // 0 console.log(1+true) // 2 console.log(1-'a') // NaN console.log(1+undefined) // NaN
转布尔
- 在取反运算时,其它数据会自动转换为布尔值
- 在分支结构或循环结构的条件判断中,其它数据会自动转换为布尔值
'1'.toString()的调用
其实在这个语句运行的过程中做了以下几件事:
// 1. 创建String类实例
var s = new String('1')
// 2. 调用实例方法
s.toString()
// 3. 执行完方法后立即销毁此实例
s = null
整个过程体现了基本包装类型
的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean、Number、String。
0.1+0.2 !== 0.3 ?
JS的Number类型遵循的是IEEE754标准,使用64位固定长度来表示。
0.1和0.2在转换成二进制后会进入无限循环,由于标准位数的限制,后面多余的位数会被截掉,此时就会出现精度丢失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会出现精度丢失。
JS机制
变量提升
JS引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行行的运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升。
console.log(a) // undefined
var a = 1
function b() {
console.log(a)
}
b() // 1
上面的代码实际的执行顺序是这样的:
第一步: 引擎将var a = 1
拆解为var a = undefined
和a = 1
,并将var a = undefined
放在最顶端,a = 1
还在原位置,这样一来代码就是:
var a = undefined; // 变量提升
console.log(a) // undefined
a = 1;
function b() {
console.log(a)
}
b() // 1
第二步: 因此JS引擎一行行从上往下执行就造成了当前的结果,这就叫变量提升。
JS的作用域链
JS属于静态作用域,即声明的作用域是根据程序正文在编译时就确定的,有时也称为此法作用域。
其本质是JS在执行过程中会创造可执行上下文,可执行上下文的词法环境中含有外部词法环境的引用,我们可以通过此引用获取外部词法环境的变量、声明等,这些引用串联起来一直指向全局的词法环境,因此形成了作用域链。
箭头函数的this
箭头函数不同于传统JS中的函数,箭头函数并没有属于自己的this,它所谓的this是捕获其所在上下文的this值作为自己的this,箭头函数不会被new调用,这个所谓的this也不会被改变。
我们可以用Babel理解一下箭头函数:
// ES6
const obj = {
getArrow() {
return () => {
console.log(this === obj)
}
}
}
转化后
// ES5,由Babel转译
var obj = {
getArrow: function getArrow() {
var _this = this;
return function() {
console.log(_this === obj)
}
}
}
ES6
块级作用域
- 用 {}包起来的,表示块级作用域
for(let i=1; i < 3; i++) {
console.log(i) // 1 2
}
// console.log(i) // ReferenceError: i is not defined
// ES6强制开启严格模式 'use script',变量未声明不能引用,ReferenceError
let
- 变量只在自己的块级作用域内有效
- 声明的变量不能重复定义
let a = 1
let a = 2 // ERROR in ./app/js/class/lesson1.1.js
const
- 声明的常量不能修改,并只能在自己的块级作用域内有效
- 声明时必须赋值
- 声明的常量为引用类型时,可以修改引用类型内部的数据的值
const PI = 3.1415926
const k = {
a: 1
}
k.b = 2
// PI = 2 // ERROR
console.log(PI,k)
解构赋值
基本使用
// 变量赋值
let a,b
[a,b] = [1,2]
// console.log(a,b) // 1 2
数组解构赋值
// ... 解构赋值的一个重要特性
[a,b,...rest] = [1,2,3,4,5,6,7,8]
// console.log(a,b,rest) // 1 2 (6) [3, 4, 5, 6, 7, 8]
// 默认值
let a,b,c
[a,b,c=3] = [1,2]
console.log(a,b,c) // 1 2 3
// 变量交换
let a = 1;
let b = 2;
[a,b] = [b,a] // console.log(a,b) // 2 1
对象解构赋值
// 默认值
let {a = 10, b = 5} = {a:3} // console.log(a,b) 3 5
// 起别名
let metaData = {
title: 'abc',
test: [{
title: 'test',
desc: 'description'
}]
}
let {title:esTitle,test:[{title:cnTitle}]} = metaData
console.log(esTitle,cnTitle) // abc test
箭头函数
- 函数体内的 this对象,就是定义时所在的对象,而不是使用时所在的对象
- 不可以用作构造函数,也就是说,不能使用 new命令
- 不可以使用 arguments对象,该对象在函数体内不存在
- 不可以使用 yield命令,因此箭头函数不能用作 Generator函数
模块化
// 导出
// utils1.js
export default {
a: 1
}
// utils2.js
export function fn1() {
console.log('fn1')
}
export function fn2() {
console.log('fn2')
}
// 引入
import utils1 from './utils1.js'
// export ... export ...等 -> import {...}
import {fn1, fn2} from './utils2.js'
console.log(utils1)
fn1()
fn2()
Promise
Promise 是 JS中进行异步编程新的解决方案
(旧的是纯回调的形式)
Promise的状态改变
- pending 变为 resolved
- pending 变为 rejected
说明:只有以上这两种变化,且
一个 promise对象只能改变一次
,无论变为成功还是失败,都会有一个结果数据,成功的结果数据一般称为 res,失败的结果数据一般称为 rej
Promise的基本流程
- new Promise() (pending状态) -> 成功了,执行resolve() -> promise对象(resolved状态) -> 回调onResolved() (then()) -> 新的 promise对象
- new Promise() (pending状态) -> 失败了,执行reject() -> promise对象(rejected状态) -> 回调onRejected() (then()/catch()) -> 新的 promise对象
Promise的基本使用
const pm = new Promise((resolve, reject) => {
setTimeout(() => {
const time = Date().now()
if (time % 2 === 0) {
resolve('success:', time)
} else {
reject('fail:', time)
}
}, 1000)
})
pm.then(
res => {
console.log('success-res:', res)
},
rej => {
console.log('fail-rej:', rej)
})
Promise详解
Promise构造函数:Promise(excutor)
- excutor执行器函数:同步执行函数(resolve, reject) => { // 异步代码 }
- resolve函数:内部定义成功时,resolve(...)
- reject函数:内部定义失败时,reject(...) 说明:excutor 会在 Promise内部立即同步回调,异步操作在执行器中执行
Promise.prototype.then方法:(onResolved, onRejected) => {...}
- onResolved函数:成功的回调函数 res => {}
- onRejected函数:失败的回调函数 rej => {}
说明:指定用于得到成功 res的成功回调和用于得到失败 rej的失败回调,
返回一个新的promise对象(链式调用的前提)
Promise.prototype.catch方法:(onRejected) => {}
- onRejected函数:失败的回调函数 rej => {} 说明:then()的语法糖,相当于:then(undefined,onRejected)
Promise.prototype.then返回的新 Promise对象的结果的确定
- 简单表达:由 then()中指定的回调函数执行的结果决定
- 详细分析:
- 如果抛出异常,返回的新 Promise对象状态变为 rejected,由 rej接收返回的异常
- 如果返回的是非 Promise对象的任意值,返回的新 Promise对象状态变为 resolved,由 res接收返回的数据
- 如果返回的是另一个新 Promise对象,此Promise对象会代替原本默认返回的 Promise对象
Promise串联多个操作任务
cosnt pm = new Promise((resolve, reject) => {
setTimeout(() => {
const time = Date().now()
if (time % 2 === 0) {
resolve('success:', time)
} else {
reject('fail:', time)
}
})
})
pm.then(res => {
console.log('success-res1:', res)
return 1
}
).then(res => {
console.log('success-res2:', res)
return new Promise((resolve, reject) => {
resolve('new success')
})
}).then(res => {
console.log('success-res2:', res)
return new Promise((resolve, reject) => {
reject('new fail')
})
}).catch(rej => {
console.log('fail-rej:', rej)
})
Class
基本使用:
- constructor
- static 静态方法
- getter/setter 属性
class MathHandle {
constructor (x, y) {
this.x = x
this.y = y
}
// 对象方法
add () {
return this.x + this.y
}
// 静态方法
static say () {
console.log('MathHandle')
}
// 属性:getter/setter 获取/设置
get longName () {
return 'class' + this.x
}
set longName (val) {
this.x = val
}
}
const mObj = new MathHandle(1, 2)
console.log(mObj.longName) // class-1
mObj.longName = 'hello'
console.log(m.longName) // class-hello
console.log(mObj.add()) // hello2
MathHandle.say() // 'MathHandle'
// 与构造器类似
typeof MathHandle // 'function'
MathHandle === MathHandle.prototype.constructor // true
// 形式上强行模仿 java C#,失去了它的本性和个性
m.__proto__ === MathHandle.prototype // 实例的隐式原型 === 构造函数的显示原型
继承:
- extends:继承
- super(...):给父类传递参数,一般写在第一行,必须写在this之前
class Animal {
constructor (name='animal') {
this.name = name
}
eat () {
console.log(`${this.name} eat`)
}
}
// extends 类似于 Dog.prototype = new Animal()
class Dog extends Animal {
constructor (name='dog', age) {
// 通过super传递参数,一般放在第一行,必须写在this之前
super(name)
this.age = age
}
say () {
console.log(`${this.name} ` + ` ${this.age} ` + 'say')
}
}
const dog = new Dog('哈士奇', 2)
dog.say()
dog.eat()
闭包
概念:
- 闭包是指有权访问另外一个函数作用域中变量的函数
- 是一个函数(比如,内部函数从父函数中返回)
- 可以访问上级函数作用域中的变量(哪怕上级函数上下文已经销毁)
闭包的三特性:
- 闭包可以访问当前函数以外的变量
function getFn () { let data = '315' function getData (str='getData') { console.log(str + data) } return getData('当前是:') // '当前是:315' } getFn()
- 即使外部函数已经返回,闭包仍能访问外部函数定义的变量
function getFn () { let data = '315' function getData (str) { console.log(str + data) } return getData; } let conFn = getFn(); conFn('当前是:') // '当前是:315'
- 闭包可以更新外部变量的值
function getFn () { let data = 0 function getData (val) { count = val console.log(count) } return getData } let setCount = getFn() setCount(818) // 818 setCount(819) // 819
深拷贝
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存
方式一:JSON.parse(JSON.stringify(obj))
const obj = {
name: 'lv',
age: 18,
career: {
companyName: '天天',
years: 3
}
}
const obj1 = JSON.parse(JSON.stringify(obj))
// obj1内数据的修改,完全不影响 obj
此方法使用时要注意以下几个问题:
- 会忽略 undefined、symbol(拷贝后,对应的属性消失)
- 不能序列化函数
- 不能正确处理 new Date(),解决方法转成 字符串或者时间戳,如下:
// 解决方法转成字符串或者时间戳就好了 let date = (new Date()).valueOf() JSON.stringify(date) // "1545620673267" JSON.parse(JSON.stringify(date)) // 1545620658688
- 不能处理正则
方式二:手写深拷贝
- 检测数据类型:Object.prototype.toString(target): 返回 target的字符串类型 [object typeName]
// 检测类型
const checkType = target => {
return Object.prototype.toString.call(target).slice(8, -1)
}
// 深拷贝
const deepClone = (target) => {
let result, targetType = checkType(target)
if(targetType === 'Object') {
result = {}
}else if(targetType === 'Array') {
result = []
}else {
return target
}
// 遍历目标数组
for(let i in target) {
if(target.hasOwnProperty(i)) {
let value = target[i]
if(checkType(value) === 'Object' || checkType(value) === 'Array') {
result[i] = deepClone(value)
}else {
result[i] = value
}
}
}
// 返回最终值
return result
}
Object.prototype.toString().call() 可以判断类型的原因
Object.prototype.toString()会返回对象的类型字符串,输出 "[object objType]"其中 objType是传入参数的构造函数。所以使用 call就可以指定任意的值和结合 toString将组成的构造函数类型返回来判断类型,同样道理换成 apply/bind同样可以判断
// call()
Object.prototype.toString.call('str') // "[object String]"
Object.prototype.toString.call(123) // "[object Number]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"
// apply()
Object.prototype.toString.apply({}) // "[object Object]"
Object.prototype.toString.apply([]) // "[object Array]"
// bind()
const f1 = Object.prototype.toString.bind({})
f1() // "[object Object]"
const f2 = Object.prototype.toString.bind([]) // "[object Array]"
call、apply、bind
call
- 基本应用:
let a = { value: 1 }
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.call(a, 'lala', 24)
// lala 24 1
- 模拟实现:
Function.prototype.myCall = function (e) {
console.log(e) // {value: 1}
let context = e || window
// 将myCall的调用者添加为context方法
context._fn = this
// 获取除第一个参数外的所有参数
let args = [...arguments].slice(1)
console.log(args) // (2) ["lal", 28]
// 调用方法
let result = context._fn(...args)
delete context._fn
}
getValue.myCall(a, 'lal', 28)
apply
- 基本应用:
let a = { value: 1 }
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.apply(a, ['yls', 26])
- 模拟实现:
Function.prototype.myApply = function (e) {
let context = e || window
context._fn = this
console.log(context)
let args = arguments[1]
console.log(args) // (2) ["yyy", 36]
// this 的隐式绑定
context._fn(...args)
delete context._fn
// 1.存储.call/apply()中的第一个参数
// 2.将.call/apply()的调用者 this设置为第一个参数的方法
// 3.存储.call/bind()除第一个参数之外的余下参数
// 4.用第一个参数去调用存储的this方法,并传递参数
// 5.删除在第一个参数中额外添加的方法
}
getValue.myApply(a, ['yyy', 36])
call、apply的模拟实现思路:
- 存储.call/apply()中的第一个参数
- 将.call/apply()的调用者 this设置为第一个参数的方法
- 存储.call/bind()除第一个参数之外的余下参数
- 用第一个参数去调用存储的this方法,并传递参数
- 删除在第一个参数中额外添加的方法
bind
- 模拟实现:
Function.prototype.myBind = function (thisArg) {
if(typeof this !== 'function') {
throw new TypeError(this + 'must be a function')
}
// 存储函数自身
var self = this
// 处理第一次调用bind的形参,去除thisArg的其它参数,转成数组
var args = [].slice.call(arguments, 1)
// var args1 = [...arguments].slice(1)
console.log(arguments, args, args1)
var bound = function() {
// bind返回的函数的参数转成数组
var boundArgs = [].slice.call(arguments)
// apply修改this指向,把两个函数的参数合并传给self函数,
// 并执行self函数,返回执行结果
return self.apply(thisArg, args.concat(boundArgs))
}
return bound
// 1.判断this是否为函数
// 1.1存储this
// 2.存储.bind()中除第一个参数之外的所有参数
// 3.声明一个函数并返回
// 3.1存储声明函数中的参数
// 3.2调用apply方法,并传递参数
}
var obj = {name: '若穿'}
function original(a, b) {
console.log(this.name)
console.log([a, b])
}
var bound = original.myBind(obj, 1)
bound(2) // 若穿 [1, 2]
防抖节流
节流:
- 核心概念:在规定的时间间隔内再次触发,会忽略此次触发,等到超过规定的时间间隔后,才能启动下一次时间间隔
// 方式一:使用 setTimeout
function throttle (fn, interval=500) {
let flag = false
return function (...args) {
const _this = this
if (flag) return
flag = true
setTimeout(() => {
fn.apply(_this, args)
flag = false
}, interval)
}
}
// 方案二:
function throttle (fn, interval=500) {
let last = 0
return function (...args) {
const now = +new Date()
if (now - last < interval) return
last = now
fn.apply(this, args)
}
}
防抖:
- 核心概念:每次事件触发则删除原来的定时器,重新计时
function debounce (fn, delay=500) {
let timeId = null
return function (...args) {
const _this = this
if (timeId) clearTimeout(timeId)
timeId = setTimeout(() => {
fn.apply(_this, args)
}, delay)
}
}
防抖 + 节流:加强版
- 防止事件触发太频繁导致一次响应都没有,所以固定时间内必须给用户一个响应
function debounceStrong (fn, delay=500) {
let timeId = null, last = 0
return function (...args) {
const _this = this
const now = +new Date()
if (now - last < delay) {
clearTimeout(timeId)
timeId = setTimeout(() => {
last = now
fn.apply(_this, args)
}, delay)
} else {
clearTimeout(timeId)
last = now
fn.apply(_this, args)
}
}
}
实例:
mounted () {
window.addEventListener('scroll', throttle(this.handleShow), true)
}
methods: {
handleShow () {
// ...
console.log('scroll')
}
}
beforeDestroy () {
window.removeEventListener('scroll', throttle(this.handleShow), true)
}
转载自:https://juejin.cn/post/6844904186656587784