前端问答整理

link 与 import 的区别?

  • link 是一种 html 标签,它没有兼容性问题,并且能够经过 js 的 DOM 操做动态的引入样式表。
  • @import 是css提供的一种引入样式表的语法,不能够动态加载,不兼容ie5如下。
  • 因为link 是html标签,因此它引入的样式是在页面加载时同时加载的,@import须要在页面加载完成以后再加载。

圣杯布局与双飞翼布局的理解和区别,并用代码实现

圣杯布局和双飞翼布局都是三栏布局,左右盒子宽度固定,中间盒子自适应(固比固),区别是他们的实现的思想。 圣杯布局:css

<div id="header">#header</div>
<div id="container">
  <div id="center" class="column">#center</div>
  <div id="left" class="column">#left</div>
  <div id="right" class="column">#right</div>
</div>

<div id="footer">#footer</div>
// style
body {
  min-width: 600px; 
}
#container {
  padding-left: 200px;  
  padding-right: 200px; 
}
#container .column {
  height: 200px;
  position: relative;
  float: left;
}
#center {
  width: 100%;
}
#left {
  width: 200px;          
  right: 200px;        
  margin-left: -100%;
}
#right {
  width: 200px;         
  margin-right: -200px;
}
#footer {
  clear: both;
}

/*** IE6 Fix ***/
* html #left {
  left: 200px;
}
复制代码

圣杯布局的思想是:给内容区设置左右 padding 防止遮挡,将中间的三栏所有 float left,经过 margin 负间距把左右内容区移到上方,再经过 relative + left 和right 的设置,将左右内容区移动到目标位置。html

双飞翼布局:前端

<body>
<div id="hd">header</div> 
  <div id="middle">
    <div id="inside">middle</div>
  </div>
  <div id="left">left</div>
  <div id="right">right</div>
  <div id="footer">footer</div>
</body>

<style>
#hd{
    height:50px;
}
#middle{
    float:left;
    width:100%;/*左栏上去到第一行*/     
    height:100px;
}
#left{
    float:left;
    width:180px;
    height:100px;
    margin-left:-100%;
}
#right{
    float:left;
    width:200px;
    height:100px;
    margin-left:-200px;
}

/*给内部div添加margin,把内容放到中间栏,其实整个背景仍是100%*/ 
#inside{
    margin:0 200px 0 180px;
    height:100px;
}
#footer{ 
   clear:both; /*记得清楚浮动*/  
   height:50px;     
} 
</style>
复制代码

双飞翼布局:给middle建立子div放置内容,再经过子div设置margin-left right 为左右两栏留出位置,其他与圣杯布局思路一致。vue

如今也可使用 flex-box 轻松实现。react

CSS3中transition和animation的属性分别有哪些

transition 过渡动画: (1) transition-property:属性名称 (2) transition-duration: 间隔时间 (3) transition-timing-function: 动画曲线 (4) transition-delay: 延迟 animation 关键帧动画: (1) animation-name:动画名称 (2) animation-duration: 间隔时间 (3) animation-timing-function: 动画曲线 (4) animation-delay: 延迟 (5) animation-iteration-count:动画次数 (6) animation-direction: 方向 (7) animation-fill-mode: 禁止模式webpack

用递归算法实现,数组长度为5且元素的随机数在2-32间不重复的值

let array = new Array(5)
    const fillArray = (arr, index = 0, min = 2, max = 32) =>{
        const num = Math.floor(Math.random() * (max - min + 1)) + min
        if(index < arr.length) {
          if(!arr.includes(num)) {
            arr[index++] = num
          }
          return fillArray(arr, index)
        } 
        return arr
    }
    console.log(fillArray(array));
复制代码

写一个方法去掉字符串中的空格

string.split(' ').join('')
// 或
string.replace(/\s/g, '')
复制代码

在页面上隐藏元素的方法有哪些?

visibility: hidden;
margin-left: -100%;
opacity: 0;
transform: scale(0);
display: none;
width: 0; height: 0; overflow: hidden;
复制代码

写一个把字符串大小写切换的方法

let str = 'aBCdEFg'
const reverseStr = (val) =>{
	return val.split('').map(item=>{
		if(item.toLowerCase() === item) {
			return item.toUpperCase()
		} else {
			return item.toLowerCase()
		}
	}).join('')
}
console.log(reverseStr(str));
复制代码

CSS3新增伪类有哪些并简要描述

CSS3 中规定伪类使用一个 : 来表示;伪元素则使用 :: 来表示nginx

简述超连接target属性的取值和做用

_top 在 frame 或者 iframe 中使用较多。直接在顶层的框架中载入目标文档,加载整个窗口。

react-router 里的 <Link> 标签和<a>标签有什么区别?

他们的本质都是 a 标签,<Link> 是 react-router 里实现路由跳转的连接,通常配合 <Route> 使用。React Router 会接管 Link 的默认跳转连接行为。Link 主要作了三件事es6

  1. 有onclick那就执行onclick
  2. click的时候阻止a标签默认事件(这样子点击123就不会跳转和刷新页面)
  3. 再取得跳转href(便是to),用history(前端路由两种方式之一,history & hash)跳转,此时只是连接变了,并无刷新页面

单页面应用(SPA)路由实现原理

  1. hash: hash 本意是用来做锚点的,方便用户在一个很长的文档里进行上下的导航,用来作 SPA 的路由控制并不是它的本意。然而,hash 知足这么一种特性:改变 url 的同时,不刷新页面,再加上浏览器也提供 onhashchange 这样的事件监听,所以,hash 能用来作路由控制。
  2. history:早期的 history 只能用于多页面进行跳转。在 HTML5 规范中,history 新增了如下几个 API
history.pushState();         // 添加新的状态到历史状态栈
history.replaceState();     // 用新的状态代替当前状态
history.state             // 返回当前状态对象
复制代码

经过history.pushState或者history.replaceState,也能作到:改变 url 的同时,不会刷新页面。可是onhashchange 能够监听hash的变化,但history的变化没法直接监听,须要经过拦截可能改变history的途径来监听history的变化。可能改变url的方法有三种web

  • 点击浏览器前进后退
  • 点击a标签
  • 直接在js中修改路由 第一种能够经过onpopstate事件监听,第二第三实际上是一种,a标签的默认事件能够经过js禁止

如何配置React Router

  1. 选择路由器类型,好比BrowserRouter 和 HashRouter,须要确保其渲染在根目录之下
  2. 使用路由匹配器,Switch 和 Route,通常用Switch 包裹 Route,Switch 用于渲染与路径匹配的第一个子 Route 或 Redirect。
  3. 使用导航组件 跳转到目标路由。若是要强制导航,可使用
  4. 可使用 React Router 提供的一些 Hooks 从组件内部进行导航
  5. 可使用 React 提供的 Lazy 与 Suspense 组件动态加载路由组件,能够延迟加载未用到的组件

居中为何要使用transform(为何不使用marginLeft/Top)

transform transform 属于合成属性(composite property),对合成属性进行 transition/animation 动画将会建立一个合成层(composite layer),这使得被动画元素在一个独立的层中进行动画。一般状况下,浏览器会将一个层的内容先绘制进一个位图中,而后再做为纹理(texture)上传到 GPU,只要该层的内容不发生改变,就不必进行重绘(repaint),浏览器会经过从新复合(recomposite)来造成一个新的帧。 margin top / left top/left属于布局属性,该属性的变化会致使重排(reflow/relayout),所谓重排即指对这些节点以及受这些节点影响的其它节点,进行CSS计算->布局->重绘过程,浏览器须要为整个层进行重绘并从新上传到 GPU,形成了极大的性能开销。算法

大文件分片上传,断点续传

思路:核心是利用 Blob.prototype.slice 方法,和数组的 slice 方法类似,调用的 slice 方法能够返回原文件的某个切片 这样咱们就能够根据预先设置好的切片最大数量将文件切分为一个个切片,而后借助 http 的可并发性,调用Promise.all同时上传多个切片,这样从本来传一个大文件,变成了同时传多个小的文件切片,能够大大减小上传时间 另外因为是并发,传输到服务端的顺序可能会发生变化,因此咱们还须要给每一个切片记录顺序。 当所有分片上传成功,通知服务端进行合并。当有一个分片上传失败时,提示“上传失败”。在从新上传时,经过文件 MD5 获得文件的上传状态,当服务器已经有该 MD5 对应的切片时,表明该切片已经上传过,无需再次上传,当服务器找不到该 MD5 对应的切片时,表明该切片须要上传,用户只需上传这部分切片,就能够完整上传整个文件,这就是文件的断点续传。

移动端适配1px的问题

产生缘由:与DPR(devicePixelRatio)设备像素比有关,它表示默认缩放为100%的状况下,设备像素和CSS像素的比值:物理像素 /CSS像素。 目前主流的屏幕DPR=2 (iPhone 8),或者3 (iPhone 8 Plus)。拿2倍屏来讲,设备的物理像素要实现1像素,而DPR=2,因此css 像素只能是 0.5。通常设计稿是按照750来设计的,它上面的1px是以750来参照的,而咱们写css样式是以设备375为参照的,因此咱们应该写的0.5px就行了啊! 试过了就知道,iOS 8+系统支持,安卓系统不支持。

我经常使用的解决方案是 使用伪元素,为伪元素设置绝对定位,而且和父元素左上角对齐。将伪元素的长和宽先放大2倍,而后再设置一个边框,以左上角为中心,缩放到原来的0.5倍 除了这个方法以外还有使用图片代替,或者使用box-shadow代替,可是没有伪元素效果好。

浅拷贝与深拷贝

首先数据类型有两种,一种基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和引用数据类型(Array,Object)。

基本数据类型是直接存储在栈内存中的。引用数据类型则是在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中得到实体。

深拷贝和浅拷贝的区别就是:浅拷贝只复制指向某个对象的指针,而不复制对象自己,新旧对象仍是共享同一块内存。但深拷贝会另外创造一个如出一辙的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

浅拷贝与赋值的区别:赋值获得的对象与原对象指向的是同一个存储空间,不管哪一个对象发生改变,其实都是改变的存储空间的内容,所以,两个对象是联动的。而浅拷贝会建立一个新对象,若是属性是基本类型,拷贝的就是基本类型的值;若是属性是内存地址(引用类型),拷贝的就是内存地址。 改变赋值对象的任意属性都会改变原对象,但改变浅拷贝对象基本类型的属性不会改变原对象基本属性的值,改变引用类型的属性才会改变原对象对应的值。

浅拷贝的实现

  1. Object.assign
  2. Array.prototype.slice()
  3. Array.prototype.concat()
  4. 解构赋值 let { ...x } = obj;

深拷贝的实现

  1. JSON.parse(JSON.stringify())
  2. lodash.cloneDeep
  3. 手写递归 遍历对象、数组直到里边都是基本数据类型,而后再去复制,就是深度拷贝

表单能够跨域吗 说说你对跨域的了解

答案:form表单是能够跨域的。 首先,跨域问题产生的缘由是浏览器的同源策略,没有同源策略的网络请求有可能会致使CSRF攻击,就是攻击者盗用了你的身份,以你的名义发送恶意请求,由于浏览器会自动将cookie附加在HTTP请求的头字段Cookie中,因此服务端会觉得攻击者的操做就是你本人的操做,因此浏览器就默认禁止了请求跨域。

经常使用的解决方式:

  1. JSONP 处理get请求
  2. 跨域资源共享 CORS(须要后端配置,通用作法)
  3. nginx反向代理解决跨域。nginx 是一款轻量级的 HTTP 服务器,能够用于服务端的反向代理。反向代理是指以代理服务器来接受请求,而后将请求转发给内部网络上的服务器,并将从服务器上获得的结果返回给请求链接的客户端,此时代理服务器对外就表现为一个服务器。咱们能够利用这个特性来处理跨域的问题,将请求转发到真正的后端域名就能够啦。

回到form表单,它也是能够跨域的,由于form提交是不会携带cookie的,你也没办法设置一个hidden的表单项,而后经过js拿到其余domain的cookie,由于cookie是基于域的,没法访问其余域的cookie,因此浏览器认为form提交到某个域,是没法利用浏览器和这个域之间创建的cookie和cookie中的session的,故而,浏览器没有限制表单提交的跨域问题。

浏览器同源策略的本质是,一个域名的 JS ,在未经容许的状况下,不得读取另外一个域名的内容。但浏览器并不阻止你向另外一个域名发送请求。

微任务,宏任务与事件循环(Event Loop)

全部任务能够分红两种,一种是同步任务(synchronous),另外一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。 运行机制:

(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。

(2)主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

复制代码

这种运行机制又称为Event Loop(事件循环)。

宏任务和微任务都是异步任务,主要区别在于他们的执行顺序。 宏任务:包括总体代码script,setTimeout,setInterval、setImmediate。 微任务:原生Promise(有些实现的promise将then方法放到了宏任务中)、process.nextTick、 MutationObserver

js异步有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入event queue,而后再执行微任务,将微任务放入event queue最骚的是,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回调函数,而后再从宏任务的queue上拿宏任务的回掉函数。

例子1:

setTimeout(()=>{
  console.log('setTimeout1')
},0)
let p = new Promise((resolve,reject)=>{
  console.log('Promise1')
  resolve()
})
p.then(()=>{
  console.log('Promise2')    
})
复制代码

输出结果:Promise1,Promise2,setTimeout1 Promise自己是同步的当即执行函数,Promise1做为同步代码直接输出,回调函数进入微任务队列 由于Promise是microtasks,会在同步任务执行完后会先把微任务队列中的值取完以后,再去宏任务队列取值。

例子2:

Promise.resolve().then(()=>{
  console.log('Promise1')  
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
})

setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')    
  })
},0)
复制代码

输出结果是Promise1,setTimeout1,Promise2,setTimeout2

  1. 查看微任务队列,取出微任务队列中的值,输出 Promise 1。
  2. 生成一个异步任务宏任务setTimeout2。
  3. 微任务队列清空,查看宏任务队列,setTimeout1在setTimeout2以前,取出setTimeout1
  4. 生成一个promise2的微任务
  5. 清空微任务队列中的值,输出 Promise2。
  6. 查看宏任务队列,输出setttimeout2

setTimeOut,Promise, async/await的区别

考察这三者在事件循环中的区别,事件循环中分为宏任务队列和微任务队列。

  1. settimeout的回调函数放到宏任务队列里,等到执行栈清空之后执行;
  2. promise.then里的回调函数会放到微任务队列里,等宏任务里面的同步代码执行完再执行;
  3. async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操做完成,再执行函数体内后面的语句。能够理解为,是让出了线程,跳出了 async 函数体。

Promise

对象用于表示一个异步操做的最终完成 (或失败), 及其结果值.

promise 的方法

1. Promise.resolve(value)

返回一个状态由给定value决定的Promise对象。

  1. 若是传入的 value 自己就是 Promise 对象,则该对象做为 Promise.resolve 方法的返回值返回。
  2. 若是该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定
  3. 其余状况,返回一个状态已变成 resolved 的 Promise 对象。

2. Promise.reject

类方法,且与 resolve 惟一的不一样是,返回的 promise 对象的状态为 rejected。

3. Promise.all

类方法,多个 Promise 任务同时执行。 若是所有成功执行,则以数组的方式返回全部 Promise 任务的执行结果。 若是有一个 Promise 任务 rejected,则只返回 rejected 任务的结果。

4. Promise.race

类方法,多个 Promise 任务同时执行,返回最早执行结束的 Promise 任务的结果,无论这个 Promise 结果是成功仍是失败。

5. 其余

实例方法:

  • Promise.prototype.then 为 Promise 注册回调函数
  • Promise.prototype.catch 实例方法,捕获异常

Promise 状态

Promise 对象有三个状态,而且状态一旦改变,便不能再被更改成其余状态。

pending,异步任务正在进行。 resolved (也能够叫fulfilled),异步任务执行成功。 rejected,异步任务执行失败

使用总结

首先初始化一个 Promise 对象,能够经过两种方式建立,这两种方式都会返回一个 Promise 对象。

一、new Promise(fn)
二、Promise.resolve(fn)
复制代码

而后调用上一步返回的 promise 对象的 then 方法,注册回调函数。最后注册 catch 异常处理函数,

Promise All 实现

  1. 接收一个 Promise 实例的数组或具备 Iterator 接口的对象,
  2. 若是元素不是 Promise 对象,则使用 Promise.resolve 转成 Promise 对象
  3. 若是所有成功,状态变为 resolved,返回值将组成一个数组传给回调
  4. 只要有一个失败,状态就变为 rejected,返回值将直接传递给回调
  5. Promise.all() 的返回值也是新的 Promise 对象

Async/Await内部实现

async 作了什么

async 函数会返回一个 Promise 对象,若是在函数中 return 一个直接量,async 会把这个直接量经过 Promise.resolve() 封装成 Promise 对象。

await 在等啥

await 能够用于等待一个 async 函数的返回值,注意到 await 不只仅用于等 Promise 对象,它能够等任意表达式的结果,因此,await 后面实际是能够接普通函数调用或者直接量的。

await 等到了要等的,而后呢

若是它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

若是它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,而后获得 resolve 的值,做为 await 表达式的运算结果。

async/await 优点在哪?

async/await 的优点在于处理 then 链,尤为是每个步骤都须要以前每一个步骤的结果时,async/await 的代码对比promise很是清晰明了,简直像同步代码同样。

实现原理

async/await 就是 Generator 的语法糖,使得异步操做变得更加方便。async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成await。 不同的是:

  1. async函数内置执行器,函数调用以后,会自动执行,输出最后结果。而Generator须要调用next。
  2. 返回值是Promise,async函数的返回值是 Promise 对象,Generator的返回值是 Iterator(迭代器),Promise 对象使用起来更加方便。

简单实习代码:

function asyncToGenerator(generatorFunc) {
  return function() {
    // 先调用generator函数 生成迭代器
    const gen = generatorFunc.apply(this, arguments)
    return new Promise((resolve, reject) => {
      function step(key, arg) {
        let generatorResult
        try {
          generatorResult = gen[key](arg)
        } catch (error) {
          return reject(error)
        }
        const { value, done } = generatorResult
        if (done) {
          return resolve(value)
        } else {
          return Promise.resolve(
            value
          ).then(
            function onResolve(val) {
              step("next", val)
            },
            function onReject(err) {
              step("throw", err)
            },
          )
        }
      }
      step("next")
    })
  }
}
复制代码

如何避免给每一个 async 写 try/catch

写一个辅助函数:

async function errorCaptured(asyncFunc) {
    try {
        let res = await asyncFunc()
        return [null, res]
    } catch (e) {
        return [e, null]
    }
}
复制代码

使用方式:

async function func() {
    let [err, res] = await errorCaptured(asyncFunc)
    if (err) {
        //... 错误捕获
    }
    //...
}
复制代码

移动端适配

  1. rem适配:本质是布局等比例的缩放,经过动态设置html的font-size来改变rem的大小。
    • 动态改写<meta>标签中的缩放比例
    • <html>元素添加data-dpr属性,而且动态改写data-dpr的值
    • <html>元素添加font-size属性,而且动态改写font-size的值
    • 用插件将将px转成rem。
  2. vw 方案: 若是设计稿使用750px宽度,则100vw = 750px,即1vw = 7.5px。那么咱们能够根据设计图上的px值直接转换成对应的vw值。若是不想本身计算,咱们可使用PostCSS的插件postcss-px-to-viewport,让咱们能够直接在代码中写px。
  3. 搭配rem和vw 给根元素大小设置随着视口变化而变化的vw单位,这样就能够实现动态改变其大小。限制根元素字体大小的最大最小值,配合body加上最大宽度和最小宽度

闭包

简单来讲,闭包就是指有权访问另外一个函数做用域中的变量的函数。建立闭包最多见方式,就是在一个函数内部建立或返回另外一个函数。 好比:

var a = function () {
  var test = {};
  setTimeout(function () {
    console.log(test);
  }, 1000);
}
复制代码

上面的例子中,test在a中定义,但在setTimeout的参数(函数)中对它保持了引用。当a被执行了,尽管a已经执行完(已经执行完),理论上来讲a这个函数执行过程当中产生的变量、对象均可以被销毁。但test因为被引用,因此不能随这个函数执行结束而被销毁,直到定时器里的函数被执行掉。

防抖 与 节流

防抖:将屡次高频操做优化为只在最后一次执行,一般使用的场景是:用户输入,只需再输入完成后作一次输入校验便可。

实现逻辑:在事件被触发n秒后再执行回调,若是在这n秒内又被触发,则从新计时。 实现代码:

// 定时器
const debounce = (fn, ms = 0) => {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), ms);
  };
};
复制代码

节流:每隔一段时间后执行一次,也就是下降频率,将高频操做优化成低频操做,一般使用场景: 滚动条事件 或者 resize 事件,一般每隔 100~500 ms执行一次便可。

实现逻辑:规定在一个单位时间内,只能触发一次函数。若是这个单位时间内触发屡次函数,只有一次生效 实现代码:

//定时器
const throttle = function (func, delay) {
  let timeoutId = null;
  return function (...args) {
    if (!timeoutId) {
      timeoutId = setTimeout(function () {
        func.apply(this, args);
        timeoutId = null
      }, delay);
    }
  }
}

// 时间戳
const throttle = function(func, delay) {
    let prev = Date.now();
    return function(...args) {
        let now = Date.now();
        if (now - prev >= delay) {
            func.apply(this, args);
            prev = Date.now();
        }
    }    
}
复制代码

原型与原型链

JavaScript并不是经过类而是直接经过构造函数来建立实例。

function Dog(name, color) {
    this.name = name
    this.color = color
    this.bark = () => {
        console.log('wangwang~')
    }
}

const dog1 = new Dog('dog1', 'black')
const dog2 = new Dog('dog2', 'white')
复制代码

上述代码就是声明一个构造函数并经过构造函数建立实例的过程。在上面的代码中,有两个实例被建立,它们有本身的名字、颜色,但它们的bark方法是同样的,而经过构造函数建立实例的时候,每建立一个实例,都须要从新建立这个方法,再把它添加到新的实例中,形成了很大的浪费。

所以须要用到原型(prototype),每个构造函数都拥有一个prototype属性,能够经过原型来定义一个共享的方法,让这两个实例对象的方法指向相同的位置。

Person.prototype.bark = function() {
  console.log("wangwang~~");
}
复制代码

JavaScript中全部的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有本身的原型对象,这样层层上溯,就造成了一个相似链表的结构,这就是原型链。 原型链与原型的关系能够用下面这个图来概览。

this、bind、call、apply

首先this的指向:this 永远指向最后调用它的那个对象 怎么改变this的指向:

1. 使用 ES6 的箭头函数

箭头函数的 this 始终指向函数定义时的 this,而非执行时。

2. 在函数内部使用 _this = this

3. 使用 apply、call、bind

apply、call、bind 都是能够改变 this 的指向的,可是这三个函数稍有不一样

  1. fun.call(thisArg[, arg1[, arg2[, ...]]])
  2. fun.apply(thisArg, [argsArray])

call 与 apply是类似的,只是调用方法不一样,apply只能接收两个对象:一个新的this对象和一个参数数组。call 则能够接收一个this对象和多个参数

例子:

function add(a, b){
  return a + b;  
}
function sub(a, b){
  return a - b;  
}

// apply() 的用法
var a1 = add.apply(sub, [4, 2]); // sub 调用 add 的方法
var a2 = sub.apply(add, [4, 2]);

a1; // 6 将sub的指针指向了add,因此最后执行的是add
a2; // 2

// call() 的用法
var a1 = add.call(sub, 4, 2);
复制代码

他们与bind 的区别是,这两个方法会当即调用,bind()不会当即调用,须要手动去调用: 例子:

window.onload = function() {
  var fn = {
    num: 2,
    fun: function() {
      document.getElementById("box").onclick = (function() {
        console.log(this.num);
      }).bind(this);
      // }).call(this);
      // }).apply(this);
    }
        /*
         * 这里的 this 是 fun,因此能够正确地访问 num,
         * 若是使用 bind(),会在点击以后打印 2;
         * 若是使用 call() 或者 apply(),那么在刷新网页的时候就会打印 2
        */
    }
    fn.fun();
}

复制代码

js设计模式

1.工厂模式

建立一个函数,返回相同的属性和方法,能够无数次调用。经常使用于产生大量类似的商品,去作一样的事情,实现一样的效果。

2.单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,若是存在则直接返回,若是不存在就建立了再返回,这就确保了一个类只有一个实例对象。

适用场景:一个单一对象。好比:弹窗,不管点击多少次,弹窗只应该被建立一次。

3. 策略模式

定义:定义一系列的算法,把他们一个个封装起来,而且使他们能够相互替换。

策略模式的目的就是将算法的使用算法的实现分离开来。

例子:

/*策略类*/
var levelOBJ = {
    "A": function(money) {
        return money * 4;
    },
    "B" : function(money) {
        return money * 3;
    },
    "C" : function(money) {
        return money * 2;
    } 
};
/*环境类*/
var calculateBouns =function(level,money) {
    return levelOBJ[level](money);
};
console.log(calculateBouns('A',10000)); // 40000
复制代码

4.代理模式

定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。

经常使用的虚拟代理形式:某一个花销很大的操做,能够经过虚拟代理的方式延迟到这种须要它的时候才去建立(例:使用虚拟代理实现图片懒加载)

中介者模式

定义:经过一个中介者对象,其余全部的相关对象都经过该中介者对象来通讯,而不是相互引用,当其中的一个对象发生改变时,只须要通知中介者对象便可。经过中介者模式能够解除对象与对象之间的紧耦合关系。

发布-订阅模式

发布-订阅模式实际上是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,全部依赖于它的对象都将获得状态改变的通知。

订阅者(Subscriber)把本身想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。 例子:Vue的EventBus

观察者模式

观察者模式也是定义对象间的一种一对多依赖关系,使得当每个被依赖对象状态发生改变时,其相关依赖对象皆获得通知并被自动更新。它的目标与发布-订阅者模式是一致的。

被观察对象经过 subscribe 方法和 unsubscribe 方法添加和删除一个观察者,经过 broadcast 方法向观察者推送消息。

document.body.addEventListener('click', ()=>{}};
复制代码

上面的例子就是一个最简单的观察者模式。document.body 在这里就是一个被观察对象, 全局对象是观察者,当 click 事件触发的时候,观察者会调用 clickHandler 方法。

与发布订阅者模式的区别:观察者模式中主体和观察者仍是存在必定的耦合性,而发布订阅者模式中,在主体与观察者之间引入消息调度中心,全部的消息传递过程都经过消息调度中心完成,也就是说具体的业务逻辑代码将会是在消息调度中心内,而主体和观察者之间实现了彻底的松耦合,与此同时带来的问题就是,程序的可读性下降了。

数组判断方式

1. Array.isArray()

2. instanceof

instanceof 用于检测构造函数的 prototype 属性是否出如今某个实例对象的原型链上。语法

object instanceof constructor
复制代码

因此若是这个Object的原型链上可以找到Array构造函数的话,那么这个Object应该及就是一个数组,反之则不是

const a = [];
console.log(a instanceof Array);//true
复制代码

3. Object.prototype.toString

不能够直接调用数组自身的toString()方法,由于返回的会是内容的字符串,只有对象的toString方法会返回对象的类型。因此咱们须要“借用”对象的toString方法,须要使用call或者apply方法来改变toString方法的执行上下文。

const a = ['Hello','Howard'];
Object.prototype.toString.call(a);//"[object Array]"
复制代码

4.constructor

实例化的数组拥有一个constructor属性,这个属性指向生成这个数组的方法。

const a = [];
console.log(a.constructor == Array);//true
复制代码

可是constructor属性是能够改写的,一旦被改写,那这种判断方式就无效了。

数组去重

  1. [...new Set(array)]

    set 是 es6 提供的新的数据结构,它相似数组,可是成员的值都是惟一的,

  2. for 循环嵌套,splice去重

  3. 新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,若是有相同的值则跳过,不相同则push进数组。判断是否存在的方法有indexOf,array.includes

  4. 利用filter

function unique(arr) {
  return arr.filter(function(item, index, arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,不然返回当前元素
    return arr.indexOf(item, 0) === index;
  });
}
复制代码

js 数据类型

7 种原始类型:

  • Boolean
  • Null
  • Undefined
  • Number
  • BigInt
  • String
  • Symbol 引用类型
  • Object
  • Array

MVC,MVP,MVVM

MVC(model,view,controller)

MVC除了把应用程序分红View、Model层,还额外的加了一个Controller层,它的职责为进行Model和View之间的协做(路由、输入预处理等)的应用逻辑(application logic);Model进行处理业务逻辑。

用户的对View操做之后,View捕获到这个操做,会把处理的权利交移给Controller(Pass calls);Controller会对来自View数据进行预处理、决定调用哪一个Model的接口;而后由Model执行相关的业务逻辑;当Model变动了之后,会经过观察者模式(Observer Pattern)通知View;View经过观察者模式收到Model变动的消息之后,会向Model请求最新的数据,而后从新更新界面。

优势:

  1. 把业务逻辑和展现逻辑分离,模块化程度高。且当应用逻辑须要变动的时候,不须要变动业务逻辑和展现逻辑,只须要Controller换成另一个Controller就好了(Swappable Controller)。

  2. 观察者模式能够作到多视图同时更新。 缺点:

  3. Controller测试困难

  4. View没法组件化。View是强依赖特定的Model的,若是须要把这个View抽出来做为一个另一个应用程序可复用的组件就困难了。由于不一样程序的的Domain Model是不同的。

MVP

MVP模式把MVC模式中的Controller换成了Presenter。

和MVC模式同样,用户对View的操做都会从View交移给Presenter。Presenter会执行相应的应用程序逻辑,而且对Model进行相应的操做;而这时候Model执行完业务逻辑之后,也是经过观察者模式把本身变动的消息传递出去,可是是传给Presenter而不是View。Presenter获取到Model变动的消息之后,经过View提供的接口更新界面。

优势

  1. 测试简单
  2. 能够组件化

缺点

  1. presenter中除了应用逻辑之外,还有大量的View->Model,Model->View的手动同步逻辑,形成Presenter比较笨重,维护起来会比较困难。

MVVM(Model-View-ViewMode)

ViewModel的含义就是 "Model of View",视图的模型。

MVVM的调用关系和MVP同样。可是,在ViewModel当中会有一个叫Binder,之前所有由Presenter负责的View和Model之间数据同步操做交由给Binder处理。你只须要在View的模版语法当中,指令式地声明View上的显示的内容是和Model的哪一块数据绑定的。当ViewModel对进行Model更新的时候,Binder会自动把数据更新到View上去,当用户对View进行操做(例如表单输入),Binder也会自动把数据更新到Model上去。这种方式称为:双向数据绑定。

优势

  1. 便于维护
  2. 可组件化
  3. 简化测试

缺点:

  1. 对于于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高

Vue的生命周期有哪些?

  • 建立:beforeCreate,created;
  • 载入:beforeMount,mounted;
  • 更新:beforeUpdate,updated;
  • 销毁:beforeDestroy,destroyed;

Vue 组件通讯

1. props/$emit

2. $children/$parent

子实例能够经过this.$parent访问父实例,子实例则被推入父实例的$children数组中,拿到实例表明能够访问此组件的全部方法和data。this.$parent是一个对象,$children是一个数组。

3. provide/ inject

provide/ inject 是vue2.2.0新增的api, 简单来讲就是父组件中经过provide来提供变量, 而后再子组件中经过inject来注入变量。

注意:这里不论子组件嵌套有多深, 只要调用了inject 那么就能够注入provide中的数据,而不局限于只能从当前父组件的props属性中回去数据。

例子: A 是 B 的父组件,B是C的父组件

// A.vue

<template>
 <div>
   <comB></comB>
 </div>
</template>

<script>
 import comB from '../components/test/comB.vue'
 export default {
   name: "A",
   provide: {
     for: "demo"
   },
   components:{
     comB
   }
 }
</script>

// B.vue

<template>
 <div>
   {{demo}}
   <comC></comC>
 </div>
</template>

<script>
 import comC from '../components/test/comC.vue'
 export default {
   name: "B",
   inject: ['for'],
   data() {
     return {
       demo: this.for
     }
   },
   components: {
     comC
   }
 }
</script>
// C.vue
<template>
 <div>
   {{demo}}
 </div>
</template>

<script>
 export default {
   name: "C",
   inject: ['for'],
   data() {
     return {
       demo: this.for
     }
   }
 }
</script>
复制代码

4. ref/this.$refs

若是在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;若是用在子组件上,引用就指向组件实例,能够经过实例直接调用组件的方法或访问数据。

5. Vuex

6. 本地存储localStorage / sessionStorage

7. $attrs$listeners

若是三个嵌套组件A->B,B->C,若是须要在A中对C组件赋值,并监听C组件的emit事件,可使用vue2.4.0引入的$attrs$listeners,和新增的inheritAttrs选项。

// A组件
...
<div>
 <h2>组件A 数据项:{{myData}}</h2>
 <B @changeMyData="changeMyData" :myData="myData"></B>
</div>
...

// B组件
...
<div>
  <h3>组件B</h3>
  <C v-bind="$attrs" v-on="$listeners"></C>
</div>
...
<script>
 import C from './C'
 export default {
 components: { C },
 inheritAttrs: false,
 props: ['pChild1'],
 mounted () {
   this.$emit('onTest1')
 }
}
</script>
...

// C组件
<template>
 <div>
   <h5>组件C</h5>
   <input v-model="myc" @input="hInput" />
 </div>
</template>
<script>
export default {
 props: { myData: { String } },
 created() {
   this.myc = this.myData;  // 在组件A中传递过来的属性
   console.info(this.$attrs, this.$listeners);
 },
 methods: {
   hInput() {
     this.$emit("changeMyData", this.myc); // // 在组件A中传递过来的事件
   }
 }
};
</script>
复制代码

注意:inheritAttrs属性,他用来判断组件的根元素是否继承,那些它没有从父组件继承的属性,在上面的例子中咱们将它设为false,区别见下图

// 默认为true

// 改成false

8.EventBus

建立EventBus方法: 引入Vue 并导出它的一个实例,也能够直接在main.js中初始化,这样咱们获取到的 EventBus 是一个 全局的事件总线

Vue.prototype.$bus = new Vue()
复制代码

而后就能够经过on/off/emit,发布订阅事件了。

React 组件通讯

父->子组件 prop传递

子->父组件

  1. 父组件定义方法setValue直接经过传递给子组件,子组件在内部直接调用父组件方法更改状态

兄弟组件

  1. context
  2. 状态管理库
  3. 发布订阅,相似Vue的eventBus

Vue 响应式原理

原理

当你把一个普通的 JavaScript 对象传入 Vue 实例做为 data 选项,Vue 将遍历此对象全部的 property,并使用 Object.defineProperty 把这些 property 所有转为 getter/setter。它们让 Vue 可以追踪依赖,在 property 被访问和修改时通知变动。

每一个组件实例都对应一个 watcher 实例,它会在组件渲染的过程当中把“接触”过的数据 property 记录为依赖。以后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件从新渲染。

可是Vue不能检测数组和对象的变化。

  • 对于对象:

Vue 没法检测 property 的添加或移除。因为 Vue 会在初始化实例时对 property 执行 getter/setter 转化,因此 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。可是,可使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。

  • 对于数组:

    • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
    • 当你修改数组的长度时,例如:vm.items.length = newLength

    这两种状况不能监测

因为 Vue 不容许动态添加根级响应式 property,因此你必须在初始化实例前声明全部根级响应式 property,哪怕只是一个空值。

异步更新队列

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的全部数据变动。若是同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做是很是重要的。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工做。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,若是执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

当你设置 vm.someData = 'new value',该组件不会当即从新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。为了在数据变化以后等待 Vue 完成更新 DOM,能够在数据变化以后当即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
复制代码

由于 $nextTick() 返回一个 Promise 对象,因此你可使用新的 ES2017 async/await 语法完成相同的事情:

methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}
复制代码

React 核心原理

理解虚拟DOM

virtual dom 其实是对实际Dom的一个抽象,是一个js对象。react全部的表层操做其实是在操做virtual dom。

它的内部逻辑是:用JavaScript 对象表示 DOM 信息和结构,根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树。当状态变动的时候,用新渲染的对象树去和旧的树进行对比,记录这两棵树差别(diff算法)。记录下来的不一样就是咱们须要对页面真正的 DOM 操做,而后把它们应用在真正的 DOM 树上,页面就变动了。这样就能够作到:视图的结构确实是整个全新渲染了,可是最后操做DOM的时候确实只变动有不一样的地方。

DOM是很慢的,一个简单的div元素就能够打印出来不少东西,并且操做起来一不当心就会致使页面重排。相对于 DOM 对象,原生的 JavaScript 对象处理起来更快,并且更简单。DOM 树上的结构、属性信息咱们均可以很容易地用 JavaScript 对象表示出来。既然原来 DOM 树的信息均可以用 JavaScript 对象来表示,反过来,你就能够根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树。

Virtual DOM 本质上就是在 JS 和 DOM 之间作了一个缓存。能够类比 CPU 和硬盘,既然硬盘这么慢,咱们就在它们之间加个缓存:既然 DOM 这么慢,咱们就在它们 JS 和 DOM 之间加个缓存。JS只操做Virtual DOM,最后的时候再把变动写入DOM。

JSX

JSX(Javscriptのxml)是JavaScript的一个语法扩展,是 React.createElement(component, props, ...children) 函数的语法糖。React.createElement函数最会生成一个对象,咱们称之为React对象或者另外一个名字--虚拟DOM。

  • 你能够把它做为一个变量的值,在if和for循环中使用它
  • 也能把它当作参数传递给函数
  • 还能做为函数的返回值返回。
  • jsx中的变量用{}包裹,能够防止xss攻击

服务端渲染

React 支持服务端渲染。

为何要服务端渲染(SSR)呢?这与单页应用(SPA )的兴起息息相关。与传统的 SSR 应用相比, SPA 在速度和用户体验方面具备很大的优点。 可是这里有一个问题。SPA 的初始服务端请求一般返回一个没有 DOM 结构的 HTML 文件,其中只包含一堆 CSS 和 JS links。而后,应用须要另外 fetch 一些数据来呈现相关的 HTML 标签。 这意味着用户将不得不等待更长时间的初始渲染。这也意味着爬虫可能会将你的页面解析为空。 所以,关于这个问题的解决思路是:首先在服务端上渲染你的 app(渲染首屏),接着再在客户端上使用 SPA。

SSR优势

  • 加快了首屏渲染时间
  • 完整的可索引的 HTML 页面(有利于 SEO)

咱们可使用next.js构建支持服务端渲染的React应用程序。

数据流

React中,数据流是自上而下的单向数据流。

React 最新的生命周期

  1. 挂载
    • constructor()
    • static getDerivedStateFromProps()
    • render()
    • componentDidMount()
  2. 更新
    • static getDerivedStateFromProps()
    • shouldComponentUpdate()
    • render()
    • getSnapshotBeforeUpdate()
    • componentDidUpdate()
  3. 卸载
    • componentWillUnmount()
  4. 抛出错误时
    • static getDerivedStateFromError()
    • componentDidCatch()

即将被废除的三个生命周期

  • UNSAFE_componentWillMount()
  • UNSAFE_componentWillUpdate()
  • UNSAFE_componentWillReceiveProps()

static getDerivedStateFromProps

getDerivedStateFromProps 会在调用 render 方法以前调用,而且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,若是返回 null 则不更新任何内容。 当 props 变化时,建议使用getDerivedStateFromProps 生命周期更新 state,UNSAFE_componentWillReceiveProps即将被弃用

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)以前调用。它使得组件能在发生更改以前从 DOM 中捕获一些信息(例如,滚动位置)。今生命周期的任何返回值将做为参数传递给 componentDidUpdate()。

Vue 中 computed 与watch 有什么区别

计算属性(computed)

适用于须要获取依赖某项或多项变量复杂计算获取到的结果的场景。 好比:

computed:{
    reversedMessage: function () {
      return this.message.split('').reverse().join('')
    }
}
复制代码

你也能够经过在表达式中调用方法来达到相同的效果,不一样的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会从新求值。这就意味着只要 message 尚未发生改变,屡次访问 reversedMessage 计算属性会当即返回以前的计算结果,而没必要再次执行函数。

侦听属性(watch)

Watch 观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数获得的参数为新值和旧值。表达式只接受监督的键路径('a.b.c')。对于更复杂的表达式,用一个函数取代。

此外,它还有两个属性 deep和immediate

Watch中能够执行任何逻辑,如函数节流、Ajax异步获取数据,甚至操做 DOM(不建议)

他们的区别

  • watch:监测的是属性值, 只要属性值发生变化,其都会触发执行回调函数来执行一系列操做;
  • computed:监测的是依赖值,依赖值不变的状况下其会直接读取缓存进行复用,变化的状况下才会从新计算。
  • 有点很重要的区别是:计算属性不能执行异步任务,计算属性必须同步执行。也就是说计算属性不能向服务器请求或者执行异步任务。若是遇到异步任务,就交给侦听属性。Watch也能够检测computed属性。

总结

计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来的;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

React 浅比较

因为PureComponent的shouldeComponentUpdate里,实际是对props/state进行了一个浅对比,因此对于嵌套的对象不适用,没办法比较出来。这是由于React 的浅比较中,当对比的类型为Object的时候而且key的长度相等的时候,浅比较也仅仅是用Object.is()对Object的value作了一个基本数据类型的比较,因此若是key里面是对象的话,有可能出现比较不符合预期的状况,因此浅比较是不适用于嵌套类型的比较

React 与 Vue 的区别

1. 监听数据变化的原理不一样

  • Vue 经过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不须要特别的优化就能达到很好的性能
  • React 默认是经过比较引用的方式进行的,若是不优化(PureComponent/shouldComponentUpdate)可能致使大量没必要要的VDOM的从新渲染

2. 数据流的不一样

  • Vue 支持双向绑定
  • React 是单向数据流,称之为 onChange/setState()模式。

3. 模板渲染方式不一样

  • React 是经过JSX渲染模板,深层来说是react 的模板渲染是经过原生JS实现模板中的常见语法(好比插值,条件,循环等)来实现的

  • Vue是经过一种拓展的HTML语法进行渲染。它是在和组件JS代码分离的单独的模板中,经过指令来实现的,好比条件语句就须要 v-if 来实现

  • react中render函数是支持闭包特性的,因此咱们import的组件在render中能够直接调用。可是在Vue中,因为模板中使用的数据都必须挂在 this 上进行一次中转,因此咱们import 一个组件完了以后,还须要在 components 中再声明下。

React怎么作数据的检查和变化

Vue经过Object.defineProporty来劫持对象的get,set方法,实现双向绑定。 相比较react而言,react是单向数据流动的ui渲染框架,自己不存在数据的检测这一机制,全部的数据改变都是经过setState来手动实现的。

vue和react都在其内部实现了‘虚拟dom’的概念,即将须要渲染的真实dom虚拟成一个js对象,渲染的时候,经过对这个新旧js对象进行比较,来计算出真实dom须要渲染的最小操做,以达到优化性能的目的。react和vue都有其diff算法,原理也很相似。

React 高阶组件(HOC)

高阶组件的概念应该是来源于JavaScript的高阶函数:

高阶函数就是接受函数做为输入或者输出的函数,高阶组件(HOC)是一个接受组件组做输入并返回组件的函数。HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 经过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有反作用。实际上是装饰器的语法糖。

在高阶函数以前,React 也曾使用过mixin,但在工程中大量使用mixin会致使一些问题,

  1. 破坏组件封装性: Mixin可能会引入不可见的属性。例如在渲染组件中使用Mixin方法,给组件带来了不可见的属性(props)和状态(state)。而且Mixin可能会相互依赖,相互耦合,不利于代码维护。
  2. 不一样的Mixin中的方法可能会相互冲突

HOC 能够实现什么功能

他能够:

  1. 组合渲染,条件渲染
  2. 操做props
  3. 获取refs
  4. 操做state

使用 HOC 的优势

  1. 抽取重复代码,实现组件复用,常见场景:页面复用。
  2. 条件渲染,控制组件的渲染逻辑(渲染劫持),常见场景:权限控制。
  3. 捕获/劫持被处理组件的生命周期,常见场景:组件渲染性能追踪、日志打点。

使用HOC的缺点

  1. HOC须要在原组件上进行包裹或者嵌套,若是大量使用HOC,将会产生很是多的嵌套,这让调试变得很是困难。
  2. HOC能够劫持props,在不遵照约定的状况下也可能形成冲突。

React Hooks

Hooks 是16.7添加的新特性。它的优势是

  1. 减小状态逻辑复用的风险
  2. 避免地狱式嵌套
  3. 让组件更容易理解

写一个自定义的Hook

export const useInterval = (callback, delay) => {
  const savedCallback = useRef();

  // 记住最新的回调函数.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // 设置定时器
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
    // 这里至关于一个做用在组件生命周期里的 setInterval 和 clearInterval 的组合。
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

复制代码

React Fiber

react在进行组件渲染时,从setState开始到渲染完成整个过程是同步的(“一鼓作气”)。若是须要渲染的组件比较庞大,js执行会占据主线程时间较长,会致使页面响应度变差,使得react在动画、手势等应用中效果比较差。

卡顿缘由:Stack(原来的算法)的工做流程很像函数的调用过程。父组件里调子组件,能够类比为函数的递归。在setState后,react会当即开始从父节点开始遍历,以找出不一样。将全部的Virtual DOM遍历完成后,才能给出当前须要修改真实DOM的信息,并传递给renderer,进行渲染,而后屏幕上才会显示这次更新内容。对于特别庞大的vDOM树来讲,这个过程会很长(x00ms),在这期间,主线程是被js占用的,所以任何交互、布局、渲染都会中止,给用户的感受就是页面被卡住了。

为了解决这个问题,react团队通过两年的工做,重写了react中核心算法。并在v16版本中发布了这个新的特性,简称为Fiber。

Fiber实现了本身的组件调用栈,它以链表的形式遍历组件树,能够灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback这一 API。官方的解释是这样的:

window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可让开发者在主事件循环中执行后台或低优先级的任务,并且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数通常会按先进先调用的顺序执行,除非函数在浏览器调用它以前就到了它的超时时间。

由于浏览器是单线程,它将GUI描绘,时间器处理,事件处理,JS执行,远程资源加载通通放在一块儿。当作某件事,只有将它作完才能作下一件事。若是有足够的时间,浏览器是会对咱们的代码进行编译优化。只有让浏览器休息好,他才能跑的更快。

React 优化

代码分割

  1. import()
  2. lazy + Suspense

前端优化

SPA 首页白屏优化

引起缘由

SPA的应用启动的方式都是极其相似的,都是在html 中提供一个 root 节点,而后把应用挂载到这个节点上。

这样的模式,使用 webpack 打包以后,通常就是三个文件:

  1. 一个体积很小、除了提供个 root 节点之外的没什么卵用的html(大概 1-4 KB)
  2. 一个体积很大的 js(50 - 1000 KB 不等)
  3. 一个 css 文件(固然若是你把 css 打进 js 里了,也可能没有)

这样形成的直接后果就是,用户在 js 文件加载、执行完毕以前,页面是彻底空白的。 也就是说,这个时候

首屏体积(首次渲染须要加载的资源体积) = html + js + css

优化方式

  1. SSR(服务端渲染)
  2. prerender-spa-plugin(第三方插件)

咱们可使用 prerender-spa-plugin,为项目添加骨架屏和 Loading 状态

懒加载

  1. 图片懒加载
  2. 组件懒加载

减小请求次数

  • 小图片合并雪碧图;
  • JS、CSS文件选择性合并;
  • 避免重复的资源请求。

减小文件大小

  • 压缩CSS、JS、图片;
  • 尽量控制DOM节点数;
  • 精简css、 JavaScript,移除注释、空格、重复css和脚本。
  • 开启Gzip
相关文章
相关标签/搜索