这是第 86 篇不掺水的原创,想获取更多原创好文,请搜寻公众号关注咱们吧~ 本文首发于政采云前端博客:【Flutter 技能篇】你不得不会的状态治理 Provider
前言
Provider,Google 官网举荐的一种 Flutter 页面状态治理组件,它的本质其实就是对 InheritedWidget 的包装,使它们更易于应用和重用。对于 InheritedWidget 不做过多介绍,本篇文章次要较全面地介绍 Provider 的相干用法,能在业务场景中有所使用。
本篇文章示例源码:https://github.com/xiaomanzijia/FlutterProvider
用法
Step1:增加依赖
dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. provider: ^4.0.4
Step2:察看构造
执行flutter pub get
后,能够在工程看到 provider 的 sdk 源码,构造如下:
Step3:示例简介
本示例将解说 Provider 根底组件的应用,包含但不限于 ChangeNotifier, NotifierProvider, Consumer, Selector, ProxyProvider, FutureProvider, StreamProvider。
Step4:创立一个ChangeNotifier
咱们先新建一个 Model1,继承 ChangeNotifier,使之成为咱们的数据提供者之一。
class Model1 extends ChangeNotifier { int _count = 1; int get count => _count; set count(int value) { _count = value; notifyListeners(); }}
追踪 ChangeProvider 源码,咱们发现它并不属于 Provider,它其实是定义在 Flutter SDK foundation 上面的 change_provider.dart 文件。ChangeNotifier 实现了 Listenable 抽象类,外面保护了一个 ObserverList。
Listenable 类源码:
abstract class Listenable { const Listenable(); factory Listenable.merge(List<Listenable> listenables) = _MergingListenable; void addListener(VoidCallback listener); void removeListener(VoidCallback listener);}
能够看出,次要提供了 addListener 和 removeListener 两个办法。
ChangeNotifier 类源码:
class ChangeNotifier implements Listenable { ObserverList<VoidCallback> _listeners = ObserverList<VoidCallback>(); bool _debugAssertNotDisposed() { assert(() { if (_listeners == null) { throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary('A $runtimeType was used after being disposed.'), ErrorDescription('Once you have called dispose() on a $runtimeType, it can no longer be used.') ]); } return true; }()); return true; } @protected bool get hasListeners { assert(_debugAssertNotDisposed()); return _listeners.isNotEmpty; } void addListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); _listeners.add(listener); } @override void removeListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); _listeners.remove(listener); } @mustCallSuper void dispose() { assert(_debugAssertNotDisposed()); _listeners = null; } @protected @visibleForTesting void notifyListeners() { assert(_debugAssertNotDisposed()); if (_listeners != null) { final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners); for (VoidCallback listener in localListeners) { try { if (_listeners.contains(listener)) listener(); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'foundation library', context: ErrorDescription('while dispatching notifications for $runtimeType'), informationCollector: () sync* { yield DiagnosticsProperty<ChangeNotifier>( 'The $runtimeType sending notification was', this, style: DiagnosticsTreeStyle.errorProperty, ); }, )); } } } }}
除了实现 addListener 和 removeListener 外,还提供了 dispose 和 notifyListeners 两个办法。Model1 中,当咱们更改 count 值时,就会调用 notifyListeners 办法告诉UI更新。
Step5:创立ChangeNotifierProvider
示例简介
形式一:通过ChangeNotifierProvider
return ChangeNotifierProvider( create: (context) { return Model1(); }, child: MaterialApp( theme: ArchSampleTheme.theme, home: SingleStatsView(), ),);
这里通过 ChangeNotifierProvider 的 create 把 ChangeNotifier(即 Model1)建立联系,作用域的范畴在 child 指定的 MaterialApp,这里咱们将 SingleStatsView 作为首页,SingleStatsView 外面应用了 Model1 作为数据源。须要留神的是,不要把所有状态的作用域都放在 MaterialApp,依据理论业务需要严格控制作用域范畴,全局状态多了会重大影响利用的性能。
class SingleStatsView extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ FlatButton( color: Colors.blue, child: Text('Model1 count++'), onPressed: () { Provider.of<Model1>(context, listen: false).count++; }, ), Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text('Model count值变动监听', style: Theme.of(context).textTheme.title), ), Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text('Model1 count:${Provider.of<Model1>(context).count}', style: Theme.of(context) .textTheme .subhead .copyWith(color: Colors.green)), ), ], ), ); }}
形式二:通过ChangeNotifierProvider.value
return ChangeNotifierProvider.value( value: Model1(), child: MaterialApp( theme: ArchSampleTheme.theme, home: SingleStatsView(), ));
能够看出,和形式一相差无几,形式一用的 create 创立的 ChangeNotifier,这里用的 value 创立。追溯 ChangeNotifierProvider 源码:
class ChangeNotifierProvider<T extends ChangeNotifier> extends ListenableProvider<T> { static void _dispose(BuildContext context, ChangeNotifier notifier) { notifier?.dispose(); } ChangeNotifierProvider({ Key key, @required Create<T> create, bool lazy, Widget child, }) : super( key: key, create: create, dispose: _dispose, lazy: lazy, child: child, ); ChangeNotifierProvider.value({ Key key, @required T value, Widget child, }) : super.value( key: key, value: value, child: child, );}
Step6:在页面中监听状态变更,其余应用形式
示例
咱们先理解下 ValueListenableBuilder,它能够监听指定值的变动进行 UI 更新,用法如下:
ValueNotifier
1.新建ValueNotifier
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
在 builder 办法将之指定到 Model1 的 count ,这样当 Model1 中的 count 变动时 _counter 也能监听到。
_counter.value = Provider.of<Model1>(context).count;
2.关联ValueListenableBuilder
ValueListenableBuilder 的 valueListenable 能够绑定一个 ValueNotifier,用于监听 ValueNotifier 的值变动。
ValueListenableBuilder( valueListenable: _counter, builder: (context, count, child) => Text( 'ValueListenableBuilder count:$count'),),
ValueNotifier源码:
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> { ValueNotifier(this._value); @override T get value => _value; T _value; set value(T newValue) { if (_value == newValue) return; _value = newValue; notifyListeners(); } @override String toString() => '${describeIdentity(this)}($value)';}
能够看出,ValueNotifer 也是继承 ChangeNotifier,并实现了 ValueListenable,特别之处是在 set value 的时候调用了 notifyListeners,从而实现了状态变更监听。
MultiProvider
示例简介
一旦业务场景简单,咱们的页面可能须要监听多个 ChangeNotifier 的数据源,这时候MultiProvider 就派上用场了。该示例在 SingleStatsView 上进行了扩大,这里咱们新建一个 MultiStatsView,监听 Model1 和 Model2 的数据变动。
Model2
class Model2 extends ChangeNotifier { int _count = 1; int get count => _count; set count(int value) { _count = value; notifyListeners(); }}
MultiStatsView
class MultiStatsView extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ FlatButton( color: Colors.blue, child: Text('Model1 count++'), onPressed: () { Provider.of<Model1>(context, listen: false).count++; }, ), FlatButton( color: Colors.blue, child: Text('Model2 count++'), onPressed: () { Provider.of<Model2>(context, listen: false).count++; }, ), Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text('Model count值变动监听', style: Theme.of(context).textTheme.title), ), Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text('Model1 count:${Provider.of<Model1>(context).count}', style: Theme.of(context) .textTheme .subhead .copyWith(color: Colors.green)), ), Padding( padding: const EdgeInsets.only(bottom: 24.0), child: Text('Model2 count:${Provider.of<Model2>(context).count}', style: Theme.of(context) .textTheme .subhead .copyWith(color: Colors.red)), ), ], ), ); }}
应用 MultiProvider 关联 MultiStatsView,能够看出 MultiProvider 提供了 providers 数组,咱们能够把 ChangeNotifierProvider 放进去。
return MultiProvider( providers: [ ChangeNotifierProvider.value(value: Model1()), ChangeNotifierProvider.value(value: Model2()), ], child: MaterialApp( theme: ArchSampleTheme.theme, localizationsDelegates: [ ArchSampleLocalizationsDelegate(), ProviderLocalizationsDelegate(), ], home: MultiStatsView(), ),);
若针对 MultiStatsView 仅提供 Model1 关联的 ChangeNotifierProvider,你会看到如下报错:
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════The following ProviderNotFoundException was thrown building MultiStatsView(dirty, dependencies:[_LocalizationsScope-[GlobalKey#48c61], _InheritedTheme, _DefaultInheritedProviderScope<Model1>]):Error: Could not find the correct Provider<Model2> above this MultiStatsView WidgetTo fix, please: * Ensure the Provider<Model2> is an ancestor to this MultiStatsView Widget * Provide types to Provider<Model2> * Provide types to Consumer<Model2> * Provide types to Provider.of<Model2>() * Ensure the correct `context` is being used.
这是因为 Model2 没有注册导致。
ProxyProvider
从3.0.0开始,开始提供ProxyProvider
。ProxyProvider
能够将其余 provider 的值聚合为一个新对象,并且将后果传递给Provider
。新对象会在其依赖的宿主 provider 更新后被更新。
新建一个 User 类
class User { var name; User(this.name);}
咱们把 Model1 通过ProxyProvider
聚合成 User 对象,而后取 User 对象中的name
进行渲染。s
ProxyProvider<Model1, User>( update: (context, value, previous) => User("change value ${value.count}"), builder: (context, child) => Text( 'ProxyProvider: ${Provider.of<User>(context).name}', style: Theme.of(context).textTheme.title), )
能够看出,通过ProxyProvider
形式,咱们间接调用Provider.of<User>(context)
取值,关联User
的Provider咱们并没有注册,也能无效运行。
FutureProvider
通过名字能够看出,这个 Provider 和异步执行无关,用法相似于FutureBuilder
。这里用FutureProvider
模仿2秒后更新 Model1 的初始值。能够在initialData
指定初始值,create 办法指定具体的异步工作,builder 办法中能够用Provider.of
取出异步工作执行返回的值进行页面渲染。还能够定义catchError
捕捉异样,updateShouldNotify
比拟新旧值是否 rebuild,新的 create/update 回调函数是懒加载的,也就是说它们在对应的值第一次被读取的时候才被调用,而非 provider 首次被创立时。如果不须要这个个性,能够将lazy
属性值置为 false。
FutureProvider<Model1>( create: (context) { return Future.delayed(Duration(seconds: 2)) .then((value) => Model1()..count = 11); }, initialData: Model1(), builder: (context, child) => Text( 'FutureProvider ${Provider.of<Model1>(context).count}', style: Theme.of(context).textTheme.title), ),
StreamProvider
通过名字能够看出,StreamProvider
也是一个异步执行无关的 Provider,用法相似于 StreamBuilder。这里用StreamProvider
模仿每隔1秒更新 Model1 的初始值。其余参数和FutureProvider
用法相似。
StreamProvider(create: (context) { return Stream.periodic(Duration(seconds: 1), (data) => Model1()..count = data); }, initialData: Model1(), builder: (context, child) => Text( 'StreamProvider: ${Provider.of<Model1>(context).count}', style: Theme.of(context).textTheme.title), ),
Consumer
具体用法如下,builder 中的参数别离是 Context context, T value, Widget child,value 即Model1,value 的类型和 Model1 类型统一,builder 办法返回的是 Widget,也就是被 Consumer 包裹的 widget,当监听的 model 值产生扭转,此 widget 会被 Rebuild。
Consumer<Model1>( builder: (context, model, child) { return Text('Model1 count:${model.count}'); }, )
Selector
能够看出,Selector 和 Consumer 很类似,惟一不同的是,Selector 能够自定义返回类型,如下 Selector,咱们这里监听 Model1 中的 count 变动,所以这里返回类型定义为 Int 类型。其中 builder 办法中的参数别离是 Context context, T value, Widget child,这里的 value 的类型和 Selector 中定义的返回类型统一。builder 办法返回的是 Widget,也就是被 Selector 包裹的 widget,咱们能够指定监听 ChangeNotifier 中的某个值的变动,从而可触发此 widget Rebuild。
Selector<Model1, int>( builder: (context, count, child) => Text( "Selector示例演示: $count", style: Theme.of(context).textTheme.title, ), selector: (context, model) => model.count,),
源码:
class Selector<A, S> extends Selector0<S> { Selector({ Key key, @required ValueWidgetBuilder<S> builder, @required S Function(BuildContext, A) selector, ShouldRebuild<S> shouldRebuild, Widget child, }) : assert(selector != null), super( key: key, shouldRebuild: shouldRebuild, builder: builder, selector: (context) => selector(context, Provider.of(context)), child: child, );}
能够看出 Selector 继承自 Selector0,追踪 Selector0 源码,它通过 buildWithChild 创立 Widget
class _Selector0State<T> extends SingleChildState<Selector0<T>> { T value; Widget cache; Widget oldWidget; @override Widget buildWithChild(BuildContext context, Widget child) { final selected = widget.selector(context); var shouldInvalidateCache = oldWidget != widget || (widget._shouldRebuild != null && widget._shouldRebuild.call(value, selected)) || (widget._shouldRebuild == null && !const DeepCollectionEquality().equals(value, selected)); if (shouldInvalidateCache) { value = selected; oldWidget = widget; cache = widget.builder( context, selected, child, ); } return cache; }}
这里的 A,S,能够看出 A 是 selector 函数的入参,S 是函数的返回值,这里将 A 通过Provider.of(context) 转换成了 Provider。比照上述 Selector 的例子,这里的A对应 Model1,S 对应 count。这里还有一个 shouldRebuild,看看函数的定义:
typedef ShouldRebuild<T> = bool Function(T previous, T next);
通过比拟前一个值和以后值,决定是否从新 rebuild 页面,如果返回 true,则页面会被从新渲染一次,返回 false,页面不会被从新渲染。具体判断逻辑能够参考 _Selector0State 的 buildWithChild 办法。
能够看出,绝对于 Consumer ,Selector 放大了数据监听的范畴,并且能够依据本身的业务逻辑自定义是否刷新页面,从而防止了很多不必要的页面刷新,从而进步了性能。
在 Provider SDK 代码构造的 selector.dart 文件里,能够看出还定义了 Selector2、Selector3、Selector4、Selector5、Selector6。这里以 Selector2 做示例解说其用法:
class Selector2<A, B, S> extends Selector0<S> { Selector2({ Key key, @required ValueWidgetBuilder<S> builder, @required S Function(BuildContext, A, B) selector, ShouldRebuild<S> shouldRebuild, Widget child, }) : assert(selector != null), super( key: key, shouldRebuild: shouldRebuild, builder: builder, selector: (context) => selector( context, Provider.of(context), Provider.of(context), ), child: child, );}
能够看出,Selector2 同样也是继承了 Selector0,不同的是 selector 函数有了两个入参 A 和B,S 是函数的返回值。也就是说能够通过 Selector2 监听2个 Provider,能够从这2个 Provider 中自定义 S 的值变动监听。其余 Selector 只是监听的 Provider 更多罢了。 如果大于6个 Provider 须要监听,就须要自定义 Selector 办法了。
示例中咱们用 Selector2 同时监听 Model1 和 Model2 的变动,对两个 Model 中的 count 进行加和计算。
Selector2<Model1, Model2, int>( selector: (context, model1, model2) { return model1.count + model2.count; }, builder: (context, totalCount, child) { return Text( 'Model1和Model2 count共计:$totalCount'); }
Selector3, Selector4…,还有 Consumer2,Consumer3…,这里就不做赘述了。
Consumer和Selector性能验证
通过下面的示例,咱们曾经对 Consumer 和 Selector 有所理解。Consumer 能够防止 widget 多余的 rebuild,当 Consumer 中监听的 value 不发生变化,其包裹的 widget 不会 Rebuild。 Selector 在 Consumer 根底上提供了更加准确的监听,还反对自定义 rebuild,能够更加灵便地管制 widget rebuild 问题。
上面咱们一起来验证下 Consumer 和 Selector rebuild 的状况。Step3 图示中,咱们定义了两个按钮,一个用于累加 Model1 中的 count,一个用于累加 Model2 中的 count;同时演示了 Selector2 和 Consumer 的用法;定义了 Widget1,Widget2,带 Selector 的 Widget4,用于验证 rebuild 状况。Model1的 count 值用绿色标识,Model2 的 count 值用红色标识。
本篇文章示例源码:https://github.com/xiaomanzijia/FlutterProvider
Widget1,在 build 办法中打印 “Widget1 build”。
class Widget1 extends StatefulWidget { @override State<StatefulWidget> createState() => StateWidget1();}class StateWidget1 extends State<Widget1> { @override Widget build(BuildContext context) { print('Widget1 build'); return Text('Widget1', style: Theme.of(context).textTheme.subhead); }}
Widget2, 在 build 办法中打印 "Widget2 build"。
Widget3, 监听了 Model2 count 变动,在 builder 办法中打印 “Widget3 build”。
Widget4,在 build 办法中打印 "Widget4 build",build 办法返回一个 Selector,在 Selector 的 builder 办法中打印 “Widget4 Selector build”,Selector 监听 Model1 count 变动。
class Widget4 extends StatefulWidget { @override State<StatefulWidget> createState() => StateWidget4();}class StateWidget4 extends State<Widget4> { @override Widget build(BuildContext context) { print('Widget4 build'); return Selector<Model1, int>( builder: (context, count, child) { print('Widget4 Selector build'); return Text('Widget4 Model1 count:$count', style: Theme.of(context).textTheme.subhead.copyWith(color: Colors.green),); }, selector: (context, model) => model.count, ); }}
所有条件具备,咱们运行 StatsView 页面,日志打印如下:
Selector2 buildModel1 Consumer buildModel2 Consumer buildWidget1 buildWidget2 buildWidget3 buildWidget4 buildWidget4 Selector build
能够看出,widget 渲染程序是依据页面元素布局程序从上到下开始渲染。
点击 Model1 count++ 按钮,能够看到所有绿色标识的中央,count 已更新。日志打印如下:
Selector2 buildModel1 Consumer buildModel2 Consumer buildWidget1 buildWidget2 buildWidget3 buildWidget4 buildWidget4 Selector build
啥!!!怎么和第一次加载页面日志一样,更新 Model1 的 count,不应该只 build 监听 Model1 相干的 widget 吗?咱们改下代码,把 Widget4 作为全局变量,在 initState 的时候初始化。
class StateStatsView extends State<StatsView> { final ValueNotifier<int> _counter = ValueNotifier<int>(0); var widget4; @override void initState() { widget4 = Widget4(); super.initState(); } @override Widget build(BuildContext context) { _counter.value = Provider.of<Model1>(context).count; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ..., //此处省略局部实例源码 Widget1(), Widget2(), Widget3(), widget4, ], ), ); }}
运行后再次点击 Model1 count++ 按钮,日志打印如下:
Selector2 buildModel1 Consumer buildModel2 Consumer buildWidget1 buildWidget2 buildWidget3 buildWidget4 Selector build
能够看到,“Widget4 build” 的日志不会再打印,然而 “Widget4 Selector build” 的日志仍在打印。咱们再改下代码,把 Widget4 中的 selector 作为全局变量,在 initState 的时候初始化。
class Widget4 extends StatefulWidget { @override State<StatefulWidget> createState() => StateWidget4();}class StateWidget4 extends State<Widget4> { Selector<Model1, int> selector; @override void initState() { selector = buildSelector(); super.initState(); } @override Widget build(BuildContext context) { print('Widget4 build'); return selector; } Selector<Model1, int> buildSelector() { return Selector<Model1, int>( builder: (context, count, child) { print('Widget4 Selector build'); return Text('Widget4 Model1 count:$count', style: Theme.of(context).textTheme.subhead.copyWith(color: Colors.green),); }, selector: (context, model) => model.count, ); }}
运行后再次点击Model1 count++按钮,日志打印如下:
Selector2 buildModel1 Consumer buildModel2 Consumer buildWidget1 buildWidget2 buildWidget3 build
能够看到,“Widget4 Selector build” 的日志没有打印了,Widget4 中监听的 Model1 中的 count 也失常更新了。
通过后面3步的验证,咱们能够得悉当 ChangeNotifier(这里即 Model1)告诉更新(notifyListener)时,在 Model1 作用域下的 Widget 都会触发 build,Selector,Consumer 本质也就是一个Widget,当咱们的数据须要 Selector 或 Consumer 包裹时,倡议在 initState 的时候先把 widget 创立好,能够防止不必要的 build。
其余
1.listen
如果咱们把 “Model1 count++” 按钮点击事件代码改下
FlatButton( color: Colors.blue, child: Text('Model1 count++'), onPressed: () { Provider.of<Model1>(context).count++; }, ),
区别在于少了 listen:false,点击按钮你会看到上面的谬误日志:
Tried to listen to a value exposed with provider, from outside of the widget tree.This is likely caused by an event handler (like a button's onPressed) that calledProvider.of without passing `listen: false`.To fix, write:Provider.of<Model1>(context, listen: false);It is unsupported because may pointlessly rebuild the widget associated to theevent handler, when the widget tree doesn't care about the value.
官网正文页对 listen 做了阐明,listen=true,意味着被监听的 ChangeNotifier 中的值发生变化,对应的 widget 就会被 rebuild,listen=false,则不会被 rebuild。在 widget 树外调用 Provider.of 办法,必须加上 listen:false。
2.扩大
Provider 从 4.1.0 版本开始反对了扩大办法,以后示例基于 4.0.5+1 解说,这里暂不做赘述,具体可看Changelog。
before | after |
---|---|
Provider.of(context, listen: false) | context.read() |
Provider.of(context) | context.watch |
其余状态治理组件
组件 | 介绍 |
---|---|
Provider | 官网举荐,基于 InheritedWidget 实现 |
ScopedModel | 基于 InheritedWidget 实现,和 Provider 原理和写法都很相似 |
BLoC | 基于 Stream 实现的 ,该模式须要对响应式编程(比方 RxDart,RxJava)有肯定的了解。外围概念:输出事件Sink<Event> input ,输入事件Stream<Data> output |
Redux | Web 开发中 React 生态链中 Redux 包的 Flutter 实现,在前端比拟风行,一种单向数据流架构。外围概念:存储对象Store 、事件操作Action 、解决和散发事件Reducer 、组件刷新View |
Mobx | 原本是一个 JavaScript 的状态治理库,后迁徙到 dart 版本。外围概念:Observables 、Actions 、Reactions |
这里不对其余组件做赘述,读者有趣味能够钻研一下,理解其余组件的实现原理。
总结
本篇文章次要介绍了官网举荐应用的 Provider 组件,联合源码和平时业务开发过程中遇到的问题,介绍了几种罕用的应用形式,心愿大家能纯熟应用,在业务场景中能灵活运用。
举荐浏览
联合React源码,五分钟带你把握优先队列
编写高质量可保护的代码:组件的形象与粒度
招贤纳士
政采云前端团队(ZooTeam),一个年老富裕激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员形成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端利用、数据分析及可视化等方向进行技术摸索和实战,推动并落地了一系列的外部技术产品,继续摸索前端技术体系的新边界。
如果你想扭转始终被事折腾,心愿开始能折腾事;如果你想扭转始终被告诫须要多些想法,却无从破局;如果你想扭转你有能力去做成那个后果,却不须要你;如果你想扭转你想做成的事须要一个团队去撑持,但没你带人的地位;如果你想扭转既定的节奏,将会是“5 年工作工夫 3 年工作教训”;如果你想扭转原本悟性不错,但总是有那一层窗户纸的含糊… 如果你置信置信的力量,置信平凡人能成就不凡事,置信能遇到更好的本人。如果你心愿参加到随着业务腾飞的过程,亲手推动一个有着深刻的业务了解、欠缺的技术体系、技术发明价值、影响力外溢的前端团队的成长历程,我感觉咱们该聊聊。任何工夫,等着你写点什么,发给 ZooTeam@cai-inc.com