日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

在開始今天的文章之前,大家不妨先想一下觸發(fā) React 組件 re-render 的原因有哪些,或者說什么時(shí)候 React 組件會(huì)發(fā)生 re-render 。

先說結(jié)論:

  • 狀態(tài)變化
  • 父組件 re-render
  • Context 變化
  • Hooks 變化

?

這里有個(gè)誤解:props 變化也會(huì)導(dǎo)致 re-render。
其實(shí)不會(huì)的,props 的變化往上追溯是因?yàn)楦附M件的 state 變化導(dǎo)致父組件 re-render,從而引起了子組件的 re-render,與 props 是否變化無關(guān)的。只有那些使用了 React.memo 和 useMemo 的組件,props 的變化才會(huì)觸發(fā)組件的 re-render。

?

針對上述造成 re-render 的原因,又該通過怎樣的策略優(yōu)化呢?感興趣的朋友可以看這篇文章:React re-renders guide: everything, all at once。

接下來開始我們今天的主題:如何優(yōu)雅的使用 React Context。上面我們提到了 Context 的變化也會(huì)觸發(fā)組件的 re-render,那 React Context 又是怎么工作呢?先簡單介紹一下 Context 的工作原理。

Context 的工作原理

?

Context 是 React 提供的一種直接訪問祖先節(jié)點(diǎn)上的狀態(tài)的方法,從而避免了多級組件層層傳遞 props 的頻繁操作。

?

創(chuàng)建 Context

通過 React.createContext 創(chuàng)建 Context 對象

export function createContext(
  defaultValue
) {
  const context = {
    $$typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue, 
    _currentValue2: defaultValue, 
    _threadCount: 0,
    Provider: (null: any),
    Consumer: (null: any),
  };

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = context;
  return context;
}

React.createContext 的核心邏輯:

  1. 將初始值存儲(chǔ)在 context._currentValue
  2. 創(chuàng)建 Context.Provider 和 Context.Consumer 對應(yīng)的 ReactElement 對象

在 fiber 樹渲染時(shí),通過不同的 workInProgress.tag 處理 Context.Provider 和 Context.Consumer 類型的節(jié)點(diǎn)。

主要看下針對 Context.Provider 的處理邏輯:

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  const providerType = workInProgress.type;
  const context = providerType._context;
  
  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;
  
  const newValue = newProps.value;

  pushProvider(workInProgress, context, newValue);

  if (oldProps !== null) {
    // 更新 context 的核心邏輯
  }

  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

消費(fèi) Context

在 React 中提供了 3 種消費(fèi) Context 的方式

  1. 直接使用 Context.Consumer 組件(也就是上面 createContext 時(shí)創(chuàng)建的 Consumer)
  2. 類組件中,可以通過靜態(tài)屬性 contextType 消費(fèi) Context
  3. 函數(shù)組件中,可以通過 useContext 消費(fèi) Context

這三種方式內(nèi)部都會(huì)調(diào)用 prepareToReadContext 和 readContext 處理 Context。prepareToReadContext 中主要是重置全局變量為readContext 做準(zhǔn)備。

接下來主要看下readContext :

export function readContext<T>(
  context: ReactContext<T>,
  observedBits: void | number | boolean,
): T {
  const contextItem = {
    context: ((context: any): ReactContext<mixed>),
    observedBits: resolvedObservedBits,
    next: null,
  };

  if (lastContextDependency === null) {
    lastContextDependency = contextItem;
    currentlyRenderingFiber.dependencies = {
      lanes: NoLanes,
      firstContext: contextItem,
      responders: null,
    };
  } else {
    lastContextDependency = lastContextDependency.next = contextItem;
  }

  // 2. 返回 currentValue
  return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}

readContext的核心邏輯:

  1. 構(gòu)建 contextItem 并添加到 workInProgress.dependencies 鏈表(contextItem 中保存了對當(dāng)前 context 的引用,這樣在后續(xù)更新時(shí),就可以判斷當(dāng)前 fiber 是否依賴了 context ,從而判斷是否需要 re-render)
  2. 返回對應(yīng) context 的 _currentValue 值

更新 Context

當(dāng)觸發(fā) Context.Provider 的 re-render 時(shí),重新走 updateContextProvider 中更新的邏輯:

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  // ...
  // 更新邏輯
  if (oldProps !== null) {
      const oldValue = oldProps.value;
      if (is(oldValue, newValue)) {
        // 1. value 未發(fā)生變化時(shí),直接走 bAIlout 邏輯
        if (
          oldProps.children === newProps.children &&
          !hasLegacyContextChanged()
        ) {
          return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderLanes,
          );
        }
      } else {
        // 2. value 變更時(shí),走更新邏輯
        propagateContextChange(workInProgress, context, renderLanes);
      }
  //...
}

接下來看下 propagateContextChange (核心邏輯在 propagateContextChange_eager 中) 的邏輯:

function propagateContextChange_eager < T > (
    workInProgress: Fiber,
    context: ReactContext < T > ,
    renderLanes: Lanes,
): void {
    let fiber = workInProgress.child;
    if (fiber !== null) {
        fiber.return = workInProgress;
    }
    // 從子節(jié)點(diǎn)開始匹配是否存在消費(fèi)了當(dāng)前 Context 的節(jié)點(diǎn)
    while (fiber !== null) {
        let nextFiber;

        const list = fiber.dependencies;
        if (list !== null) {
            nextFiber = fiber.child;

            let dependency = list.firstContext;
            while (dependency !== null) {
                // 1. 判斷 fiber 節(jié)點(diǎn)的 context 和當(dāng)前 context 是否匹配
                if (dependency.context === context) {
                    // 2. 匹配時(shí),給當(dāng)前節(jié)點(diǎn)調(diào)度一個(gè)更新任務(wù)
                    if (fiber.tag === ClassComponent) {}

                    fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
                    const alternate = fiber.alternate;
                    if (alternate !== null) {
                        alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
                    }
                    // 3. 向上標(biāo)記 childLanes
                    scheduleContextWorkOnParentPath(
                        fiber.return,
                        renderLanes,
                        workInProgress,
                    );

                    list.lanes = mergeLanes(list.lanes, renderLanes);
                    break;
                }
                dependency = dependency.next;
            }
        } else if (fiber.tag === ContextProvider) {} else if (fiber.tag === DehydratedFragment) {} else {}

        // ...
        fiber = nextFiber;
    }
}

核心邏輯:

  1. 從 ContextProvider 的節(jié)點(diǎn)出發(fā),向下查找所有 fiber.dependencies 依賴當(dāng)前 Context 的節(jié)點(diǎn)
  2. 找到消費(fèi)節(jié)點(diǎn)時(shí),從當(dāng)前節(jié)點(diǎn)出發(fā),向上回溯標(biāo)記父節(jié)點(diǎn) fiber.childLanes,標(biāo)識其子節(jié)點(diǎn)需要更新,從而保證了所有消費(fèi)了該 Context 的子節(jié)點(diǎn)都會(huì)被重新渲染,實(shí)現(xiàn)了 Context 的更新

總結(jié)

  1. 在消費(fèi)階段,消費(fèi)者通過 readContext 獲取最新狀態(tài),并通過 fiber.dependencies 關(guān)聯(lián)當(dāng)前 Context
  2. 在更新階段,從 ContextProvider 節(jié)點(diǎn)出發(fā)查找所有消費(fèi)了該 context 的節(jié)點(diǎn)

如何避免 Context 引起的 re-render

?

從上面分析 Context 的整個(gè)工作流程,我們可以知道當(dāng) ContextProvider 接收到 value 變化時(shí)就會(huì)找到所有消費(fèi)了該 Context 的組件進(jìn)行 re-render,若 ContextProvider 的 value 是一個(gè)對象時(shí),即使沒有使用到發(fā)生變化的 value 的組件也會(huì)造成多次不必要的 re-render。

?

那我們怎么做優(yōu)化呢?直接說方案:

  1. 將 ContextProvider 的值做 memoize 處理
  2. 對數(shù)據(jù)和 API 做拆分(或者說是將 getter(state)和 setter(API)做拆分)
  3. 對數(shù)據(jù)做拆分(細(xì)粒度拆分)
  4. Context Selector

具體的 case 可參考上述提到的優(yōu)化文章:React re-renders guide: everything, all at once。

接下來開始我們今天的重點(diǎn):Context Selector。開始之前先來個(gè) case1:

import React, { useState } from "react";
const StateContext = React.createContext(null);

const StateProvider = ({ children }) => {
 console.log("StateProvider render");
 
 const [count1, setCount1] = useState(1);
 const [count2, setCount2] = useState(1);
 return (
  <StateContext.Provider 
   value={{ count1, setCount1, count2, setCount2 }}>
   {children}
  </StateContext.Provider>
 );
};

const Counter1 = () => {
 console.log("count1 render");
 
 const { count1, setCount1 } = React.useContext(StateContext);
 return (
  <>
   <div>Count1: {count1}</div>
   <button 
    onClick={() => setCount1((n) => n + 1)}>setCount1</button>
 </>
);
};

const Counter2 = () => {
 console.log("count2 render");
 
 const { count2, setCount2 } = React.useContext(StateContext);
 
 return (
  <>
   <div>Count2: {count2}</div>
   <button onClick={() => setCount2((n) => n + 1)}>setCount2</button>
  </>
 );
};

const App = () => {
 return (
  <StateProvider>
   <Counter1 />
   <Counter2 />
  </StateProvider>
 );
};

export default App;

?

開發(fā)環(huán)境記得關(guān)閉 StrictMode 模式,否則每次 re-render 都會(huì)走兩遍。具體使用方式和 StrictMode 的意義可參考官方文檔。

?

通過上面的 case,我們會(huì)發(fā)現(xiàn)在 count1 觸發(fā)更新時(shí),即使 Counter2 沒有使用 count1 也會(huì)進(jìn)行 re-render。這是因?yàn)?nbsp;count1 的更新會(huì)引起 StateProvider 的 re-render,從而會(huì)導(dǎo)致 StateProvider 的 value 生成全新的對象,觸發(fā) ContextProvider 的 re-render,找到當(dāng)前 Context 的所有消費(fèi)者進(jìn)行 re-render。

如何做到只有使用到 Context 的 value 改變才觸發(fā)組件的 re-render 呢?社區(qū)有一個(gè)對應(yīng)的解決方案 dai-shi/use-context-selector: React useContextSelector hook in userland。

接下來我們改造一下上述的 case2:

import React, { useState } from 'react';
import { createContext, useContextSelector } from 'use-context-selector';

const context = createContext(null);

const Counter1 = () => {
  const count1 = useContextSelector(context, v => v[0].count1);
  const setState = useContextSelector(context, v => v[1]);
  const increment = () => setState(s => ({
    ...s,
    count1: s.count1 + 1,
  }));
  return (
    <div>
      <span>Count1: {count1}</span>
      <button type="button" onClick={increment}>+1</button>
      {Math.random()}
    </div>
  );
};

const Counter2 = () => {
  const count2 = useContextSelector(context, v => v[0].count2);
  const setState = useContextSelector(context, v => v[1]);
  const increment = () => setState(s => ({
    ...s,
    count2: s.count2 + 1,
  }));
  return (
    <div>
      <span>Count2: {count2}</span>
      <button type="button" onClick={increment}>+1</button>
      {Math.random()}
    </div>
  );
};

const StateProvider = ({ children }) => (
  <context.Provider value={useState({ count1: 0, count2: 0 })}>
    {children}
  </context.Provider>
);

const App = () => (
  <StateProvider>
    <Counter1 />
    <Counter2 />
  </StateProvider>
);

export default App

?

這時(shí)候問題來了,不是說好精準(zhǔn)渲染的嗎?怎么還是都會(huì)進(jìn)行 re-render。
解決方案:將 react 改為 v17 版本(v17對應(yīng)的case3),后面我們再說具體原因(只想說好坑..)。

?

use-context-selector

接下來我們主要分析下 createContext 和 useContextSelector 都做了什么(官方還有其他的 API ,感興趣的朋友可以自行查看,核心還是這兩個(gè) API)。

createContext

簡化一下,只看核心邏輯:

import { createElement, useLayoutEffect, useRef, createContext as createContextOrig } from 'react'
const CONTEXT_VALUE = Symbol();
const ORIGINAL_PROVIDER = Symbol();

const createProvider = (
  ProviderOrig
) => {
  const ContextProvider = ({ value, children }) => {
    const valueRef = useRef(value);
    const contextValue = useRef();
    
    if (!contextValue.current) {
      const listeners = new Set();
      contextValue.current = {
        [CONTEXT_VALUE]: {
          /* "v"alue     */ v: valueRef,
          /* "l"isteners */ l: listeners,
        },
      };
    }
    useLayoutEffect(() => {
      valueRef.current = value;
  contextValue.current[CONTEXT_VALUE].l.forEach((listener) => {
          listener({ v: value });
        });
    }, [value]);
    
    return createElement(ProviderOrig, { value: contextValue.current }, children);
  };
  return ContextProvider;
};

export function createContext(defaultValue) {
  const context = createContextOrig({
    [CONTEXT_VALUE]: {
      /* "v"alue     */ v: { current: defaultValue },
      /* "l"isteners */ l: new Set(),
    },
  });
  context[ORIGINAL_PROVIDER] = context.Provider;
  context.Provider = createProvider(context.Provider);
  delete context.Consumer; // no support for Consumer
  return context;
}

對原始的 createContext 包一層,同時(shí)為了避免 value 的意外更新造成消費(fèi)者的不必要 re-render ,將傳遞給原始的 createContext 的 value 通過 uesRef 進(jìn)行存儲(chǔ),這樣在 React 內(nèi)部對比新舊 value 值時(shí)就不會(huì)再操作 re-render(后續(xù) value 改變后派發(fā)更新時(shí)就需要通過 listener 進(jìn)行 re-render 了),最后返回包裹后的 createContext 給用戶使用。

useContextSelector

接下來看下簡化后的 useContextSelector :

export function useContextSelector(context, selector) {
 const contextValue = useContextOrig(context)[CONTEXT_VALUE];
 const {
 /* "v"alue */ v: { current: value },
 /* "l"isteners */ l: listeners
 } = contextValue;
 
 const selected = selector(value);
 const [state, dispatch] = useReducer(
  (prev, action) => {
   if ("v" in action) {
    if (Object.is(prev[0], action.v)) {
     return prev; // do not update
    }
    const nextSelected = selector(action.v);
    if (Object.is(prev[1], nextSelected)) {
     return prev; // do not update
    }
    return [action.v, nextSelected];
   }
  },
  [value, selected]
 );
 
 useLayoutEffect(() => {
  listeners.add(dispatch);
  return () => {
   listeners.delete(dispatch);
  };
 
 }, [listeners]);
 
 return state[1];
}

核心邏輯:

  1. 每次渲染時(shí),通過 selector 和 value 獲取最新的 selected
  2. 同時(shí)將 useReducer 對應(yīng)的 dispatch 添加到 listeners
  3. 當(dāng) value 改變時(shí),就會(huì)執(zhí)行 listeners 中收集到 dispatch 函數(shù),從而在觸發(fā) reducer 內(nèi)部邏輯,通過對比 value 和 selected 是否有變化,來決定是否觸發(fā)當(dāng)前組件的 re-render

在 react v18 下的 bug

回到上面的 case 在 react v18 的表現(xiàn)和在原始 Context 的表現(xiàn)幾乎一樣,每次都會(huì)觸發(fā)所有消費(fèi)者的 re-render。再看 use-context-selector 內(nèi)部是通過 useReducer 返回的 dispatch 函數(shù)派發(fā)組件更新的。

接下來再看下 useReducer 在 react v18 和 v17 版本到底有什么不一樣呢?
看個(gè)簡單的 case:

import React, { useReducer } from "react";

const initialState = 0;
const reducer = (state, action) => {
 switch (action) {
  case "increment":
   return state;
  default:
   return state;
 }

};

export const App = () => {
 console.log("UseReducer Render");
 const [count, dispatch] = useReducer(reducer, initialState);
 
 return (
  <div>
   <div>Count = {count}</div>
   <button onClick={() => dispatch("increment")}>Inacrement</button>
  </div>
 );
};

簡單描述下:多次點(diǎn)擊按鈕「Inacrement」,在 react 的 v17 和 v18 版本分別會(huì)有什么表現(xiàn)?

先說結(jié)論:

  • v17:只有首次渲染會(huì)觸發(fā) App 組件的 render,后續(xù)點(diǎn)擊將不再觸發(fā) re-render
  • v18:每次都會(huì)觸發(fā) App 組件的 re-render(即使?fàn)顟B(tài)沒有實(shí)質(zhì)性的變化也會(huì)觸發(fā) re-render)

?

這就要說到【eager state 策略】了,在 React 內(nèi)部針對多次觸發(fā)更新,而最后狀態(tài)并不會(huì)發(fā)生實(shí)質(zhì)性變化的情況,組件是沒有必要渲染的,提前就可以中斷更新了。

?

也就是說 useReducer 內(nèi)部是有做一定的性能優(yōu)化的,而這優(yōu)化會(huì)存在一些 bug,最后 React 團(tuán)隊(duì)也在 v18 后移除了該優(yōu)化策略(注:useState 還是保留該優(yōu)化),詳細(xì)可看該相關(guān) PR Remove usereducer eager bailout。當(dāng)然該 PR 在社區(qū)也存在一些討論(Bug: useReducer and same state in React 18),畢竟無實(shí)質(zhì)性的狀態(tài)變更也會(huì)觸發(fā) re-render,對性能還是有一定影響的。

回歸到 useContextSelector ,無優(yōu)化版本的 useReducer 又是如何每次都觸發(fā)組件 re-render 呢?

具體原因:在上面 useReducer 中,是通過 Object.is 判斷 value 是否發(fā)生了實(shí)質(zhì)性變化,若沒有,就返回舊的狀態(tài),在 v17 有優(yōu)化策略下,就不會(huì)再去調(diào)度更新任務(wù)了,而在 v18 沒有優(yōu)化策略的情況下,每次都會(huì)調(diào)度新的更新任務(wù),從而引發(fā)組件的 re-render。

通過 useSyncExternalStore 優(yōu)化

通過分析知道造成 re-render 的原因是使用了 useReducer,那就不再依賴該 hook,使用 react v18 新的 hook useSyncExternalStore 來實(shí)現(xiàn) useContextSelector(優(yōu)化后的 case4)。

export function useContextSelector(context, selector) {
 const contextValue = useContextOrig(context)[CONTEXT_VALUE];
 const {
 /* "v"alue */ v: { current: value },
 /* "l"isteners */ l: listeners
 } = contextValue;
 
 const lastSnapshot = useRef(selector(value));
 const subscribe = useCallback(
  (callback) => {
   listeners.add(callback);
   return () => {
    listeners.delete(callback);
   };
  },
  [listeners]
 );
 
 const getSnapshot = () => {
  const {
  /* "v"alue */ v: { current: value }
  } = contextValue;
  
  const nextSnapshot = selector(value);
  lastSnapshot.current = nextSnapshot;
  return nextSnapshot;
 };
 
 return useSyncExternalStore(subscribe, getSnapshot);
}

實(shí)現(xiàn)思路:

  1. 收集訂閱函數(shù) subscribe 的 callback(即 useSyncExternalStore 內(nèi)部的 handleStoreChange )
  2. 當(dāng) value 發(fā)生變化時(shí),觸發(fā) listeners 收集到的 callback ,也就是執(zhí)行 handleStoreChange 函數(shù),通過 getSnapshot 獲取新舊值,并通過 Object.is 進(jìn)行對比,判斷當(dāng)前組件是否需要更新,從而實(shí)現(xiàn)了 useContextSelector 的精確更新

當(dāng)然除了 useReducer 對應(yīng)的性能問題,use-context-selector 還存在其他的性能,感興趣的朋友可以查看這篇文章從 0 實(shí)現(xiàn) use-context-selector。同時(shí),use-context-selector 也是存在一些限制,比如說不支持 Class 組件、不支持 Consumer …

?

針對上述文章中,作者提到的問題二和問題三,個(gè)人認(rèn)為這并不是 use-context-selector 的問題,而是 React 底層自身帶來的問題。
比如說:問題二,React 組件是否 re-render 跟是否使用了狀態(tài)是沒有關(guān)系的,而是和是否觸發(fā)了更新狀態(tài)的 dispatch 有關(guān),如果一定要和狀態(tài)綁定一起,那不就是 Vue 了嗎。
對于問題三,同樣是 React 底層的優(yōu)化策略處理并沒有做到極致這樣。

?

總結(jié)

回到 React Context 工作原理來看,只要有消費(fèi)者訂閱了該 Context,在該 Context 發(fā)生變化時(shí)就會(huì)觸達(dá)所有的消費(fèi)者。也就是說整個(gè)工作流程都是以 Context 為中心的,那只要把 Context 拆分的粒度足夠小就不會(huì)帶來額外的渲染負(fù)擔(dān)。但是這樣又會(huì)帶來其他問題:ContextProvider 會(huì)嵌套多層,同時(shí)對于粒度的把握對開發(fā)者來說又會(huì)帶來一定的心智負(fù)擔(dān)。

從另一條路出發(fā):Selector 機(jī)制,通過選擇需要的狀態(tài)從而規(guī)避掉無關(guān)的狀態(tài)改變時(shí)帶來的渲染開銷。除了社區(qū)提到的 use-context-selector ,React 團(tuán)隊(duì)也有一個(gè)相應(yīng)的 RFC 方案 RFC: Context selectors,不過這個(gè) RFC 從 19 年開始目前還處于持續(xù)更新階段。

最后,對于 React Context 的使用,個(gè)人推薦:「不頻繁更改的全局狀態(tài)(比如說:自定義主題、賬戶信息、權(quán)限信息等)可以合理使用 Context,而對于其他頻繁修改的全局狀態(tài)可以通過其他數(shù)據(jù)流方式維護(hù),可以更好的避免不必要的 re-render 開銷」

參考

  1. https://www.developerway.com/posts/react-re-renders-guide
  2. https://react.dev/reference/react/StrictMode#enabling-strict-mode-for-entire-app
  3. https://Github.com/dai-shi/use-context-selector
  4. https://github.com/facebook/react/pull/22445
  5. https://github.com/facebook/react/issues/24596
  6. https://react.dev/reference/react/useSyncExternalStore
  7. https://juejin.cn/post/7197972831795380279
  8. https://github.com/reactjs/rfcs/pull/119
  9. case1:https://codesandbox.io/s/serverless-frost-9ryw2x?file=/src/App.js
  10. case2:https://codesandbox.io/s/use-context-selector-vvs93q?file=/src/App.js
  11. case3:https://codesandbox.io/s/elegant-montalcini-nkrvlh?file=/src/App.js
  12. case4:https://codesandbox.io/s/use-context-selector-smsft3?file=/src/App.js

作者:郭曉波

來源:微信公眾號:大轉(zhuǎn)轉(zhuǎn)FE

出處
:https://mp.weixin.qq.com/s/mQyl3baPRvEI_34kT1Us_g

分享到:
標(biāo)簽:React Context
用戶無頭像

網(wǎng)友整理

注冊時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定