最近去了一趟深圳大鹏官湖村。典型的海边小镇,地方不大,沙滩挺大。有一个路口在滤镜加持下,居然有点像灌篮高手片头里,樱木在车站前的经典场景。



最近去了一趟深圳大鹏官湖村。典型的海边小镇,地方不大,沙滩挺大。有一个路口在滤镜加持下,居然有点像灌篮高手片头里,樱木在车站前的经典场景。
Promise 应该大家都很熟悉了。大部分的库也都用上了Promise。
这文章主要记录下 Promise.all、Promise.allSettled、Promise.race 这几个的用法。
Promise.all:
参数是一个数组,数组元素是各个Promise。要数组里面所有的 Promise 都返回成功,那这个 Promise.all 的结果才会是成功。成功返回数组,就是元素成功的结果集合。失败的话返回单值,是元素里第一个失败的结果。
Promise.allSettled:
跟 Promise.all 参数一样,是一个数组,数组元素是各个Promise。不同的是这个不会因为数组元素的成败来返回成功失败。返回的是个数组,记录着元素 Promise 的各个结果,包括成败。
先准备四个小函数,然后再混起来用:
function PromiseSuccess0() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('PromiseSuccess0')
}, 0)
})
}
function PromiseSuccess1000() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('PromiseSuccess1000')
}, 1000)
})
}
function PromiseError2000() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('PromiseError2000')
}, 2000)
})
}
function PromiseError3000() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('PromiseError3000')
}, 3000)
})
}
Promise.all([PromiseSuccess0(), PromiseError3000(), PromiseError2000()])
.then((data) => {
console.log('success all-1: ', data)
})
.catch((error) => {
console.error('error all-1: ', error)
// error all-1: PromiseError2000
})
Promise.all([PromiseSuccess0(), PromiseSuccess1000()])
.then((data) => {
console.log('success all-2: ', data)
// success all-2: (2) ["PromiseSuccess0", "PromiseSuccess1000"]
})
.catch((error) => {
console.error('error all-2: ', error)
})
Promise.allSettled([PromiseError2000(), PromiseSuccess1000()])
.then((data) => {
console.log('success allSettled: ', data)
// success allSettled:
// (2) [{…}, {…}]
// 0: {status: "rejected", reason: "PromiseError2000"}
// 1: {status: "fulfilled", value: "PromiseSuccess1000"}
})
.catch((error) => {
console.error('error allSettled: ', error)
})
Promise.race
顾名思义,竞赛。参数依然是个元素为 Promise 的数组。会返回第一个跑完的元素结果。话不多说,上码:
function load1000() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('load1000')
}, 1000)
})
}
function load3000() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('load3000')
}, 3000)
})
}
function errorTimer() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('timeout'))
}, 2000)
})
}
Promise.race([load1000(), errorTimer()])
.then((data) => {
console.log('success race-1: ', data)
// success race-1: load1000
})
.catch((error) => {
console.error('error race-1: ', error)
})
Promise.race([load3000(), errorTimer()])
.then((data) => {
console.log('success race-2: ', data)
})
.catch((error) => {
console.error('error race-2: ', error)
// error race-2: Error: timeout
})
具体的使用场景可以是,比如下载个东西,给其设定个时间限定,超过这个时间就报错。稍微封装一下:
function addTimer(fn, time) {
const errorTimer = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('timeout'))
}, time)
})
return Promise.race([fn(), errorTimer])
}
addTimer(load1000, 2000)
.then((data) => {
console.log('success addTimer: ', data)
})
.catch((error) => {
console.error('error addTimer: ', error)
})
webpack一般是打成一份文件,但有时也需要打成几份,生成的文件可以把其中的公共部分抽取出来,省点代码。
这里就免不了依赖文件顺序的问题。这里用了很巧妙的方法,来连接不同的文件,以及依赖的前置性问题。
相当用心思,这里稍微记录下。
先准备两份文件 foo.js,bar.js,util.js:
// foo.js
import util from './util'
console.log('foo')
console.log(util('foo'))
// bar.js
import util from './util'
console.log(util('bar'))
console.log('bar')
export default function () {
return 'bar-function'
}
// util.js
console.log('util')
export default function (flag) {
return 'util' + flag
}
webpack配置文件:
const { execSync } = require('child_process')
var webpack = require('webpack');
execSync(`rm -rf build`)
module.exports = {
mode: 'development',
devtool: 'none',//配置生成Source Maps,选择合适的选项
entry: {
foo: __dirname + "/foo.js",
bar: __dirname + "/bar.js"
},//已多次提及的唯一入口文件
output: {
path: __dirname + "/build",//打包后的文件存放的地方
filename: "[name].js"//打包后输出文件的文件名
},
optimization: {
splitChunks: {
chunks: 'all',
minSize: 1,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 6,
maxInitialRequests: 4,
automaticNameDelimiter: '~',
automaticNameMaxLength: 30,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
}
会在 dist 文件夹生成三个文件:foo.js,bar.js,defaultbarfoo.js
看得出 default-bar-foo.js 就是共用代码,这里就是 util.js,代码是:
// window["webpackJsonp"]是个数组,push了一个数组。元素1还是一个数组,里面只有一个值"default~bar~foo"
// 其实就是这份文件名字,或者说是这份chunk名字
// 而第二个值就又是各个module的集合。比如这里只有一个util.js,结合chunk名就很明显了,
// util.js就是foo.js和bar.js共同用到的。如果还有其他共用到的,那么还有多几个字段
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["default~bar~foo"],{
/***/ "./util.js":
/*!*****************!*\
!*** ./util.js ***!
\*****************/
/*! exports provided: default */
// __webpack_exports__=module.exports
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
console.log('util')
/* harmony default export */ __webpack_exports__["default"] = (function (flag) {
return 'util' + flag
});
/***/ })
}]);
如果平时有看过 webpack 生成的代码的话,就能很明显看出与入口文件打出来的内容很不一样,缺少了webpackBootstrap
。
看看 foo.js,(bar.js也长得差不多),几个名词先说一下:
module:每一份文件的js代码。比如a.js引入了b.js,把a.js作为entry。那么到时打出来的就是只有一份a.js(该输出文件就是一份chunk,输出文件名字可变)。里面有两个模module,a.js和b.js。
chunk:要输出的每一份文件就是一个chunk(一个或多个module组成一份chunk),一般是一个entry对应一个chunk。如果像这种抽离公共js的,也是一个chunk,也会多于entry数。
foo.js是
一个自执行的函数,传入一个json对象。只有一个key值,是”./foo.js”。相对应的value其实是一个函数模块,就是foo.js的代码内容。
打包出来的js靠全局变量维系,window[“webpackJsonp”]。在每个entry chunk的最上面都有webpack加载代码,而附属chunk则没有,只有看起来像数组push的代码,如default-bar-foo.js文件所示。
/******/ (function(modules) { // webpackBootstrap
/******/ // install a JSONP callback for chunk loading
// 这data就是window["webpackJsonp"] push进来的东西
/******/ function webpackJsonpCallback(data) {
/******/ var chunkIds = data[0];// chunkId="default~bar~foo"
/******/ var moreModules = data[1];// {'./util.js':f}
/******/ var executeModules = data[2]; // 暂不知何用
/******/
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [];
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
// resolves暂不知作何用
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
// 标记chunk安装
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
// 把每个chunk的每份module记录在modules,用以调用运行
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
// 当有parentJsonpFunction这方法适合,就执行这个方法,参数为每个chunk代码
// window["webpackJsonp"]里面的元素其实就是非入口chunk代码块
// parentJsonpFunction取自上一个window["webpackJsonp"]的push的方法,
// 当第一个script的入口chunk,它的parentJsonpFunction就是普通数组的push
// 同时在第一个script的入口chunk,把window["webpackJsonp"]的push改写为第一个script的入口chunk的webpackJsonpCallback方法
// 也就是第二个script的入口chunk里,这个parentJsonpFunction就是第一个script的入口chunk的webpackJsonpCallback方法
// 这样就把两份chunk的代码通过这种方式联系起来
// 当push(改写后的push)了非入口chunk,可以通过这种联系一层层传递到各个chunk的webpackJsonpCallback方法
// webpackJsonpCallback里面再调用checkDeferredModules方法,来检查并执行相应的模块代码
// 这样一来无论script顺序怎样,只要调用webpackJsonpCallback方法,就能达到加载的模块(chunk即为模块合集)传递到各个入口chunk
/******/ if(parentJsonpFunction) parentJsonpFunction(data);
/******/
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/
/******/ // add entry modules from loaded chunk to deferred list
// 看原注释是添加新的前置依赖
// 暂不知为何是添加executeModules而不是一开始就写在deferredModules
/******/ deferredModules.push.apply(deferredModules, executeModules || []);
/******/
/******/ // run deferred modules when all chunks ready
/******/ return checkDeferredModules();
/******/ };
// 检查依赖到齐没有,并执行
// 在主流程里会运行一次
// 在webpackJsonpCallback里最后再运行一次,也就是每次有chunk的加载都会检查运行一次
/******/ function checkDeferredModules() {
/******/ var result;
/******/ for(var i = 0; i < deferredModules.length; i++) {
/******/ var deferredModule = deferredModules[i];
/******/ var fulfilled = true;
/******/ for(var j = 1; j < deferredModule.length; j++) {
// depId="./foo.js","default~bar~foo"
/******/ var depId = deferredModule[j];
// 是否已安装记录在installedChunks
/******/ if(installedChunks[depId] !== 0) fulfilled = false;
/******/ }
// 全部安装完毕
/******/ if(fulfilled) {
// 把 deferredModules 清空
/******/ deferredModules.splice(i--, 1);
// deferredModule[0]本chunk的名字,这里是"./foo.js"
/******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
/******/ }
/******/ }
/******/
/******/ return result;
/******/ }
/******/
// 先看同步代码,从这里开始看起
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // Promise = chunk loading, 0 = chunk loaded
// 执行到这里,代表本身这个chunk也就是这个文件已加载
// 这里同时也会标记别的chunk加载了没有,来判断前置依赖有了没有
/******/ var installedChunks = {
/******/ "foo": 0
/******/ };
/******/
/******/ var deferredModules = [];
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
// 每个加载的模块结果缓存起来
// 这里每个module就是未打包前的每一份js
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
// 这里注意到具体模块去看,会把这个模块的导出值赋值到module.exports
// 然后再 return 出去,所以 __webpack_require__ 模块会返回加载运行模块的结果
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
// 标记这个 module 加载过
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // 部分 __webpack_require__.字段 这里未体现作用
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
// 打上 ES6
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
// 初始化window["webpackJsonp"]为一个数组
/******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 把旧的window["webpackJsonp"]的push方法缓存起来,当然第一个就是数组的push方法
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 改写window["webpackJsonp"]的push方法为webpackJsonpCallback
/******/ jsonpArray.push = webpackJsonpCallback;
/******/ jsonpArray = jsonpArray.slice();
// 把window["webpackJsonp"]的chunk传入webpackJsonpCallback执行一下
/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
// 这里再次把旧的window["webpackJsonp"]的push方法赋值到parentJsonpFunction
// parentJsonpFunction会存在在这个chunk的作用域,在这个chunk的webpackJsonpCallback里被调用
/******/ var parentJsonpFunction = oldJsonpFunction;
/******/
/******/
/******/ // add entry module to deferred list
// 首位为本身chunk,后面为所依赖的chunk
// 所以可以得知靠着installedChunks和deferredModules这两个变量来判断到底所需的文件加载好了没有
// 以至于在script标签里的引入顺序会干扰到依赖加载从而影响执行
// 注意这里是二维数组
/******/ deferredModules.push(["./foo.js","default~bar~foo"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })
/************************************************************************/
/******/ ({
/***/ "./foo.js":
/*!****************!*\
!*** ./foo.js ***!
\****************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// 标记为 ES6 模块
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./util */ "./util.js");
console.log('foo')
console.log(Object(_util__WEBPACK_IMPORTED_MODULE_0__["default"])('foo'))
/***/ })
/******/ });
通过全局的 webpackJsonp 这个数组来连接多份文件。改写了这个数组的 push 方法。让其在每次 push 的时候,都一层一层找上之前加载过的文件。
触发每份入口文件里的检查依赖(checkDeferredModules)
,来判断前置依赖是否准备好。好则开始执行。
用过 vue 的同学应该都知道,双向绑定中,更改了数据会去更新dom,但不是马上更新的。
直接跟在修改数据后,拿到的 dom 还是旧的。
vue有个this.$nextTick
用法,用这个的回调可以保证拿到更新后的dom。
直接看看 next-tick 源码
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
let pending = false
// 清异步队列,全部执行
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 判断有没有 promise 且有没有被重写过,
// promise完好无损就使用 promise 做异步队列的触发,设置使用微任务标志为true
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
// 特殊场景用MutationObserver
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
// 再不济看看有没有 setImmediate,虽然也是宏任务,但总比 setTimeout 强
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
// 实在没办法了,setTimeout 兜底
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// nextTick 具体操作所在,其他队列方法也是用这个,比如 data 改动后通知 dom 修改,也是用这个
// 即 vue 里面的响应式,不是实时更新的,而是存在一个队列,在下一回合进行任务更新。不然太耗损性能
// 也就是解释了,在修改了 data 之后,dom 是没有马上更新,而在 $nextTick 的回调后,则可以看到 dom 更新
// 原因就在于同样的异步任务,$nextTick 的回调,是在 dom 修改的操作后面,所以 $nextTick 能看到 dom 更新
// 以下以 this.$nextTick(()=>{}) 为例子,cb 为回调,ctx 为 vue 实例
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 把任务塞进异步队列
callbacks.push(() => {
// 有回调就触发回调
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
// 这里注意,结合下面来看
//用 promise 的 resolve 触发,把 vue 实例当成参数传进去
} else if (_resolve) {
_resolve(ctx)
}
})
// 在清任务 flushCallbacks 里赋值 pending 为 false
// 目的为了 timerFunc 在周期内只触发一次
// 执行 timerFunc,让其下一周期执行清异步队列任务
if (!pending) {
pending = true
timerFunc()
}
// 当没有回调且 Promise 正常
// $nextTick 返回的是一个 promise,触发时机与上面有回调一样
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
除了常规的回调用法之外,还可以这样用
this.$nextTick().then( vue实例 => { console.log(vue实例 === this) }) // => true
如果不传入回调,则返回promise,因为是 promise,也可以直接就 await 等待。参考官网。
接着来个简陋版简单模拟一下这个 nextTick 的实现。这其中还涉及到事件循环的知识。
// 模拟vue nextTick
// 存储所有$nextTick的回调,变量的set修改后通知dom更新的,与$nextTick的回调进入同个微队列
const callbacks = []
// 微队列的触发载体,这里是Promise.resolve()
// vue是先找Promise.resolve,没有就找setTimeout
let run = null
function next() {
callbacks.forEach((cb) => {
cb()
})
}
// 这里的change可以看成修改了变量,里面的回调就是与nextTick的回调如出一辙
function change() {
nextTick(() => {
// 这里面是同步
console.log('update dom -- 1')
const child = document.createElement('div')
child.id = 'new'
document.getElementById('box').appendChild(child)
console.log('update dom')
console.log('update dom -- 2')
})
}
function nextTick(cb) {
if(!run) {
// 初始化下个队列
run = Promise.resolve()
// 把next方法放在下个队列运行
// 看上面的next函数,是把callbacks的函数遍历执行
run.then(next)
}
// nextTick的回调加入数组等着,遍历触发里面的每个函数
// 由于是同步,所以其实只是一个微任务,
callbacks.push(cb)
}
// 调用了第一次nextTick
nextTick(() => {
// 表示该次微队列任务开始
console.log('nextTick -- 1')
})
Promise.resolve().then(() => {
console.log('then -- ')
})
// 修改变量,更改了dom
change()
// 同步任务,最早打印,但此时没有new元素
console.log('script -- ', document.getElementById('new'))
// 调用了第二次nextTick
nextTick(() => {
// 在修改变量之后调nextTick,已有new元素
console.log('nextTick -- 2', document.getElementById('new'))
})
// nextTick -- 2 之所以在 then -- 前打印,是因为回调都放在callbacks里,同步触发了
四种方法,先在外面放四个容器, 赋予 “.container” 类。让其子元素居中,先赋予 “.box” 类,水平居中。
<!DOCTYPE>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>baidu</title>
</head>
<style type="text/css">
.container {
width: 22%;
height: 300px;
float: left;
background-color: #abcdef;
margin: 0 10px;
position: relative;
}
.box {
width: 100px;
height: 100px;
background-color: red;
text-align: center;
}
<!-- 绝对定位大法,设置为绝对定位,四个方向位移为0,外边距设为auto -->
.middle1 {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
}
<!-- flex弹性布局,特别容易完成这个事 -->
.middle2 {
display: flex;
justify-content: center;
align-items: center;
}
<!-- 利用表格居中,注意的是还要多一层table-cell,真正居中是在table-cell -->
.middle3 {
display: table-cell;
vertical-align: middle;
background-color: initial;
}
.block {
display: inline-block;
}
<!-- 绝对定位加translate大法,水平垂直位移50%,translate里面的50%是相对于自己的,所以不知宽高也可以使用 -->
.middle4 {
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
</style>
<body>
<div class="container"><div class="box middle1"></div></div>
<div class="container middle2"><div class="box"></div></div>
<div class="container" style="display: table;">
<div class="box middle3">
<p class="box block"></p>
</div>
</div>
<div class="container"><div class="box middle4"></div></div>
</body>
</html>
总结:个人觉得,首先 flex 布局最方便也最帅气。然后绝对定位加 translate 也好理解。至于第一种绝对定位加四个方向位移为0,其实不太理解其原理~ = =!