通過「百度搜尋」來學習Jsonp,Promise,bind,apply,debounce

LeeRayno發表於2019-03-30

前言

最近在溫習基礎知識,如:jsonp,promise,bind,apply,debounce 等。那通過什麼來測試練習了,就想到了「百度搜尋」功能滿足上面的測試。

Jsonp

jsonp 主要是用來解決跨域問題的。應用非常廣泛,關於更多的解決跨域方案請看前端面試總結之:js跨域問題

jsonp的原理是什麼了? 比如我們定義一個函式foo,然後呼叫它

// 定義
function foo() {
    console.log('foo')
}

// 呼叫
foo()
複製程式碼

那我們將呼叫foo()的這段程式碼放在一個新建的js檔案,比如a.js然後通過script標籤引入a.js

// a.js
foo()
複製程式碼
function foo() {
    console.log('foo')
}

<script src="./a.js"></srcipt>
複製程式碼

jsonp原理與之類似:
我們在本地定義好一個函式,如jsonp_1234565,然後將這個函式名通過特定識別符號如cb=jsonp_1234565通過scriptsrc屬性去請求一個js資源(一個get請求),即動態建立script標籤。如:<script src="https://www.baidu.com?a=1&b=2&cb=jsonp_1234565"></script>後臺通過cb這個特定識別符號得到前端定義的函式名為jsonp_1234565然後將前端真正要的的資料放在jsonp_1234565的引數裡,並將這個函式返回給前端如:jsonp_1234565({status: 0, data: {...}}) 程式碼如下

function jsonp({url = '', data = {}, cb='cb'} = {}) {
    if (!url) return
    // myPromise 請看下面實現,可以用成功回撥的,因為學習特意用了Promise
    return myPromise((resolve, reject) => {
        const cbFn = `jsonp_${Date.now()}` // 定義函式名
        data[cb] = cbFn // 將函式名放在`cb`識別符號裡
        
        const oHead = document.querySelector('head')
        const oScript = document.create('script')
        
        const src = `${url}?${data2Url(data)}`
        oScript.src = src
        oHead.appendChild(oScript) // 將script標籤插入head,以傳送get請求
        
        // 定義函式,後臺返回就呼叫
        window[cbFn] = function(res) {
            res ? resolve(res) : reject('error')
            // 如果不用Promise用回撥的話只需在引數中加個success引數然後呼叫即可
            // success && success(res)
            
            oHead.removeChild(oScript) // 請求回來之後就沒用了。如果不刪除,每次請求之後就會建立一個script標籤,導致頁面很多的script標籤,所以將它刪除。用完了就扔,感覺有點過河拆橋的意思
            window[cbFn] = null
        }
    })
}

function data2Url(data) {
    return Object.keys(data).reduce((acc, cur) => {
        acc.push(`${cur}=${data[cur]}`)
        return acc
    }, []).join('&')
}

// 可以先把myPromise改成原生的Promise測試百度搜尋的介面
jsonp({
    url: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',
    data: {
        wd: 'a'
    },
    cb: 'cb'
}).then(res => {
    console.log(res)
})
複製程式碼

測試建議用 EGOIST開源的codepan.net/ 類似於JSBin/CodePen/JSFiddle

Promise

簡易版的Promise實現,沒有遵循A+規範,檢視原文JavaScript Promises - Understand JavaScript Promises by Building a Simple Promise Example

new Promise((resolve, reject) => {
    // 一系列操作 虛擬碼
    if (true) {
       resolve(res) 
    } else {
        reject(err)
    }
})
.then(fn1)
.then(fn2)
...
.catch(handleError)

複製程式碼

大致意思就是Promise這個類接受一個函式作為引數,這個引數函式又接受兩個函式作為引數
new Promise 這個例項有thencatch這兩個方法,thencatch又都接受函式作為引數,並且可以鏈式呼叫

核心思路就是定義個陣列promiseChianFn用來裝then的回撥函式,then一次,就往promiseChianFn push一條then的回撥函式,當在呼叫resolve函式的時候,就迴圈執行promiseChianFn的函式

class myPromise {
    constructor(excuteFn) {
        this.promiseChianFn = []
        this.handleError = () => {}
        // mybind 請看下面實現
        this._resolve = this._resolve.mybind(this)
        this._reject = this._reject.mybind(this)
        // 立即執行
        excuteFn(this._resolve, this._reject)
    }
    
    then(fn) {
        this.promiseChianFn.push(fn)
        
        return this // 原生Promise返回的是一個新的Promise
    }
    
    catch(handleError) {
        this.handleError = handleError
        
        return this
    }
    
    _resolve(val) {
        try {
            let storeVal = val
            // 迴圈執行,並把第一個函式執行的返回值賦值給storeVal 共下個函式接收 如:
           /**
            * .then(res => {
            *    renturn 1
            *  })
            * .then(res => {
            *    console.log(res) // 1
            * })
            *
            */
            this.promiseChianFn.forEach(fn => {
                storeVal = fn(storeVal)
            })
        } catch(err) {
            this.promiseChianFn = []
            this._reject(err)
        }
    }
    
    _reject(err) {
        this.handleError(err)
    }
    
}

// 現在可以用myPromise 測試上面的jsonp了
複製程式碼

apply

callapply都是用來改變函式的上下文裡面的this的,即改變this指向。
注意:上下文包含 VO(variable Object--變數物件),作用域鏈this這三個東西,具體請看js引擎的執行過程(一)

// 將 foo裡面的上下文指向 ctx
foo.call(ctx, 1,2,3)
foo.apply(ctx, [1,2,3])
複製程式碼

callapply的原理就是方法借用
在知乎上面看到一篇文章的比喻 貓吃魚,狗吃肉
那貓要吃肉,就借用狗吃肉的方法 即 狗.吃肉.call(貓)
那狗要吃魚,就借用貓吃魚的方法 即 貓.吃魚.call(狗)

// fn.call(ctx) 既然是方法借用,那就給ctx新增一個該方法就可以了
Function.prototype.myapply = function(ctx, args = []) {
    const hash = Date.now() // 用時間戳是防止 ctx 上面的屬性衝突
    ctx[hash] = this // 給 ctx 新增一個方法,this 就是 fn
    
    const res = ctx[hash](...args)
    delete ctx[hash] // 過河拆橋
    return res
}

// call 的話,只需將 args = [] 改為 ...args 即可

// 測試
const a = {
    name: 'a',
    getName() {
        console.log(this.name)
    }
}

const b = {
    name: 'b'
}

a.getName() // a
a.getName.myapply(b) // b
複製程式碼

bind

bind 返回一個新函式,並永久改變 this 指向,返回的新函式無論之後再怎麼call,apply,bind都不會改變 this 指向了,並有偏函式的效果 如果要考慮 New的情況請參照MDN

// fn2 = fn.bind(ctx)
Function.prototype.mybind = function(ctx, ...args1) {
    const _this = this
    return function(...args2) {
        // 永遠指向 ctx
        return _this.myapply(ctx, args1.concat(args2))
    }
}
// 測試
const fn = a.getName.mybind(b)
fn() // b
const fn2 = fn.bind(a)
fn2() // b
複製程式碼

debounce

「百度搜尋」並沒有加入 debounce ,我們可以給他加個 debounce 看下效果
debounce(防抖) 和 throttle(節流)主要是用來做效能優化
debounce 就像壓彈簧,只要手不鬆開,彈簧就不會彈起來,常見應用場景就是input輸入框,我們在停止輸入後才去做相關操作
throttle 就像擰緊水龍頭,讓水龍頭隔一秒鐘滴一滴水,常見應用場景為頁面滾動優化

function debounce(cb, delay = 300) {
    let timer
    return function(...args) {
        timer && clearTimeout(timer)
        timer = setTimeout(() => {
            cb && cb.apply(this, args)
        }, delay)
    }
}
複製程式碼

接下來我們模擬「百度搜尋」加上debounce 想傳 GIF 傳不了, 請上 codepan.net/ 測試

全部程式碼

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <input type="text">
</body>
</html>
複製程式碼
function jsonp({url = '', data = {}, cb = 'cb'} = {}) {
  
    return new myPromise((resolve, reject) => {
        if (!url) return
        const cbFn = `jsonp_${Date.now()}`
        data[cb] = cbFn
        
        const oHead = document.querySelector('head') 
        const oScript = document.createElement('script')
        
        const src = `${url}?${data2Url(data)}`
        oScript.src = src
        
        oHead.appendChild(oScript)
        
        window[cbFn] = function(res) {
            resolve(res)
            oHead.removeChild(oScript)
            window[cbFn] = null
        }
    })
}

function data2Url(data) {
    return Object.keys(data).reduce((acc, cur) => {
        acc.push(`${cur}=${data[cur]}`)
        return acc
    }, []).join('&')
}

class myPromise {
    constructor(excuteFn) {
        this.promiseChainFn = []
        this.handleError = () => {}
        this._resolve = this._resolve.myBind(this)
        this._reject = this._reject.myBind(this)
        excuteFn(this._resolve, this._reject)
    }
  
    then(cb) {
        this.promiseChainFn.push(cb)
        return this
    }
  
    catch(handleError) {
        this.handleError = handleError
        return this
    }
  
    _resolve(res) {
        try {
            let storeVal = res
            this.promiseChainFn.forEach(fn => {
                storeVal = fn(storeVal)
            })
        } catch(e) {
            this.promiseChainFn = []
            this._reject(e)
        }
    }
  
    _reject(err) {
        return this.handleError(err)
    }
}

Function.prototype.myApply = function(ctx, args = []) {
  const hash = Date.now()
  ctx[hash] = this
  
  const res = ctx[hash](...args)
  delete ctx[hash]
  return res
}

Function.prototype.myBind = function(ctx, ...args1) {
  const _this = this
  return function(...args2) {
    return _this.myApply(ctx, args1.concat(args2))
  }
}

function debounce(cb, delay = 300) {
  let timer
  return function(...args) {
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      cb && cb.myApply(this, args)
    }, delay)
  }
}


const oInput = document.querySelector('input')

oInput.oninput = debounce(handleInput, 1000)

function handleInput(v) {
  jsonp({
    url: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',
    data: { wd: this.value},
    cb: 'cb'
  }).then(res => {
    console.log(res)
    return 1
  }).then(res => {
    console.log(res)
    return 2
  }).then(res => {
    console.log(res)
  }).catch(function a(err) {
    console.log(err)
  })
}
複製程式碼

相關文章