本文为笔者认为很棒的一些JS手写,当然除此之外还有
Promise(A+规范)以及Promise相关API全部实现
Webpack实现
Axios实现
虚拟列表实现
等这些也非常有意义,但由于篇幅略长,因此单独开辟一篇文章,有需要的直接去看
当可以不看源码就将它实现时,我想,那就意味着,你真正的掌握了它,因为世界上并没有那么多记性很好的人
—— 沃滋基·硕德
金额千分位切割
function split3(str = '123456789012') {
return str.replace(/(?<!^)(?=(\d{3})+$)/g, '-')
}
console.log(split3()) // 123-456-789-012
银行卡四位分割
function split4(str = '123456789012') {
return str.replace(/(?<=^(\d{4})+)(?!$)/g, '-')
}
console.log(split4()) // 1234-5678-9012
CommonJS 实现
const { resolve, extname } = require('path')
const fs = require('fs')
const { runInThisContext } = require('vm')
// Module类(module.exports那个module的类)
class Module {
constructor() {
this.exports = {}
}
}
// 能够处理的文件类型
Module.extnames = ['.js', '.json']
// CJS缓存存放位置
Module.cache = {}
// require实现
function require_(path) {
// 将路径处理位绝对路径
path = resolve(path)
// 如果文件类型不满足js文件或json文件,则不做任何处理
if (!Module.extnames.includes(extname(path))) return
// 判断文件是否可以访问(如果路径文件不存在,则退出)
try {
fs.accessSync(path)
} catch (error) {
return
}
// 判断缓存中是否存在当前路径对应缓存导出结果,如果存在则直接返回
if (Module.cache[path]) return Module.cache[path].exports
// 创建当前文件路径的module,保存导出结果
const module = new Module()
// 将该module加入缓存(缓存对象的key位该文件绝对路径,这一步必须在处理文件内容之前做,保证我们的CJS可以和官方CJS一样处理循环引用)
Module.cache[path] = module
// 如果是JSON文件,则直接读取文件内容,保存在module.exports中,并返回
if (extname(path) === '.json') {
const content = fs.readFileSync(path, 'utf8')
module.exports = content
return module.exports
}
// 如果是JS文件
else if (extname(path) === '.js') {
// 读取文件内容,包裹在字符串(function(require,module,exports){ })中
const script = `(function (require, module, exports) { ${fs.readFileSync(path, 'utf8')} })`
// 然后在全局环境(global)中运行该字符串,获取该函数(防止污染局部作用域)
const fn = runInThisContext(script)
// 执行该函数,传入实现的require,module,module.exports,函数执行完毕,模块内导出的结果将保存在module.exports中
fn.call(null, require_, module, module.exports)
// 返回module.exports
return module.exports
}
}
冒泡排序
// 稳定 O(n^2) O(1)
function bubble(arr) {
for (let i = arr.length; i >= 0; i--) {
for (let j = 0; j < i; j++) {
arr[j] > arr[j + 1] && ([arr[j], arr[j + 1]] = [arr[j + 1], arr[j]])
}
}
return arr
}
console.log('bubble:', bubble([3, 1, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]))
选择排序
// 不稳定 O(n^2) O(1)
function select(arr) {
let maxIdx
for (let i = arr.length - 1; i >= 0; i--) {
maxIdx = i
for (let j = 0; j < i; j++) {
arr[j] > arr[maxIdx] && (maxIdx = j)
}
[arr[i], arr[maxIdx]] = [arr[maxIdx], arr[i]]
}
return arr
}
console.log('select:', select([3, 1, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]))
插入排序
// 稳定 O(n^2) O(1)
function insert(arr) {
for (let i = 1; i < arr.length; i++) {
const curr = arr[i]
for (let j = i - 1; j >= -1; j--) {
if (curr < arr[j]) {
arr[j + 1] = arr[j]
} else {
arr[j + 1] = curr; break
}
}
}
return arr
}
console.log('insert:', insert([3, 1, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]))
快速排序
// 不稳定 O(n*log2n) O(log2n)
function quick(arr) {
if (arr.length <= 1) return arr
const mid = arr.pop()
const left = [], right = []
for (const v of arr) {
v < mid ? left.push(v) : right.push(v)
}
return [...quick(left), mid, ...quick(right)]
}
console.log(' quick:', quick([3, 1, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]))
归并排序
// 稳定 O(n*log2n) O(1)
function merge(arr) {
if (arr.length <= 1) return arr
const mid = Math.floor(arr.length / 2)
const left = arr.slice(0, mid), right = arr.slice(mid)
return mergeSort(merge(left), merge(right))
}
function mergeSort(left, right) {
const res = []
while (left.length && right.length) {
left[0] < right[0] ? res.push(left.shift()) : res.push(right.shift())
}
return res.concat(left, right)
}
console.log(' merge:', merge([3, 1, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]))
希尔排序
// 不稳定 O(n^1.3) O(1)
function shell(arr) {
for (let i = Math.floor(arr.length / 2); i >= 1; i = Math.floor(i / 2)) {
for (let j = i; j < arr.length; j++) {
const curr = arr[j]
for (let k = j - i; k >= -i; k -= i) {
if (curr < arr[k]) {
arr[k + i] = arr[k]
} else {
arr[k + i] = curr; break
}
}
}
}
return arr
}
console.log(' shell:', shell([3, 1, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]))
数组转树 && 树转数组
const arr = [
{ id: 1, parentId: null, name: 'a' },
{ id: 2, parentId: null, name: 'b' },
{ id: 3, parentId: 1, name: 'c' },
{ id: 4, parentId: 2, name: 'd' },
{ id: 5, parentId: 1, name: 'e' },
{ id: 6, parentId: 3, name: 'f' },
{ id: 7, parentId: 4, name: 'g' },
{ id: 8, parentId: 7, name: 'h' },
]
// 数组转树
function toTree(arr) {
const trees = []
const map = new Map()
for (const v of arr) map.set(v.id, v)
for (const v of arr) {
const pId = v.parentId
// 如果map中不存在当前元素parentId,则该元素为根元素
if (!map.has(pId)) {
trees.push(v)
}
// 如果map中存在 当前元素的parentId,则该元素不为根元素
else {
const parent = map.get(pId)
!(parent.children instanceof Array) && (parent.children = [])
parent.children.push(v)
}
}
return trees
}
console.log(trees = toTree(arr))
// 树转数组(广度优先)
function toArr(trees) {
const arr = [...trees]
for (let i = 0; i < arr.length; i++) {
(arr[i].children instanceof Array) && arr.push(...arr[i].children)
}
return arr
}
console.log(toArr(trees))
Function.prototype.call 实现
Function.prototype.call_ = function (that, ...args) {
// call方法传入的this,既该函数中的that,当that是null或undefined时指向全局上下文,如果that是基本类型则指向该类型的包装类型,如果是引用类型则指向该引用类型
that = [null, undefined].includes(that) ? window : Object(that)
const symbol = Symbol()
that[symbol] = this
const res = that[symbol](...args)
delete that[symbol]
return res
}
Function.prototype.bind 实现
Function.prototype.bind_ = function (that, ...args) {
// call方法传入的this,既该函数中的that,当that是null或undefined时指向全局上下文,如果that是基本类型则指向该类型的包装类型,如果是引用类型则指向该引用类型
that = [null, undefined].includes(that) ? window : Object(that)
const excutor = this
function fn(...args1) {
return excutor.call(this instanceof fn ? this : that, ...args, ...args1)
}
fn.prototype = excutor.prototype
return fn
}
防抖
// pre标识 控制做防抖的函数是在点击那一刻立即执行,还是延迟时间之后才执行
function debounce(callback, timeout, pre) {
let timer = null
let flag = false
return function (...args) {
if (pre) {
clearTimeout(timer)
!flag && (flag = true) && callback.call(this, ...args)
timer = setTimeout(() => flag = false, timeout);
} else {
clearTimeout(timer)
timer = setTimeout(() => callback.call(this, ...args), timeout);
}
}
}
节流
// pre标识 控制做节流的函数是在点击那一刻立即执行,还是延迟时间之后才执行
function throttle(callback, timeout, pre) {
let timer = null
return function (...args) {
if (pre) {
if (!timer) {
callback.call(this, ...args)
timer = setTimeout(() => timer = null, timeout);
}
} else {
if (!timer) {
timer = setTimeout(() => { callback.call(this, ...args); timer = null }, timeout);
}
}
}
}
柯理化
function curry(fn) {
const length = fn.length, args = []
return function step(...args1) {
args.push(...args1)
return args.length >= length ? fn(...args) : step
}
}
compose
function compose(...fns) {
if (fns.length === 0) return val => val
if (fns.length === 1) return fns[0]
return fns.reduce((p, c) => (...args) => p(c(...args)))
}
观察者模式
class Observer {
constructor(callback) {
this.callback = callback
}
run(...args) {
this.callback(...args)
}
}
class Subject {
constructor() {
this.observers = []
}
add(...observers) {
this.observers.push(...observers)
}
notify(...args) {
this.observers.forEach(ob => ob.run(...args))
}
}
发布订阅模式
class EventEmit {
constructor() {
this._events = {}
}
on(type, callback) {
if (!this._events[type]) this._events[type] = []
this._events[type].push(callback)
}
off(type, callback) {
if (!this._events[type]) return
this._events[type] = this._events[type].filter(cb => cb !== callback)
}
emit(type, ...args) {
if (!this._events[type]) return
this._events[type].forEach(cb => cb(...args))
}
once(type, callback) {
// 注意:这里必须用箭头函数,否则this捕获不正确
const wrapper = (...args) => {
callback(...args)
this.off(type, wrapper)
}
this.on(type, wrapper)
}
}
深拷贝
// type:精确类型判断
const type = val => Object.prototype.toString.call(val).slice(8).replace(/]/, '').toLowerCase()
// wm:用来处理循环引用的 缓存 WeakMap
const wm = new WeakMap()
// 深拷贝实现:
function deepCopy(val) {
// 如果是基本类型直接返回,注意: !(val instanceof Object) 用来处理基本类型的包装类型
if (['number', 'string', 'boolean', 'null', 'undefined', 'symbol'].includes(type(val)) && !(val instanceof Object)) return val
// 如果是对象,数组,arguments对象
else if (['object', 'array', 'arguments'].includes(type(val))) {
// 处理循环引用
if (wm.has(val)) return wm.get(val)
const copyVal = new val.constructor()
wm.set(val, copyVal)
// 递归copy
Object.keys(val).forEach(k => copyVal[k] = deepCopy(val[k]))
return copyVal
}
// 如果是set
else if (['set'].includes(type(val))) {
// 处理循环引用
if (wm.has(val)) return wm.get(val)
const copyVal = new val.constructor()
wm.set(val, copyVal)
// 递归copy
val.forEach(v => copyVal.add(deepCopy(v)))
return copyVal
}
// 如果是map
else if (['map'].includes(type(val))) {
// 处理循环引用
if (wm.has(val)) return wm.get(val)
const copyVal = new val.constructor()
wm.set(val, copyVal)
// 递归copy
val.forEach((v, k) => copyVal.set(k, deepCopy(v)))
return copyVal
}
// 如果是正则
else if (['regexp'].includes(type(val))) {
const source = val.source
const modify = val.toString().match(/(?<=\/)\w*$/)[0]
const copyVal = new val.constructor(source, modify)
copyVal.lastIndex = val.lastIndex
return copyVal
}
// function(a, b, c) { return a + b + c }
// 如果是函数
else if (['function'].includes(type(val))) {
// 如果是箭头函数(判断依据是否有原型对象,当然可能存在非箭头函数但是也没有原型对象,但这里不考虑),直接eval原函数字符串返回
if (!val.prototype) return eval(val.toString())
// 如果是普通函数,正则获取函数参数与函数体,重新new Function(参数,函数体)获取拷贝函数返回
const param = val.toString().match(/(?<=\(\s*)(\n|.)*(?=\s*\)\s*\{)/)[0]
const body = val.toString().match(/(?<=\s*\)\s*\{)[\d\D]*(?=\})/)[0]
return param ? new Function(...param.split(','), body) : new Function(body)
}
// 如果其他引用类型值,直接使用该值构造函数并传入该值,返回拷贝后的该值
else {
return new val.constructor(val)
}
}
// 测试deepCopy函数:
; (function testDeepCopy(deepCopy) {
const origin = [1, '', false, null, undefined, Symbol(), [0], { a: 1 }, new Set([2]), new Map([[1, 2]]), /\w*/g, () => 1, function c(a, b, c) { return a + b + c }]
const copy = deepCopy(origin)
console.log('拷贝结果:', copy);
const res = []
origin.forEach((v, i) => {
res.push(copy[i] === origin[i])
i === 10 && res.push(copy[i])
i === 11 && res.push(copy[i]())
i === 12 && res.push(copy[i](1, 2, 3))
})
//
console.log('如果测试结果与测试结果返回的数组每一个元素开起来相同,意味着当前测试通过:')
console.log('测试结果:', res);
console.log('正确结果:', [true, true, true, true, true, true, false, false, false, false, false, /\w*/g, false, 1, false, 6]);
})(deepCopy);
generator 函数执行器
// spawn:generator函数执行器(理解这个函数有助于理解generator与async函数)
function spawn(generator) {
return new Promise((resolve, reject) => {
const gen = generator()
function step(nextFn) {
let nextVal
try {
nextVal = nextFn()
} catch (error) {
return reject(error)
}
if (nextVal.done) return resolve(nextVal.value)
Promise.resolve(nextVal.value).then(
v => step(() => gen.next(v)),
err => step(() => gen.throw(err))
)
}
step(() => gen.next())
})
}
// 测试spawn函数
; (function testSpawn(deepCopy) {
function* generator() {
const v1 = yield 1
const v2 = yield Promise.resolve(2)
const v3 = yield new Promise(r => setTimeout(() => r(v1 + v2), 1000))
console.log(v3);
}
async function async_() {
const v1 = await 1
const v2 = await Promise.resolve(2)
const v3 = await new Promise(r => setTimeout(() => r(v1 + v2), 1000))
console.log(v3);
}
console.log('如果下面两个返回最终值相同,则当前测试通过:');
console.log('async:', async_());
console.log('gener:', spawn(generator));
})(spawn);
图片懒加载(交叉观察器版本)
// lazyLoad中的参数imgs是个img元素的数组,每个imgs元素需要有data-src属性保存图片地址,一个规范的img元素类似这样: <img data-src='图片地址' />
function lazyLoad(...imgs) {
// 创建交叉观察器
const io = new IntersectionObserver(entries =>
entries.forEach(item => {
// 如果观察到图片出现,则请求图片资源,并取消对当前图片的观察
if (item.isIntersecting) {
item.target.src = item.target.dataset.src
io.unobserve(item.target)
}
}))
// 使用交叉观察器监视所有图片
imgs.forEach(v => io.observe(v))
}
图片懒加载(基础版本)
// lazyLoad中的参数imgs是个img元素的数组,每个imgs元素需要有data-src属性保存图片地址,一个规范的img元素类似这样: <img data-src='图片地址' />
function lazyLoad(...imgs) {
// 获取视口高度
const height = window.innerHeight
// 滚动回调
function callback() {
// 如果没有需要监听元素,则删除当前滚动监听
if (!imgs.length) window.removeEventListener('scroll', callback)
// imgs 的浅拷贝,用于imgs的读写分离,类似redux源码中createStore中监听队列的读写分离操作
const nextImgs = imgs.slice()
// 对每一个图片元素进行处理
imgs.forEach((item, i) => {
// 获取当前元素举例视口顶部的高度
const top = item.getBoundingClientRect().top
// 获取当前元素到视口底部的距离
const distance = top - height
// 当该距离小于0,说明该元素出现在时候中,此时添加该元素的src,获取图片资源,同时取消对该元素的监听
if (distance <= 0) {
item.src = item.dataset.src
nextImgs.splice(i, 1)
}
})
// 将读元素(imgs) 同步为 写元素(nextImgs)
imgs = nextImgs
}
// 监听滚动
window.addEventListener('scroll', callback)
}
AJAX
function ajax(method, url, data, success, failed) {
method = method.toLowerCase()
const xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 304 || (xhr.status >= 200 && xhr.status < 300)) {
success(xhr.response)
} else {
failed(xhr)
}
}
}
xhr.open(method, url)
method === 'post' && xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send(data || null)
}
Redux 中 createStore 实现
function createStore(reducer, preloadedState, enhancer) {
// 1,如果有增强器,则直接返回增强器处理的结果
if (enhancer) return enhancer(createStore)(reducer, preloadedState)
// 2,Redux状态保存位置
let currentState = preloadedState
// 3,Redux保存(读)订阅函数的数组
let currentListeners = []
// 4,Redux保存(写)订阅函数的数组
let nextListeners = currentListeners
// 5,当前是否正在 dispatch 的标志
let isDispatching = false
// 6,getState实现(注意:当dispatch时,不允许getState,既不允许读Redux状态)
function getState() {
if (isDispatching) throw new Error('isDispatching')
return currentState
}
// 7,最常见的dispatch实现
function dispatch(action) {
// 当dispatch时,不允许又dispatch
if (isDispatching) throw new Error('isDispatching')
// dispatch标识为true,表示正在dispatch,同时使用reducer更新Redux的state,不管这个过程是否成功,最后都得关闭dispatch表示,既将isDispatching恢复为false
try {
isDispatching = true
currentState = reducer(currentState, action)
} finally {
isDispatching = false
}
// 将 写订阅函数的数组 赋值给 读订阅函数的数组(既读取的时候要拿最新的订阅函数数组),
// 再赋值给这里声明的listener(这一步操作结合subscribe的实现,目测是进行内存优化)
let listeners = currentListeners = nextListeners
// 执行所有订阅函数
for (let i = 0; i < listeners.length; i++) {
// 不直接 listeners[i]() ,目测应该是防止订阅函数能够拿到this,既当前订阅函数数组listener,从而造成隐患(既订阅函数可以修改this)
const listener = listeners[i]
listener()
}
// 返回dispatch接受的action
return action
}
// 8,添加订阅函数 实现
function subscribe(listener) {
// 当添加订阅函数时,不允许dispatch
if (isDispatching) throw new Error('isDispatching')
// 将currenListeners与nextListener分离,目的是,这里添加订阅函数属于写操作,那么不能干扰读操作中的订阅函数,因此进行读写分离
shadowCopy()
// 添加订阅函数
nextListeners.push(listener)
// 声明isSubscribed,标识当前订阅函数是否被订阅
let isSubscribed = true
// 返回取消订阅函数
return () => {
// 如果当前函数没有被订阅,则什么都不做(目测应用场景既防止 取消函数的订阅之后,又取消)
if (!isSubscribed) return
// 当取消订阅函数时,不允许dispatch
if (isDispatching) throw new Error('isDispatching')
// 订阅表示恢复为false,表示该函数的订阅取消
isSubscribed = false
// 将currenListeners与nextListener分离,目的是,这里删除订阅函数属于写操作,那么不能干扰读操作中的订阅函数,因此进行读写分离
shadowCopy()
// 从读订阅函数数组中 删除当前订阅函数
const idx = nextListeners.indexOf(listener)
nextListeners.splice(idx, 1)
// 目测是为了内存优化,防止每个订阅函数都指向一个巨大的currentListeners,以至于该内存不能被释放
// 呼应dispatch中的 代码:let listeners = currentListeners = nextListeners
// 即使将currentListeners置为null,因为listener保存了所有在dispatch那一刻所存在的订阅函数,因此 currentListeners = null 是没问题的
currentListeners = null
}
}
// shadowCopy:用于读订阅函数数组(currentListeners),写订阅函数数组(nextListeners)分离
function shadowCopy() {
currentListeners === nextListeners && (nextListeners = currentListeners.slice())
}
// 执行一次dispatch,初始化数据,type这里应该是个随机值(避免与reducer内部重名)
dispatch({ type: '123456789' })
// 返回 getState, dispatch, subscribe
return { getState, dispatch, subscribe }
}
Redux 中 combineReducers 实现
function combineReducers(composeReducers) {
// goodReducers:过滤掉composeReducers中不是函数的reducer之后留下的所有reducer
// composeReducersKeys:composeReducers的key
const goodReducers = {}, composeReducersKeys = Object.keys(composeReducers)
// 过滤开始
for (const key of composeReducersKeys) {
typeof composeReducers[key] === 'function' && (goodReducers[key] = composeReducers[key])
}
// 返回所有合法reducer(既是reducer是函数)最终整合在一起的终极reducer
return (state = {}, action) => {
// goodReducersKeys:所有合法reducer的key
const goodReducersKeys = Object.keys(goodReducers)
// nextState:保存所有reducer更新后的state
// hasChanged:判断nextState与上一次redux中state是否发生变化的标识
let nextState = {}, hasChanged = false
// 执行所有合法reducer
for (const key of goodReducersKeys) {
// 获取每一个reducer
const reducer = goodReducers[key]
// 每一个reducer开始更新上一次redux的state
nextState[key] = reducer(state[key], action)
// 如果每一个reducer更新后返回的state(既这里的nextState[key])与上次的state(既这里的state[key])不同,
// 则说明redux状态state发生了变化,因此将hasChanged置为true,既state发生变化
hasChanged = hasChanged || nextState[key] !== state[key]
}
// 目测是为了处理一种特殊情况,既state中没有该子reducer对应的子state,所有state[key]返回undefined
// 但是确实存在子reducer,且该reducer返回值恰好也是undefined,如果出现这种情况,那也意味着Redux中state前后发生了变化
// 因此hasChangedx需要置为true,既state发生变化
hasChanged = hasChanged || goodReducersKeys.length !== Object.keys(state).length
// 如果Redux state 在reducer更新前后发生了变化,则返回最新的state(既这里的nextState),否则直接返回原state
// 这样做的目的,网上说是内存优化(优化一点是一点)
return hasChanged ? nextState : state
}
}
Redux 中 applyMiddleware 实现
function applyMiddleware(...middlewares) {
return createStore => (reducer, preloadedState) => {
// 获取原始store
const store = createStore(reducer, preloadedState)
// 声明一个将来会被中间件增强的dispatch(虽然此刻这个dispatch什么都不能做)
let dispatch = () => { throw new Error('dont dispatch') }
// 创建一个容器,保存getState,与dispatch,之后将用来交给每一个中间件
const storeAPI = {
getState: store.getState,
// 注意:容器的dispatch不是直接用之前声明的dispatch做赋值,而是形成闭包,这么做的目的是为了保证之后交给中间件的dispatch一直都是指向同一个既上面声明的那个dispatch,这可能不太好理解,不过我建议结合redux-thunk源码,多看几遍就能明白这段代码中的奥妙~
dispatch: (...args) => dispatch(...args)
}
// 将每一个中间件传入上面的容器对象,给中间件使用getState与dispatch(一直都是上面声明的那个dispatch)的能力
const chain = middlewares.map(middleware => middleware(storeAPI))
// 使用compose函数将所有中间件进行组合,最终再传入原始的dispatch,这个行为既完整了对原始dispatch行为的增强,这时候返回的既增强后的dispatch
// 将增强后的dispatch赋值给上面声明的dispatch(每个中间件接受到的dispatch最终用的都是这个dispatch),这可能不太好理解,但希望你能理解~
dispatch = compose(...chain)(store.dispatch)
// 返回 createStore 的api,getState与subscribe
// 以及被中间件增强后的dispatch(既该函数内声明的dispatch)
return { ...store, dispatch }
}
}
// compose函数
function compose(...fns) {
if (fns.length === 0) return val => val
if (fns.length === 1) return fns[0]
return fns.reduce((p, c) => (...args) => p(c(...args)))
}
Redux-Thunk 实现
function createReduxThunk(...args) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
// 建议结合 Redux 中 applyMiddleware 源码理解
return action(dispatch, getState, ...args)
}
return next(action)
}
}
基本拖拽实现
<!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>
<style>
#container {
margin: 100px 0 0 100px;
width: 500px;
height: 500px;
background-color: blue;
position: relative;
}
#app {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
top: 100px;
left: 100px;
}
</style>
</head>
<body>
<div id='container'>
<div id='app'></div>
</div>
<script>
function drag(id) {
const app = document.getElementById(id)
app.onmousedown = (e) => {
const ev = e
// 鼠标距离拖动区域上 左距离
const innerTop = e.clientY - app.offsetTop
const innerLeft = e.clientX - app.offsetLeft
document.onmousemove = (ev) => {
// app拖动距离 = 鼠标距离拖动元素的父级距离 - 鼠标距离拖动区域距离
let moveTop = ev.clientY - innerTop
let moveLeft = ev.clientX - innerLeft
// 设置限制区域 阻止app拖动到父元素外面
const dad = app.parentNode
if (moveLeft < 0) moveLeft = 0
if (moveLeft > dad.offsetWidth - app.offsetWidth) moveLeft = dad.offsetWidth - app.offsetWidth
if (moveTop < 0) moveTop = 0
if (moveTop > dad.offsetHeight - app.offsetHeight) moveTop = dad.offsetHeight - app.offsetHeight
// 设置app拖动距离 到定位top left值
app.style.top = moveTop + 'px'
app.style.left = moveLeft + 'px'
}
document.onmouseup = (e) => {
// 抬起鼠标 解除onmousemove onmouseup绑定事件
document.onmousemove = null
document.onmouseup = null
}
}
}
drag('app')
</script>
</body>
</html>