最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 前端问答整理-前端学习必看经典问答

    正文概述 掘金(爱创课堂前端技术分享)   2020-12-17   314

    link 与 import 的区别?

    • link 是一种 html 标签,它没有兼容性问题,而且可以通过 js 的 DOM 操作动态的引入样式表。
    • @import 是css提供的一种引入样式表的语法,不可以动态加载,不兼容ie5以下。
    • 由于link 是html标签,所以它引入的样式是在页面加载时同时加载的,@import需要在页面加载完成之后再加载。

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

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

    <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 的设置,将左右内容区移动到目标位置。

    双飞翼布局:

    <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 为左右两栏留出位置,其余与圣杯布局思路一致。

    现在也可以使用 flex-box 轻松实现。

    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: 禁止模式

    用递归算法实现,数组长度为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 中规定伪类使用一个 : 来表示;伪元素则使用 :: 来表示

    前端问答整理-前端学习必看经典问答

    简述超链接target属性的取值和作用

    前端问答整理-前端学习必看经典问答

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

    react-router 里的 标签和标签有什么区别?

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

    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的方法有三种

    • 点击浏览器前进后退
    • 点击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 对象。

    1、new Promise(fn)
    2、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/children/children/parent

    子实例可以通过this.$parent访问父实例,子实例则被推入父实例的$children数组中,拿到实例代表可以访问此组件的所有方法和data。this.$parent是一个对象,$children是一个数组。

    3. provide/ inject

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

    例子: 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. attrsattrs与 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的高阶函数:

    在高阶函数之前,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。官方的解释是这样的:

    因为浏览器是单线程,它将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 文件加载、执行完毕之前,页面是完全空白的。 也就是说,这个时候

    优化方式

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

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

    懒加载

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

    减少请求次数

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

    减少文件大小

    • 压缩CSS、JS、图片;

    • 尽可能控制DOM节点数;

    • 精简css、 JavaScript,移除注释、空格、重复css和脚本。

    • 开启Gzip

      } #footer { 前端学习培训、视频教程、学习路线,添加威信 kaixin666haoyun }


    起源地下载网 » 前端问答整理-前端学习必看经典问答

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元