Skip to content

Commit

Permalink
Updates
Browse files Browse the repository at this point in the history
  • Loading branch information
Yakima-Teng committed Mar 24, 2024
1 parent f72f07f commit a03dd9d
Show file tree
Hide file tree
Showing 7 changed files with 607 additions and 250 deletions.
Binary file added img/render-big-data.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 47 additions & 33 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<!-- 国产360浏览器默认采用高速模式渲染页面 -->
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>iframe application</title>
<title>iframe 微前端应用实例</title>
<link rel="shortcut icon" href="./favicon.ico" type="image-x-icon">
<link rel="stylesheet" href="./css/reset.min.css">
<link rel="stylesheet" type="text/css" href="./css/font-awesome.min.css">
Expand All @@ -24,50 +24,43 @@
<div class="menu-toggle-wrapper">
<i id="btnToggleMenu" class="fa fa-list"></i>
</div>
<ul class="menus">
<li class="menu">
<i class="fa fa-glass"></i>
<span data-route-name="rainbow" class="title">彩色条纹</span>
</li>
<li class="menu">
<i class="fa fa-cog"></i>
<span data-route-name="city" class="title">不重复的颜色</span>
</li>
<li class="menu">
<i class="fa fa-file-image-o"></i>
<span data-route-name="particles" class="title">粒子效果</span>
</li>
<li class="menu">
<i class="fa fa-bell-o"></i>
<span data-route-name="guess" class="title">猜字母游戏</span>
</li>
<li class="menu">
<i class="fa fa-cloud-upload"></i>
<span data-route-name="upload" class="title">图片压缩上传</span>
</li>
</ul>
<ul class="menus"></ul>
</nav>
<article class="app-iframe-wrapper">
<iframe id="appIframe" class="app-content-wrapper" src="" frameborder="0"></iframe>
</article>
</div>
<script src="./lib/jquery.min.js"></script>
<script src="./lib/handlebars-v4.0.5.min.js"></script>
<script id="menuTemplate" type="text/template">
<li class="menu">
<i class="fa {{ icon }}"></i>
<span data-route-name="{{ routeName }}" class="title">{{ menuTitle }}</span>
</li>
</script>
<script>
var $menuWrapper = $('#menuWrapper')
const elemAppIframe = document.querySelector('#appIframe')
var $appIframe = $('#appIframe')
var $menusWrapper = $('#menuWrapper .menus')
var $menus = $('#menuWrapper .menu')
var $btnToggleMenu = $('#btnToggleMenu')

const menuList = [
{ icon: 'fa-database', name: 'big-data', title: '渲染一百万条数据' },
{ icon: 'fa-bell-o', name: 'guess', title: '猜字母游戏' },
{ icon: 'fa-cloud-upload', name: 'upload', title: '图片压缩上传' },
{ icon: 'fa-glass', name: 'rainbow', title: '彩色条纹' },
{ icon: 'fa-cog', name: 'city', title: '不重复的颜色' },
{ icon: 'fa-file-image-o', name: 'particles', title: '粒子效果' },
{ icon: 'fa-cubes', name: 'webgl2', title: 'WebGL2' },
]

// 自定义路由
var routesConfig = {
rainbow: './pages/rainbow/index.html',
city: './pages/city/index.html',
guess: './pages/guess/index.html',
particles: './pages/particles/index.html',
upload: './pages/upload/index.html'
}
const routesConfig = menuList.reduce((prev, curr) => {
prev[curr.name] = `./pages/${curr.name}/index.html`
return prev
}, {})
function Route (routesConfig) {
this.routers = routesConfig
}
Expand All @@ -78,25 +71,46 @@
var targetUrl = this.routers[routeName]
$appIframe.prop('src', targetUrl)

$menus.removeClass('active')
$menus.find('span[data-route-name=' + routeName + ']').parents('.menu').addClass('active')
document.querySelectorAll('#menuWrapper span[data-route-name=' + routeName + ']').forEach((elem) => {
elem.parentElement.classList.add('active')
})
document.querySelectorAll('#menuWrapper span:not([data-route-name=' + routeName + '])').forEach((elem) => {
elem.parentElement.classList.remove('active')
})
// $menus.removeClass('active')
// $menus.find('span[data-route-name=' + routeName + ']').parents('.menu').addClass('active')
}
var router = new Route(routesConfig)

// 加载路由页面后自动更新页面 title
elemAppIframe.addEventListener('load', () => {
document.title = elemAppIframe.contentWindow.document.title
})

$menuWrapper.on('click', '.menu', function (e) {
var $this = $(this)
var routeName = $this.find('.title').data('route-name')
router.go({ path: routeName })
})

/**
* 填充菜单
*/
const menuTemplate = document.querySelector('#menuTemplate').innerHTML
document.querySelector('#menuWrapper .menus').innerHTML = menuList.map((item) => {
return menuTemplate
.replace(/{{ icon }}/, item.icon)
.replace(/{{ routeName }}/, item.name)
.replace(/{{ menuTitle }}/, item.title)
}).join('')
$btnToggleMenu.click(function () {
$menusWrapper.toggleClass('show')
})

window.addEventListener('load', function () {
var hash = window.location.hash.replace('#', '')
if (Object.keys(routesConfig).indexOf(hash) === -1) {
router.go({ path: 'particles' })
router.go({ path: menuList[0].name })
} else {
router.go({ path: hash })
}
Expand Down
236 changes: 236 additions & 0 deletions pages/big-data/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>渲染一百万条数据</title>
<style>
* {
padding: 0;
margin: 0;
border-style: none;
}
html, body {
display: block;
width: 100%;
height: 100%;
font-size: 16px;
}
.desc {
height: 60px;
line-height: 60px;
text-align: center;
}
.list {
display: block;
height: calc(100% - 60px);
overflow-x: hidden;
overflow-y: auto;
}
.list .items {
display: block;
box-sizing: border-box;
padding-top: 0;
}
.list .block {
display: block;
}
.list .row {
display: block;
height: 30px;
line-height: 30px;
background-color: #ffffff;
}
.list .block.odd .row:nth-of-type(2n) {
background-color: skyblue;
}
.list .block.even .row:nth-of-type(2n +1) {
background-color: skyblue;
}
</style>
</head>
<body>
<div class="desc">现在有一百万条数据,如果直接渲染一百万条数据会非常卡顿,下面是一种纯前端解决方案。</div>
<div class="list">
<ul class="items"></ul>
</div>
<script>
const elemContainer = document.querySelector('.list')
const containerHeight = elemContainer.clientHeight
const singleLineHeight = 30
const numOfItemsInSingleScreen = Math.ceil(containerHeight / singleLineHeight)

// 生成一百万条数据
const max = 1000000

const elemList = elemContainer.querySelector('.items')
elemList.style.height = `${Math.ceil(max * singleLineHeight)}px`
elemList.style.paddingTop = '0px'
elemList.style.paddingTop = '0px'

function fillList (opts) {
const {
oldStartIdx,
oldEndIdx,
startIdx,
endIdx,
} = opts

// 分页索引没变化的话不需要做任何处理
if (oldStartIdx === startIdx && oldEndIdx === endIdx) {
return
}

// 直接下拉/上拉到新区域,之前已渲染的部分可以直接丢弃,所以可以通过 innerHTML 直接替换
if (startIdx > oldEndIdx || endIdx < oldStartIdx) {
// 注意下设置一个合理的 margin-top
let html = ''
elemList.style.paddingTop = `${startIdx * singleLineHeight * numOfItemsInSingleScreen}px`
for (let pageIdx = startIdx; pageIdx < endIdx; pageIdx++) {
html += `<div class="block ${pageIdx % 2 ? 'even' : 'odd'} block-${pageIdx}">`
const rowStartIdx = pageIdx * numOfItemsInSingleScreen
const rowEndIdx = Math.min(
(pageIdx + 1) * numOfItemsInSingleScreen,
max + 1
)
for (let rowIdx = rowStartIdx; rowIdx < rowEndIdx; rowIdx++) {
html += `<div class="row">${ rowIdx }</div>`
}
html += '</div>'
}
elemList.innerHTML = html
return
}

// 处理尾部新增页内容
if (endIdx > oldEndIdx) {
const fragment = document.createDocumentFragment()
for (let pageIdx = oldEndIdx; pageIdx < endIdx; pageIdx++) {
const block = document.createElement('div')
block.classList.add('block', pageIdx % 2 ? 'even' : 'odd', `block-${pageIdx}`)
const rowStartIdx = pageIdx * numOfItemsInSingleScreen
const rowEndIdx = Math.min(
(pageIdx + 1) * numOfItemsInSingleScreen,
max + 1
)
for (let rowIdx = rowStartIdx; rowIdx < rowEndIdx; rowIdx++) {
const div = document.createElement('div')
div.classList.add('row')
div.textContent = `${rowIdx}`
block.appendChild(div)
}
fragment.appendChild(block)
}
elemList.appendChild(fragment)
}

// 处理尾部删减页内容
if (endIdx < oldEndIdx) {
for (let pageIdx = endIdx; pageIdx < oldEndIdx; pageIdx++) {
const block = document.querySelector(`.block-${pageIdx}`)
block.parentNode.removeChild(block)
}
}

// 处理头部新增页内容
if (startIdx < oldStartIdx) {
const fragment = document.createDocumentFragment()
elemList.style.paddingTop = `${startIdx * numOfItemsInSingleScreen * singleLineHeight}px`
for (let pageIdx = startIdx; pageIdx < oldStartIdx; pageIdx++) {
const block = document.createElement('div')
block.classList.add('block', pageIdx % 2 ? 'even' : 'odd', `block-${pageIdx}`)
const rowStartIdx = pageIdx * numOfItemsInSingleScreen
const rowEndIdx = Math.min(
(pageIdx + 1) * numOfItemsInSingleScreen,
max + 1
)
for (let rowIdx = rowStartIdx; rowIdx < rowEndIdx; rowIdx++) {
const div = document.createElement('div')
div.classList.add('row')
div.textContent = `${rowIdx}`
block.appendChild(div)
}
fragment.appendChild(block)
}
elemList.insertBefore(fragment, elemList.firstChild)
}

// 处理头部删减页内容
if (startIdx > oldStartIdx) {
for (let pageIdx = oldStartIdx; pageIdx < startIdx; pageIdx++) {
const block = document.querySelector(`.block-${pageIdx}`)
block.parentNode.removeChild(block)
}
elemList.style.paddingTop = `${startIdx * numOfItemsInSingleScreen * singleLineHeight}px`
}
}

// 渲染出来的页面索引
let startIdx = -1
let endIdx = -1

// 最大页数
const maxPageIdx = Math.ceil(max / numOfItemsInSingleScreen)
console.log('maxPageIdx', maxPageIdx)

const onScroll = (() => {
let handlerId = 0

return () => {
if (handlerId) {
cancelAnimationFrame(handlerId)
handlerId = 0
}
handlerId = requestAnimationFrame(() => {
const scrollTop = elemContainer.scrollTop

// 根据滚动距离,推算出当前应该渲染的页索引
const idx = Math.floor(scrollTop / singleLineHeight / numOfItemsInSingleScreen)

const oldStartIdx = startIdx
const oldEndIdx = endIdx

// 当前页往前 1 页
startIdx = Math.max(0, idx - 1)
// 当前页往后 1 页(endIdx 是不渲染的界限,所以加了 2,只会渲染到 idx + 1,即 < idx + 2)
endIdx = idx + 2

/**
* 滚动到最大页就不让继续滚了
* 这里原本正常写法是判断滚动距离加已渲染内容的总长如果达到容器高度就不继续加载后续页面
* 但是判断已渲染内容高度需要频繁读取 DOM 属性,不如直接判断分页数是否达到最大页性能好
*/
if (endIdx > maxPageIdx) {
endIdx = maxPageIdx
startIdx = Math.max(0, endIdx - 3)
}

fillList({
oldStartIdx,
oldEndIdx,
startIdx,
endIdx,
})
handlerId = 0
})
}
})()

/**
* 优化方案
*/
function goodScheme () {
onScroll()
}

elemContainer.addEventListener('scroll', onScroll, {
capture: false,
passive: true,
})

goodScheme()
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion pages/upload/importer.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a03dd9d

Please sign in to comment.