03.我们不背诵API,只实现API。

570 阅读4分钟

为什么要实现API?

  • 一直是面试要考察的点
  • 加深对API的记忆和理解
  • 更能体现编程思维和能力

jQery offset 方法实现

使然 jQery 已经是"上古"神器了,但很多API都值得学习,也还是很经典。

问:如何获取文档任意元素与文档顶部的距离? 思路:

  • 通过递归实现
  • 通过getBoundingClienetReact

1.通过递归实现方案

我们可以通过便利目标元素、目标元素父节点,父节点的父节点。。。依次溯源,并累加这些遍历过的节点相对于其最近祖先节点(其position属性是非static的)的偏移量,向上溯源直到document,即可得到累加结果

实现如下:

const offset = (ele) => {
  let result = {
    top: 0,
    left: 0,
  }
  const getOffset = (node, init) => {
    if (node.nodeType !== 1) {
      return
    }
    position = window.getComputedStyle(node)['position']
    if (typeof init === 'undefined' && position === 'static') {
      getOffset(node.parentNode)
      return
    }
    result.top = node.offsetTop + result.top - node.scrollTop
    result.left = node.offsetLeft + result.left - node.scrollLeft
    if (position === 'fixed') {
      return
    }
    getOffset(node.parentNode)
  }
  if (window.getComputedStyle(ele)['display'] === 'none') {
    return result
  }
  let position
  getOffset(ele, true)
  return result
}

上述代码不难理解,使用递归实现。

  • 如果节点node.nodeType的类型不是Element(1),则跳出;
  • 如果相关节点position属性为static,则不计入计算,进入下一个节点(其父节点)的递归;
  • 如果相关属性的display属性为nonde,则应该直接返回0作为结果;

这里只是粗略的启发示例,对于边界情况没有一一处理。但是却考察了递归的初级应用。

2.通过getBoundingClientRect方法

  • 对于某一节点getBoundingClientRect方法,返回值是一个DOMRect类型的对象。这个对象表示一个矩形盒子,其中包含由left,top,right和bottom等只读属性。

实现如下:

const offset = (ele) => {
  let result = {
    top: 0,
    left: 0,
  }
  if (!ele.getClientRects().length) {
    return result
  }
  if (window.getComputedStyle(ele)['display'] === 'none') {
    return result
  }
  result = ele.getBoundingClientRect()
  var docElement = ele.ownerDocument.documentElement
  return {
    top: result.top + window.pageYOffset - docElement.clientTop,
    left: result.left + window.pageXOffset - docElement.clientLeft,
  }
}
  • ele.ownerDocument代表文档,包含,documentElement两个节点;
  • docElement.clientTop中的clientTop表示元素顶部边框的宽度,不包含外边距和内边距
  • getBoundingClientRect方法是用来进行简单的几何运算,边界CSS处理和兼容性处理的

数组reduce 方法的实现

reduce 这个方法很好的体现了"函数式"理念;

reduce方法通过给定一个执行函数,对数组的每一项取值从左往右进行累加,最终产生一个结果

粗略实现如下:

  Array.prototype._reduce = function(fn, initVal) {
    let arr = this
    let base = typeof initVal === 'undefined' ? arr[0] : initVal 
    let starPoint = typeof initVal === 'undefined' ? 1 : 0
    arr.slice(starPoint).forEach(function(val, index){
    	base = fn(base, val, index+starPoint, arr)
    })
    return base
  }

应用场景一:通过reduce实现runPromiseSsquence

const runPromiseSsquence = (array,value) => array.reduce(
	(promiseChain, currentFunction) => promiseChain.then(currentFunction),
    Promise.resolve(value)
)

runPromiseSsquence将会被一个每一项都返回一个Promise的数组调用,并且依次执行数组中的每一个Promise,参考下示例:

const f1 = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('p1 running')
      resolve(1)
    }, 2000)
  })
const f2 = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('p2 running')
      resolve(2)
    }, 1000)
  })
const array = [f1, f2]
const runPromiseSsquence = (array, value) =>
  array.reduce(
    (promiseChain, currentFunction) => promiseChain.then(currentFunction),
    Promise.resolve(value)
  )
runPromiseSsquence(array, 'init')

应用场景二:通过reduce实现pipe(柯里化函数)

const pip = (...functions) => input => functions.reduce(
	(acc, fn) => fn(acc),
    input
)

应用场景三:通过Koa only 模块源码认识reduce

Koa only 模块示例:

var o = {
	a: 'a',
    b: 'b',
    c: 'c'
}
only(o,['a','b']) // {a: 'a', b: 'b'}

only模块返回一个经过指定筛选属性的新对象,该模块的代码实现如下:

var only = function (obj, keys) {
  obj = obj || {}
  if ('string' == typeof keys) keys = keys.split(/ +/)
  return keys.reduce(function (ret, key) {
    if (null == obj[key]) return ret
    ret[key] = obj[key]
    return ret
  }, {})
}

实现compose方法

compose方法和前面提到的pipe方法一样,主要用于执行一串不定长度的任务(方法)

let funcs = [func1, func2, func3, func4]
let composeFunc = compose(...funcs)

  • 执行 composeFunc(args)
  • 相当与执行 func1(func2(func3(func4(args))))

它跟pipe方法的差别在于调用顺序不体哦那个

// compose
func1(func2(func3(func4(args))))
// pipe
func4(func3(func2(func1(args))))

实现compose方法的最简单方案式面向过程的:

const compose = function (...args) {
  let length = args.length
  let count = length - 1
  let result
  return function f1(...args1) {
    result = args[count].apply(this, arg)
    if (count <= 0) {
      count = length - 1
      return result
    }
    count--
    return f1.call(null, result)
  }
}

利用reduce实现方案:

const compose = (...args) =>
        args.reverse().reduce(reduceFunc, args.shift())

利用Promise实现方案:

const compose = (...args) => {
  let init = args.pop()
  return (...arg) => {
    args.reverse().reduce((sequence, func) => {
      sequence.then((result) => func.call(null, result)),
        Promise.resolve(init.apply(null, arg))
    })
  }
}

其实还可以使用lodash和Redux来实现compose方法的方案。

函数式概念确实有些抽象,但是一旦顿悟,必然会感受到其中的优雅和简洁。

本文出自:《前端开发核心知识进阶:从夯实基础到突破瓶颈》