/* @flow */
import type Router from '../index'
import { assert } from './warn'
//getStateKey: 用于获取时间戳key;
//setStateKey: 用于设置时间戳key;
import { getStateKey, setStateKey } from './state-key'
//浅拷贝对象的属性。
import { extend } from './misc'
//用于保存对应的页面的 scrollposition 位置。
const positionStore = Object.create(null)
export function setupScroll() {
/*
history.scrollRestoration。它提供两个值,auto,作为它的默认值,可以像你所见的大多数情况一样工作,
另一个manual,意味着作为一个开发者你拥有了自主掌控任何所需的scroll改变,当用户循环往复于app的history中。
如果需要,你可以跟踪scroll的位置轨迹,当你使用history.pushState(),push history的时候。
*/
//用于组织浏览器在 history popstate 事件中自动滚动窗口。
// Prevent browser scroll behavior on History popstate
if ('scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual'
}
// Fix for #1585 for Firefox
// Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678
// Fix for #2774 Support for apps loaded from Windows file shares not mapped to network drives: replaced location.origin with
// window.location.protocol + '//' + window.location.host
// location.host contains the port and location.hostname doesn't
//浏览器地址为: http://192.168.10.73:10095/message/inforList
//protocolAndPath: "http://192.168.10.73:10095"
const protocolAndPath = window.location.protocol + '//' + window.location.host
//absolutePath: message/inforList
const absolutePath = window.location.href.replace(protocolAndPath, '')
// preserve existing history state as it could be overriden by the user
// stateCopy: {
// key: "777.900"
// }
const stateCopy = extend({}, window.history.state)
//替换掉 stateCopy 的 key 的值。
stateCopy.key = getStateKey()
//repalce 方式跳转到 absolutePath 路径。
window.history.replaceState(stateCopy, '', absolutePath)
//开始监听 history 的 popstate 事件。
window.addEventListener('popstate', handlePopState)
return () => {
//移除对 history 的 popstate 事件的监听。
window.removeEventListener('popstate', handlePopState)
}
}
/*
handleScroll() 函数
router: 路由对象。
to: 前往的路由
from: 上一个路由。
isPop: 是否是退出。
*/
export function handleScroll(
router: Router,
to: Route,
from: Route,
isPop: boolean
) {
//不存在 router 当前指向的 vue 实例。
if (!router.app) {
return
}
//获取创建 router 时,配置的 scrollBehavior 函数。
const behavior = router.options.scrollBehavior
//如果 scrollBehavior 没有配置,则直接返回。
if (!behavior) {
return
}
//scrollBehavior 必须是一个函数。否则警告提示。
if (process.env.NODE_ENV !== 'production') {
assert(typeof behavior === 'function', `scrollBehavior must be a function`)
}
// wait until re-render finishes before scrolling
//在下一个 event loop 周期执行一下函数。
router.app.$nextTick(() => {
//获取最后一次保存的滚动位置记录。
const position = getScrollPosition()
//shouldScroll 是对于滚动位置的配置。
const shouldScroll = behavior.call(
router,
to,
from,
isPop ? position : null
)
//如果shouldScroll 为 null。只直接返回。
if (!shouldScroll) {
return
}
//如果 shouldScroll 是 prommise 对象。
if (typeof shouldScroll.then === 'function') {
//通过 promise.then() 的方式获取要滚动的坐标。
shouldScroll
.then((shouldScroll) => {
scrollToPosition((shouldScroll: any), position)
})
.catch((err) => {
//如果不是生产环境,且产生了异常,则输出错误信息。
if (process.env.NODE_ENV !== 'production') {
assert(false, err.toString())
}
})
} else {
//滚动到指定的位置。
// shouldScroll: 是关于滚动行为的配置。
// position: 是上一次窗口滚动的位置。
scrollToPosition(shouldScroll, position)
}
})
}
/**
* 保存当前滚动的位置。
*/
export function saveScrollPosition() {
//获取一个根据当前时间的时间戳生成的key。
const key = getStateKey()
//如果key存在
if (key) {
//则 positionStore[key] 形式记录滚动的x,y位置。
positionStore[key] = {
x: window.pageXOffset,
y: window.pageYOffset,
}
}
}
/*
用户触发了 popState 事件时,会调用 handlePopState() 方法。
1、调用history.pushState()或者history.replaceState()不会触发popstate事件.
2、popstate事件只会在浏览器某些行为下触发, 比如:
(1)用户主动触发的:点击后退、前进按钮。
(2)代码主动触发:在JavaScript中调用history.back()、history.forward()、history.go()方法。
*/
function handlePopState(e) {
//记录当前 window 滚动的位置。
saveScrollPosition()
//e.state 就是 pushState, replaceState 的第一个参数。
if (e.state && e.state.key) {
//更新存储的 key。
setStateKey(e.state.key)
}
}
/*
获取最后一次保存的滚动记录。
*/
function getScrollPosition(): ?Object {
//获取时间戳生成的 key。
const key = getStateKey()
//如果 key 存在,返回以该 key 存储的滚动位置记录。
if (key) {
return positionStore[key]
}
}
/*
el: 被 selector 选择器指定的 dom 节点。
offset: 偏移量
*/
function getElementPosition(el: Element, offset: Object): Object {
const docEl: any = document.documentElement
//Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。
const docRect = docEl.getBoundingClientRect()
//获取被选中元素的元素的大小,以及相对于视口的位置。
const elRect = el.getBoundingClientRect()
return {
//elRect.left - docRect.left 表示相对于网页 x 轴方向的间距。
x: elRect.left - docRect.left - offset.x,
//elRect.top - doctRect.top 表示相对于网页 y 轴方向的间距。
y: elRect.top - docRect.top - offset.y,
}
}
/*
isValidPosition()
判断是不是可用的坐标。只需要 x 或者 y 一个方向有值即可。
*/
function isValidPosition(obj: Object): boolean {
//判断 x 或者 y 坐标有值,且不为0;
return isNumber(obj.x) || isNumber(obj.y)
}
/*
归一化坐标位置。
*/
function normalizePosition(obj: Object): Object {
//针对只需要 x 或者 y 方向滚动的坐标。不需要滚动的坐标进行数据补齐。
return {
x: isNumber(obj.x) ? obj.x : window.pageXOffset,
y: isNumber(obj.y) ? obj.y : window.pageYOffset,
}
}
/*
归一化偏移位置
*/
function normalizeOffset(obj: Object): Object {
return {
x: isNumber(obj.x) ? obj.x : 0,
y: isNumber(obj.y) ? obj.y : 0,
}
}
/*
判断是不是 number 类型的数据。
*/
function isNumber(v: any): boolean {
return typeof v === 'number'
}
//用于判断是不是 “#数字” 的形式开头的字符串的 的正则表达式对象。
const hashStartsWithNumberRE = /^#\d/
/**
* position: 为上一次保存下来的滚动坐标。
* shouldScroll: 包含滚动位置,元素等的对象。 {
* selector: "xxx" //如果指定了该属性,那么就是根据指定的dom元素的位置计算滚动位置。
* offset: { x, y } //指定 dom 元素的偏移量。
* x, //x坐标
* y, //y坐标
* behavior: "auto|smooth" //指定滚动行为。
* }
*
* 会将滚动坐标转化为 window 窗口的滚动坐标。
*/
function scrollToPosition(shouldScroll, position) {
const isObject = typeof shouldScroll === 'object'
//如果 shouldScroll.selector 存在, 且是字符串类型。
// 则指定的滚动偏移位置,是针对 selector 指定的 dom 元素的偏移位置。
if (isObject && typeof shouldScroll.selector === 'string') {
//获取 selector 指定的锚地 dom 节点。
const el = hashStartsWithNumberRE.test(shouldScroll.selector) // $flow-disable-line
? //shouldScroll.selector.slice(1) 用于去掉 # 号
document.getElementById(shouldScroll.selector.slice(1)) // $flow-disable-line
: document.querySelector(shouldScroll.selector)
//dom元素存在
if (el) {
//获取 shouldScroll 的 offset,作为要滚动的坐标。
let offset =
shouldScroll.offset && typeof shouldScroll.offset === 'object'
? shouldScroll.offset
: {}
//归一化偏移位置。
offset = normalizeOffset(offset)
//需要滚动到的坐标。
position = getElementPosition(el, offset)
} else if (isValidPosition(shouldScroll)) {
//需要滚动的坐标。
position = normalizePosition(shouldScroll)
}
//如果 shouldScroll 是对象类型,且x,y都是数字。
} else if (isObject && isValidPosition(shouldScroll)) {
position = normalizePosition(shouldScroll)
}
}
/* 如果
scrollBehavior( to, from, savedPosition ){
if( savedPosition ){
return savedPosition;
} else {
return { x:0, y:0 }
}
}
返回的是一个对象,则必须带有滚动的坐标。则不再使用 scrollToPosition() 的第二个参数的数据。
如果返回的不是对象,则滚动的坐标就使用 scrollToPosition() 的第二个参数的数据。
*/
//将两种滚动方式的 position 归一化为窗口滚动的坐标后
if (position) {
//判断当前浏览器是否支持 scrollBehavior 的 scroll-behavior 的属性。如果支持,则可以指定 behavior 属性。
// scroll-behavior 属性的值有: smooth,auto; 其中 auto 是默认值。
// (1) auto: 默认值,表示滚动框立即滚动到指定位置。
// (2) smooth: 表示允许滚动时采用平滑过度,而不是直接滚动到相应的位置。
if ('scrollBehavior' in document.documentElement.style) {
window.scrollTo({
left: position.x,
top: position.y,
behavior: shouldScroll.behavior,
})
} else {
//将窗口滚动到 { x:"xxx", y:"xxx" } 的位置。
window.scrollTo(position.x, position.y)
}
}
}
vue-router3 源码注释系列 /src/util/scroll.js
于 2022-03-25 21:00:16 首次发布