Предупреждение о длинном тексте, если вы считаете, что прелюдия слишком длинная, вы можете посмотреть ее прямо из главы 3~
Эта статья основана наReact 16.8.6объяснить
Используемый пример кода:
import React, { useState } from 'react'
import './App.css'
export default function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Star');
// 调用三次setCount便于查看更新队列的情况
const countPlusThree = () => {
setCount(count+1);
setCount(count+2);
setCount(count+3);
}
return (
<div className='App'>
<p>{name} Has Clicked <strong>{count}</strong> Times</p>
<button onClick={countPlusThree}>Click *3</button>
</div>
)
}
Код очень простой, нажмите кнопку, чтобы сделать count+3, и значение count будет отображаться на экране.
1. Необходимые знания
1. Компоненты класса функций и компонентов
Ссылка на этот раздел:How Are Function Components Different from Classes?
Основные понятия в этом разделе:
- Разница между функциональными компонентами и компонентами класса
- Как React различает эти два компонента
Давайте взглянем на простой компонент Greeting, который поддерживает два свойства, определенные как классы и функции. При его использовании не важно, как оно определено.
// 是类还是函数 —— 无所谓
<Greeting /> // <p>Hello</p>
еслиGreeting
это функция, которую React должен вызвать.
// Greeting.js
function Greeting() {
return <p>Hello</p>;
}
// React 内部
const result = Greeting(props); // <p>Hello</p>
но еслиGreeting
Это класс, и React должен сначала создать его экземпляр, а затем вызвать тот, который только что сгенерировал экземпляр.render
метод:
// Greeting.js
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
// React 内部
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
React определяет тип компонента:
// React 内部
class Component {}
Component.prototype.isReactComponent = {};
// 检查方式
class Greeting extends React.Component {}
console.log(Greeting.prototype.isReactComponent); // {}
2. React Fiber
Ссылки в этом разделе:A cartoon intro to fiber
Основные понятия этого раздела (просто поймите):
- Рендеринг React теперь запланирован Fiber
- Два этапа в процессе планирования волокна (ограничено рендерингом)
Волокно (которое можно перевести как шелк) обладает более тонкой детализацией управления, чем потоки, и является новой функцией в React 16, цель которой — более тонкие настройки процесса рендеринга.
причина:
- Согласование перед волокном (называемое согласователем стека) рекурсия сверху вниз
mount/update
, который не может быть прерван (постоянно занимает основной поток), поэтому периодические задачи, такие как макет, анимация и интерактивные ответы в основном потоке, не могут быть обработаны немедленно, что влияет на работу. - Нет приоритета во время рендеринга
Путь React Fiber:
Разделите трудоемкую задачу на множество маленьких частей. Время выполнения каждой маленькой части очень короткое. Хотя общее время по-прежнему очень велико, после выполнения каждой маленькой части другим задачам дается возможность выполниться, так что только поток не будет эксклюзивным, другие задачи все еще имеют возможность запускаться.
React Fiber фрагментирует процесс обновления. Процесс выполнения показан на рисунке ниже. После выполнения каждого процесса обновления управление возвращается модулю React, отвечающему за координацию задач, чтобы узнать, есть ли другие срочные задачи, которые необходимо выполнить. нет, то Продолжить обновление, если есть срочная задача, сделать срочную задачу.
Структура данных, которая поддерживает каждый сегмент, — это Fiber.
После сегментирования стек вызовов процесса обновления показан на рисунке ниже.Каждая впадина посередине представляет собой выполнение процесса углубления в сегмент. Позвольте потоку заниматься другими делами
Процесс планирования оптоволокна делится на следующие два этапа:
этап рендеринга/согласования— Все функции жизненного цикла в нем могут выполняться несколько раз, поэтому старайтесь сохранять состояние неизменным
- componentWillMount
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
Стадия фиксации— нельзя прервать, выполняется только один раз
- componentDidMount
- componentDidUpdate
- compoenntWillunmount
Инкрементное обновление Fiber требует больше контекстной информации, а предыдущее дерево vDOM, очевидно, трудно удовлетворить, поэтому дерево Fiber (то есть дерево vDOM контекста Fiber) расширяется Процесс обновления заключается в построении нового дерева Fiber. на основе входных данных и существующего дерева волокон Дерево волокон (дерево workInProgress)
Все коды, относящиеся к волокну, находятся по адресуpackages/react-reconciler, подробное определение узла Fiber выглядит следующим образом:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag; this.key = key; this.elementType = null;
this.type = null; this.stateNode = null;
// Fiber
this.return = null; this.child = null; this.sibling = null;
this.index = 0; this.ref = null; this.pendingProps = pendingProps;
this.memoizedProps = null; this.updateQueue = null;
// 重点
this.memoizedState = null;
this.contextDependencies = null; this.mode = mode;
// Effects
/** 细节略 **/
}
мы просто обращаем вниманиеthis.memoizedState
этоkey
Используется для хранения конечного узла, полученного во время последнего прохода рендеринга.state
,каждый разrender
Раньше React вычислял последние значения текущего компонента.state
Затем назначьте его компоненту, а затем выполнитеrender
.— Доступны как компоненты класса, так и компоненты функций, использующие useState.
Помните приведенное выше предложение, memoizedState часто будет упоминаться позже.
Конкретное значение каждого ключа Fiber см.комментарии к исходному коду
3. React визуализатор и setState
Ссылки в этом разделе:How Does setState Know What to Do?
Основные понятия в этом разделе:
- Что такое рендерер React
- Почему setState может запускать обновление
Из-за сложности системы React и разнообразия целевых платформ.react
Пакеты предоставляют только некоторые API, определяющие компоненты. Подавляющее большинство реализаций React живут в рендерерах.
react-dom
,react-dom/server
,react-native
,react-test-renderer
,react-art
являются обычными рендерерами
Вот почему независимо от целевой платформы,react
Пакеты доступны. отreact
Все экспортируется в пакете, какReact.Component
,React.createElement
,React.Children
а такжеHooks
не зависят от целевой платформы. Компоненты можно импортировать и использовать одинаково при запуске React DOM, React DOM Server или React Native.
Поэтому, когда мы хотим использовать новые функции,react
а такжеreact-dom
Оба требуют обновления.
Например, когда в React 16.3 был добавлен Context API,
React.createContext()
API будет предоставлен пакетом React. ноReact.createContext()
На самом деле нет _реализованного_ контекста. Потому что один и тот же API должен иметь разные реализации в React DOM и React DOM Server. такcreateContext()
Возвращаются только некоторые простые объекты: ** Итак, если вы обновляете react до версии 16.3+, но не обновляете react-dom, то вы используете рендерер, который еще не знает типы Provider и Consumer. **Вот почему старая версияreact-dom
МогуОшибка о том, что эти типы недействительны.
ЭтоsetState
Причина обновления DOM при вызове, несмотря на то, что она определена в пакете React. Он читает набор React DOMthis.updater
, пусть React DOM запланирует и обработает обновление.
Component.setState = function(partialState, callback) {
// setState所做的一切就是委托渲染器创建这个组件的实例
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Средство обновления в каждом средстве визуализации запускает обновление для разных платформ.
// React DOM 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;
// React DOM Server 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;
// React Native 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;
Что касается конкретной реализации апдейтера, то она здесь не в центре обсуждения, давайте официально перейдем к теме этой статьи: React Hooks.
2. Понимание состояния использования
1. Знакомство с useState и запуском обновлений
Основные понятия в этом разделе:
- Как вводится и вызывается useState
- Почему useState может инициировать обновление компонента
Все крючки вReact.js
Представлено в , смонтировано в объектах React
// React.js
import {
useCallback,
useContext,
useEffect,
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from './ReactHooks';
мы входимReactHooks.js
приходите посмотреть и открытьuseState
Реализация на удивление проста, всего две короткие строчки
// ReactHooks.js
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
Кажется, на этом акцент сделанdispatcher
начальство,dispatcher
пройти черезresolveDispatcher()
получить эту функцию тоже очень просто, достаточноReactCurrentDispatcher.current
Значение присваиваетсяdispatcher
// ReactHooks.js
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
такuseState(xxx)
ЭквивалентноReactCurrentDispatcher.current.useState(xxx)
Увидев это, давайте рассмотрим средство визуализации React и setState, упомянутые в третьем подразделе главы 1. Вам это кажется немного знакомым?
Подобно средству обновления, ядро которого setState может запускать обновления,ReactCurrentDispatcher.current.useState
даuseState
Основная причина, по которой можно запустить обновление, заключается в том, что реализация этого метода не находится в пакете реагирования. Разберем пример конкретного обновления.
2. Пример анализа
В качестве примера возьмем код, приведенный в начале полного текста.
Начнем с планирования оптоволокна:ReactFiberBeginwork
давай поговорим
Как я уже говорил, React может различать разные компоненты, поэтому он по-разному помечает разные типы компонентов, см. подробности.shared/ReactWorkTags.js.
Таким образом, в функции beginWork вы можете использовать различные методы для загрузки или обновления компонентов в соответствии со значением тега на workInProgess (то есть на узле Fiber).
// ReactFiberBeginWork.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
/** 省略与本文无关的部分 **/
// 根据不同的组件类型走不同的方法
switch (workInProgress.tag) {
// 不确定组件
case IndeterminateComponent: {
const elementType = workInProgress.elementType;
// 加载初始组件
return mountIndeterminateComponent(
current,
workInProgress,
elementType,
renderExpirationTime,
);
}
// 函数组件
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
// 更新函数组件
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
// 类组件
case ClassComponent {
/** 细节略 **/
}
}
Давайте выясним, где в игру вступает useState.
2.1 Загрузка в первый раз
выполнение процесса монтированияmountIndeterminateComponent
, будет выполняться до тех пор, покаrenderWithHooks
эта функция
function mountIndeterminateComponent(
_current,
workInProgress,
Component,
renderExpirationTime,
) {
/** 省略准备阶段代码 **/
// value就是渲染出来的APP组件
let value;
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderExpirationTime,
);
/** 省略无关代码 **/
}
workInProgress.tag = FunctionComponent;
reconcileChildren(null, workInProgress, value, renderExpirationTime);
return workInProgress.child;
}
Перед выполнением: nextChildren = value
После выполнения:value= виртуальное DOM-представление компонента
Что касается того, как это значение отображается в реальном узле DOM, нас не волнует, значение состояния было получено и отображено с помощью renderWithHooks.
2.2 Обновление
Нажмите кнопку: в это время счетчик изменится с 0 на 3
Процесс обновления выполняет функцию updateFunctionComponent, которая также выполнит функцию renderWithHooks.Давайте посмотрим на изменения до и после выполнения этой функции:
Перед выполнением: nextChildren = undefined
**После выполнения:**nextChildren=Виртуальное DOM-представление обновленного компонента
Точно так же, как этот nextChildren визуализируется в реальный узел DOM, нас не волнует последнее значение состояния, которое мы получили и отрендерили с помощью renderWithHooks.
так,renderWithHooks
Функции являются основной частью логики обработки различных хуков.
3. Анализ основных шагов
ReactFiberHooks.jsСодержит различную логическую обработку хуков, код в этой главе взят из этого файла.
1. Зацепить объект
Представленный в предыдущей главе, FibermemorizedStated
используется для хранения состояния
в компоненте классаstate
представляет собой цельный объект, который можно комбинировать сmemoizedState
Переписка один на один. Но когдаHooks
, React не знает, сколько раз мы вызывалиuseState
,Итак, React монтирует объект Hook вmemorizedStated
до сохранения функционального компонентаstate
Структура объекта Hook следующая:
// ReactFiberHooks.js
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
};
ФокусmemoizedState
а такжеnext
-
memoizedState
используется для записи текущегоuseState
результат, который должен быть возвращен -
queue
: кэшировать очередь, хранить несколько вариантов поведения при обновлении. -
next
: указать на следующийuseState
Соответствующий объект Hook.
Взгляните на пример кода:
import React, { useState } from 'react'
import './App.css'
export default function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Star');
// 调用三次setCount便于查看更新队列的情况
const countPlusThree = () => {
setCount(count+1);
setCount(count+2);
setCount(count+3);
}
return (
<div className='App'>
<p>{name} Has Clicked <strong>{count}</strong> Times</p>
<button onClick={countPlusThree}>Click *3</button>
</div>
)
}
При первом нажатии кнопки для запуска обновления структура memoizedState выглядит следующим образом.
Это просто соответствует предыдущему анализу структуры объекта Hook, но структура в очереди кажется немного странной, и мы проанализируем ее в главе 3, разделе 2.
2. renderWithHooks
Процесс выполнения renderWithHooks выглядит следующим образом:
// ReactFiberHooks.js
export function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
props: any,
refOrContext: any,
nextRenderExpirationTime: ExpirationTime,
): any {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
// 如果current的值为空,说明还没有hook对象被挂载
// 而根据hook对象结构可知,current.memoizedState指向下一个current
nextCurrentHook = current !== null ? current.memoizedState : null;
// 用nextCurrentHook的值来区分mount和update,设置不同的dispatcher
ReactCurrentDispatcher.current =
nextCurrentHook === null
// 初始化时
? HooksDispatcherOnMount
// 更新时
: HooksDispatcherOnUpdate;
// 此时已经有了新的dispatcher,在调用Component时就可以拿到新的对象
let children = Component(props, refOrContext);
// 重置
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const renderedWork: Fiber = (currentlyRenderingFiber: any);
// 更新memoizedState和updateQueue
renderedWork.memoizedState = firstWorkInProgressHook;
renderedWork.updateQueue = (componentUpdateQueue: any);
/** 省略与本文无关的部分代码,便于理解 **/
}
2.1 Во время инициализации
основной:Создайте новый хук, инициализируйте состояние и привяжите триггеры
фаза инициализацииReactCurrentDispatcher.current
укажет наHooksDispatcherOnMount
объект
// ReactFiberHooks.js
const HooksDispatcherOnMount: Dispatcher = {
/** 省略其它Hooks **/
useState: mountState,
};
// 所以调用useState(0)返回的就是HooksDispatcherOnMount.useState(0),也就是mountState(0)
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 访问Hook链表的下一个节点,获取到新的Hook对象
const hook = mountWorkInProgressHook();
//如果入参是function则会调用,但是不提供参数
if (typeof initialState === 'function') {
initialState = initialState();
}
// 进行state的初始化工作
hook.memoizedState = hook.baseState = initialState;
// 进行queue的初始化工作
const queue = (hook.queue = {
last: null,
dispatch: null,
eagerReducer: basicStateReducer, // useState使用基础reducer
eagerState: (initialState: any),
});
// 返回触发器
const dispatch: Dispatch<BasicStateAction<S>,>
= (queue.dispatch = (dispatchAction.bind(
null,
//绑定当前fiber结点和queue
((currentlyRenderingFiber: any): Fiber),
queue,
));
// 返回初始state和触发器
return [hook.memoizedState, dispatch];
}
// 对于useState触发的update action来说(假设useState里面都传的变量),basicStateReducer就是直接返回action的值
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
Сосредоточьтесь на возвращенной функции обновленияdispatchAction
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
/** 省略Fiber调度相关代码 **/
// 创建新的新的update, action就是我们setCount里面的值(count+1, count+2, count+3…)
const update: Update<S, A> = {
expirationTime,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// 重点:构建query
// queue.last是最近的一次更新,然后last.next开始是每一次的action
const last = queue.last;
if (last === null) {
// 只有一个update, 自己指自己-形成环
update.next = update;
} else {
const first = last.next;
if (first !== null) {
update.next = first;
}
last.next = update;
}
queue.last = update;
/** 省略特殊情况相关代码 **/
// 创建一个更新任务
scheduleWork(fiber, expirationTime);
}
существуетdispatchAction
Структура данных запроса поддерживается в .
запрос представляет собой циклический связанный список, правила таковы:
- query.last указывает на последнее обновление
- last.next указывает на первое обновление
- И так далее, и, наконец, предпоследнее обновление указывает на последнее, образуя кольцо.
Поэтому каждый раз, когда вы вставляете новое обновление, вам нужно сначала указать оригинал на query.last.next. Затем укажите update на query.next и, наконец, укажите query.last на update.
Следующее объединено с образцом кода, чтобы проиллюстрировать рисунок:
Значение запроса в memorizedState при первом обновлении кнопки дается ранее
Процесс его построения показан на следующем рисунке:
Это делается для того, чтобы query.last всегда было последним действием, а query.last.next всегда было действием: 1
2.2 При обновлении
основной: получить очередь в объекте Hook, которая содержит ряд данных для этого обновления, и обновить ее.
фаза обновленияReactCurrentDispatcher.current
укажет наHooksDispatcherOnUpdate
объект
// ReactFiberHooks.js
// 所以调用useState(0)返回的就是HooksDispatcherOnUpdate.useState(0),也就是updateReducer(basicStateReducer, 0)
const HooksDispatcherOnUpdate: Dispatcher = {
/** 省略其它Hooks **/
useState: updateState,
}
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
// 可以看到updateReducer的过程与传的initalState已经无关了,所以初始值只在第一次被使用
// 为了方便阅读,删去了一些无关代码
// 查看完整代码:https://github.com/facebook/react/blob/487f4bf2ee7c86176637544c5473328f96ca0ba2/packages/react-reconciler/src/ReactFiberHooks.js#L606
function updateReducer(reducer, initialArg, init) {
// 获取初始化时的 hook
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 开始渲染更新
if (numberOfReRenders > 0) {
const dispatch = queue.dispatch;
if (renderPhaseUpdates !== null) {
// 获取Hook对象上的 queue,内部存有本次更新的一系列数据
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
renderPhaseUpdates.delete(queue);
let newState = hook.memoizedState;
let update = firstRenderPhaseUpdate;
// 获取更新后的state
do {
const action = update.action;
// 此时的reducer是basicStateReducer,直接返回action的值
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
// 对 更新hook.memoized
hook.memoizedState = newState;
// 返回新的 state,及更新 hook 的 dispatch 方法
return [newState, dispatch];
}
}
}
// 对于useState触发的update action来说(假设useState里面都传的变量),basicStateReducer就是直接返回action的值
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
2.3 Резюме
Поведение обновления одного хука полностью подвешено под Hooks.queue, поэтому ядром возможности управления очередью является
- Инициализировать очередь — mountState
- Оходение в очередь - диспециация
- очередь обновлений - updateReducer
В сочетании с примером кода:
- когда мы впервые звоним
[count, setCount] = useState(0)
, создать очередь - каждый звонок
setCount(x)
, просто отправьте действие, содержимое которого равно x (производительность действия: установите счетчик равным x), действие сохраняется в очереди и поддерживается правилами кольцевого списка, описанными выше. - Эти действия заканчиваются
updateReducer
вызывается, обновляется доmemorizedState
, чтобы мы могли получить последнее значение состояния.
4. Резюме
1. Понимание правил хуков в официальных документах
официальная документацияЕсть два требования к использованию хуков:
2.1 Почему это нельзя выполнить в цикле/условном операторе
Возьмите useState в качестве примера:
В отличие от компонентов класса, которые хранят состояние, React не знает, сколько раз мы вызывалиuseState
, хранение хуков в порядке (см. структуру Hook), и следующий объект хука указывает на следующие хуки. Итак, когда мы устанавливаем соответствующие отношения в примере кода, структура хука выглядит следующим образом:
// hook1: const [count, setCount] = useState(0) — 拿到state1
{
memorizedState: 0
next : {
// hook2: const [name, setName] = useState('Star') - 拿到state2
memorizedState: 'Star'
next : {
null
}
}
}
// hook1 => Fiber.memoizedState
// state1 === hook1.memoizedState
// hook1.next => hook2
// state2 === hook2.memoizedState
Таким образом, если крючок1 помещается в оператор if, когда он не выполняется, состояние, полученное крюком2, на самом деле является состоянием после выполнения последнего крючка1 (вместо последнего времени после выполнения последнего крючка2). Это, очевидно, вызовет ошибку.
Если вы хотите узнать больше об этом, пожалуйста, прочитайтеэта статья
2.2 Почему хуки можно использовать только в функциональных компонентах
Только обновление функционального компонента запустит функцию renderWithHooks для обработки логики, связанной с хуками.
Возьмем setState в качестве примера, логика повторного рендеринга компонентов класса и функциональных компонентов отличается:
Компонент класса:Запустите средство обновления с помощью setState и повторно выполните метод рендеринга в компоненте.
Компонент функции:Используйте функцию setter, возвращаемую useState, для отправки действия обновления, запуска обновления (scheduleWork в конце dispatchAction), используйте updateReducer для обработки логики обновления и возврата последнего значения состояния (аналогично Redux).
2. Резюме общего процесса работы useState
Сказав так много, наконец, кратко подытожим процесс выполнения useState~
инициализация:Построить диспетчерскую функцию и начальное значение
При обновлении:
- Вызовите функцию диспетчера и вставьте обновление по порядку (фактически действие)
- Собирайте обновления и планируйте обновление React
- В процессе обновления будет
ReactCurrentDispatcher.current
Указывает на диспетчера, ответственного за обновление - Когда функциональный компонент App() выполняется,
useState
Будет выполнено повторно, и диспетчер, ответственный за обновление, будет назначен на этапе разрешения диспетчера. -
useState
получит объект Hook,Hook.query
Очередь обновлений хранится в , и после последовательного обновления вы можете получить последнее состояние - Значение счетчика в nextChild, возвращаемое после выполнения функционального компонента App(), является последним. в Файберноде
memorizedState
также устанавливается в последнее состояние - Fiber отображает настоящий DOM. Конец обновления.
насчет нас:
МыТехническая группа Ant Insurance Experience, от Ant Financial Insurance Group. Мы молодая команда (без бремени исторического стека технологий), текущий средний возраст 92 года (убрать самый высокий балл 8х лет - лидер команды, убрать самый низкий балл 97 лет - брат стажер). Мы поддерживаем практически весь страховой бизнес Ali Group. В 2018 году созданное нами общее сокровище произвело фурор в страховой отрасли, а в 2019 году мы готовили и реализовывали несколько крупных проектов. Теперь, с быстрым развитием бизнес-группы, команда также быстро расширяется.Приглашаем всех мастеров фронтенда присоединиться к нам~
Мы надеемся, что вы обладаете: прочной технической базой, глубокими знаниями в определенной области (узлы/интерактивный маркетинг/визуализация данных и т. д.); способны быстро и непрерывно учиться в процессе обучения; оптимистичны, веселы, живы и общительны.
Если вы заинтересованы в том, чтобы присоединиться к нам, пожалуйста, отправьте свое резюме по электронной почте: xingyan.hyx@antfin.com
Автор этой статьи: Ant Insurance - Experience Technology Group - Xingyan
Адрес Наггетс:ЗВЕЗДА🌟