Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat intersection observer #1649

Merged
merged 16 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ENV_OBJ } from '../../../common/js'

function createIntersectionObserver (component, options = {}) {
if (options.observeAll) {
options.selectAll = options.observeAll
}
return ENV_OBJ.createIntersectionObserver(options)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import IntersectionObserver from './rnIntersectionObserver'

function createIntersectionObserver (comp, opt, config) {
return new IntersectionObserver(comp, opt, config)
}

export {
createIntersectionObserver
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { isArray, isObject, isString, noop } from '@mpxjs/utils'
import throttle from 'lodash/throttle'
import { Dimensions } from 'react-native'
import { getFocusedNavigation } from '../../../common/js'

const WindowRefStr = 'window'
const IgnoreTarget = 'ignore'
const DefaultMargin = { top: 0, bottom: 0, left: 0, right: 0 }
let idCount = 0

class RNIntersectionObserver {
constructor (component, options, intersectionCtx) {
this.id = idCount++
this.component = component
this.options = options
this.thresholds = options.thresholds.sort((a, b) => a - b) || [0]
this.initialRatio = options.initialRatio || 0
this.observeAll = options.observeAll || false

// 组件上挂载对应的observers,用于在组件销毁的时候进行批量disconnect
this.component._intersectionObservers = this.component.__intersectionObservers || []
this.component._intersectionObservers.push(this)

this.observerRefs = null
this.relativeRef = null
this.margins = DefaultMargin
this.callback = noop

this.throttleMeasure = this.getThrottleMeasure(options.throttleTime || 100)

// 记录上一次相交的比例
this.previousIntersectionRatio = []

// 添加实例添加到上下文中,滚动组件可以获取到上下文内的实例从而触发滚动
if (intersectionCtx && isObject(intersectionCtx)) {
this.intersectionCtx = intersectionCtx
this.intersectionCtx[this.id] = this
}
return this
}

// 支持传递ref 或者 selector
relativeTo (selector, margins = {}) {
let relativeRef
if (isString(selector)) {
relativeRef = this.component.__selectRef(selector, 'node')
}
if (isObject(selector)) {
relativeRef = selector.nodeRefs?.[0]
}
if (relativeRef) {
this.relativeRef = relativeRef
this.margins = Object.assign({}, DefaultMargin, margins)
} else {
console.warn(`node ${selector}is not found. The relative node for intersection observer will be ignored`)
}
return this
}

relativeToViewport (margins = {}) {
this.relativeRef = WindowRefStr
this.margins = Object.assign({}, DefaultMargin, margins)
return this
}

observe (selector, callback) {
if (this.observerRefs) {
console.error('"observe" call can be only called once in IntersectionObserver')
return
}
let targetRef = null
if (this.observeAll) {
targetRef = this.component.__selectRef(selector, 'node', true)
} else {
targetRef = this.component.__selectRef(selector, 'node')
}
if (!targetRef || targetRef.length === 0) {
console.error('intersection observer target not found')
return
}
this.observerRefs = isArray(targetRef) ? targetRef : [targetRef]
this.callback = callback
this._measureTarget(true)
}

_getWindowRect () {
if (this.windowRect) return this.windowRect
const navigation = getFocusedNavigation()
const screen = Dimensions.get('screen')
const navigationLayout = navigation.layout || {
x: 0,
y: 0,
width: screen.width,
height: screen.height
}

const windowRect = {
top: navigationLayout.y + this.margins.top,
left: navigationLayout.x + this.margins.left,
right: navigationLayout.width - this.margins.right,
bottom: navigationLayout.y + navigationLayout.height - this.margins.bottom
}

this.windowRect = windowRect
return this.windowRect
}

_getReferenceRect (targetRef) {
const targetRefs = isArray(targetRef) ? targetRef : [targetRef]
const targetPromiseQueue = []
targetRefs.forEach((targetRefItem) => {
if (targetRefItem === WindowRefStr) {
thuman marked this conversation as resolved.
Show resolved Hide resolved
targetPromiseQueue.push(this._getWindowRect())
return
}
// 当节点前面存在后面移除的时候可能会存在拿不到target的情况,此处直接忽略留一个占位不用做计算即可
// 测试节点移除之后 targetRefItem.getNodeInstance().nodeRef都存在,只是current不存在了
if (!targetRefItem || !targetRefItem.getNodeInstance().nodeRef.current) {
targetPromiseQueue.push(Promise.resolve(IgnoreTarget))
return
}
const target = targetRefItem.getNodeInstance().nodeRef.current
targetPromiseQueue.push(new Promise((resolve) => {
target.measureInWindow(
(x, y, width, height) => {
const boundingClientRect = {
left: x,
top: y,
right: x + width,
bottom: y + height,
width: width,
height: height
}
resolve(boundingClientRect)
}
)
}))
})

if (isArray(targetRef)) {
return Promise.all(targetPromiseQueue)
} else {
return targetPromiseQueue[0]
}
}

_restrictValueInRange (start = 0, end = 0, value = 0) {
return Math.min(Math.max(start, value), end)
}

_isInsectedFn (intersectionRatio, previousIntersectionRatio, thresholds) {
// console.log('nowintersectionRatio, previousIntersectionRatio', [intersectionRatio, previousIntersectionRatio])
let nowIndex = -1
let previousIndex = -1
thresholds.forEach((item, index) => {
if (intersectionRatio >= item) {
nowIndex = index
}
if (previousIntersectionRatio >= item) {
previousIndex = index
}
})
return !(nowIndex === previousIndex)
}

// 计算相交区域
_measureIntersection ({ observeRect, relativeRect, observeIndex, isInit }) {
const visibleRect = {
left: this._restrictValueInRange(relativeRect.left, relativeRect.right, observeRect.left),
top: this._restrictValueInRange(relativeRect.top, relativeRect.bottom, observeRect.top),
right: this._restrictValueInRange(relativeRect.left, relativeRect.right, observeRect.right),
bottom: this._restrictValueInRange(relativeRect.top, relativeRect.bottom, observeRect.bottom)
}

const targetArea = (observeRect.bottom - observeRect.top) * (observeRect.right - observeRect.left)
const visibleArea = (visibleRect.bottom - visibleRect.top) * (visibleRect.right - visibleRect.left)
const intersectionRatio = targetArea ? visibleArea / targetArea : 0

const isInsected = isInit ? intersectionRatio > this.initialRatio : this._isInsectedFn(intersectionRatio, this.previousIntersectionRatio[observeIndex], this.thresholds)
this.previousIntersectionRatio[observeIndex] = intersectionRatio

return {
intersectionRatio,
intersectionRect: {
top: visibleRect.top,
bottom: relativeRect.bottom,
left: visibleRect.left,
right: relativeRect.right
},
isInsected
}
}

getThrottleMeasure (throttleTime) {
return throttle(() => {
this._measureTarget()
}, throttleTime)
}

// 计算节点的rect信息
_measureTarget (isInit = false) {
Promise.all([
this._getReferenceRect(this.observerRefs),
this._getReferenceRect(this.relativeRef)
]).then(([observeRects, relativeRect]) => {
if (relativeRect === IgnoreTarget) return
observeRects.forEach((observeRect, index) => {
if (observeRect === IgnoreTarget) return
const { intersectionRatio, intersectionRect, isInsected } = this._measureIntersection({
observeRect,
observeIndex: index,
relativeRect,
isInit
})
// 初次调用的
if (isInsected) {
this.callback({
// index: index,
id: this.observerRefs[index].getNodeInstance().props?.current?.id,
dataset: this.observerRefs[index].getNodeInstance().props?.current?.dataset || {},
intersectionRatio: Math.round(intersectionRatio * 100) / 100,
intersectionRect,
boundingClientRect: observeRect,
relativeRect: relativeRect,
time: Date.now()
})
}
})
}).catch((e) => {
console.log('_measureTarget fail', e)
})
}

disconnect () {
if (this.intersectionCtx) delete this.intersectionCtx[this.id]
}
}

export default RNIntersectionObserver
5 changes: 5 additions & 0 deletions packages/core/src/core/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ export default class MpxProxy {
if (this.update) this.update.active = false
this.callHook(UNMOUNTED)
this.state = UNMOUNTED
if (this._intersectionObservers) {
this._intersectionObservers.forEach((observer) => {
observer.disconnect()
})
}
}

isUnmounted () {
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/platform/export/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

export {
watchEffect,
watchSyncEffect,
Expand Down
33 changes: 20 additions & 13 deletions packages/core/src/platform/patch/react/getDefaultOptions.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import * as ReactNative from 'react-native'
import { ReactiveEffect } from '../../../observer/effect'
import { watch } from '../../../observer/watch'
import { reactive, set, del } from '../../../observer/reactive'
import { hasOwn, isFunction, noop, isObject, error, getByPath, collectDataset, hump2dash } from '@mpxjs/utils'
import { hasOwn, isFunction, noop, isObject, getByPath, collectDataset, hump2dash } from '@mpxjs/utils'
import MpxProxy from '../../../core/proxy'
import { BEFOREUPDATE, ONLOAD, UPDATED, ONSHOW, ONHIDE, ONRESIZE, REACTHOOKSEXEC } from '../../../core/innerLifecycle'
import mergeOptions from '../../../core/mergeOptions'
import { queueJob } from '../../../observer/scheduler'
import { createSelectorQuery } from '@mpxjs/api-proxy'
import { createSelectorQuery, createIntersectionObserver } from '@mpxjs/api-proxy'
import { IntersectionObserverContext } from '@mpxjs/webpack-plugin/lib/runtime/components/react/dist/context'

function getSystemInfo () {
const window = ReactNative.Dimensions.get('window')
Expand Down Expand Up @@ -68,7 +69,7 @@ function getRootProps (props) {
return rootProps
}

function createInstance ({ propsRef, type, rawOptions, currentInject, validProps, components, pageId }) {
function createInstance ({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx }) {
const instance = Object.create({
setData (data, callback) {
return this.__mpxProxy.forceUpdate(data, { sync: true }, callback)
Expand Down Expand Up @@ -183,8 +184,8 @@ function createInstance ({ propsRef, type, rawOptions, currentInject, validProps
createSelectorQuery () {
return createSelectorQuery().in(this)
},
createIntersectionObserver () {
error('createIntersectionObserver is not supported in react native, please use ref instead')
createIntersectionObserver (opt) {
return createIntersectionObserver(this, opt, intersectionCtx)
},
...rawOptions.methods
}, {
Expand Down Expand Up @@ -349,12 +350,13 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
const defaultOptions = memo(forwardRef((props, ref) => {
const instanceRef = useRef(null)
const propsRef = useRef(null)
const intersectionCtx = useContext(IntersectionObserverContext)
const pageId = useContext(RouteContext)
propsRef.current = props
let isFirst = false
if (!instanceRef.current) {
isFirst = true
instanceRef.current = createInstance({ propsRef, type, rawOptions, currentInject, validProps, components, pageId })
instanceRef.current = createInstance({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx })
}
const instance = instanceRef.current
useImperativeHandle(ref, () => {
Expand Down Expand Up @@ -418,6 +420,7 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
const pageConfig = Object.assign({}, global.__mpxPageConfig, currentInject.pageConfig)
const Page = ({ navigation, route }) => {
const currentPageId = useMemo(() => ++pageId, [])
const intersectionObservers = useRef({})
usePageStatus(navigation, currentPageId)

useLayoutEffect(() => {
Expand Down Expand Up @@ -472,12 +475,17 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
{
value: currentPageId
},
createElement(defaultOptions,
{
navigation,
route,
id: currentPageId
}
createElement(IntersectionObserverContext.Provider,
{
value: intersectionObservers.current
},
createElement(defaultOptions,
{
navigation,
route,
id: currentPageId
}
)
)
)
)
Expand All @@ -487,6 +495,5 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
}
return Page
}

return defaultOptions
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export interface FormContextValue {
reset: () => void
}

export interface IntersectionObserver {
[key: number]: {
throttleMeasure: () => void
}
}

export const MovableAreaContext = createContext({ width: 0, height: 0 })

export const FormContext = createContext<FormContextValue | null>(null)
Expand All @@ -38,3 +44,5 @@ export const LabelContext = createContext<LabelContextValue | null>(null)
export const PickerContext = createContext(null)

export const VarContext = createContext({})

export const IntersectionObserverContext = createContext<IntersectionObserver | null>(null)
Loading
Loading