玩转Elasticsearch源码-ActionModule启动分析

ActionModule的用途org.elasticsearch.action.ActionModule主要维护了请求和响应相关组件,包括客户端请求处理器(actions,restController)和过滤器(actionFilters),它们可能来自ES本身或者来自plugin。源码分析构造方法先看看构造方法,每一步都简单加了解释 public ActionModule(boolean transportClient, Settings settings, IndexNameExpressionResolver indexNameExpressionResolver, IndexScopedSettings indexScopedSettings, ClusterSettings clusterSettings, SettingsFilter settingsFilter, ThreadPool threadPool, List<ActionPlugin> actionPlugins, NodeClient nodeClient, CircuitBreakerService circuitBreakerService, UsageService usageService) { this.transportClient = transportClient;//服务端为false,客户端为true this.settings = settings; this.indexNameExpressionResolver = indexNameExpressionResolver;//将提供的索引表达式转换为实际的具体索引 this.indexScopedSettings = indexScopedSettings;//index级别的配置 this.clusterSettings = clusterSettings;//集群级别的配置 this.settingsFilter = settingsFilter;//允许通过简单正则表达式模式或完整设置键筛选设置对象的类。它用于rest层上的响应过滤,例如过滤出access keys等敏感信息。 this.actionPlugins = actionPlugins;//actionPlugins主要实现了 ActionPlugin 的 getActions() actions = setupActions(actionPlugins);//安装actions actionFilters = setupActionFilters(actionPlugins);//从ActionPlugin中获取,后面在TransportAction里面requestFilterChain会去调用,以后再细讲 autoCreateIndex = transportClient ? null : new AutoCreateIndex(settings, clusterSettings, indexNameExpressionResolver);//封装是否创建新索引的逻辑 destructiveOperations = new DestructiveOperations(settings, clusterSettings);//帮助处理破坏性操作和通配符使用。 Set<String> headers = actionPlugins.stream().flatMap(p -> p.getRestHeaders().stream()).collect(Collectors.toSet()); UnaryOperator<RestHandler> restWrapper = null;//RestHandler包装器,下面从actionPlugins里面遍历获取 for (ActionPlugin plugin : actionPlugins) { UnaryOperator<RestHandler> newRestWrapper = plugin.getRestHandlerWrapper(threadPool.getThreadContext()); if (newRestWrapper != null) { logger.debug(“Using REST wrapper from plugin " + plugin.getClass().getName()); if (restWrapper != null) { throw new IllegalArgumentException(“Cannot have more than one plugin implementing a REST wrapper”); } restWrapper = newRestWrapper; } } if (transportClient) {//客户端不需要暴露http服务 restController = null; } else {//服务端创建一个RestController,实现了Dispatcher接口,系统接受到请求后从netty的handler一路调用到 dispatchRequest 方法 restController = new RestController(settings, headers, restWrapper, nodeClient, circuitBreakerService, usageService); } }首先设置了settings等参数,然后进入setupActions代码比较好理解,直接贴源码:static Map<String, ActionHandler<?, ?>> setupActions(List<ActionPlugin> actionPlugins) { // Subclass NamedRegistry for easy registration class ActionRegistry extends NamedRegistry<ActionHandler<?, ?>> { ActionRegistry() { super(“action”); } public void register(ActionHandler<?, ?> handler) { register(handler.getAction().name(), handler); } public <Request extends ActionRequest, Response extends ActionResponse> void register( GenericAction<Request, Response> action, Class<? extends TransportAction<Request, Response>> transportAction, Class<?>… supportTransportActions) { register(new ActionHandler<>(action, transportAction, supportTransportActions)); } } ActionRegistry actions = new ActionRegistry(); actions.register(MainAction.INSTANCE, TransportMainAction.class); actions.register(NodesInfoAction.INSTANCE, TransportNodesInfoAction.class); actions.register(RemoteInfoAction.INSTANCE, TransportRemoteInfoAction.class); actions.register(NodesStatsAction.INSTANCE, TransportNodesStatsAction.class); actions.register(NodesUsageAction.INSTANCE, TransportNodesUsageAction.class); actions.register(NodesHotThreadsAction.INSTANCE, TransportNodesHotThreadsAction.class); actions.register(ListTasksAction.INSTANCE, TransportListTasksAction.class); actions.register(GetTaskAction.INSTANCE, TransportGetTaskAction.class); actions.register(CancelTasksAction.INSTANCE, TransportCancelTasksAction.class); actions.register(ClusterAllocationExplainAction.INSTANCE, TransportClusterAllocationExplainAction.class); actions.register(ClusterStatsAction.INSTANCE, TransportClusterStatsAction.class); actions.register(ClusterStateAction.INSTANCE, TransportClusterStateAction.class); actions.register(ClusterHealthAction.INSTANCE, TransportClusterHealthAction.class); actions.register(ClusterUpdateSettingsAction.INSTANCE, TransportClusterUpdateSettingsAction.class); actions.register(ClusterRerouteAction.INSTANCE, TransportClusterRerouteAction.class); actions.register(ClusterSearchShardsAction.INSTANCE, TransportClusterSearchShardsAction.class); actions.register(PendingClusterTasksAction.INSTANCE, TransportPendingClusterTasksAction.class); actions.register(PutRepositoryAction.INSTANCE, TransportPutRepositoryAction.class); actions.register(GetRepositoriesAction.INSTANCE, TransportGetRepositoriesAction.class); actions.register(DeleteRepositoryAction.INSTANCE, TransportDeleteRepositoryAction.class); actions.register(VerifyRepositoryAction.INSTANCE, TransportVerifyRepositoryAction.class); actions.register(GetSnapshotsAction.INSTANCE, TransportGetSnapshotsAction.class); actions.register(DeleteSnapshotAction.INSTANCE, TransportDeleteSnapshotAction.class); actions.register(CreateSnapshotAction.INSTANCE, TransportCreateSnapshotAction.class); actions.register(RestoreSnapshotAction.INSTANCE, TransportRestoreSnapshotAction.class); actions.register(SnapshotsStatusAction.INSTANCE, TransportSnapshotsStatusAction.class); actions.register(IndicesStatsAction.INSTANCE, TransportIndicesStatsAction.class); actions.register(IndicesSegmentsAction.INSTANCE, TransportIndicesSegmentsAction.class); actions.register(IndicesShardStoresAction.INSTANCE, TransportIndicesShardStoresAction.class); actions.register(CreateIndexAction.INSTANCE, TransportCreateIndexAction.class); actions.register(ShrinkAction.INSTANCE, TransportShrinkAction.class); actions.register(ResizeAction.INSTANCE, TransportResizeAction.class); actions.register(RolloverAction.INSTANCE, TransportRolloverAction.class); actions.register(DeleteIndexAction.INSTANCE, TransportDeleteIndexAction.class); actions.register(GetIndexAction.INSTANCE, TransportGetIndexAction.class); actions.register(OpenIndexAction.INSTANCE, TransportOpenIndexAction.class); actions.register(CloseIndexAction.INSTANCE, TransportCloseIndexAction.class); actions.register(IndicesExistsAction.INSTANCE, TransportIndicesExistsAction.class); actions.register(TypesExistsAction.INSTANCE, TransportTypesExistsAction.class); actions.register(GetMappingsAction.INSTANCE, TransportGetMappingsAction.class); actions.register(GetFieldMappingsAction.INSTANCE, TransportGetFieldMappingsAction.class, TransportGetFieldMappingsIndexAction.class); actions.register(PutMappingAction.INSTANCE, TransportPutMappingAction.class); actions.register(IndicesAliasesAction.INSTANCE, TransportIndicesAliasesAction.class); actions.register(UpdateSettingsAction.INSTANCE, TransportUpdateSettingsAction.class); actions.register(AnalyzeAction.INSTANCE, TransportAnalyzeAction.class); actions.register(PutIndexTemplateAction.INSTANCE, TransportPutIndexTemplateAction.class); actions.register(GetIndexTemplatesAction.INSTANCE, TransportGetIndexTemplatesAction.class); actions.register(DeleteIndexTemplateAction.INSTANCE, TransportDeleteIndexTemplateAction.class); actions.register(ValidateQueryAction.INSTANCE, TransportValidateQueryAction.class); actions.register(RefreshAction.INSTANCE, TransportRefreshAction.class); actions.register(FlushAction.INSTANCE, TransportFlushAction.class); actions.register(SyncedFlushAction.INSTANCE, TransportSyncedFlushAction.class); actions.register(ForceMergeAction.INSTANCE, TransportForceMergeAction.class); actions.register(UpgradeAction.INSTANCE, TransportUpgradeAction.class); actions.register(UpgradeStatusAction.INSTANCE, TransportUpgradeStatusAction.class); actions.register(UpgradeSettingsAction.INSTANCE, TransportUpgradeSettingsAction.class); actions.register(ClearIndicesCacheAction.INSTANCE, TransportClearIndicesCacheAction.class); actions.register(GetAliasesAction.INSTANCE, TransportGetAliasesAction.class); actions.register(AliasesExistAction.INSTANCE, TransportAliasesExistAction.class); actions.register(GetSettingsAction.INSTANCE, TransportGetSettingsAction.class); actions.register(IndexAction.INSTANCE, TransportIndexAction.class); actions.register(GetAction.INSTANCE, TransportGetAction.class); actions.register(TermVectorsAction.INSTANCE, TransportTermVectorsAction.class); actions.register(MultiTermVectorsAction.INSTANCE, TransportMultiTermVectorsAction.class, TransportShardMultiTermsVectorAction.class); actions.register(DeleteAction.INSTANCE, TransportDeleteAction.class); actions.register(UpdateAction.INSTANCE, TransportUpdateAction.class); actions.register(MultiGetAction.INSTANCE, TransportMultiGetAction.class, TransportShardMultiGetAction.class); actions.register(BulkAction.INSTANCE, TransportBulkAction.class, TransportShardBulkAction.class); actions.register(SearchAction.INSTANCE, TransportSearchAction.class); actions.register(SearchScrollAction.INSTANCE, TransportSearchScrollAction.class); actions.register(MultiSearchAction.INSTANCE, TransportMultiSearchAction.class); actions.register(ExplainAction.INSTANCE, TransportExplainAction.class); actions.register(ClearScrollAction.INSTANCE, TransportClearScrollAction.class); actions.register(RecoveryAction.INSTANCE, TransportRecoveryAction.class); //Indexed scripts actions.register(PutStoredScriptAction.INSTANCE, TransportPutStoredScriptAction.class); actions.register(GetStoredScriptAction.INSTANCE, TransportGetStoredScriptAction.class); actions.register(DeleteStoredScriptAction.INSTANCE, TransportDeleteStoredScriptAction.class); actions.register(FieldCapabilitiesAction.INSTANCE, TransportFieldCapabilitiesAction.class, TransportFieldCapabilitiesIndexAction.class); actions.register(PutPipelineAction.INSTANCE, PutPipelineTransportAction.class); actions.register(GetPipelineAction.INSTANCE, GetPipelineTransportAction.class); actions.register(DeletePipelineAction.INSTANCE, DeletePipelineTransportAction.class); actions.register(SimulatePipelineAction.INSTANCE, SimulatePipelineTransportAction.class); actionPlugins.stream().flatMap(p -> p.getActions().stream()).forEach(actions::register); return unmodifiableMap(actions.getRegistry()); }注册了一大堆Action然后返回 Map<String, ActionHandler<?, ?>> 对象可以注意到ActionHandler包装了action和transportAction的Class对象。action是对各个类型请求的request和response的包装,并非是真正的功能实现者,可以看到它只是提供了两个新建response和request的方法,及一个字NAME字段,这个NAME字段会用于后面action调用中。每个action对应的功能实现是在对应的transportAction中。configure方法configure方法由ES封装的注入器Injector调用,看看调用栈可以知道由ES启动时候Node构造方法里面发起的injector = modules.createInjector();configure里面逻辑其实就是绑定一些对象到Injector中:protected void configure() { bind(ActionFilters.class).toInstance(actionFilters); bind(DestructiveOperations.class).toInstance(destructiveOperations); if (false == transportClient) { // Supporting classes only used when not a transport client bind(AutoCreateIndex.class).toInstance(autoCreateIndex); bind(TransportLivenessAction.class).asEagerSingleton(); // register GenericAction -> transportAction Map used by NodeClient @SuppressWarnings(“rawtypes”) MapBinder<GenericAction, TransportAction> transportActionsBinder = MapBinder.newMapBinder(binder(), GenericAction.class, TransportAction.class); for (ActionHandler<?, ?> action : actions.values()) { // bind the action as eager singleton, so the map binder one will reuse it bind(action.getTransportAction()).asEagerSingleton(); transportActionsBinder.addBinding(action.getAction()).to(action.getTransportAction()).asEagerSingleton(); for (Class<?> supportAction : action.getSupportTransportActions()) { bind(supportAction).asEagerSingleton(); } } } }initRestHandlersinitRestHandlers是在Node构造方法里面直接调用的,主要是注册一大堆RestHandler到restController用于处理http 请求 public void initRestHandlers(Supplier<DiscoveryNodes> nodesInCluster) { List<AbstractCatAction> catActions = new ArrayList<>(); Consumer<RestHandler> registerHandler = a -> { if (a instanceof AbstractCatAction) { catActions.add((AbstractCatAction) a); } }; registerHandler.accept(new RestMainAction(settings, restController)); registerHandler.accept(new RestNodesInfoAction(settings, restController, settingsFilter)); registerHandler.accept(new RestRemoteClusterInfoAction(settings, restController)); registerHandler.accept(new RestNodesStatsAction(settings, restController)); registerHandler.accept(new RestNodesUsageAction(settings, restController)); registerHandler.accept(new RestNodesHotThreadsAction(settings, restController)); registerHandler.accept(new RestClusterAllocationExplainAction(settings, restController)); registerHandler.accept(new RestClusterStatsAction(settings, restController)); registerHandler.accept(new RestClusterStateAction(settings, restController, settingsFilter)); registerHandler.accept(new RestClusterHealthAction(settings, restController)); registerHandler.accept(new RestClusterUpdateSettingsAction(settings, restController)); registerHandler.accept(new RestClusterGetSettingsAction(settings, restController, clusterSettings, settingsFilter)); registerHandler.accept(new RestClusterRerouteAction(settings, restController, settingsFilter)); registerHandler.accept(new RestClusterSearchShardsAction(settings, restController)); registerHandler.accept(new RestPendingClusterTasksAction(settings, restController)); registerHandler.accept(new RestPutRepositoryAction(settings, restController)); registerHandler.accept(new RestGetRepositoriesAction(settings, restController, settingsFilter)); registerHandler.accept(new RestDeleteRepositoryAction(settings, restController)); registerHandler.accept(new RestVerifyRepositoryAction(settings, restController)); registerHandler.accept(new RestGetSnapshotsAction(settings, restController)); registerHandler.accept(new RestCreateSnapshotAction(settings, restController)); registerHandler.accept(new RestRestoreSnapshotAction(settings, restController)); registerHandler.accept(new RestDeleteSnapshotAction(settings, restController)); registerHandler.accept(new RestSnapshotsStatusAction(settings, restController)); registerHandler.accept(new RestGetAllAliasesAction(settings, restController)); registerHandler.accept(new RestGetAllMappingsAction(settings, restController)); registerHandler.accept(new RestGetAllSettingsAction(settings, restController, indexScopedSettings, settingsFilter)); registerHandler.accept(new RestGetIndicesAction(settings, restController, indexScopedSettings, settingsFilter)); registerHandler.accept(new RestIndicesStatsAction(settings, restController)); registerHandler.accept(new RestIndicesSegmentsAction(settings, restController)); registerHandler.accept(new RestIndicesShardStoresAction(settings, restController)); registerHandler.accept(new RestGetAliasesAction(settings, restController)); registerHandler.accept(new RestIndexDeleteAliasesAction(settings, restController)); registerHandler.accept(new RestIndexPutAliasAction(settings, restController)); registerHandler.accept(new RestIndicesAliasesAction(settings, restController)); registerHandler.accept(new RestCreateIndexAction(settings, restController)); registerHandler.accept(new RestShrinkIndexAction(settings, restController)); registerHandler.accept(new RestSplitIndexAction(settings, restController)); registerHandler.accept(new RestRolloverIndexAction(settings, restController)); registerHandler.accept(new RestDeleteIndexAction(settings, restController)); registerHandler.accept(new RestCloseIndexAction(settings, restController)); registerHandler.accept(new RestOpenIndexAction(settings, restController)); registerHandler.accept(new RestUpdateSettingsAction(settings, restController)); registerHandler.accept(new RestGetSettingsAction(settings, restController, indexScopedSettings, settingsFilter)); registerHandler.accept(new RestAnalyzeAction(settings, restController)); registerHandler.accept(new RestGetIndexTemplateAction(settings, restController)); registerHandler.accept(new RestPutIndexTemplateAction(settings, restController)); registerHandler.accept(new RestDeleteIndexTemplateAction(settings, restController)); registerHandler.accept(new RestPutMappingAction(settings, restController)); registerHandler.accept(new RestGetMappingAction(settings, restController)); registerHandler.accept(new RestGetFieldMappingAction(settings, restController)); registerHandler.accept(new RestRefreshAction(settings, restController)); registerHandler.accept(new RestFlushAction(settings, restController)); registerHandler.accept(new RestSyncedFlushAction(settings, restController)); registerHandler.accept(new RestForceMergeAction(settings, restController)); registerHandler.accept(new RestUpgradeAction(settings, restController)); registerHandler.accept(new RestClearIndicesCacheAction(settings, restController)); registerHandler.accept(new RestIndexAction(settings, restController)); registerHandler.accept(new RestGetAction(settings, restController)); registerHandler.accept(new RestGetSourceAction(settings, restController)); registerHandler.accept(new RestMultiGetAction(settings, restController)); registerHandler.accept(new RestDeleteAction(settings, restController)); registerHandler.accept(new org.elasticsearch.rest.action.document.RestCountAction(settings, restController)); registerHandler.accept(new RestTermVectorsAction(settings, restController)); registerHandler.accept(new RestMultiTermVectorsAction(settings, restController)); registerHandler.accept(new RestBulkAction(settings, restController)); registerHandler.accept(new RestUpdateAction(settings, restController)); registerHandler.accept(new RestSearchAction(settings, restController)); registerHandler.accept(new RestSearchScrollAction(settings, restController)); registerHandler.accept(new RestClearScrollAction(settings, restController)); registerHandler.accept(new RestMultiSearchAction(settings, restController)); registerHandler.accept(new RestValidateQueryAction(settings, restController)); registerHandler.accept(new RestExplainAction(settings, restController)); registerHandler.accept(new RestRecoveryAction(settings, restController)); // Scripts API registerHandler.accept(new RestGetStoredScriptAction(settings, restController)); registerHandler.accept(new RestPutStoredScriptAction(settings, restController)); registerHandler.accept(new RestDeleteStoredScriptAction(settings, restController)); registerHandler.accept(new RestFieldCapabilitiesAction(settings, restController)); // Tasks API registerHandler.accept(new RestListTasksAction(settings, restController, nodesInCluster)); registerHandler.accept(new RestGetTaskAction(settings, restController)); registerHandler.accept(new RestCancelTasksAction(settings, restController, nodesInCluster)); // Ingest API registerHandler.accept(new RestPutPipelineAction(settings, restController)); registerHandler.accept(new RestGetPipelineAction(settings, restController)); registerHandler.accept(new RestDeletePipelineAction(settings, restController)); registerHandler.accept(new RestSimulatePipelineAction(settings, restController)); // CAT API registerHandler.accept(new RestAllocationAction(settings, restController)); registerHandler.accept(new RestShardsAction(settings, restController)); registerHandler.accept(new RestMasterAction(settings, restController)); registerHandler.accept(new RestNodesAction(settings, restController)); registerHandler.accept(new RestTasksAction(settings, restController, nodesInCluster)); registerHandler.accept(new RestIndicesAction(settings, restController, indexNameExpressionResolver)); registerHandler.accept(new RestSegmentsAction(settings, restController)); // Fully qualified to prevent interference with rest.action.count.RestCountAction registerHandler.accept(new org.elasticsearch.rest.action.cat.RestCountAction(settings, restController)); // Fully qualified to prevent interference with rest.action.indices.RestRecoveryAction registerHandler.accept(new org.elasticsearch.rest.action.cat.RestRecoveryAction(settings, restController)); registerHandler.accept(new RestHealthAction(settings, restController)); registerHandler.accept(new org.elasticsearch.rest.action.cat.RestPendingClusterTasksAction(settings, restController)); registerHandler.accept(new RestAliasAction(settings, restController)); registerHandler.accept(new RestThreadPoolAction(settings, restController)); registerHandler.accept(new RestPluginsAction(settings, restController)); registerHandler.accept(new RestFielddataAction(settings, restController)); registerHandler.accept(new RestNodeAttrsAction(settings, restController)); registerHandler.accept(new RestRepositoriesAction(settings, restController)); registerHandler.accept(new RestSnapshotAction(settings, restController)); registerHandler.accept(new RestTemplatesAction(settings, restController)); for (ActionPlugin plugin : actionPlugins) { for (RestHandler handler : plugin.getRestHandlers(settings, restController, clusterSettings, indexScopedSettings, settingsFilter, indexNameExpressionResolver, nodesInCluster)) { registerHandler.accept(handler); } } registerHandler.accept(new RestCatAction(settings, restController, catActions)); }再深入去看RestController里面的注册逻辑可以看到是用Trie树来做路径匹配的,具体可以看TrieNode类。 ...

January 10, 2019 · 5 min · jiezi

玩转Elasticsearch源码-一图看懂ES启动流程

开篇直接看图上图中虚线表示进入具体流程,实线表示下一步,为了后面讲解方便每个步骤都加了编号。先简单介绍下启动流程主要涉及的类:org.elasticsearch.bootstrap.Elasticsearch: 启动入口,main方法就在这个类里面,执行逻辑对应图中绿色部分org.elasticsearch.bootstrap.Bootstrap:包含主要启动流程代码,执行逻辑对应图中红色部分org.elasticsearch.node.Node:代表集群中的节点,执行逻辑对应图中蓝色部分流程讲解1. main方法2. 设置了一个空的SecurityManager:// we want the JVM to think there is a security manager installed so that if internal policy decisions that would be based on the // presence of a security manager or lack thereof act as if there is a security manager present (e.g., DNS cache policy)//我们希望JVM认为已经安装了一个安全管理器,这样,如果基于安全管理器的存在或缺少安全管理器的内部策略决策就会像有一个安全管理器一样(e.g.、DNS缓存策略) // grant all permissions so that we can later set the security manager to the one that we want//授予所有权限,以便稍后可以将安全管理器设置为所需的权限添加StatusConsoleListener到STATUS_LOGGER:We want to detect situations where we touch logging before the configuration is loaded . If we do this , Log 4 j will status log an error message at the error level . With this error listener , we can capture if this happens . More broadly , we can detect any error - level status log message which likely indicates that something is broken . The listener is installed immediately on startup , and then when we get around to configuring logging we check that no error - level log messages have been logged by the status logger . If they have we fail startup and any such messages can be seen on the console我们希望检测在加载配置之前进行日志记录的情况。如果这样做,log4j将在错误级别记录一条错误消息。使用这个错误监听器,我们可以捕捉到这种情况。更广泛地说,我们可以检测任何错误级别的状态日志消息,这些消息可能表示某个东西坏了。侦听器在启动时立即安装,然后在配置日志记录时,我们检查状态日志记录器没有记录错误级别的日志消息。如果它们启动失败,我们可以在控制台上看到任何此类消息。实例化Elasticsearch:Elasticsearch() { super(“starts elasticsearch”, () -> {}); // () -> {} 是启动前的回调 //下面解析version,daemonize,pidfile,quiet参数 versionOption = parser.acceptsAll(Arrays.asList(“V”, “version”), “Prints elasticsearch version information and exits”); daemonizeOption = parser.acceptsAll(Arrays.asList(“d”, “daemonize”), “Starts Elasticsearch in the background”) .availableUnless(versionOption); pidfileOption = parser.acceptsAll(Arrays.asList(“p”, “pidfile”), “Creates a pid file in the specified path on start”) .availableUnless(versionOption) .withRequiredArg() .withValuesConvertedBy(new PathConverter()); quietOption = parser.acceptsAll(Arrays.asList(“q”, “quiet”), “Turns off standard output/error streams logging in console”) .availableUnless(versionOption) .availableUnless(daemonizeOption); }3.注册ShutdownHook,用于关闭系统时捕获IOException到terminal shutdownHookThread = new Thread(() -> { try { this.close(); } catch (final IOException e) { try ( StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { e.printStackTrace(pw); terminal.println(sw.toString()); } catch (final IOException impossible) { // StringWriter#close declares a checked IOException from the Closeable interface but the Javadocs for StringWriter // say that an exception here is impossible throw new AssertionError(impossible); } } }); Runtime.getRuntime().addShutdownHook(shutdownHookThread);然后调用beforeMain.run(),其实就是上面实例化Elasticsearch对象时创建的()->{} lambda表达式。4.进入Command类的mainWithoutErrorHandling方法 void mainWithoutErrorHandling(String[] args, Terminal terminal) throws Exception { final OptionSet options = parser.parse(args);//根据提供给解析器的选项规范解析给定的命令行参数 if (options.has(helpOption)) { printHelp(terminal); return; } if (options.has(silentOption)) {//terminal打印最少内容 terminal.setVerbosity(Terminal.Verbosity.SILENT); } else if (options.has(verboseOption)) {//terminal打印详细内容 terminal.setVerbosity(Terminal.Verbosity.VERBOSE); } else { terminal.setVerbosity(Terminal.Verbosity.NORMAL); } execute(terminal, options); }5.进入EnvironmentAwareCommand的execute方法protected void execute(Terminal terminal, OptionSet options) throws Exception { final Map<String, String> settings = new HashMap<>(); for (final KeyValuePair kvp : settingOption.values(options)) { if (kvp.value.isEmpty()) { throw new UserException(ExitCodes.USAGE, “setting [” + kvp.key + “] must not be empty”); } if (settings.containsKey(kvp.key)) { final String message = String.format( Locale.ROOT, “setting [%s] already set, saw [%s] and [%s]”, kvp.key, settings.get(kvp.key), kvp.value); throw new UserException(ExitCodes.USAGE, message); } settings.put(kvp.key, kvp.value); } //确保给定的设置存在,如果尚未设置,则从系统属性中读取它。 putSystemPropertyIfSettingIsMissing(settings, “path.data”, “es.path.data”); putSystemPropertyIfSettingIsMissing(settings, “path.home”, “es.path.home”); putSystemPropertyIfSettingIsMissing(settings, “path.logs”, “es.path.logs”); execute(terminal, options, createEnv(terminal, settings)); }6.进入InternalSettingsPreparer的prepareEnvironment方法,读取elasticsearch.yml并创建Environment。细节比较多,后面再细讲。7.判断是否有-v参数,没有则准备进入init流程 protected void execute(Terminal terminal, OptionSet options, Environment env) throws UserException { if (options.nonOptionArguments().isEmpty() == false) { throw new UserException(ExitCodes.USAGE, “Positional arguments not allowed, found " + options.nonOptionArguments()); } if (options.has(versionOption)) { //如果有 -v 参数,打印版本号后直接退出 terminal.println(“Version: " + Version.displayVersion(Version.CURRENT, Build.CURRENT.isSnapshot()) + “, Build: " + Build.CURRENT.shortHash() + “/” + Build.CURRENT.date() + “, JVM: " + JvmInfo.jvmInfo().version()); return; } final boolean daemonize = options.has(daemonizeOption); final Path pidFile = pidfileOption.value(options); final boolean quiet = options.has(quietOption); try { init(daemonize, pidFile, quiet, env); } catch (NodeValidationException e) { throw new UserException(ExitCodes.CONFIG, e.getMessage()); } }8.调用Bootstrap.init9.实例化Boostrap。保持keepAliveThread存活,可能是用于监控Bootstrap() { keepAliveThread = new Thread(new Runnable() { @Override public void run() { try { keepAliveLatch.await(); } catch (InterruptedException e) { // bail out } } }, “elasticsearch[keepAlive/” + Version.CURRENT + “]”); keepAliveThread.setDaemon(false); // keep this thread alive (non daemon thread) until we shutdown 保持这个线程存活(非守护进程线程),直到我们关机 Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { keepAliveLatch.countDown(); } }); }10.加载elasticsearch.keystore文件,重新创建Environment,然后调用LogConfigurator的静态方法configure,读取config目录下log4j2.properties然后配log4j属性11.创建pid文件,检查lucene版本,不对应则抛出异常 private static void checkLucene() { if (Version.CURRENT.luceneVersion.equals(org.apache.lucene.util.Version.LATEST) == false) { throw new AssertionError(“Lucene version mismatch this version of Elasticsearch requires lucene version [” + Version.CURRENT.luceneVersion + “] but the current lucene version is [” + org.apache.lucene.util.Version.LATEST + “]”); } }12.设置ElasticsearchUncaughtExceptionHandler用于打印fatal日志 // install the default uncaught exception handler; must be done before security is // initialized as we do not want to grant the runtime permission // 安装默认未捕获异常处理程序;必须在初始化security之前完成,因为我们不想授予运行时权限 // setDefaultUncaughtExceptionHandler Thread.setDefaultUncaughtExceptionHandler( new ElasticsearchUncaughtExceptionHandler(() -> Node.NODE_NAME_SETTING.get(environment.settings())));13.进入Boostrap.setup14.spawner.spawnNativePluginControllers(environment);尝试为给定模块生成控制器(native Controller)守护程序。 生成的进程将通过其stdin,stdout和stderr流保持与此JVM的连接,但对此包之外的代码不能使用对这些流的引用。15.初始化本地资源 initializeNatives():检查用户是否作为根用户运行,是的话抛异常;系统调用和mlockAll检查;尝试设置最大线程数,最大虚拟内存,最大FD等。初始化探针initializeProbes(),用于操作系统,进程,jvm的监控。16.又加一个ShutdownHook if (addShutdownHook) { Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { try { IOUtils.close(node, spawner); LoggerContext context = (LoggerContext) LogManager.getContext(false); Configurator.shutdown(context); } catch (IOException ex) { throw new ElasticsearchException(“failed to stop node”, ex); } } }); }17.比较简单,直接看代码 try { // look for jar hell JarHell.checkJarHell(); } catch (IOException | URISyntaxException e) { throw new BootstrapException(e); } // Log ifconfig output before SecurityManager is installed IfConfig.logIfNecessary(); // install SM after natives, shutdown hooks, etc. try { Security.configure(environment, BootstrapSettings.SECURITY_FILTER_BAD_DEFAULTS_SETTING.get(settings)); } catch (IOException | NoSuchAlgorithmException e) { throw new BootstrapException(e); }18.实例化Node重写validateNodeBeforeAcceptingRequests方法。具体主要包括三部分,第一是启动插件服务(es提供了插件功能来进行扩展功能,这也是它的一个亮点),加载需要的插件,第二是配置node环境,最后就是通过guice加载各个模块。下面22~32就是具体步骤。19.进入Boostrap.start20.node.start启动节点21.keepAliveThread.start22.Node实例化第一步,创建NodeEnvironment23.生成nodeId,打印nodeId,nodeName和jvmInfo和进程信息24.创建 PluginsService 对象,创建过程中会读取并加载所有的模块和插件25.又创建Environment // create the environment based on the finalized (processed) view of the settings 根据设置的最终(处理)视图创建环境 // this is just to makes sure that people get the same settings, no matter where they ask them from 这只是为了确保人们得到相同的设置,无论他们从哪里询问 this.environment = new Environment(this.settings, environment.configFile());26.创建ThreadPool,然后给DeprecationLogger设置ThreadContext27.创建NodeClient,用于执行actions28.创建各个Service:ResourceWatcherService、NetworkService、ClusterService、IngestService、ClusterInfoService、UsageService、MonitorService、CircuitBreakerService、MetaStateService、IndicesService、MetaDataIndexUpgradeService、TemplateUpgradeService、TransportService、ResponseCollectorService、SearchTransportService、NodeService、SearchService、PersistentTasksClusterService29.创建并添加modules:ScriptModule、AnalysisModule、SettingsModule、pluginModule、ClusterModule、IndicesModule、SearchModule、GatewayModule、RepositoriesModule、ActionModule、NetworkModule、DiscoveryModule30.Guice绑定和注入对象31.初始化NodeClient client.initialize(injector.getInstance(new Key<Map<GenericAction, TransportAction>>() {}), () -> clusterService.localNode().getId());32.初始化rest处理器,这个非常重要,后面会专门讲解if (NetworkModule.HTTP_ENABLED.get(settings)) { logger.debug(“initializing HTTP handlers …”); // 初始化http handler actionModule.initRestHandlers(() -> clusterService.state().nodes()); }33.修改状态为State.STARTED34.启动pluginLifecycleComponents35.通过 injector 获取各个类的对象,调用 start() 方法启动(实际进入各个类的中 doStart 方法)LifecycleComponent、IndicesService、IndicesClusterStateService、SnapshotsService、SnapshotShardsService、RoutingService、SearchService、MonitorService、NodeConnectionsService、ResourceWatcherService、GatewayService、Discovery、TransportService36.启动HttpServerTransport和TransportService并绑定端口if (WRITE_PORTS_FILE_SETTING.get(settings)) { if (NetworkModule.HTTP_ENABLED.get(settings)) { HttpServerTransport http = injector.getInstance(HttpServerTransport.class); writePortsFile(“http”, http.boundAddress()); } TransportService transport = injector.getInstance(TransportService.class); writePortsFile(“transport”, transport.boundAddress()); }总结本文只是讲解了ES启动的整体流程,其中很多细节会在本系列继续深入讲解ES的源码读起来还是比较费劲的,流程比较长,没有Spring源码读起来体验好,这也是开源软件和开源框架的区别之一,前者会遇到大量的流程细节,注重具体功能的实现,后者有大量扩展点,更注重扩展性。为什么要读开源源码?1.知道底层实现,能够更好地使用,出问题能够快速定位和解决。2.学习别人优秀的代码和处理问题的方式,提高自己的系统设计能力。3.有机会可以对其进行扩展和改造。 ...

January 10, 2019 · 4 min · jiezi

「读懂源码系列1」还在恐惧读源码?看完这篇就不怕了

一个小需求事情的起因,是昨天老板提出的一个需求。他要实现让我们自己定制的弹出层,具备按下 ESC 也能退出的功能。我把任务交给了同组的小伙伴S去实现。(这个项目用到了vue技术栈,以及饿了么的UI框架。)我开完会回来,发现他还在处理那个功能,但好像遇到了什么瓶颈。于是,我就问他,卡在了什么地方。小伙伴S说,他百度了不少资料,还查了官方文档,并且尝试其中的办法,但就是无法触发按下 ESC 的回调方法,很是郁闷。我看了他的代码,他的写法是这样的:<div class=“custom-modal” @keydown.27=“handlePressEscape”> …</div>…handlePressEscape () { console.log(‘press escape!’);},…他的想法不错,因为是自定义的弹出层,所以就想着把 keydown 事件,绑定在最外层的 div 上,让整个弹出层都能监听到。他给我看了他查的资料,几乎都是在 input 上绑定 keydown 事件的例子,而 vue 的官方文档里也是类似的例子,实践后却陷入了瓶颈。但是他忽略了一个问题,keydown 事件,并非绑在任意一个标签上,都会起作用。一种思路我没有直接把答案告诉他,而是给他提供了一个思路:在我们常用的 element-ui 的 el-dialog 组件里,有个属性叫做 close-on-press-escape,它的解释如下图:从文档的解释,可以看出 el-dialog 在默认情况下,已经实现了我们需要解决的需求。所以,我让他看看 el-dialog 的源码,是如何实现的。他一听要看源码,就露出了恐惧之情。源码是所有框架和API的根基,因为其高深度和复杂性,让人望而却步。我自己也经历过这个阶段,所以非常理解他的心情,鼓励他一起做一次尝试。查找源码首先,我们在 node_modules 里,找到了 element-ui的文件夹,它大致长这个样子:接着,我们找到了 packages 里的 dialog 文件夹,再从 index 入口,找到了组件 component.vue。可是,点进去找了半天,也只找到个 closeOnPressEscape 属性的定义,却没有实现的方法。…closeOnPressEscape: { type: Boolean, default: true},…这么神奇么?只定义一个属性,就能实现一个事件的交互了?感觉不太可能啊?!? 为了揭开迷雾,继续找。。。仔细浏览了 component.vue 文件,发现在 script 里,引入下面 3 个文件:import Popup from ’element-ui/src/utils/popup’;import Migrating from ’element-ui/src/mixins/migrating’;import emitter from ’element-ui/src/mixins/emitter’;…在第一个引入的 Popup 中,竟然也发现了 closeOnPressEscape,感觉似乎找对方向了。但令人沮丧的是,Popup 中同样只有 closeOnPressEscape 的属性定义,却没有实现。不过,它却引入了另一个辅助文件 PopupManager,再点进去找。哇!终于找到了!它的实现,是这样的:// handle esc key when the popup is shownwindow.addEventListener(‘keydown’, function(event) { if (event.keyCode === 27) { const topPopup = getTopPopup(); if (topPopup && topPopup.closeOnPressEscape) { topPopup.handleClose ? topPopup.handleClose() : (topPopup.handleAction ? topPopup.handleAction(‘cancel’) : topPopup.close()); } }});原来,是在 window 上添加了事件监听 keydown,当监测到是 ESC 的 keyCode 时,则执行相关操作。模仿源码ok,现在已经知晓了原理,那就按照我们的实际需求,模仿改造一下:…props: { … closeOnPressEscape: { type: Boolean, default: true }},…mounted () { window.addEventListener(‘keydown’, this.handlePressEscape);},destroyed () { window.removeEventListener(‘keydown’, this.handlePressEscape);},methods: { … handlePressEscape (event) { if (this.closeOnPressEscape && event.keyCode === 27) { this.handleClose(); } }}在上述实现中,有2个需要注意的点:代码方面,在 mounted 中,给 window 添加事件监听之后,要记得在 destroyed 时,去除监听。业务方面,这是一个我们定制的通用的弹出层组件,所以在 props 中定义了一个 closeOnPressEscape 属性,以方便在某些业务场景下,不需要按 ESC 就退出这个功能的时候,直接设置它为 false 即可。源码真有那么可怕吗?源码一词,乍一听就是神秘、高大上、吊炸天的代名词,让很多的前端同学闻风丧胆。回想当初,我也曾一度对它有一股深深的恐惧。源码真的这么可怕吗?从以上的事例中可以看出,其实并没有。例子中的element-ui源码并不复杂,我和小伙伴S一起看源码时,他也大概都能看得明白。最后因为弄懂了背后的原理,进行简单应用,比较轻松地就解决了问题。对于源码的恐惧,让我们渐渐思维固化,自己告诉自己不要去碰源码,时间长了就遗忘了还有这样一条路可走。面试中的应用关于对源代码的考察,我也会经常应用在面试中。在面试中,如果候选人给我的感觉不错,我的惯用伎俩是问下面这个问题:刚才你说到,用过一段时间 xxx 框架,xx API属性也用过,也很清楚它达到的效果。那么现在,如果需要你实现一个类似的效果,抛开 xxx 框架以及 xx API属性,你会如何去实现,有没有其他思路?这个问题,意在考量候选人的思维方式和解决问题的能力,以及把他思考的过程阐述清楚的表达能力。这三种能力,往往比使用过某些框架的经验,更让我看中。这道题的回答思路,其实就是可以通过挖掘源码,去实现功能。另外也可以通过海量地查找资料,发现原生js的实现方式,但这条路没有直接挖掘源码来得快。在遇到实际的业务问题的时候,参考源码的原理和写法,往往能更快地解决问题。这是我自己对这道题目,给出的答案。一点点思考昨天的案例,引发了我的一连串思考:现代框架的确降低了前端入门的门槛,提高了开发效率。但是,在使用这些框架的过程中,我们到底学到了什么?脱离了框架和它的API,我们脑海中还剩下些什么?以至于,当下一个更新更棒的框架出现的时候,我们是否能够用已经学到的知识,帮助自己更迅速地上手?知其然,并知其所以然,学习所有的知识都应当有这种探索精神。我们不仅仅是代码的搬运工。领悟这些深层次的原理,比起仅仅熟练地掌握一门框架,要实用得多。PS:欢迎关注我的公众号 “超哥前端小栈”,交流更多的想法与技术。 ...

January 9, 2019 · 1 min · jiezi

Vue源码探究-组件的持久活跃

Vue源码探究-组件的持久活跃本篇代码位于vue/src/core/components/keep-alive.js较新版本的Vue增加了一个内置组件 keep-alive,用于存储组件状态,即便失活也能保持现有状态不变,切换回来的时候不会恢复到初始状态。由此可知,路由切换的钩子所触发的事件处理是无法适用于 keep-alive 组件的,那如果需要根据失活与否来给予组件事件通知,该怎么办呢?如前篇所述,keep-alive 组件有两个特有的生命周期钩子 activated 和 deactivated,用来响应失活状态的事件处理。来看看 keep-alive 组件的实现,代码文件位于 components 里,目前入口文件里也只有 keep-alive 这一个内置组件,但这个模块的分离,会不会预示着官方将在未来开发更多具有特殊功能的内置组件呢?// 导入辅助函数import { isRegExp, remove } from ‘shared/util’import { getFirstComponentChild } from ‘core/vdom/helpers/index’// 定义VNodeCache静态类型// 它是一个包含key名和VNode键值对的对象,可想而知它是用来存储组件的type VNodeCache = { [key: string]: ?VNode };// 定义getComponentName函数,用于获取组件名称,传入组件配置对象function getComponentName (opts: ?VNodeComponentOptions): ?string { // 先尝试获取配置对象中定义的name属性,或无则获取标签名称 return opts && (opts.Ctor.options.name || opts.tag)}// 定义matches函数,进行模式匹配,传入匹配的模式类型数据和name属性function matches (pattern: string | RegExp | Array<string>, name: string): boolean { // 匹配数组模式 if (Array.isArray(pattern)) { // 使用数组方法查找name,返回结果 return pattern.indexOf(name) > -1 } else if (typeof pattern === ‘string’) { // 匹配字符串模式 // 将字符串转换成数组查找name,返回结果 return pattern.split(’,’).indexOf(name) > -1 } else if (isRegExp(pattern)) { // 匹配正则表达式 // 使用正则匹配name,返回结果 return pattern.test(name) } / istanbul ignore next */ // 未匹配正确模式则返回false return false}// 定义pruneCache函数,修剪keep-alive组件缓存对象// 接受keep-alive组件实例和过滤函数function pruneCache (keepAliveInstance: any, filter: Function) { // 获取组件的cache,keys,_vnode属性 const { cache, keys, _vnode } = keepAliveInstance // 遍历cache对象 for (const key in cache) { // 获取缓存资源 const cachedNode: ?VNode = cache[key] // 如果缓存资源存在 if (cachedNode) { // 获取该资源的名称 const name: ?string = getComponentName(cachedNode.componentOptions) // 当名称存在 且不匹配缓存过滤时 if (name && !filter(name)) { // 执行修剪缓存资源操作 pruneCacheEntry(cache, key, keys, _vnode) } } }}// 定义pruneCacheEntry函数,修剪缓存条目// 接受keep-alive实例的缓存对象和键名缓存对象,资源键名和当前资源function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array<string>, current?: VNode) { // 检查缓存对象里是否已经有以key值存储的资源 const cached = cache[key] // 如果有旧资源并且没有传入新资源参数或新旧资源标签不同 if (cached && (!current || cached.tag !== current.tag)) { // 销毁该资源 cached.componentInstance.$destroy() } // 置空key键名存储资源 cache[key] = null // 移除key值的存储 remove(keys, key)}// 定义模式匹配接收的数据类型const patternTypes: Array<Function> = [String, RegExp, Array]// 导出keep-alive组件实例的配置对象export default { // 定义组件名称 name: ‘keep-alive’, // 设置abstract属性 abstract: true, // 设置组件接收的属性 props: { // include用于包含模式匹配的资源,启用缓存 include: patternTypes, // exclude用于排除模式匹配的资源,不启用缓存 exclude: patternTypes, // 最大缓存数 max: [String, Number] }, created () { // 实例创建时定义cache属性为空对象,用于存储资源 this.cache = Object.create(null) // 设置keys数组,用于存储资源的key名 this.keys = [] }, destroyed () { // 实例销毁时一并销毁存储的资源并清空缓存对象 for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { // DOM加载完成后,观察include和exclude属性的变动 // 回调执行修改缓存对象的操作 this.$watch(‘include’, val => { pruneCache(this, name => matches(val, name)) }) this.$watch(’exclude’, val => { pruneCache(this, name => !matches(val, name)) }) }, render () { // 实例渲染函数 // 获取keep-alive包含的子组件结构 // keep-alive组件并不渲染任何真实DOM节点,只渲染嵌套在其中的组件资源 const slot = this.$slots.default // 将嵌套组件dom结构转化成虚拟节点 const vnode: VNode = getFirstComponentChild(slot) // 获取嵌套组件的配置对象 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 如果配置对象存在 if (componentOptions) { // 检查是否缓存的模式匹配 // check pattern // 获取嵌套组件名称 const name: ?string = getComponentName(componentOptions) // 获取传入keep-alive组件的include和exclude属性 const { include, exclude } = this // 如果有included,且该组件不匹配included中资源 // 或者有exclude。且该组件匹配exclude中的资源 // 则返回虚拟节点,不继续执行缓存 if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } // 获取keep-alive组件的cache和keys对象 const { cache, keys } = this // 获取嵌套组件虚拟节点的key const key: ?string = vnode.key == null // 同样的构造函数可能被注册为不同的本地组件,所以cid不是判断的充分条件 // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? ::${componentOptions.tag} : ‘’) : vnode.key // 如果缓存对象里有以key值存储的组件资源 if (cache[key]) { // 设置当前嵌套组件虚拟节点的componentInstance属性 vnode.componentInstance = cache[key].componentInstance // make current key freshest // 从keys中移除旧key,添加新key remove(keys, key) keys.push(key) } else { // 缓存中没有该资源,则直接存储资源,并存储key值 cache[key] = vnode keys.push(key) // 如果设置了最大缓存资源数,从最开始的序号开始删除存储资源 // prune oldest entry if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } // 设置该资源虚拟节点的keepAlive标识 vnode.data.keepAlive = true } // 返回虚拟节点或dom节点 return vnode || (slot && slot[0]) }}keep-alive 组件的实现也就这百来行代码,分为两部分:第一部分是定义一些处理具体实现的函数,比如修剪缓存对象存储资源的函数,匹配组件包含和过滤存储的函数;第二部分是导出一份 keep-alive 组件的应用配置对象,仔细一下这跟我们在实际中使用的方式是一样的,但这个组件具有已经定义好的特殊功能,就是缓存嵌套在它之中的组件资源,实现持久活跃。那么实现原理是什么,在代码里可以清楚得看到,这里是利用转换组件真实DOM节点为虚拟节点将其存储到 keep-alive 实例的 cache 对象中,另外也一并存储了资源的 key 值方便查找,然后在渲染时检测其是否符合缓存条件再进行渲染。keep-alive 的实现就是以上这样简单。最初一瞥此段代码时,不知所云。然而当开始逐步分析代码之后,才发现原来只是没有仔细去看,误以为很深奥,由此可见,任何不用心的行为都不能直抵事物的本质,这是借由探索这一小部分代码而得到的教训。因为在实际中有使用过这个功能,所以体会更深,有时候难免会踩到一些坑,看了源码的实现之后,发现原来是自己使用方式不对,所以了解所用轮子的实现还是很有必要的。 ...

January 9, 2019 · 3 min · jiezi

MyBatis 源码阅读之数据库连接

MyBatis 源码阅读之数据库连接MyBatis 的配置文件所有配置会被 org.apache.ibatis.builder.xml.XMLConfigBuilder 类读取,我们可以通过此类来了解各个配置是如何运作的。而 MyBatis 的映射文件配置会被 org.apache.ibatis.builder.xml.XMLMapperBuilder 类读取。我们可以通过此类来了解映射文件的配置时如何被解析的。本文探讨 事务管理器 和 数据源 相关代码配置environment以下是 mybatis 配置文件中 environments 节点的一般配置。<!– mybatis-config.xml –><environments default=“development”> <environment id=“development”> <transactionManager type=“JDBC”> <property name="…" value="…"/> </transactionManager> <dataSource type=“POOLED”> <property name=“driver” value="${driver}"/> <property name=“url” value="${url}"/> <property name=“username” value="${username}"/> <property name=“password” value="${password}"/> </dataSource> </environment></environments>environments 节点的加载也不算复杂,它只会加载 id 为 development 属性值的 environment 节点。它的加载代码在 XMLConfigBuilder 类的 environmentsElement() 方法中,代码不多,逻辑也简单,此处不多讲。TransactionManager接下来我们看看 environment 节点下的子节点。transactionManager 节点的 type 值默认提供有 JDBC 和 MANAGED ,dataSource 节点的 type 值默认提供有 JNDI 、 POOLED 和 UNPOOLED 。它们对应的类都可以在 Configuration 类的构造器中找到,当然下面我们也一个一个来分析。现在我们大概了解了配置,然后来分析这些配置与 MyBatis 类的关系。TransactionFactorytransactionManager 节点对应 TransactionFactory 接口,使用了 抽象工厂模式 。MyBatis 给我们提供了两个实现类:ManagedTransactionFactory 和 JdbcTransactionFactory ,它们分别对应者 type 属性值为 MANAGED 和 JDBC 。TransactionFactory 有三个方法,我们需要注意的方法只有 newTransaction() ,它用来创建一个事务对象。void setProperties(Properties props);Transaction newTransaction(Connection conn);Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);其中 JdbcTransactionFactory 创建的事务对象是 JdbcTransaction 的实例,它是对 JDBC 事务的简单封装;ManagedTransactionFactory 创建的事务对象是 ManagedTransaction 的实例,它本身并不控制事务,即 commit 和 rollback 都是不做任何操作,而是交由 JavaEE 容器来控制事务,以方便集成。DataSourceFactoryDataSourceFactory 是获取数据源的接口,也使用了 抽象工厂模式 ,代码如下,方法极为简单:public interface DataSourceFactory { /** * 可传入一些属性配置 / void setProperties(Properties props); DataSource getDataSource();}MyBatis 默认支持三种数据源,分别是 UNPOOLED 、 POOLED 和 JNDI 。对应三个工厂类:UnpooledDataSourceFactory 、 PooledDataSourceFactory 和 JNDIDataSourceFactory 。其中 JNDIDataSourceFactory 是使用 JNDI 来获取数据源。我们很少使用,并且代码不是非常复杂,此处不讨论。我们先来看看 UnpooledDataSourceFactory :public class UnpooledDataSourceFactory implements DataSourceFactory { private static final String DRIVER_PROPERTY_PREFIX = “driver.”; private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length(); protected DataSource dataSource; public UnpooledDataSourceFactory() { this.dataSource = new UnpooledDataSource(); } @Override public void setProperties(Properties properties) { Properties driverProperties = new Properties(); // MetaObject 用于解析实例对象的元信息,如字段的信息、方法的信息 MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); for (Object key : properties.keySet()) { String propertyName = (String) key; if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { // 添加驱动的配置属性 String value = properties.getProperty(propertyName); driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); } else if (metaDataSource.hasSetter(propertyName)) { // 为数据源添加配置属性 String value = (String) properties.get(propertyName); Object convertedValue = convertValue(metaDataSource, propertyName, value); metaDataSource.setValue(propertyName, convertedValue); } else { throw new DataSourceException(“Unknown DataSource property: " + propertyName); } } if (driverProperties.size() > 0) { metaDataSource.setValue(“driverProperties”, driverProperties); } } @Override public DataSource getDataSource() { return dataSource; } /* * 将 String 类型的值转为目标对象字段的类型的值 / private Object convertValue(MetaObject metaDataSource, String propertyName, String value) { Object convertedValue = value; Class<?> targetType = metaDataSource.getSetterType(propertyName); if (targetType == Integer.class || targetType == int.class) { convertedValue = Integer.valueOf(value); } else if (targetType == Long.class || targetType == long.class) { convertedValue = Long.valueOf(value); } else if (targetType == Boolean.class || targetType == boolean.class) { convertedValue = Boolean.valueOf(value); } return convertedValue; }}虽然代码看起来复杂,实际上非常简单,在创建工厂实例时创建它对应的 UnpooledDataSource 数据源。setProperties() 方法用于给数据源添加部分属性配置,convertValue() 方式时一个私有方法,就是处理 当 DataSource 的属性为整型或布尔类型时提供对字符串类型的转换功能而已。最后我们看看 PooledDataSourceFactory ,这个类非常简单,仅仅是继承了 UnpooledDataSourceFactory ,然后构造方法替换数据源为 PooledDataSource 。public class PooledDataSourceFactory extends UnpooledDataSourceFactory { public PooledDataSourceFactory() { this.dataSource = new PooledDataSource(); }}虽然它的代码极少,实际上都在 PooledDataSource 类中。DataSource看完了工厂类,我们来看看 MyBatis 提供的两种数据源类: UnpooledDataSource 和 PooledDataSource 。UnpooledDataSourceUnpooledDataSource 看名字就知道是没有池化的特征,相对也简单点,以下代码省略一些不重要的方法import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;public class UnpooledDataSource implements DataSource { private ClassLoader driverClassLoader; private Properties driverProperties; private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>(); private String driver; private String url; private String username; private String password; private Boolean autoCommit; // 事务隔离级别 private Integer defaultTransactionIsolationLevel; static { // 遍历所有可用驱动 Enumeration<Driver> drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); registeredDrivers.put(driver.getClass().getName(), driver); } } // …… private Connection doGetConnection(Properties properties) throws SQLException { // 每次获取连接都会检测驱动 initializeDriver(); Connection connection = DriverManager.getConnection(url, properties); configureConnection(connection); return connection; } /* * 初始化驱动,这是一个 同步 方法 / private synchronized void initializeDriver() throws SQLException { // 如果不包含驱动,则准备添加驱动 if (!registeredDrivers.containsKey(driver)) { Class<?> driverType; try { // 加载驱动 if (driverClassLoader != null) { driverType = Class.forName(driver, true, driverClassLoader); } else { driverType = Resources.classForName(driver); } Driver driverInstance = (Driver)driverType.newInstance(); // 注册驱动代理到 DriverManager DriverManager.registerDriver(new DriverProxy(driverInstance)); // 缓存驱动 registeredDrivers.put(driver, driverInstance); } catch (Exception e) { throw new SQLException(“Error setting driver on UnpooledDataSource. Cause: " + e); } } } private void configureConnection(Connection conn) throws SQLException { // 设置是否自动提交事务 if (autoCommit != null && autoCommit != conn.getAutoCommit()) { conn.setAutoCommit(autoCommit); } // 设置 事务隔离级别 if (defaultTransactionIsolationLevel != null) { conn.setTransactionIsolation(defaultTransactionIsolationLevel); } } private static class DriverProxy implements Driver { private Driver driver; DriverProxy(Driver d) { this.driver = d; } /* * Driver 仅在 JDK7 中定义了本方法,用于返回本驱动的所有日志记录器的父记录器 * 个人也不是十分明确它的用法,毕竟很少会关注驱动的日志 / public Logger getParentLogger() { return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); } // 其他方法均为调用 driver 对应的方法,此处省略 }}这里 DriverProxy 仅被注册到 DriverManager 中,这是一个注意点,我也不懂这种操作,有谁明白的可以留言相互讨论。这里的方法也不是非常复杂,我都已经标有注释,应该都可以看懂,不再细说。以上便是 UnpooledDataSource 的初始化驱动和获取连接关键代码。PooledDataSource接下来我们来看最后一个类 PooledDataSource ,它也是直接实现 DataSource ,不过因为拥有池化的特性,它的代码复杂不少,当然效率比 UnpooledDataSource 会高出不少。PooledDataSource 通过两个辅助类 PoolState 和 PooledConnection 来完成池化功能。PoolState 是记录连接池运行时的状态,定义了两个 PooledConnection 集合用于记录空闲连接和活跃连接。PooledConnection 内部定义了两个 Connection 分别表示一个真实连接和代理连接,还有一些其他字段用于记录一个连接的运行时状态。先来详细了解一下 PooledConnection/* * 此处使用默认的访问权限 * 实现了 InvocationHandler /class PooledConnection implements InvocationHandler { private static final String CLOSE = “close”; private static final Class<?>[] IFACES = new Class<?>[] { Connection.class }; /* hashCode() 方法返回 / private final int hashCode; private final Connection realConnection; private final Connection proxyConnection; // 省略 checkoutTimestamp、createdTimestamp、lastUsedTimestamp private boolean valid; / * Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in * * @param connection - the connection that is to be presented as a pooled connection * @param dataSource - the dataSource that the connection is from / public PooledConnection(Connection connection, PooledDataSource dataSource) { this.hashCode = connection.hashCode(); this.realConnection = connection; this.dataSource = dataSource; this.createdTimestamp = System.currentTimeMillis(); this.lastUsedTimestamp = System.currentTimeMillis(); this.valid = true; this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); } / * 设置连接状态为不正常,不可使用 / public void invalidate() { valid = false; } / * Method to see if the connection is usable * * @return True if the connection is usable / public boolean isValid() { return valid && realConnection != null && dataSource.pingConnection(this); } /* * 自动上一次使用后经过的时间 / public long getTimeElapsedSinceLastUse() { return System.currentTimeMillis() - lastUsedTimestamp; } /* * 存活时间 / public long getAge() { return System.currentTimeMillis() - createdTimestamp; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { // 对于 close() 方法,将连接放回池中 dataSource.pushConnection(this); return null; } else { try { if (!Object.class.equals(method.getDeclaringClass())) { checkConnection(); } return method.invoke(realConnection, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } } private void checkConnection() throws SQLException { if (!valid) { throw new SQLException(“Error accessing PooledConnection. Connection is invalid.”); } }}本类实现了 InvocationHandler 接口,这个接口是用于 JDK 动态代理的,在这个类的构造器中 proxyConnection 就是创建了此代理对象。来看看 invoke() 方法,它拦截了 close() 方法,不再关闭连接,而是将其继续放入池中,然后其他已实现的方法则是每次调用都需要检测连接是否合法。再来说说 PoolState 类,这个类没什么可说的,都是一些统计字段,没有复杂逻辑,不再讨论;注意该类是针对一个 PooledDataSource 对象统计的。也就是说 PoolState 的统计字段是关于整个数据源的,而一个PooledConnection 则是针对单个连接的。最后我们回过头来看 PooledDataSource 类,数据源的操作就只有两个,获取连接,释放连接,先来看看获取连接public class PooledDataSource implements DataSource { private final UnpooledDataSource dataSource; @Override public Connection getConnection() throws SQLException { return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return popConnection(username, password).getProxyConnection(); } /* * 获取一个连接 / private PooledConnection popConnection(String username, String password) throws SQLException { boolean countedWait = false; PooledConnection conn = null; long t = System.currentTimeMillis(); int localBadConnectionCount = 0; // conn == null 也可能是没有获得连接,被通知后再次走流程 while (conn == null) { synchronized (state) { // 是否存在空闲连接 if (!state.idleConnections.isEmpty()) { // 池里存在空闲连接 conn = state.idleConnections.remove(0); } else { // 池里不存在空闲连接 if (state.activeConnections.size() < poolMaximumActiveConnections) { // 池里的激活连接数小于最大数,创建一个新的 conn = new PooledConnection(dataSource.getConnection(), this); } else { // 最坏的情况,无法获取连接 // 检测最早使用的连接是否超时 PooledConnection oldestActiveConnection = state.activeConnections.get(0); long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); if (longestCheckoutTime > poolMaximumCheckoutTime) { // 使用超时连接,对超时连接的操作进行回滚 state.claimedOverdueConnectionCount++; state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; state.accumulatedCheckoutTime += longestCheckoutTime; state.activeConnections.remove(oldestActiveConnection); if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { try { oldestActiveConnection.getRealConnection().rollback(); } catch (SQLException e) { / * Just log a message for debug and continue to execute the following statement * like nothing happened. Wrap the bad connection with a new PooledConnection, * this will help to not interrupt current executing thread and give current * thread a chance to join the next competition for another valid/good database * connection. At the end of this loop, bad {@link @conn} will be set as null. */ log.debug(“Bad connection. Could not roll back”); } } conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp()); conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp()); oldestActiveConnection.invalidate(); } else { // 等待可用连接 try { if (!countedWait) { state.hadToWaitCount++; countedWait = true; } long wt = System.currentTimeMillis(); state.wait(poolTimeToWait); state.accumulatedWaitTime += System.currentTimeMillis() - wt; } catch (InterruptedException e) { break; } } } } // 已获取连接 if (conn != null) { // 检测连接是否可用 if (conn.isValid()) { // 对之前的操作回滚 if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); conn.setCheckoutTimestamp(System.currentTimeMillis()); conn.setLastUsedTimestamp(System.currentTimeMillis()); // 激活连接池数+1 state.activeConnections.add(conn); state.requestCount++; state.accumulatedRequestTime += System.currentTimeMillis() - t; } else { // 连接坏掉了,超过一定阈值则抛异常提醒 state.badConnectionCount++; localBadConnectionCount++; conn = null; if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) { // 省略日志 throw new SQLException( “PooledDataSource: Could not get a good connection to the database.”); } } } } } if (conn == null) { // 省略日志 throw new SQLException( “PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.”); } return conn; }}上面的代码都已经加了注释,总体流程不算复杂:while => 连接为空能否直接从池里拿连接 => 可以则获取连接并返回不能,查看池里的连接是否没满 => 没满则创建一个连接并返回满了,查看池里最早的连接是否超时 => 超时则强制该连接回滚,然后获取该连接并返回未超时,等待连接可用检测连接是否可用释放连接操作,更为简单,判断更少protected void pushConnection(PooledConnection conn) throws SQLException { // 同步操作 synchronized (state) { // 从活动池中移除连接 state.activeConnections.remove(conn); if (conn.isValid()) { // 不超过空闲连接数 并且连接是同一类型的连接 if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { state.accumulatedCheckoutTime += conn.getCheckoutTime(); if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } // 废弃原先的对象 PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); state.idleConnections.add(newConn); newConn.setCreatedTimestamp(conn.getCreatedTimestamp()); newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp()); // 该对象已经不能用于连接了 conn.invalidate(); if (log.isDebugEnabled()) { log.debug(“Returned connection " + newConn.getRealHashCode() + " to pool.”); } state.notifyAll(); } else { state.accumulatedCheckoutTime += conn.getCheckoutTime(); if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } // 关闭连接 conn.getRealConnection().close(); if (log.isDebugEnabled()) { log.debug(“Closed connection " + conn.getRealHashCode() + “.”); } conn.invalidate(); } } else { if (log.isDebugEnabled()) { log.debug(“A bad connection (” + conn.getRealHashCode() + “) attempted to return to the pool, discarding connection.”); } state.badConnectionCount++; } }}部分码注释已添加,这里就说一下总体流程:从活动池中移除连接如果该连接可用连接池未满,则连接放回池中满了,回滚,关闭连接总体流程大概就是这样其他还有两个方法代码较多,但逻辑都很简单:pingConnection() 执行一条 SQL 检测连接是否可用。forceCloseAll() 回滚并关闭激活连接池和空闲连接池中的连接 ...

January 7, 2019 · 7 min · jiezi

vue虚拟dom原理剖析

在vue2.0渲染层做了根本性的改动,那就是引入了虚拟DOM。vue的虚拟dom是基于 snabbdom 改造过来的。了解 snabbdom的原理之后再回过头来看 vue的虚拟dom结构的实现。就难度不大了!于是,这里将自己写的 snabbdom 源码解析的一系列文章做一个汇总。snabbdom源码解析(一) 准备工作snabbdom源码解析(二) h函数snabbdom源码解析(三) vnode对象snabbdom源码解析(四) patch 方法snabbdom源码解析(五) 钩子snabbdom源码解析(六) 模块snabbdom源码解析(七) 事件处理换种方式阅读 ? 请查看:个人博客:snabbdom源码解析系列其他:GitHub个人博客常用代码片段整理

December 28, 2018 · 1 min · jiezi

snabbdom源码解析(一) 准备工作

准备工作前言虚拟 DOM 结构概念随着 react 的诞生而火起来,之后 vue2.0 也加入了虚拟 DOM 的概念。阅读 vue 源码的时候,想了解虚拟 dom 结构的实现,发现在 src/core/vdom/patch.js 的地方。作者说 vue 的虚拟 DOM 的算法是基于 snabbdom 进行改造的。于是 google 一下,发现 snabbdom 实现的十分优雅,代码更易读。 所以决定先去把 snabbdom 的源码啃了之后,再回过头来啃 vue 虚拟 DOM 这一块的实现。什么是虚拟 DOM 结构(Virtual DOM)为什么需要 Virtual DOM在前端刀耕火种的时代,jquery 可谓是一家独大。然而慢慢的人们发现,在我们的代码中布满了一系列操作 DOM 的代码。这些代码难以维护,又容易出错。而且也难以测试。所以,react 利用了 Virtual DOM 简化 dom 操作,让数据与 dom 之间的关系更直观更简单。实现 Virtual DOMVirtual DOM 主要包括以下三个方面:使用 js 数据对象 表示 DOM 结构 -> VNode比较新旧两棵 虚拟 DOM 树的差异 -> diff将差异应用到真实的 DOM 树上 -> patch下面开始来研究 snabbdom 是如何实现这些方面的目录项目路径 : https://github.com/snabbdom/snabbdom首先看一下整体的目录结构,源码主要是在 src 里面,其他的目录:test 、examples 分别是测试用例以及例子。这里我们先关注源码部分── h.ts 创建vnode的函数── helpers └── attachto.ts── hooks.ts 定义钩子── htmldomapi.ts 操作dom的一些工具类── is.ts 判断类型── modules 模块 ├── attributes.ts ├── class.ts ├── dataset.ts ├── eventlisteners.ts ├── hero.ts ├── module.ts ├── props.ts └── style.ts── snabbdom.bundle.ts 入口文件── snabbdom.ts 初始化函数── thunk.ts 分块── tovnode.ts dom元素转vnode── vnode.ts 虚拟节点对象snabbdom.bundle.ts 入口文件我们先从入口文件开始看起import { init } from ‘./snabbdom’;import { attributesModule } from ‘./modules/attributes’; // for setting attributes on DOM elementsimport { classModule } from ‘./modules/class’; // makes it easy to toggle classesimport { propsModule } from ‘./modules/props’; // for setting properties on DOM elementsimport { styleModule } from ‘./modules/style’; // handles styling on elements with support for animationsimport { eventListenersModule } from ‘./modules/eventlisteners’; // attaches event listenersimport { h } from ‘./h’; // helper function for creating vnodes// 入口文件// 初始化,传入需要更新的模块。var patch = init([ // Init patch function with choosen modules attributesModule, classModule, propsModule, styleModule, eventListenersModule]) as (oldVNode: any, vnode: any) => any;// 主要导出 snabbdomBundle , 主要包含两个函数,一个是 修补函数 , 一个是 h 函数export const snabbdomBundle = { patch, h: h as any };export default snabbdomBundle;我们可以看到,入口文件主要导出两个函数 ,patch函数 , 由 snabbdom.ts 的 init 方法,根据传入的 module 来初始化h函数 ,在 h.ts 里面实现。看起来 h函数比 patch 要简单一些,我们去看看到底做了些什么。 ...

December 26, 2018 · 2 min · jiezi

从零讲解搭建一个NIO消息服务端

本文首发于猫叔的博客 | MySelf,如需转载,请申明出处.假设假设你已经了解并实现过了一些OIO消息服务端,并对异步消息服务端更有兴趣,那么本文或许能带你更好的入门,并了解JDK部分源码的关系流程,正如题目所说,笔者将竟可能还原,以初学者能理解的角度,讲诉并构建一个NIO消息服务端。启动通道并注册选择器启动模式感谢Java一直在持续更新,对应的各个API也做得越来越好了,我们本次生成 服务端套接字通道 也是使用到JDK提供的一个方式 open ,我们将启动一个 ServerSocketChannel ,他是一个 支持同步异步模式 的 服务端套接字通道 。它是一个抽象类,官方给了推荐的方式 open 来开启一个我们需要的 服务端套接字通道实例 。(如下的官方源码相关注释)/** * A selectable channel for stream-oriented listening sockets. / public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel{ /* * Opens a server-socket channel. / public static ServerSocketChannel open() throws IOException { return SelectorProvider.provider().openServerSocketChannel(); }}那么好了,我们现在可以确定我们第一步的代码是什么样子的了!没错,和你想象中的一样,这很简单。public class NioServer { public void server(int port) throws IOException{ //1、打开服务器套接字通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); }}本节的重点是 启动模式 ,那么这意味着,我们需要向 ServerSocketChannel 进行标识,那么它是否提供了对用的方法设置 同步异步(阻塞非阻塞) 呢?这很明显,它是提供的,这也是它的核心功能之一,其实应该是它继承的 父抽象类AbstractSelectableChannel 的实现方法: configgureBlocking(Boolean),这个方法将标识我们的 服务端套接字通道 是否阻塞模式。(如下的官方源码相关注释)/* * Base implementation class for selectable channels. / public abstract class AbstractSelectableChannel extends SelectableChannel{ /* * Adjusts this channel’s blocking mode. / public final SelectableChannel configureBlocking(boolean block) throws IOException { synchronized (regLock) { if (!isOpen()) throw new ClosedChannelException(); if (blocking == block) return this; if (block && haveValidKeys()) throw new IllegalBlockingModeException(); implConfigureBlocking(block); blocking = block; } return this; }}那么,我们现在可以进行 启动模式的配置 了,读者很聪明。我们的项目Demo可以这样写: false为非阻塞模式、true为阻塞模式 。public class NioServer { public void server(int port) throws IOException{ //1、打开服务器套接字通道 ServerSocketChannel serverSocketzhannel = ServerSocketChannel.open(); //2、设定为非阻塞、调整此通道的阻塞模式。 serverSocketChannel.configureBlocking(false); }}若未配置阻塞模式,注册选择器 会报 java.nio.channels.IllegalBlockingModeException 异常,相关将于该小节大致讲解说明。套接字地址端口绑定做过消息通讯服务器的朋友应该都清楚,我们需要向服务端 指定IP与端口 ,即使是NIO服务器也是一样的,否则,我们的客户端会报 java.net.ConnectException: Connection refused: connect 异常对于NIO的地址端口绑定,我们也需要用到 ServerSocket服务器套接字 。我们知道在写OIO服务端的时候,我们可能仅仅需要写一句即可,如下。 //将服务器绑定到指定端口 final ServerSocket socket = new ServerSocket(port);当然,JDK在实现NIO的时候就已经想到了,同样,我们可以使用 服务器套接字通道 来获取一个 ServerSocket服务器套接字 。这时的它并没有绑定端口,我们需要对应绑定地址,这个类自身就有一个 bind 方法。(如下源码相关注释)/* * This class implements server sockets. A server socket waits for * requests to come in over the network. It performs some operation * based on that request, and then possibly returns a result to the requester. /public class ServerSocket implements java.io.Closeable { /* * * Binds the {@code ServerSocket} to a specific address * (IP address and port number). / public void bind(SocketAddress endpoint) throws IOException { bind(endpoint, 50); }}通过源码,我们知道,绑定iP与端口 需要一个SocketAddress类,我们仅需要将 IP与端口配置到对应的SocketAddress类 中即可。其实JDK中,已经有了一个更加方便且继承了SocketAddress的类:InetSocketAddress。InetSocketAddress有一个需要一个port为参数的构造方法,它将创建 一个ip为通配符、端口为指定值的套接字地址 。这很方便我们的开发,对吧?(如下源码相关注释)/* * * This class implements an IP Socket Address (IP address + port number) * It can also be a pair (hostname + port number), in which case an attempt * will be made to resolve the hostname. If resolution fails then the address * is said to be <I>unresolved</I> but can still be used on some circumstances * like connecting through a proxy. / public class InetSocketAddress extends SocketAddress{ /* * Creates a socket address where the IP address is the wildcard address * and the port number a specified value. / public InetSocketAddress(int port) { this(InetAddress.anyLocalAddress(), port); }}好了,那么接下来我们的项目代码可以继续添加绑定IP与端口了,我想聪明的你应该有所感觉了。public class NioServer { public void server(int port) throws IOException{ //1、打开服务器套接字通道 ServerSocketChannel serverSocketzhannel = ServerSocketChannel.open(); //2、设定为非阻塞、调整此通道的阻塞模式。 serverSocketChannel.configureBlocking(false); //3、检索与此通道关联的服务器套接字。 ServerSocket serverSocket = serverSocketChannel.socket(); //4、此类实现 ip 套接字地址 (ip 地址 + 端口号) InetSocketAddress address = new InetSocketAddress(port); //5、将服务器绑定到选定的套接字地址 serverSocket.bind(address); }}正如开头我们所说的,你的项目中不添加3-5环节的代码并没有问题,但是当客户端接入时,则会报错,因为客户端将要 接入的地址是连接不到的 ,如会报这样的错误。java.net.ConnectException: Connection refused: connect at sun.nio.ch.Net.connect0(Native Method) at sun.nio.ch.Net.connect(Net.java:457) at sun.nio.ch.Net.connect(Net.java:449) at sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:647) at com.github.myself.WebClient.main(WebClient.java:16)注册选择器接下来会是 NIO实现的重点 ,可能有点难理解,如果希望大家能一次理解,完全深入有点难讲明白,不过先大致点一下。首先要先介绍以下JDK实现NIO的核心:多路复用器(Selector)——选择器先简单并抽象的理解下,Java通过 选择器来实现处理多个Channel链接 ,将空闲未进行数据操作的搁置,优先执行有需求的数据传输,即 通过一个选择器来选择谁需要谁不需要使用共享的线程 。由此,理所当然,这样的选择器应该也有Java自己定义的获取方法, 其自身的 open 就是启动一个这样的选择器。(如下源码相关注释)/* * A multiplexor of {@link SelectableChannel} objects. / public abstract class Selector implements Closeable { /* * Opens a selector. / public static Selector open() throws IOException { return SelectorProvider.provider().openSelector(); } }那么现在,我们还要考虑一件事情,我们的 服务器套接字通道 要如何与 选择器 相关联呢?ServerSocketChannel 有一个注册的方法,这个方法就是将它们两个进行了关联,同时这个注册方法 除了关联选择器外,还标识了注册的状态 ,让我们先看看源码吧。以下的 ServerSocketChannel 继承 —》 AbstractSelectableChannel 继承 —》 SelectableChannel/* * A channel that can be multiplexed via a {@link Selector}. / public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel{ /* * Registers this channel with the given selector, returning a selection * key. / public final SelectionKey register(Selector sel, int ops) throws ClosedChannelException { return register(sel, ops, null); }}我们一般需要将选择器注册上去,并将 ServerSocketChannel 标识为 接受连接 的状态。我们先看看我们的项目代码应该如何写。public class NioServer { public void server(int port) throws IOException{ //1、打开服务器套接字通道 ServerSocketChannel serverSocketzhannel = ServerSocketChannel.open(); //2、设定为非阻塞、调整此通道的阻塞模式。 serverSocketChannel.configureBlocking(false); //3、检索与此通道关联的服务器套接字。 ServerSocket serverSocket = serverSocketChannel.socket(); //4、此类实现 ip 套接字地址 (ip 地址 + 端口号) InetSocketAddress address = new InetSocketAddress(port); //5、将服务器绑定到选定的套接字地址 serverSocket.bind(address); //6、打开Selector来处理Channel Selector selector = Selector.open(); //7、将ServerSocket注册到Selector已接受连接,注册会判断是否为非阻塞模式 SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer readBuff = ByteBuffer.allocate(1024); final ByteBuffer msg = ByteBuffer.wrap(“Hi!\r\n”.getBytes()); while(true){ //下方代码….. } }}注意: 我们前面说到,如果 ServerSocketChannel 没有启动非阻塞模式,那么我们在启动的时候会报 java.lang.IllegalArgumentException 异常,这是为什么呢? 我想我们可能需要更深入底层去看看 register 这个方法(如下源码注释)/* * Base implementation class for selectable channels. / public abstract class AbstractSelectableChannel extends SelectableChannel{ /* * Registers this channel with the given selector, returning a selection key. / public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException { synchronized (regLock) { if (!isOpen()) throw new ClosedChannelException(); if ((ops & ~validOps()) != 0) throw new IllegalArgumentException(); if (blocking) throw new IllegalBlockingModeException(); SelectionKey k = findKey(sel); if (k != null) { k.interestOps(ops); k.attach(att); } if (k == null) { // New registration synchronized (keyLock) { if (!isOpen()) throw new ClosedChannelException(); k = ((AbstractSelector)sel).register(this, ops, att); addKey(k); } } return k; } }}我想我们终于真相大白了,原来注册这个方法会对 ServerSocketChannel 的一系列参数进行 校验 ,只有通过,才能注册成功,所以我们也明白了,为什么 非阻塞是false,同时我们也可以看到,它还对我们所给的标识做了校验,一点要优先注册 接受连接(OP_ACCEPT) 这个状态才行,不然依旧会报 java.lang.IllegalArgumentException 异常。这里解释一下,之所以只接受 OP_ACCEPT ,是因为如果没有一个接受其他链接的主服务,那么通信根本无从说起,同时这样的标识在我们的NIO服务端中 只允许标识一次(一个ServerSocketChannel) 。 可能大家还会好奇有什么标识,我想源码的说明确实写的很清楚了。/* * Operation-set bit for read operations. / public static final int OP_READ = 1 << 0;/* * Operation-set bit for write operations. / public static final int OP_WRITE = 1 << 2;/* * Operation-set bit for socket-connect operations. / public static final int OP_CONNECT = 1 << 3;/* * Operation-set bit for socket-accept operations. / public static final int OP_ACCEPT = 1 << 4;好了,这里给一个调试截图,希望大家也可以慢慢的摸索一下。注意这里的服务端并没有构建完成哦,我们还需要下面的几个步骤。NIO选择实例与兴趣点客户端代码说到这里,我们暂时先休息下,转头看看 客户端的代码 吧,这里就简单的介绍下,我们将建立 一个针对服务地址端口的连接 ,然后不停的循环 写操作与读操作 ,没有对客户端进行 关闭操作。大家如果有兴趣的话,也可以自己调试,并看看部分类的JDK源码,如下给出本项目案例的客户端代码。public class WebClient { public static void main(String[] args) throws IOException { try { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress(“0.0.0.0”,8090)); ByteBuffer writeBuffer = ByteBuffer.allocate(32); ByteBuffer readBuffer = ByteBuffer.allocate(32); writeBuffer.put(“hello”.getBytes()); writeBuffer.flip(); while (true){ writeBuffer.rewind(); socketChannel.write(writeBuffer); readBuffer.clear(); socketChannel.read(readBuffer); readBuffer.flip(); System.out.println(new String(readBuffer.array())); } }catch (IOException e){ e.printStackTrace(); } }}准备IO接入操作这里有点复杂,我也尽可能的思考了表达的方式,首先我们先明确一下,所有的连接都会被Selector所囊括,即我们要获取新接入的连接,也要通过Selector来获取,我们一开始启动的 服务器套接字通道ServerSocketChannel 起到一个接入入口(或许不够准确)的作用,客户端连接通过IP与端口进入后,会 被注册的Selector所获取 到,成为 Selector 其中的一员。但是这里的一员 并不会包括一开始注册并被标志为接收连接 的 ServerSocketChannel 。Selector有这样一个方法,它会自动去等待新的连接事件,如果没有连接接入,那么它将一直处于阻塞状态。通过字面意思我们可以大致这样写代码。while(true){ try{ //1、等到需要处理的新事件:阻塞将一直持续到下一个传入事件 selector.select(); }catch(IOException e){ e.printStackTrace(); break; }}那么这样写好像有点像样,毕竟异常我们也捕获了,同时也使用了刚刚 开启并注册完毕的选择器Selector。让我们看看源码中对于这个方法 select 的注释吧。/* * A multiplexor of {@link SelectableChannel} objects. / public abstract class Selector implements Closeable { /* * Selects a set of keys whose corresponding channels are ready for I/O * operations. */ public abstract int select() throws IOException; }好的,看样子是对的,它将返回一组套接字通道已经准备好执行I/O操作的键。那么这个Key究竟是什么呢?这里可能直观的感受下会更好。如下图是我调试下看到的key对象,我想大家应该可以理解了,这个Key中也会 存放对应连接的Channel与Selector 。具体的内部更深层的就探讨了。那么这也解决了我们接下来的 一个疑问 ,我们要怎么向Selector拿连接进来的实例呢?答案很明显,我们仅需要 获取到这个Keys 就好了。选择键集合操作对于获取Keys这个现在应该已经不是什么问题了,通过上面章节的了解,我想大家也可以想到这样的大致语法。//获取所有接收事件的SelectionKey实例Set<SelectionKey> readykeys = selector.selectedKeys();大家或许会好奇,这里的Key对象居然是前面的 SelectionKey.OP_ACCEPT 对象,是的,这也是接下来要讲的,这很奇妙,也很好玩。前面说到的标识,这是每一个Key自有的,并且是可以 改变的状态 ,在刚刚连接的时候,或许我应该大致的描述一下 一个新连接进入选择器后的流程 :select方法将接受到新接入的连接事件,它会被Selector以Key的形式存储,这时我们需要 对其进行判断 ,是否是已经就绪可以被接受的连接,如果是,这时我们需要 获取这个连接 ,同时也将其设定为 非阻塞的状态 ,并将它 注册到选择器上(当然,这时的标识就不能是一开始的 OP_ACCEPT ),你可以选择性的 注册它的标识 ,之后我们可以通过循环遍历Keys来,让 某一标识的连接去执行对应的操作 。说到这里,我想部分新手可能会有点模糊,我想我还是把接下来的代码都一起放出来吧,大家先看看是否能够再次结合文本进行了解。while (true){ try { //等到需要处理的新事件:阻塞将一直持续到下一个传入事件 selector.select(); }catch (IOException e){ e.printStackTrace(); break; } //获取所有接收事件的SelectionKey实例 Set<SelectionKey> readykeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = readykeys.iterator(); while(iterator.hasNext()){ SelectionKey key = iterator.next(); iterator.remove(); try { //检查事件是否是一个新的已经就绪可以被接受的连接 if (key.isAcceptable()){ //channel:返回为其创建此键的通道。 即使在取消密钥后, 此方法仍将继续返回通道。 ServerSocketChannel server = (ServerSocketChannel)key.channel(); //可选择的通道, 用于面向流的连接插槽。 SocketChannel client = server.accept(); //设定为非阻塞 client.configureBlocking(false); //接受客户端,并将它注册到选择器,并添加附件 client.register(selector,SelectionKey.OP_WRITE | SelectionKey.OP_READ,msg.duplicate()); System.out.println(“Accepted connection from " + client); } //检查套接字是否已经准备好读数据 if (key.isReadable()){ SocketChannel client = (SocketChannel)key.channel(); readBuff.clear(); client.read(readBuff); readBuff.flip(); System.out.println(“received:"+new String(readBuff.array())); //将此键的兴趣集设置为给定的值。 OP_WRITE key.interestOps(SelectionKey.OP_WRITE); } //检查套接字是否已经准备好写数据 if (key.isWritable()){ SocketChannel client = (SocketChannel)key.channel(); //attachment : 检索当前附件 ByteBuffer buffer = (ByteBuffer)key.attachment(); buffer.rewind(); client.write(buffer); //将此键的兴趣集设置为给定的值。 OP_READ key.interestOps(SelectionKey.OP_READ); } }catch (IOException e){ e.printStackTrace(); } }}提示:读到此处,还请各位读者能运行整个demo,并调试下,看看与自己理解的是否有差别。流程效果以下我简单叙述一下,我在调试时的理解与效果。1、启动服务端后,运行到 selector.select(); 后阻塞,因为没有监听到新的连接。2、启动客户端后,selector.select() 监听到新连接,往下执行获取到的Keys的size为1,进入Key标识分支判断3、key.isAcceptable() 首次接入为true,设置为非阻塞,并注释到选择器中修改标识为 SelectionKey.OP_WRITE | SelectionKey.OP_READ ,同时添加附件信息 msg.duplicate() ,首次循环结束4、二次循环,连接未关闭,获取到的Keys的size为1,进入Key标识分支判断。5、由于第一次该Key标识改变,所以这次 key.isAcceptable() 为false,而由于改了标识,所以接下来的 key.isReadable() 、 key.isWritable() 都为true,执行读写操作,循环结束。6、接下来的循环,基本上是key.isReadable() 、 key.isWritable() 都为true,执行读写操作。7、设想一下,如果多加一条链接是什么效果。回顾这里给出几个代码的注意点,希望大家可以自己去了解学习。1、关于 ByteBuffer 本文并不重点讲解,大家可以自行了解2、关于Key标识判断的代码,以下两句的删减是否会对代码有所影响呢?key.interestOps(SelectionKey.OP_WRITE);key.interestOps(SelectionKey.OP_READ);3、如果删除了2中的代码,并把客户端注册选择器并给标识的代码改为以下,那么项目运行效果怎么样呢?client.register(selector, SelectionKey.OP_READ,msg.duplicate());4、如果改了3的代码,可是不删除2的代码,那么效果又是怎么样呢?答案留给读者去揭晓吧,如果你有答案,欢迎留言。个人相关项目InChat : 一个轻量级、高效率的支持多端(应用与硬件Iot)的异步网络应用通讯框架 ...

December 26, 2018 · 5 min · jiezi

【源】ArrayDeque,Collection框架中不起眼的一个类

最近盯上了java collection框架中一个类——ArrayDeque。很多人可能没用过甚至没听说过这个类(i’m sorry,what’s fu*k this?),毕竟你坐在面试官面前的时候,关于数组链表的掌握情况,99%的可能性听到问题会是:说说ArrayList和LinkedList的区别?今天从ArrayDeque入手,换一个角度来检验下我们是否真正掌握了数组、链表。父类和接口不着急分析这个类的核心方法,先看下它的父类和接口,以便在Java Collection宇宙中找准它的定位,顺带从宏观角度窥探下Java Collection框架设计。父类父类是AbstractCollection,看下它的方法add、addAll、remove、clear、iterator、size……是不是都很常见?在你常用的xxList中经常会使用这些方法吧?可以说,AbstractCollection这个抽象类,是这种结构(数组、链表等等)的骨架!接口首先是Queue接口,定义出了最基本的队列功能:那么Deque接口呢?入眼各种xxFirst、xxLast,这种定义决定了它是双端队列的代表!框架设计相继看了接口和父类,楼主你到底想表达啥?嘿嘿,别急,我再反问一个经典问题——抽象类和接口有什么区别?你可能会有各种回答,比如抽象类能自己有自己的实现之类的。不能说不对,但这种答案相当于迷惑于奇技淫巧当中,未得正统。以设计角度来看,其实是is-a(抽象类)和has-a(接口)的区别!抽象类相当于某一个种族的基石比如定义汽车AbstractCar,会规定有轮子有发动机能跑的就是汽车;各家厂商生产的汽车都逃不出这个范畴,甭管你是大众宝马玛莎拉蒂。接口则关注各种功能有些汽车多了座椅加热;有些增设了天窗打开功能。但这些功能都是增强型的,并不是每种汽车都会有!抽象类和接口合理的组合,就产生了奇妙的效果:技能保证种族(类)的结构,又能对其进行扩展(接口)。给出大家熟悉的ArrayList和LinkedList,仔细感受下:这种设计不仅仅限于Java Collection,开源框架中也是如此,比如Spring IOC中的Context、Factory那部分……分析回归到本文的主角 ArrayDeque,既然它实现了Deque,自然具备双端队列的特性。类名中的 Array姓氏,无时无刻不在提醒我们,它是基于数组实现的。类注释中,有句话引起了我的注意:/** * This class is likely to be faster than * {@link Stack} when used as a stack, and faster than {@link LinkedList} * when used as a queue. */(Stack先不管)后半句说,ArrayDeque作为队列时比LinkedList快,看看它是怎么办到的!三大属性:transient Object[] elements; //基于数组实现transient int head; //头指针transient int tail; //尾巴指针技术敏感的同学已经能猜到它是怎么实现的了:数组作为基底,两个指分指头尾,插入删除操作时移动指针;如果头尾指针重合,则需要扩容……下面看看源码实现,是否和我们猜测的一致。构造器private static final int MIN_INITIAL_CAPACITY = 8;// ****** Array allocation and resizing utilities ******private static int calculateSize(int numElements) { int initialCapacity = MIN_INITIAL_CAPACITY; // Find the best power of two to hold elements. // Tests “<=” because arrays aren’t kept full. if (numElements >= initialCapacity) { initialCapacity = numElements; initialCapacity |= (initialCapacity >>> 1); initialCapacity |= (initialCapacity >>> 2); initialCapacity |= (initialCapacity >>> 4); initialCapacity |= (initialCapacity >>> 8); initialCapacity |= (initialCapacity >>> 16); initialCapacity++; if (initialCapacity < 0) // Too many elements, must back off initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements } return initialCapacity;}规定最小值MIN_INITIAL_CAPACITY = 8,如果入参小于8,数组大小就定义成8;如果大于等于8,这一通右移是啥操作?假如我们传入了16,二进制10000,逐步分析下:1.initialCapacity |= (initialCapacity >>> 1)右移1位作|操作,10000->01000,‘或’ 操作后110002.initialCapacity |= (initialCapacity >>> 2)接上一步,右移2位作|操作,11000->00110,‘或’ 操作后111103.initialCapacity |= (initialCapacity >>> 4)接上一步,右移4位作|操作,11110->00001,‘或’ 操作后 11111……后面就两步都是11111 | 00000,结果就是 111114.initialCapacity++二进制数11111,+1之后100000,转换成十进制32最终的负值判断(用于处理超int正向范围情况),先不考虑。结论:这些’或’ 操作,最终得到了大于入参的2的次幂中最小的一个。底层数组始终是2的次幂,为什么如此?带着这个问题继续往下分析// The main insertion and extraction methods are addFirst,// addLast, pollFirst, pollLast. The other methods are defined in// terms of these.以上注释有云,核心方法就4个,我们从add方法入手。插入addFirstpublic void addFirst(E e) { if (e == null) throw new NullPointerException(); elements[head = (head - 1) & (elements.length - 1)] = e; //关键 if (head == tail) doubleCapacity();}head = (head - 1) & (elements.length - 1),玄机就在这里。如果你对1.8的HashMap足够了解,就会知道hashmap的数组大小同样始终是2的次幂。其中很重要的一个原因就是:当lengh是2的次幂的时候,某数字 x 的操作 x & (length - 1) 等价于 x % length,而对二进制的计算机来说 & 操作要比 % 操作效率更好!而且head = (head - 1) & (elements.length - 1),(head初始值0)第一次就将head指针定位到数组末尾了。画图分析下:可见,head指针从后向前移动。addLastpublic void addLast(E e) { if (e == null) throw new NullPointerException(); elements[tail] = e; if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity();}addLast和addFirst原理相同,只是addLast控制tail指针,从前向后移动!上图中再做一次add操作,指针将会重合。比如,再一次addFirst之后:if (head == tail) doubleCapacity(); //扩容触发扩容private void doubleCapacity() { assert head == tail; int p = head; int n = elements.length; int r = n - p; // number of elements to the right of p int newCapacity = n << 1; //左移,等价乘2,依然保持2的次幂 if (newCapacity < 0) throw new IllegalStateException(“Sorry, deque too big”); Object[] a = new Object[newCapacity]; System.arraycopy(elements, p, a, 0, r); System.arraycopy(elements, 0, a, r, p); elements = a; head = 0; tail = n;}通过数组拷贝和重新调整指针,完成了扩容。至于pollFirst、pollLast是addFirst、addLast的相反操作,原理相似,不多做分析……参考这次,彻底弄懂接口及抽象类Jdk1.6 Collections Framework源码解析(3)-ArrayDeque ...

December 22, 2018 · 2 min · jiezi

编写React组件项目实践分析

通过实例给大家分享了编写React组件项目实践的全过程,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。开始前:我们使用ES6、ES7语法如果你不是很清楚展示组件和容器组件的区别,建议您从阅读这篇文章开始如果您有任何的建议、疑问都清在评论里留言 基于类的组件现在开发React组件一般都用的是基于类的组件。下面我们就来一行一样的编写我们的组件:import React, { Component } from ‘react’;import { observer } from ‘mobx-react’; import ExpandableForm from ‘./ExpandableForm’;import ‘./styles/ProfileContainer.css’;其实我很喜欢css in javascript。但是,这个写样式的方法还是太新了。所以我们在每个组件里引入css文件。而且本地引入的import和全局的import会用一个空行来分割。初始化Stateimport React, { Component } from ‘react’import { observer } from ‘mobx-react’ import ExpandableForm from ‘./ExpandableForm’import ‘./styles/ProfileContainer.css’ //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860export default class ProfileContainer extends Component { state = { expanded: false }可以使用了老方法在constructor里初始化state。更多相关可以看这里。但是我们选择更加清晰的方法。同时,我们确保在类前面加上了export default。(译者注:虽然这个在使用了redux的时候不一定对)。propTypes and defaultPropsimport React, { Component } from ‘react’import { observer } from ‘mobx-react’import { string, object } from ‘prop-types’ import ExpandableForm from ‘./ExpandableForm’import ‘./styles/ProfileContainer.css’ export default class ProfileContainer extends Component { state = { expanded: false } static propTypes = { model: object.isRequired, title: string } static defaultProps = { model: { id: 0 }, title: ‘Your Name’ } // …}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860propTypes和defaultProps是静态属性。尽可能在组件类的的前面定义,让其他的开发人员读代码的时候可以立刻注意到。他们可以起到文档的作用。如果你使用了React 15.3.0或者更高的版本,那么需要另外引入prop-types包,而不是使用React.PropTypes。更多内容移步这里。你所有的组件都应该有prop types。方法import React, { Component } from ‘react’import { observer } from ‘mobx-react’import { string, object } from ‘prop-types’ import ExpandableForm from ‘./ExpandableForm’import ‘./styles/ProfileContainer.css’ export default class ProfileContainer extends Component { state = { expanded: false } static propTypes = { model: object.isRequired, title: string } static defaultProps = { model: { id: 0 }, title: ‘Your Name’ } handleSubmit = (e) => { e.preventDefault() this.props.model.save() } handleNameChange = (e) => { this.props.model.changeName(e.target.value) } handleExpand = (e) => { e.preventDefault() this.setState({ expanded: !this.state.expanded }) } // … }//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860在类组件里,当你把方法传递给子组件的时候,需要确保他们被调用的时候使用的是正确的this。一般都会在传给子组件的时候这么做:this.handleSubmit.bind(this)。使用ES6的箭头方法就简单多了。它会自动维护正确的上下文(this)。给setState传入一个方法在上面的例子里有这么一行:this.setState({ expanded: !this.state.expanded });setState其实是异步的!React为了提高性能,会把多次调用的setState放在一起调用。所以,调用了setState之后state不一定会立刻就发生改变。所以,调用setState的时候,你不能依赖于当前的state值。因为i根本不知道它是值会是神马。解决方法:给setState传入一个方法,把调用前的state值作为参数传入这个方法。看看例子:this.setState(prevState => ({ expanded: !prevState.expanded }))拆解组件import React, { Component } from ‘react’import { observer } from ‘mobx-react’ import { string, object } from ‘prop-types’import ExpandableForm from ‘./ExpandableForm’import ‘./styles/ProfileContainer.css’ export default class ProfileContainer extends Component { state = { expanded: false } //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 static propTypes = { model: object.isRequired, title: string } static defaultProps = { model: { id: 0 }, title: ‘Your Name’ } handleSubmit = (e) => { e.preventDefault() this.props.model.save() } handleNameChange = (e) => { this.props.model.changeName(e.target.value) } handleExpand = (e) => { e.preventDefault() this.setState(prevState => ({ expanded: !prevState.expanded })) } //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 render() { const { model, title } = this.props return ( <ExpandableForm onSubmit={this.handleSubmit} expanded={this.state.expanded} onExpand={this.handleExpand}> <div> <h1>{title}</h1> <input type=“text” value={model.name} onChange={this.handleNameChange} placeholder=“Your Name”/> </div> </ExpandableForm> ) }}有多行的props的,每一个prop都应该单独占一行。就如上例一样。要达到这个目标最好的方法是使用一套工具:Prettier。装饰器(Decorator)@observerexport default class ProfileContainer extends Component {如果你了解某些库,比如mobx,你就可以使用上例的方式来修饰类组件。装饰器就是把类组件作为一个参数传入了一个方法。装饰器可以编写更灵活、更有可读性的组件。如果你不想用装饰器,你可以这样:class ProfileContainer extends Component { // Component code}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860export default observer(ProfileContainer)闭包尽量避免在子组件中传入闭包,如:<input type=“text” value={model.name} // onChange={(e) => { model.name = e.target.value }} // ^ Not this. Use the below: onChange={this.handleChange} placeholder=“Your Name”/>注意:如果input是一个React组件的话,这样自动触发它的重绘,不管其他的props是否发生了改变。一致性检验是React最消耗资源的部分。不要把额外的工作加到这里。处理上例中的问题最好的方法是传入一个类方法,这样还会更加易读,更容易调试。如:import React, { Component } from ‘react’import { observer } from ‘mobx-react’import { string, object } from ‘prop-types’// Separate local imports from dependenciesimport ExpandableForm from ‘./ExpandableForm’import ‘./styles/ProfileContainer.css’//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 // Use decorators if needed@observerexport default class ProfileContainer extends Component { state = { expanded: false } // Initialize state here (ES7) or in a constructor method (ES6) // Declare propTypes as static properties as early as possible static propTypes = { model: object.isRequired, title: string } // Default props below propTypes static defaultProps = { model: { id: 0 }, title: ‘Your Name’ } //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 // Use fat arrow functions for methods to preserve context (this will thus be the component instance) handleSubmit = (e) => { e.preventDefault() this.props.model.save() } handleNameChange = (e) => { this.props.model.name = e.target.value } handleExpand = (e) => { e.preventDefault() this.setState(prevState => ({ expanded: !prevState.expanded })) } render() { // Destructure props for readability const { model, title } = this.props return ( <ExpandableForm onSubmit={this.handleSubmit} expanded={this.state.expanded} onExpand={this.handleExpand}> // Newline props if there are more than two <div> <h1>{title}</h1> <input type=“text” value={model.name} // onChange={(e) => { model.name = e.target.value }} // Avoid creating new closures in the render method- use methods like below onChange={this.handleNameChange} placeholder=“Your Name”/> </div>//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 </ExpandableForm> ) }}方法组件这类组件没有state没有props,也没有方法。它们是纯组件,包含了最少的引起变化的内容。经常使用它们。propTypesimport React from ‘react’import { observer } from ‘mobx-react’import { func, bool } from ‘prop-types’import ‘./styles/Form.css’ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860// Component declaration我们在组件的声明之前就定义了propTypes。分解Props和defaultPropsimport React from ‘react’import { observer } from ‘mobx-react’import { func, bool } from ‘prop-types’import ‘./styles/Form.css’ ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 function ExpandableForm(props) { const formStyle = props.expanded ? {height: ‘auto’} : {height: 0} return ( <form style={formStyle} onSubmit={props.onSubmit}> {props.children} <button onClick={props.onExpand}>Expand</button> </form> )}我们的组件是一个方法。它的参数就是props。我们可以这样扩展这个组件:import React from ‘react’import { observer } from ‘mobx-react’import { func, bool } from ‘prop-types’import ‘./styles/Form.css’ //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired} function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) { const formStyle = expanded ? {height: ‘auto’} : {height: 0} return (//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form> )}现在我们也可以使用默认参数来扮演默认props的角色,这样有很好的可读性。如果expanded没有定义,那么我们就把它设置为false。但是,尽量避免使用如下的例子:const ExpandableForm = ({ onExpand, expanded, children }) => {看起来很现代,但是这个方法是未命名的。如果你的Babel配置正确,未命名的方法并不会是什么大问题。但是,如果Babel有问题的话,那么这个组件里的任何错误都显示为发生在 <>里的,这调试起来就非常麻烦了。匿名方法也会引起Jest其他的问题。由于会引起各种难以理解的问题,而且也没有什么实际的好处。我们推荐使用function,少使用const。装饰方法组件由于方法组件没法使用装饰器,只能把它作为参数传入别的方法里。import React from ‘react’import { observer } from ‘mobx-react’import { func, bool } from ‘prop-types’import ‘./styles/Form.css’ //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired} function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) { const formStyle = expanded ? {height: ‘auto’} : {height: 0} return ( <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form>//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 )}export default observer(ExpandableForm)只能这样处理:export default observer(ExpandableForm)。这就是组件的全部代码:import React from ‘react’import { observer } from ‘mobx-react’import { func, bool } from ‘prop-types’// Separate local imports from dependenciesimport ‘./styles/Form.css’ //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860// Declare propTypes here, before the component (taking advantage of JS function hoisting)// You want these to be as visible as possibleExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired} // Destructure props like so, and use default arguments as a way of setting defaultPropsfunction ExpandableForm({ onExpand, expanded = false, children, onSubmit }) { const formStyle = expanded ? { height: ‘auto’ } : { height: 0 } return ( <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form> )} // Wrap the component instead of decorating itexport default observer(ExpandableForm)条件判断某些情况下,你会做很多的条件判断:<div id=“lb-footer”> {props.downloadMode && currentImage && !currentImage.video && currentImage.blogText ? !currentImage.submitted && !currentImage.posted ? <p>Please contact us for content usage</p> : currentImage && currentImage.selected ? <button onClick={props.onSelectImage} className=“btn btn-selected”>Deselect</button> : currentImage && currentImage.submitted ? <button className=“btn btn-submitted” disabled>Submitted</button> : currentImage && currentImage.posted ? <button className=“btn btn-posted” disabled>Posted</button> : <button onClick={props.onSelectImage} className=“btn btn-unselected”>Select post</button> }//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860</div>这么多层的条件判断可不是什么好现象。有第三方库JSX-Control Statements可以解决这个问题。但是与其增加一个依赖,还不如这样来解决:<div id=“lb-footer”> {//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 (() => { if(downloadMode && !videoSrc) { if(isApproved && isPosted) { return <p>Right click image and select “Save Image As..” to download</p> } else { return <p>Please contact us for content usage</p> } } // … })() }</div>使用大括号包起来的IIFE,然后把你的if表达式都放进去。返回你要返回的组件。结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 20, 2018 · 5 min · jiezi

深入浅出之React-redux中connect的装饰器用法@connect

这篇文章主要介绍了react-redux中connect的装饰器用法@connect详解,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。通常我们需要一个reducer和一个action,然后使用connect来包裹你的Component。假设你已经有一个key为main的reducer和一个action.js. 我们的App.js一般都这么写:import React from ‘react’import {render} from ‘react-dom’import {connect} from ‘react-redux’import {bindActionCreators} from ‘redux’import action from ‘action.js’ class App extends React.Component{ render(){ return <div>hello</div> }}function mapStateToProps(state){ return state.main}function mapDispatchToProps(dispatch){ return bindActionCreators(action,dispatch)}//欢迎加入前端全栈开发交流圈一起学习交流:864305860export default connect(mapStateToProps,mapDispatchToProps)(App)这样并没有什么问题。看着connect的用法,有没有觉得很熟悉?典型的wrapper嘛,这里必须拿装饰器来装X,稍微改一改:import React from ‘react’import {render} from ‘react-dom’import {connect} from ‘react-redux’import {bindActionCreators} from ‘redux’import action from ‘action.js’ @connect( state=>state.main, dispatch=>bindActionCreators(action,dispatch))class App extends React.Component{ render(){ return <div>hello</div> }//欢迎加入前端全栈开发交流圈一起学习交流:864305860}emmm,这样舒服很多了,在我们实际项目中,可能是一个模块下面又有很多个小组件,它们都共用同样的action和reducer,我们在每个组件中都这么写,是不是有点太麻烦了?冗余代码太多了。其实是可以把connect抽取出来的,比如写一个connect.js:import {connect} from ‘react-redux’import {bindActionCreators} from ‘redux’import action from ‘action.js’ export default connect( state=>state.main, dispatch=>bindActionCreators(action,dispatch))//欢迎加入前端全栈开发交流圈一起学习交流:864305860然后在需要用到的组件中这么用:import React from ‘react’import {render} from ‘react-dom’import connect from ‘connect.js’ @connectexport default class App extends React.Component{ render(){ return <div>hello</div> }}//欢迎加入前端全栈开发交流圈一起学习交流:864305860这样就ok了,和最开始的用法比起来,是不是明显更装X更好用?需要说明的是,这里用了装饰器,需要安装模块babel-plugin-transform-decorators-legacy,然后在babel中配置:{ “plugins”:[ “transform-decorators-legacy” ]}//欢迎加入前端全栈开发交流圈一起学习交流:864305860如果你用的是vscode, 可以在项目根目录下添加jsconfig.json文件来消除代码警告:{ “compilerOptions”: { “experimentalDecorators”: true }}//欢迎加入前端全栈开发交流圈一起学习交流:864305860结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 18, 2018 · 1 min · jiezi

揭开redux,react-redux的神秘面纱

16年开始使用react-redux,迄今也已两年多。这时候再来阅读和读懂redux/react-redux源码,虽已没有当初的新鲜感,但依然觉得略有收获。把要点简单写下来,一方面供感兴趣的读者参考,另一方面也是自己做下总结。reduxreact-redux最核心的内容就是redux。内带redux,react-redux只提供了几个API来关联redux与react的组件以及react state的更新。首先,看下如何使用redux。 redux老司机可以直接滑动滚轮至下一章。 简单来说,redux有三个概念,action, reducer 和 dispatch。 action和dispatch比较好理解:动作指令和提交动作指令方法。而reducer,个人在字面上没有理解,但抽象层面上可以理解为用来生成state的函数。用一个简单案例体现这三个概念:// actionconst INCREMENT = { type: ‘INCREMENT’ }// reducerfunction count( state = 0, action ) { switch( action.type ) { case ‘INCREMENT’: return state + 1 default: return state }}// dispatch// 此处开始使用reduxconst store = redux.createStore( count )console.log( store.getState() ) // 0store.dispatch( INCREMENT )console.log( store.getState() ) // 1接下来说说redux中的两大模块:store对象中间件store对象APIcreateStore会创建了一个store对象,创建的过程中它主要做了下面两件事:初始化state暴露相关接口:getState(), dispatch( action ), subscribe( listener )等。其中getState()用来获取store中的实时state, dispatch(action)根据传入的action更新state, subscribe( listener)可以监听state的变化。中间件中间件可以用来debug或提交异步动作指令. 在初始化store的时候,我们通过createStore( reducer, state, applyMiddleware( middleware1, middleware2 ) )添加多个中间件。 为了实现多个中间件,redux专门引入了函数式编程的compose()方法,简单来说,compose将多层函数调用的写法变得优雅:// 未使用compose方法a( b( c( ’d’ ) ) )// 用compose方法compose( a, b, c )(’d’)而中间件的写法比较奇特,是多级函数,在阅读源码的时候有点绕。显然中间件的写法还可以优化,尽管现在的写法方便在源码中使用,但对redux用户来说稍显复杂,可以用单层函数。function logMiddleware({ getState }) { return nextDispatch => action => { console.log( ‘before dispatch’, getState() ) const res = nextDispatch( action ) console.log( ‘after dispatch’, getState() ) return res }}react-redux了解了redux运作原理,就可以知道react-redux的大部分使用场景是如何运作。react-redux提供了几个API将redux与react相互关联。基于上一个案例展示react-redux的用法:// actionconst increment = () => ({ type: ‘INCREMENT’ })// reducerfunction count( state = 0, action ) { switch( action.type ) { case ‘INCREMENT’: return state + 1 default: return state }}// reduxconst store = Redux.createStore( count )// react-reduxconst { Provider, connect } = ReactReduxconst mapStateToProps = state => ( { count: state } )const mapDispatchToProps = dispatch => ( { increment : () => dispatch( increment() ) } )const App = connect( mapStateToProps, mapDispatchToProps )( class extends React.Component { onClick = () => { this.props.increment() } render() { return <div> <p>Count: { this.props.count }</p> <button onClick={ this.onClick }>+</button> </div> }} )ReactDOM.render( <Provider store={ store }> <App /></Provider>, document.getElementById( ‘app’ ) )点击运行案例react-redux提供最常用的两个API是:ProviderconnectProviderProvider本质上是一个react组件,通过react的context api(使一个组件可以跨多级组件传递props)挂载redux store中的state,并且当组件初始化后开始监听state。当监听到state改变,Provider会重新setState在context上的storeState,简要实现代码如下:class Provider extends Component { constructor(props) { super(props) const { store } = props this.state = { storeState: Redux.store.getState(), } } componentDidMount() { this.subscribe() } subscribe() { const { store } = this.props store.subscribe(() => { const newStoreState = store.getState() this.setState(providerState => { return { storeState: newStoreState } }) }) } render() { const Context = React.createContext(null) <Context.Provider value={this.state}> {this.props.children} </Context.Provider> }}connect()connect方法通过connectHOC(HOC: react高阶组件)将部分或所有state以及提交动作指令方法赋值给react组件的props。小结写react不用redux就像写代码不用git, 我们需要用redux来更好地管理react应用中的state。了解redux/react-redux的运作原理会消除我们在使用redux开发时的未知和疑惑,并且在脑中有一个完整的代码执行回路,让开发流程变得透明,直观。 如果本文帮助到了你,我也十分荣幸, 欢迎点赞和收藏。如果有任何疑问或者建议,都欢迎在下方评论区提出。 ...

December 18, 2018 · 2 min · jiezi

深入解析React中的元素、组件、实例和节点

React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。React 中的元素、组件、实例和节点,是React中关系密切的4个概念,也是很容易让React 初学者迷惑的4个概念。现在,我就来详细地介绍这4个概念,以及它们之间的联系和区别,满足喜欢咬文嚼字、刨根问底的同学的好奇心。元素 (Element)React 元素其实就是一个简单JavaScript对象,一个React 元素和界面上的一部分DOM对应,描述了这部分DOM的结构及渲染效果。一般我们通过JSX语法创建React 元素,例如:const element = <h1 className=‘greeting’>Hello, world</h1>;element是一个React 元素。在编译环节,JSX 语法会被编译成对React.createElement()的调用,从这个函数名上也可以看出,JSX语法返回的是一个React 元素。上面的例子编译后的结果为:const element = React.createElement( ‘h1’, {className: ‘greeting’}, ‘Hello, world!’);最终,element的值是类似下面的一个简单JavaScript对象:const element = { type: ‘h1’, props: { className: ‘greeting’, children: ‘Hello, world’ }//欢迎加入前端全栈开发交流圈一起学习交流:864305860}React 元素可以分为两类:DOM类型的元素和组件类型的元素。DOM类型的元素使用像h1、div、p等DOM节点创建React 元素,前面的例子就是一个DOM类型的元素;组件类型的元素使用React 组件创建React 元素,例如:const buttonElement = <Button color=‘red’>OK</Button>;buttonElement就是一个组件类型的元素,它的值是:const buttonElement = { type: ‘Button’, props: { color: ‘red’, children: ‘OK’ }//欢迎加入前端全栈开发交流圈一起学习交流:864305860}对于DOM类型的元素,因为和页面的DOM节点直接对应,所以React知道如何进行渲染。但是对于组件类型的元素,如buttonElement,React是无法直接知道应该把buttonElement渲染成哪种结构的页面DOM,这时就需要组件自身提供React能够识别的DOM节点信息,具体实现方式在介绍组件时会详细介绍。有了React 元素,我们应该如何使用它呢?其实,绝大多数情况下,我们都不会直接使用React 元素,React 内部会自动根据React 元素,渲染出最终的页面DOM。更确切地说,React元素描述的是React虚拟DOM的结构,React会根据虚拟DOM渲染出页面的真实DOM。组件 (Component)React 组件,应该是大家最熟悉的React中的概念。React通过组件的思想,将界面拆分成一个个可以复用的模块,每一个模块就是一个React 组件。一个React 应用由若干组件组合而成,一个复杂组件也可以由若干简单组件组合而成。React组件和React元素关系密切,React组件最核心的作用是返回React元素。这里你也许会有疑问:React元素不应该是由React.createElement() 返回的吗?但React.createElement()的调用本身也是需要有“人”负责的,React组件正是这个“责任人”。React组件负责调用React.createElement(),返回React元素,供React内部将其渲染成最终的页面DOM。既然组件的核心作用是返回React元素,那么最简单的组件就是一个返回React元素的函数:function Welcome(props) { return <h1>Hello, {props.name}</h1>;}Welcome是一个用函数定义的组件。如果使用类(class)定义组件,返回React元素的工作具体就由组件的render方法承担,例如:class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; }//欢迎加入前端全栈开发交流圈一起学习交流:864305860}其实,使用类定义的组件,render方法是唯一必需的方法,其他组件的生命周期方法都只不过是为render服务而已,都不是必需的。现在来考虑下面这个例子:class Home extends React.Component { render() { return ( <div> <Welcome name=‘前端攻城老湿’ /> <p>Anything you like</p> </div> )//欢迎加入前端全栈开发交流圈一起学习交流:864305860 }}Home 组件使用了Welcome组件,返回的React元素为:{ type: ‘div’, props: { children: [ { type: ‘Welcome’, props: { name: ‘前端攻城老湿’ } }, { type: ‘p’, props: { children: ‘Anything you like’ }//欢迎加入前端全栈开发交流圈一起学习交流:864305860 }, ] }}对于这个结构,React 知道如何渲染type = ‘div’ 和 type = ‘p’ 的节点,但不知道如何渲染type=‘Welcome’的节点,当React 发现Welcome 是一个React 组件时(判断依据是Welcome首字母为大写),会根据Welcome组件返回的React 元素决定如何渲染Welcome节点。Welcome组件返回的React 元素为:{ type: ‘h1’, props: { children: ‘Hello, 前端攻城小牛’ }//欢迎加入前端全栈开发交流圈一起学习交流:864305860}这个结构中只包含DOM节点,React是知道如何渲染的。如果这个结构中还包含其他组件节点,React 会重复上面的过程,继续解析对应组件返回的React 元素,直到返回的React 元素中只包含DOM节点为止。这样的递归过程,让React 获取到页面的完整DOM结构信息,渲染的工作自然就水到渠成了。另外,如果仔细思考的话,可以发现,React 组件的复用,本质上是为了复用这个组件返回的React 元素,React 元素是React 应用的最基础组成单位。实例 (Instance)这里的实例特指React组件的实例。React 组件是一个函数或类,实际工作时,发挥作用的是React 组件的实例对象。只有组件实例化后,每一个组件实例才有了自己的props和state,才持有对它的DOM节点和子组件实例的引用。在传统的面向对象的开发方式中,实例化的工作是由开发者自己手动完成的,但在React中,组件的实例化工作是由React自动完成的,组件实例也是直接由React管理的。换句话说,开发者完全不必关心组件实例的创建、更新和销毁。节点 (Node)在使用PropTypes校验组件属性时,有这样一种类型:MyComponent.propTypes = { optionalNode: PropTypes.node,}PropTypes.node又是什么类型呢?这表明optionalNode是一个React 节点。React 节点是指可以被React渲染的数据类型,包括数字、字符串、React 元素,或者是一个包含这些类型数据的数组。例如:// 数字类型的节点function MyComponent(props) { return 1;} // 字符串类型的节点function MyComponent(props) { return ‘MyComponent’;}//欢迎加入前端全栈开发交流圈一起学习交流:864305860 // React元素类型的节点function MyComponent(props) { return <div>React Element</div>;} // 数组类型的节点,数组的元素只能是其他合法的React节点function MyComponent(props) { const element = <div>React Element</div>; const arr = [1, ‘MyComponent’, element]; return arr;}//欢迎加入前端全栈开发交流圈一起学习交流:864305860 // 错误,不是合法的React节点function MyComponent(props) { const obj = { a : 1} return obj;}总结一下,React 元素和组件的概念最重要,也最容易混淆;React 组件实例的概念大家了解即可,几乎使用不到;React 节点有一定使用场景,但看过本文后应该也就不存在理解问题了。结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 14, 2018 · 2 min · jiezi

深入总结Javascript原型及原型链

本篇文章给大家详细分析了javascript原型及原型链的相关知识点以及用法分享,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个原型对象,而这个原型对象中拥有的属性和方法可以被所以实例共享function Person(){}Person.prototype.name = “Nicholas”;Person.prototype.age = 29;Person.prototype.sayName = function(){alert(this.name);};var person1 = new Person();person1.sayName(); //“Nicholas"var person2 = new Person();person2.sayName(); //“Nicholas"alert(person1.sayName == person2.sayName); //true//欢迎加入前端全栈开发交流圈一起学习交流:864305860一、理解原型对象无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫 [[Prototype]] 。虽然在脚本中没有标准的方式访问 [[Prototype]] ,但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性__proto__ ;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。以前面使用 Person 构造函数和 Person.prototype 创建实例的代码为例,图 6-1 展示了各个对象之间的关系。在此, Person.prototype 指向了原型对象,而 Person.prototype.constructor 又指回了 Person 。person1 和 person2 都包含一个内部属性,该属性仅仅指向了 Person.prototype ;换句话说,它们与构造函数没有直接的关系。可以调用 person1.sayName() 。这是通过查找对象属性的过程来实现的。(会先在实例上搜索,如果搜索不到就会继续搜索原型。)用isPrototypeOf()方法判断实例与原型对象之间的关系<br>alert(Person.prototype.isPrototypeOf(person1)); //truealert(Person.prototype.isPrototypeOf(person2)) //true<br><br>用Object.getPrototypeOf() 方法返回实例的原型对象<br>alert(Object.getPrototypeOf(person1) == Person.prototype); //true<br><br>使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。<br>alert(person1.hasOwnProperty(“name”)); //false 来着原型<br>person1.name = “Greg”;<br>alert(person1.name); //“Greg”——来自实例<br>alert(person1.hasOwnProperty(“name”)); /true<br>//欢迎加入前端全栈开发交流圈一起学习交流:864305860二、更简单的原型语法前面例子中每添加一个属性和方法就要敲一遍 Person.prototype 。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。function Person(){}Person.prototype = { name : “Nicholas”, age : 29, job: “Software Engineer”, sayName : function () { alert(this.name); }//欢迎加入前端全栈开发交流圈一起学习交流:864305860};在上面的代码中,我们将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外: constructor 属性不再指向 Person 了。前面曾经介绍过,每创建一个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。var friend = new Person();alert(friend instanceof Object); //truealert(friend instanceof Person); //truealert(friend.constructor == Person); //falsealert(friend.constructor == Object); //true//欢迎加入前端全栈开发交流圈一起学习交流:864305860在此,用 instanceof 操作符测试 Object 和 Person 仍然返回 true ,但 constructor 属性则等于 Object 而不等于 Person 了。如果 constructor 的值真的很重要,可以像下面这样特意将它设置回适当的值。function Person(){}Person.prototype = { constructor : Person, name : “Nicholas”, age : 29, job: “Software Engineer”, sayName : function () { alert(this.name); }//欢迎加入前端全栈开发交流圈一起学习交流:864305860};三、原生对象的原型所有原生引用类型( Object 、 Array 、 String ,等等)都在其构造函数的原型上定义了方法。例如,在 Array.prototype 中可以找到 sort() 方法,而在 String.prototype 中可以找到substring() 方法。尽管可以这样做,但不推荐修改原生对象的原型。四、原型对象的问题原型模式的最大问题是由其共享的本性所导致的。 修改其中的一个,另一个也会受影响。function Person(){}Person.prototype = {constructor: Person,name : “Nicholas”,age : 29,job : “Software Engineer”,friends : [“Shelby”, “Court”],sayName : function () {alert(this.name);}//欢迎加入前端全栈开发交流圈一起学习交流:864305860};var person1 = new Person();var person2 = new Person();person1.friends.push(“Van”);alert(person1.friends); //“Shelby,Court,Van"alert(person2.friends); //“Shelby,Court,Van"alert(person1.friends === person2.friends); //true五、原型链其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。然后层层递进,就构成了实例与原型的链条,这就是所谓原型链的基本概念。function SuperType(){ this.property = true;}//欢迎加入前端全栈开发交流圈一起学习交流:864305860SuperType.prototype.getSuperValue = function(){ return this.property;};function SubType(){ this.subproperty = false;}//欢迎加入前端全栈开发交流圈一起学习交流:864305860//继承了 SuperTypeSubType.prototype = new SuperType();SubType.prototype.getSubValue = function (){ return this.subproperty;};//欢迎加入前端全栈开发交流圈一起学习交流:864305860var instance = new SubType();alert(instance.getSuperValue()); //trueproperty 则位于 SubType.prototype 中。这是因为 property 是一个实例属性,而 getSuperValue() 则是一个原型方法。既然 SubType.prototype 现在是 SuperType的实例,那么 property 当然就位于该实例中了结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 13, 2018 · 2 min · jiezi

解析Vue-router相关干货及工作原理

本文主要介绍了vue-router相关基础知识及单页面应用的工作原理,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。单页面工作原理是通过浏览器URL的#后面的hash变化就会引起页面变化的特性来把页面分成不同的小模块,然后通过修改hash来让页面展示我们想让看到的内容。那么为什么hash的不同,为什么会影响页面的展示呢?浏览器在这里面做了什么内容。以前#后面的内容一般会做锚点,但是会定位到一个页面的某个位置,这个是怎么做到的呢,和我们现在的路由有什么不同。(我能想到一个路由的展示就会把其他路由隐藏,是这样的吗)后面会看一看写一下这个疑惑,现在最重要的是先把基本概念弄熟。当你要把 vue-router 添加进来,我们需要做的是,将组件(components)映射到路由(routes),然后告诉 vue-router 在哪里渲染它们起步//*** router-link 告诉浏览器去哪个路由//*** router-view 告诉路由在哪里展示内容<div id=“app”> <h1>Hello App!</h1> <p> <!– 使用 router-link 组件来导航. –> <!– 通过传入 to 属性指定链接. –> <!– <router-link> 默认会被渲染成一个 &lt;a&gt; 标签 –> <router-link to="/foo">Go to Foo</router-link> <router-link to="/bar">Go to Bar</router-link> </p> <!– 路由出口 –> <!– 路由匹配到的组件将渲染在这里 –> <router-view></router-view></div>// 1. 定义(路由)组件。// 可以从其他文件 import 进来const Foo = { template: ‘<div>foo</div>’ }const Bar = { template: ‘<div>bar</div>’ }// 2. 定义路由// 每个路由应该映射一个组件。 其中"component" 可以是// 通过 Vue.extend() 创建的组件构造器,// 或者,只是一个组件配置对象。// 我们晚点再讨论嵌套路由。const routes = [ { path: ‘/foo’, component: Foo }, { path: ‘/bar’, component: Bar }]//欢迎加入前端全栈开发交流圈一起学习交流:864305860// 3. 创建 router 实例,然后传 routes 配置// 你还可以传别的配置参数, 不过先这么简单着吧。const router = new VueRouter({ routes // (缩写)相当于 routes: routes})// 4. 创建和挂载根实例。// 记得要通过 router 配置参数注入路由,// 从而让整个应用都有路由功能const app = new Vue({ router}).$mount(’#app’)// 现在,应用已经启动了!动态路由匹配相当于同一个组件,因为参数不同展示不同的组件内容,其实就是在 vue-router 的路由路径中使用『动态路径参数』const router = new VueRouter({ routes: [ // 动态路径参数 以冒号开头 { path: ‘/user/:id’, component: User } ]})那么我们进入uesr/001 和 user/002 其实是进入的同一个路由,可以根据参数的不同在内容页展示不同的内容。一般适用场景:列表,权限控制定义的时候用: 表示是动态路由使用 {{ $route.params.id }} 来拿到本路由里面参数的内容当使用路由参数时,例如从 /user/foo 导航到 /user/bar,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch(监测变化) $route 对象const User = { template: ‘…’, watch: { ‘$route’ (to, from) { // 对路由变化作出响应… }//欢迎加入前端全栈开发交流圈一起学习交流:864305860 }}有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:谁先定义的,谁的优先级就最高。嵌套路由在路由里面嵌套一个路由//路由里面也会出现 <router-view> 这是嵌套路由展示内容的地方const User = { template: &lt;div class="user"&gt; &lt;h2&gt;User {{ $route.params.id }}&lt;/h2&gt; &lt;router-view&gt;&lt;/router-view&gt; &lt;/div&gt;}//定义路由的时候在 加children 子路由属性const router = new VueRouter({ routes: [ { path: ‘/user/:id’, component: User, children: [ { // 当 /user/:id/profile 匹配成功, // UserProfile 会被渲染在 User 的 <router-view> 中 path: ‘profile’, component: UserProfile },//欢迎加入前端全栈开发交流圈一起学习交流:864305860 { // 当 /user/:id/posts 匹配成功 // UserPosts 会被渲染在 User 的 <router-view> 中 path: ‘posts’, component: UserPosts } ] } ]})设置空路由,在没有指定路由的时候就会展示空路由内容const router = new VueRouter({ routes: [ { path: ‘/user/:id’, component: User, children: [ // 当 /user/:id 匹配成功, // UserHome 会被渲染在 User 的 <router-view> 中 { path: ‘’, component: UserHome }, ]//欢迎加入前端全栈开发交流圈一起学习交流:864305860 } ]})编程式导航声明式:<router-link :to="…">编程式:router.push(…)可以想象编程式 push 可以理解为向浏览器历史里面push一个新的hash,导致路由发生变化router.replace() 修改路由但是不存在历史里面router.go(n) 有点像JS的window.history.go(n)命名路由 就是给每一个路由定义一个名字。命名视图有时候想同时(同级)展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar(侧导航) 和 main(主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default。<router-view class=“view one”></router-view><router-view class=“view two” name=“a”></router-view><router-view class=“view three” name=“b”></router-view>一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置(带上 s):const router = new VueRouter({ routes: [ { path: ‘/’, components: { default: Foo, a: Bar, b: Baz } } ]})//欢迎加入前端全栈开发交流圈一起学习交流:864305860重定向和别名重定向也是通过 routes 配置来完成,下面例子是从 /a 重定向到 /b:const router = new VueRouter({ routes: [ { path: ‘/a’, redirect: ‘/b’ } ]})一般首页的时候可以重定向到其他的地方重定向的目标也可以是一个命名的路由:const router = new VueRouter({ routes: [ { path: ‘/a’, redirect: { name: ‘foo’ }} ]//欢迎加入前端全栈开发交流圈一起学习交流:864305860})甚至是一个方法,动态返回重定向目标:const router = new VueRouter({ routes: [ { path: ‘/a’, redirect: to => { // 方法接收 目标路由 作为参数 // return 重定向的 字符串路径/路径对象 }} ]})『重定向』的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b,那么『别名』又是什么呢?/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。上面对应的路由配置为:const router = new VueRouter({ routes: [ { path: ‘/a’, component: A, alias: ‘/b’ } ]//欢迎加入前端全栈开发交流圈一起学习交流:864305860})『别名』的功能让你可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构。HTML5 History 模式ue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。const router = new VueRouter({ mode: ‘history’, routes: […]})//欢迎加入前端全栈开发交流圈一起学习交流:864305860当你使用 history 模式时,URL 就像正常的 url,例如 http://yoursite.com/user/id,也好看!不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。给个警告,因为这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。const router = new VueRouter({ mode: ‘history’, routes: [//欢迎加入前端全栈开发交流圈一起学习交流:864305860 { path: ‘*’, component: NotFoundComponent } ]})或者,如果你使用 Node.js 服务器,你可以用服务端路由匹配到来的 URL,并在没有匹配到路由的时候返回 404,以实现回退。导航守卫我的理解 就是组件或者全局级别的 组件的钩子函数正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。全局守卫const router = new VueRouter({ … })router.beforeEach((to, from, next) => { // …})每个守卫方法接收三个参数:to: Route: 即将要进入的目标 路由对象from: Route: 当前导航正要离开的路由next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。next(false): 中断当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。next(‘/’) 或者 next({ path: ‘/’ }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: ‘home’ 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。确保要调用 next 方法,否则钩子就不会被 resolved。全局后置钩子你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:router.afterEach((to, from) => { // …})\路由独享的守卫你可以在路由配置上直接定义 beforeEnter 守卫:const router = new VueRouter({ routes: [ { path: ‘/foo’, component: Foo, beforeEnter: (to, from, next) => { // …//欢迎加入前端全栈开发交流圈一起学习交流:864305860 } } ]})这些守卫与全局前置守卫的方法参数是一样的。组件内的守卫最后,你可以在路由组件内直接定义以下路由导航守卫:beforeRouteEnter beforeRouteUpdate (2.2 新增) beforeRouteLeave const Foo = { template: ..., beforeRouteEnter (to, from, next) { // 在渲染该组件的对应路由被 confirm 前调用 // 不!能!获取组件实例 this // 因为当守卫执行前,组件实例还没被创建 },//欢迎加入前端全栈开发交流圈一起学习交流:864305860 beforeRouteUpdate (to, from, next) { // 在当前路由改变,但是该组件被复用时调用 // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候, // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。 // 可以访问组件实例 this }, beforeRouteLeave (to, from, next) { // 导航离开该组件的对应路由时调用 // 可以访问组件实例 this }}beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。完整的导航解析流程导航被触发。在失活的组件里调用离开守卫。调用全局的 beforeEach 守卫。在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。在路由配置里调用 beforeEnter。解析异步路由组件。在被激活的组件里调用 beforeRouteEnter。调用全局的 beforeResolve 守卫 (2.5+)。导航被确认。调用全局的 afterEach 钩子。触发 DOM 更新。用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。路由元信息我的理解就是 他可以把路由的父路径都列举出来,完成一些任务,比如登录,user 组件需要登录,那么user下面的foo组件也需要,那么可以通过这个属性 来检测这个路由线上 的一些状态。定义路由的时候可以配置 meta 字段:const router = new VueRouter({ routes: [ {//欢迎加入前端全栈开发交流圈一起学习交流:864305860 path: ‘/foo’, component: Foo, children: [ { path: ‘bar’, component: Bar, // a meta field meta: { requiresAuth: true } } ] } ]})首先,我们称呼 routes 配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录例如,根据上面的路由配置,/foo/bar 这个 URL 将会匹配父路由记录以及子路由记录。一个路由匹配到的所有路由记录会暴露为 $route 对象(还有在导航守卫中的路由对象)的 $route.matched 数组。因此,我们需要遍历 $route.matched 来检查路由记录中的 meta 字段。下面例子展示在全局导航守卫中检查元字段: if (to.matched.some(record => record.meta.requiresAuth)) { // this route requires auth, check if logged in // if not, redirect to login page. if (!auth.loggedIn()) { next({//欢迎加入前端全栈开发交流圈一起学习交流:864305860 path: ‘/login’, query: { redirect: to.fullPath } }) } else { next() } } else { next() // 确保一定要调用 next() }//欢迎加入前端全栈开发交流圈一起学习交流:864305860})数据获取我的理解就是在哪里获取数据,可以再组件里面,也可以在组件的守卫里面,也就是组件的生命周期里面。有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示『加载中』之类的指示。导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。从技术角度讲,两种方式都不错 —— 就看你想要的用户体验是哪种。导航完成后获取数据当你使用这种方式时,我们会马上导航和渲染组件,然后在组件的 created 钩子中获取数据。这让我们有机会在数据获取期间展示一个 loading 状态,还可以在不同视图间展示不同的 loading 状态。假设我们有一个 Post 组件,需要基于 $route.params.id 获取文章数据:<template> <div class=“post”> <div class=“loading” v-if=“loading”> Loading… </div> <div v-if=“error” class=“error”> {{ error }} </div> <div v-if=“post” class=“content”> <h2>{{ post.title }}</h2> <p>{{ post.body }}</p> </div>//欢迎加入前端全栈开发交流圈一起学习交流:864305860 </div></template>export default { data () { return { loading: false, post: null, error: null } }, created () { // 组件创建完后获取数据, // 此时 data 已经被 observed 了 this.fetchData() }, watch: { // 如果路由有变化,会再次执行该方法 ‘$route’: ‘fetchData’ },//欢迎加入前端全栈开发交流圈一起学习交流:864305860 methods: { fetchData () { this.error = this.post = null this.loading = true // replace getPost with your data fetching util / API wrapper getPost(this.$route.params.id, (err, post) => { this.loading = false if (err) { this.error = err.toString() } else {//欢迎加入前端全栈开发交流圈一起学习交流:864305860 this.post = post } }) } }}在导航完成前获取数据通过这种方式,我们在导航转入新的路由前获取数据。我们可以在接下来的组件的 beforeRouteEnter 守卫中获取数据,当数据获取成功后只调用 next 方法。export default { data () { return { post: null, error: null } }, beforeRouteEnter (to, from, next) { getPost(to.params.id, (err, post) => { next(vm => vm.setData(err, post)) })//欢迎加入前端全栈开发交流圈一起学习交流:864305860 }, // 路由改变前,组件就已经渲染完了 // 逻辑稍稍不同 beforeRouteUpdate (to, from, next) { this.post = null getPost(to.params.id, (err, post) => { this.setData(err, post) next()//欢迎加入前端全栈开发交流圈一起学习交流:864305860 }) }, methods: { setData (err, post) { if (err) { this.error = err.toString() } else { this.post = post }//欢迎加入前端全栈开发交流圈一起学习交流:864305860 } }}在为后面的视图获取数据时,用户会停留在当前的界面,因此建议在数据获取期间,显示一些进度条或者别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒。结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 13, 2018 · 5 min · jiezi

vuex源码解析

vuex简介能看到此文章的人,应该大部分都已经使用过vuex了,想更深一步了解vuex的内部实现原理。所以简介就少介绍一点。官网介绍说Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。数据流的状态非常清晰,按照 组件dispatch Action -> action内部commit Mutation -> Mutation再 mutate state 的数据,在触发render函数引起视图的更新。附上一张官网的流程图及vuex的官网地址:https://vuex.vuejs.org/zh/Questions在使用vuex的时候,大家有没有如下几个疑问,带着这几个疑问,再去看源码,从中找到解答,这样对vuex的理解可以加深一些。官网在严格模式下有说明:在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。vuex是如何检测状态改变是由mutation函数引起的?通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中。为什么所有子组件都可以取到store?为什么用到的属性在state中也必须要提前定义好,vue视图才可以响应?在调用dispatch和commit时,只需传入(type, payload),为什么action函数和mutation函数能够在第一个参数中解构出来state、commit等?带着这些问题,我们来看看vuex的源码,从中寻找到答案。源码目录结构vuex的源码结构非常简洁清晰,代码量也不是很大,大家不要感到恐慌。vuex挂载vue使用插件的方法很简单,只需Vue.use(Plugins),对于vuex,只需要Vue.use(Vuex)即可。在use 的内部是如何实现插件的注册呢?读过vue源码的都知道,如果传入的参数有 install 方法,则调用插件的 install 方法,如果传入的参数本身是一个function,则直接执行。那么我们接下来就需要去 vuex 暴露出来的 install 方法去看看具体干了什么。store.jsexport function install(_Vue) { // vue.use原理:调用插件的install方法进行插件注册,并向install方法传递Vue对象作为第一个参数 if (Vue && _Vue === Vue) { if (process.env.NODE_ENV !== “production”) { console.error( “[vuex] already installed. Vue.use(Vuex) should be called only once.” ); } return; } Vue = _Vue; // 为了引用vue的watch方法 applyMixin(Vue);}在 install 中,将 vue 对象赋给了全局变量 Vue,并作为参数传给了 applyMixin 方法。那么在 applyMixin 方法中干了什么呢?mixin.jsfunction vuexInit() { const options = this.$options; // store injection if (options.store) { this.$store = typeof options.store === “function” ? options.store() : options.store; } else if (options.parent && options.parent.$store) { this.$store = options.parent.$store; } }在这里首先检查了一下 vue 的版本,2以上的版本把 vuexInit 函数混入 vuex 的 beforeCreate 钩子函数中。在 vuexInit 中,将 new Vue() 时传入的 store 设置到 this 对象的 $store 属性上,子组件则从其父组件上引用其 $store 属性进行层层嵌套设置,保证每一个组件中都可以通过 this.$store 取到 store 对象。这也就解答了我们问题 2 中的问题。通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,注入方法是子从父拿,root从options拿。接下来让我们看看new Vuex.Store()都干了什么。store构造函数store对象构建的主要代码都在store.js中,是vuex的核心代码。首先,在 constructor 中进行了 Vue 的判断,如果没有通过 Vue.use(Vuex) 进行 Vuex 的注册,则调用 install 函数注册。( 通过 script 标签引入时不需要手动调用 Vue.use(Vuex) )并在非生产环境进行判断: 必须调用 Vue.use(Vuex) 进行注册,必须支持 Promise,必须用 new 创建 store。if (!Vue && typeof window !== “undefined” && window.Vue) { install(window.Vue);}if (process.env.NODE_ENV !== “production”) { assert(Vue, must call Vue.use(Vuex) before creating a store instance.); assert( typeof Promise !== “undefined”, vuex requires a Promise polyfill in this browser. ); assert( this instanceof Store, store must be called with the new operator. );}然后进行一系列的属性初始化。其中的重点是 new ModuleCollection(options),这个我们放在后面再讲。先把 constructor 中的代码过完。const { plugins = [], strict = false } = options;// store internal statethis._committing = false; // 是否在进行提交mutation状态标识this._actions = Object.create(null); // 保存action,_actions里的函数已经是经过包装后的this._actionSubscribers = []; // action订阅函数集合this._mutations = Object.create(null); // 保存mutations,_mutations里的函数已经是经过包装后的this._wrappedGetters = Object.create(null); // 封装后的getters集合对象// Vuex支持store分模块传入,在内部用Module构造函数将传入的options构造成一个Module对象,// 如果没有命名模块,默认绑定在this._modules.root上// ModuleCollection 内部调用 new Module构造函数this._modules = new ModuleCollection(options);this._modulesNamespaceMap = Object.create(null); // 模块命名空间mapthis._subscribers = []; // mutation订阅函数集合this._watcherVM = new Vue(); // Vue组件用于watch监视变化属性初始化完毕后,首先从 this 中解构出原型上的 dispatch 和 commit 方法,并进行二次包装,将 this 指向当前 store。const store = this;const { dispatch, commit } = this;/** 把 Store 类的 dispatch 和 commit 的方法的 this 指针指向当前 store 的实例上. 这样做的目的可以保证当我们在组件中通过 this.$store 直接调用 dispatch/commit 方法时, 能够使 dispatch/commit 方法中的 this 指向当前的 store 对象而不是当前组件的 this.*/this.dispatch = function boundDispatch(type, payload) { return dispatch.call(store, type, payload);};this.commit = function boundCommit(type, payload, options) { return commit.call(store, type, payload, options);};接着往下走,包括严格模式的设置、根state的赋值、模块的注册、state的响应式、插件的注册等等,其中的重点在 installModule 函数中,在这里实现了所有modules的注册。//options中传入的是否启用严格模式this.strict = strict;// new ModuleCollection 构造出来的_mudulesconst state = this._modules.root.state;// 初始化组件树根组件、注册所有子组件,并将其中所有的getters存储到this._wrappedGetters属性中installModule(this, state, [], this._modules.root);//通过使用vue实例,初始化 store._vm,使state变成可响应的,并且将getters变成计算属性resetStoreVM(this, state);// 注册插件plugins.forEach(plugin => plugin(this));// 调试工具注册const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools;if (useDevtools) { devtoolPlugin(this);}到此为止,constructor 中所有的代码已经分析完毕。其中的重点在 new ModuleCollection(options) 和 installModule ,那么接下来我们到它们的内部去看看,究竟都干了些什么。ModuleCollection由于 Vuex 使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。Vuex 允许我们将 store 分割成模块(module),每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。例如下面这样:const childModule = { state: { … }, mutations: { … }, actions: { … }}const store = new Vuex.Store({ state, getters, actions, mutations, modules: { childModule: childModule, }})有了模块的概念,可以更好的规划我们的代码。对于各个模块公用的数据,我们可以定义一个common store,别的模块用到的话直接通过 modules 的方法引入即可,无需重复的在每一个模块都写一遍相同的代码。这样我们就可以通过 store.state.childModule 拿到childModule中的 state 状态, 对于Module的内部是如何实现的呢?export default class ModuleCollection { constructor(rawRootModule) { // 注册根module,参数是new Vuex.Store时传入的options this.register([], rawRootModule, false); } register(path, rawModule, runtime = true) { if (process.env.NODE_ENV !== “production”) { assertRawModule(path, rawModule); } const newModule = new Module(rawModule, runtime); if (path.length === 0) { // 注册根module this.root = newModule; } else { // 注册子module,将子module添加到父module的_children属性上 const parent = this.get(path.slice(0, -1)); parent.addChild(path[path.length - 1], newModule); } // 如果当前模块有子modules,循环注册 if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule, runtime); }); } }}在ModuleCollection中又调用了Module构造函数,构造一个Module。Module构造函数constructor (rawModule, runtime) { // 初始化时为false this.runtime = runtime // 存储子模块 this._children = Object.create(null) // 将原来的module存储,以备后续使用 this._rawModule = rawModule const rawState = rawModule.state // 存储原来module的state this.state = (typeof rawState === ‘function’ ? rawState() : rawState) || {} }通过以上代码可以看出,ModuleCollection 主要将传入的 options 对象整个构造为一个 Module 对象,并循环调用 this.register([key], rawModule, false) 为其中的 modules 属性进行模块注册,使其都成为 Module 对象,最后 options 对象被构造成一个完整的 Module 树。经过 ModuleCollection 构造后的树结构如下:(以上面的例子生成的树结构)模块已经创建好之后,接下来要做的就是 installModule。installModule首先我们来看一看执行完 constructor 中的 installModule 函数后,这棵树的结构如何?从上图中可以看出,在执行完installModule函数后,每一个 module 中的 state 属性都增加了 其子 module 中的 state 属性,但此时的 state 还不是响应式的,并且新增加了 context 这个对象。里面包含 dispatch 、 commit 等函数以及 state 、 getters 等属性。它就是 vuex 官方文档中所说的Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象 这个 context 对象。我们平时在 store 中调用的 dispatch 和 commit 就是从这里解构出来的。接下来让我们看看 installModule 里面执行了什么。function installModule(store, rootState, path, module, hot) { // 判断是否是根节点,跟节点的path = [] const isRoot = !path.length; // 取命名空间,形式类似’childModule/’ const namespace = store._modules.getNamespace(path); // 如果namespaced为true,存入_modulesNamespaceMap中 if (module.namespaced) { store._modulesNamespaceMap[namespace] = module; } // 不是根节点,把子组件的每一个state设置到其父级的state属性上 if (!isRoot && !hot) { // 获取当前组件的父组件state const parentState = getNestedState(rootState, path.slice(0, -1)); // 获取当前Module的名字 const moduleName = path[path.length - 1]; store._withCommit(() => { Vue.set(parentState, moduleName, module.state); }); } // 给context对象赋值 const local = (module.context = makeLocalContext(store, namespace, path)); // 循环注册每一个module的Mutation module.forEachMutation((mutation, key) => { const namespacedType = namespace + key; registerMutation(store, namespacedType, mutation, local); }); // 循环注册每一个module的Action module.forEachAction((action, key) => { const type = action.root ? key : namespace + key; const handler = action.handler || action; registerAction(store, type, handler, local); }); // 循环注册每一个module的Getter module.forEachGetter((getter, key) => { const namespacedType = namespace + key; registerGetter(store, namespacedType, getter, local); }); // 循环_childern属性 module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot); });}在installModule函数里,首先判断是否是根节点、是否设置了命名空间。在设置了命名空间的前提下,把 module 存入 store._modulesNamespaceMap 中。在不是跟节点并且不是 hot 的情况下,通过 getNestedState 获取到父级的 state,并获取当前 module 的名字, 用 Vue.set() 方法将当前 module 的 state 挂载到父 state 上。然后调用 makeLocalContext 函数给 module.context 赋值,设置局部的 dispatch、commit方法以及getters和state。那么来看一看这个函数。function makeLocalContext(store, namespace, path) { // 是否有命名空间 const noNamespace = namespace === “”; const local = { // 如果没有命名空间,直接返回store.dispatch;否则给type加上命名空间,类似’childModule/‘这种 dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); const { payload, options } = args; let { type } = args; if (!options || !options.root) { type = namespace + type; if ( process.env.NODE_ENV !== “production” && !store._actions[type] ) { console.error( [vuex] unknown local action type: ${ args.type }, global type: ${type} ); return; } } return store.dispatch(type, payload); }, // 如果没有命名空间,直接返回store.commit;否则给type加上命名空间 commit: noNamespace ? store.commit : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); const { payload, options } = args; let { type } = args; if (!options || !options.root) { type = namespace + type; if ( process.env.NODE_ENV !== “production” && !store._mutations[type] ) { console.error( [vuex] unknown local mutation type: ${ args.type }, global type: ${type} ); return; } } store.commit(type, payload, options); } }; // getters and state object must be gotten lazily // because they will be changed by vm update Object.defineProperties(local, { getters: { get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace) }, state: { get: () => getNestedState(store.state, path) } }); return local;}经过 makeLocalContext 处理的返回值会赋值给 local 变量,这个变量会传递给 registerMutation、forEachAction、registerGetter 函数去进行相应的注册。mutation可以重复注册,registerMutation 函数将我们传入的 mutation 进行了一次包装,将 state 作为第一个参数传入,因此我们在调用 mutation 的时候可以从第一个参数中取到当前的 state 值。function registerMutation(store, type, handler, local) { const entry = store._mutations[type] || (store._mutations[type] = []); entry.push(function wrappedMutationHandler(payload) { // 将this指向store,将makeLocalContext返回值中的state作为第一个参数,调用值执行的payload作为第二个参数 // 因此我们调用commit去提交mutation的时候,可以从mutation的第一个参数中取到当前的state值。 handler.call(store, local.state, payload); });}action也可以重复注册。注册 action 的方法与 mutation 相似,registerAction 函数也将我们传入的 action 进行了一次包装。但是 action 中参数会变多,里面包含 dispatch 、commit、local.getters、local.state、rootGetters、rootState,因此可以在一个 action 中 dispatch 另一个 action 或者去 commit 一个 mutation。这里也就解答了问题4中提出的疑问。function registerAction(store, type, handler, local) { const entry = store._actions[type] || (store._actions[type] = []); entry.push(function wrappedActionHandler(payload, cb) { //与mutation不同,action的第一个参数是一个对象,里面包含dispatch、commit、getters、state、rootGetters、rootState let res = handler.call( store, { dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload, cb ); if (!isPromise(res)) { res = Promise.resolve(res); } if (store._devtoolHook) { return res.catch(err => { store._devtoolHook.emit(“vuex:error”, err); throw err; }); } else { return res; } });}注册 getters,从getters的第一个参数中可以取到local state、local getters、root state、root getters。getters不允许重复注册。function registerGetter(store, type, rawGetter, local) { // getters不允许重复 if (store._wrappedGetters[type]) { if (process.env.NODE_ENV !== “production”) { console.error([vuex] duplicate getter key: ${type}); } return; } store._wrappedGetters[type] = function wrappedGetter(store) { // getters的第一个参数包含local state、local getters、root state、root getters return rawGetter( local.state, // local state local.getters, // local getters store.state, // root state store.getters // root getters ); };}现在 store 的 _mutation、_action 中已经有了我们自行定义的的 mutation 和 action函数,并且经过了一层内部报装。当我们在组件中执行 this.$store.dispatch() 和 this.$store.commit() 的时候,是如何调用到相应的函数的呢?接下来让我们来看一看 store 上的 dispatch 和 commit 函数。commitcommit 函数先进行参数的适配处理,然后判断当前 action type 是否存在,如果存在则调用 _withCommit 函数执行相应的 mutation 。 // 提交mutation函数 commit(_type, _payload, _options) { // check object-style commit //commit支持两种调用方式,一种是直接commit(‘getName’,‘vuex’),另一种是commit({type:‘getName’,name:‘vuex’}), //unifyObjectStyle适配两种方式 const { type, payload, options } = unifyObjectStyle( _type, _payload, _options ); const mutation = { type, payload }; // 这里的entry取值就是我们在registerMutation函数中push到_mutations中的函数,已经经过处理 const entry = this._mutations[type]; if (!entry) { if (process.env.NODE_ENV !== “production”) { console.error([vuex] unknown mutation type: ${type}); } return; } // 专用修改state方法,其他修改state方法均是非法修改,在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误 // 不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失。 this._withCommit(() => { entry.forEach(function commitIterator(handler) { handler(payload); }); }); // 订阅者函数遍历执行,传入当前的mutation对象和当前的state this._subscribers.forEach(sub => sub(mutation, this.state)); if (process.env.NODE_ENV !== “production” && options && options.silent) { console.warn( [vuex] mutation type: ${type}. Silent option has been removed. + “Use the filter functionality in the vue-devtools” ); } }在 commit 函数中调用了 _withCommit 这个函数, 代码如下。_withCommit 是一个代理方法,所有触发 mutation 的进行 state 修改的操作都经过它,由此来统一管理监控 state 状态的修改。在严格模式下,会深度监听 state 的变化,如果没有通过 mutation 去修改 state,则会报错。官方建议 不要在发布环境下启用严格模式! 请确保在发布环境下关闭严格模式,以避免性能损失。这里就解答了问题1中的疑问。_withCommit(fn) { // 保存之前的提交状态false const committing = this._committing; // 进行本次提交,若不设置为true,直接修改state,strict模式下,Vuex将会产生非法修改state的警告 this._committing = true; // 修改state fn(); // 修改完成,还原本次修改之前的状态false this._committing = committing;}dispatchdispatch 和 commit 的原理相同。如果有多个同名 action,会等到所有的 action 函数完成后,返回的 Promise 才会执行。// 触发action函数 dispatch(_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_type, _payload); const action = { type, payload }; const entry = this._actions[type]; if (!entry) { if (process.env.NODE_ENV !== “production”) { console.error([vuex] unknown action type: ${type}); } return; } // 执行所有的订阅者函数 this._actionSubscribers.forEach(sub => sub(action, this.state)); return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry0; }至此,整个 installModule 里涉及到的内容已经分析完毕。我们在 options 中传进来的 action 和 mutation 已经在 store 中。但是 state 和 getters 还没有。这就是接下来的 resetStoreVM 方法做的事情。resetStoreVMresetStoreVM 函数中包括初始化 store._vm,观测 state 和 getters 的变化以及执行是否开启严格模式等。state 属性赋值给 vue 实例的 data 属性,因此数据是可响应的。这也就解答了问题 3,用到的属性在 state 中也必须要提前定义好,vue 视图才可以响应。function resetStoreVM(store, state, hot) { //保存老的vm const oldVm = store._vm; // 初始化 store 的 getters store.getters = {}; // _wrappedGetters 是之前在 registerGetter 函数中赋值的 const wrappedGetters = store._wrappedGetters; const computed = {}; forEachValue(wrappedGetters, (fn, key) => { // 将getters放入计算属性中,需要将store传入 computed[key] = () => fn(store); // 为了可以通过this.$store.getters.xxx访问getters Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }); }); // use a Vue instance to store the state tree // suppress warnings just in case the user has added // some funky global mixins // 用一个vue实例来存储store树,将getters作为计算属性传入,访问this.$store.getters.xxx实际上访问的是store._vm[xxx] const silent = Vue.config.silent; Vue.config.silent = true; store._vm = new Vue({ data: { $$state: state }, computed }); Vue.config.silent = silent; // enable strict mode for new vm // 如果是严格模式,则启用严格模式,深度 watch state 属性 if (store.strict) { enableStrictMode(store); } // 若存在oldVm,解除对state的引用,等dom更新后把旧的vue实例销毁 if (oldVm) { if (hot) { // dispatch changes in all subscribed watchers // to force getter re-evaluation for hot reloading. store._withCommit(() => { oldVm._data.$$state = null; }); } Vue.nextTick(() => oldVm.$destroy()); }}开启严格模式时,会深度监听 $$state 的变化,如果不是通过this._withCommit()方法触发的state修改,也就是store._committing如果是false,就会报错。function enableStrictMode(store) { store._vm.$watch( function() { return this._data.$$state; }, () => { if (process.env.NODE_ENV !== “production”) { assert( store._committing, do not mutate vuex store state outside mutation handlers. ); } }, { deep: true, sync: true } );}让我们来看一看执行完 resetStoreVM 后的 store 结构。现在的 store 中已经有了 getters 属性,并且 getters 和 state 都是响应式的。至此 vuex 的核心代码初始化部分已经分析完毕。源码里还包括一些插件的注册及暴露出来的 API 像 mapState mapGetters mapActions mapMutation等函数就不在这里介绍了,感兴趣的可以自行去源码里看看,比较好理解。这里就不做过多介绍。总结vuex的源码相比于vue的源码来说还是很好理解的。分析源码之前建议大家再细读一遍官方文档,遇到不太理解的地方记下来,带着问题去读源码,有目的性的研究,可以加深记忆。阅读的过程中,可以先写一个小例子,引入 clone 下来的源码,一步一步分析执行过程。 ...

December 13, 2018 · 8 min · jiezi

解析Vue.js中的computed工作原理

我们通过实现一个简单版的和Vue中computed具有相同功能的函数来了解computed是如何工作的。写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。JS属性:JavaScript有一个特性是 Object.defineProperty ,它能做很多事,但我在这篇文章只专注于这个方法中的一个:var person = {};Object.defineProperty (person, ‘age’, { get: function () { console.log (“Getting the age”); return 25; }});//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860console.log (“The age is “, person.age);// Prints://// Getting the age// The age is 25(Obeject.defineProperty是Object的一个方法,第一个参数是对象名称,第二个参数是要设置的属性名,第三个参数是一个对象,它可以设置这个属性是否可修改、可写等,而这篇文章主要使用的是Obeject.defineProperty的访问器属性,感兴趣的朋友可以自行google或者查看Js高及程序设计)尽管 person.age 看起来像是访问了对象的一个属性,但其实在内部我们是运行了一个函数。一个基本可响应的Vue.jsVue.js内部构建了一个可以将普通的对象转化为可以被观察的值( 响应属性 ),下面为大家展示一个简化版的如何添加响应属性的案例:function defineReactive (obj, key, val) { Object.defineProperty (obj, key, { get: function () { return val; }, set: function (newValue) { val = newValue; } })};// 创建一个对象var person = {};// 添加可响应的属性"age"和"country"defineReactive (person, ‘age’, 25);defineReactive (person, ‘country’, ‘Brazil’);// 现在你可以随意使用person.age了if (person.age < 18) { return ‘minor’;}else { return ‘adult’;}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860// 设置person.country的值person.country = ‘Russia’;有趣的是, 25 和 ‘Brazil’ 还是一个闭包内部的变量,只有当赋给它们新值的时候 val 才会改变。 person.country 并不拥有 ‘Brazil’ 这个值,而是getter这个函数拥有 ‘Brazil’ 这个值。声明一个计算属性让我们创建一个定义计算属性的函数 defineComputed 。这个函数就跟大家平时使用computed时的一样。defineComputed ( person, // 计算属性就声明在这个对象上 ‘status’, // 计算属性的名称 function () { // 实际返回计算属性值的函数 console.log (“status getter called”) if (person.age < 18) { return ‘minor’; } else { return ‘adult’; } }, function (newValue) { // 当计算属性值更新时调用的函数 console.log (“status has changed to”, newValue) }});// 我们可以像使用一般的属性一样使用计算属性console.log (“The person’s status is: “, person.status);让我们写一个简单的 defineComputed 函数,它支持调用计算方法,但目前不需要它支持 updateCallbackfunction defineComputed (obj, key, computeFunc, updateCallback) { Object.defineProperty (obj, key, { get: function () { // 执行计算函数并且返回值 return computeFunc (); },//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 set: function () { // 什么也不做,不需要设定计算属性的值 } })}这个函数有两个问题:每次访问计算属性时都会执行一次计算函数 computeFunc ()它不知道什么时候更新 (即当我们更新某个data中的属性,计算属性中也会更新这个data属性)// 我希望最终函数执行后是这个效果:每当person.age更新值的时候,person.status也同步更新person.age = 17;// console: status 的值为 minorperson.age = 22;// console: status 的值为 adult增加一个依赖项让我们增加一个全局变量 Dep :var Dep = { target: null};这是一个依赖项,接着我们用一个骚操作来更新 defineComputed 函数:function defineComputed (obj, key, computeFunc, updateCallback) { var onDependencyUpdated = function () { // TODO }//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 Object.defineProperty (obj, key, { get: function () { // 将onDependencyUpdated 这个函数传给Dep.target Dep.target = onDependencyUpdated; var value = computeFunc (); Dep.target = null; }, set: function () { // 什么也不做,不需要设定计算属性的值 } })}现在让我们回到之前设置的响应属性上:function defineReactive (obj, key, val) { // 所有的计算属性都依赖这个数组 var deps = []; Object.defineProperty (obj, key, { get: function () { // 检查是否调用了计算属性,如果调用了,Department.target将等于一个onDependencyUpdated函数 if (Dep.target) { // 把onDependencyUpdated函数push到deos中 deps.push (target); } return val; }, set: function (newValue) { val = newValue; // 通知所有的计算属性,告诉它们有个响应属性更新了 deps.forEach ((changeFunction) => { changeFunction (); });//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 }//面向1-3年前端人员 })};我们可以在计算属性定义的函数触发更新回调后更新 onDependencyUpdated 函数。var onDependencyUpdated = function () { // 再次计算 计算属性的值 var value = computeFunc (); updateCallback (value);}//欢迎加入前端全栈开发交流圈一起学习交流:864305860把它们整合到一起:让我们重新访问我们的计算属性 person.status :person.age = 22;defineComputed ( person, ‘status’, function () { if (person.age > 18) { return ‘adult’; } }, function (newValue) { console.log (“status has changed to”, newValue) }//欢迎加入前端全栈开发交流圈一起学习交流:864305860});console.log (“Status is “, person.status);结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 13, 2018 · 2 min · jiezi

NSQ源码-NSQD

看完了nsqlookupd我们继续往下看, nsqd才是他的核心. 里面大量的使用到了go channel, 相信看完之后对你学习go有很大的帮助.相较于lookupd部分无论在代码逻辑和实现上都要复杂很多. 不过基本的代码结构基本上都是一样的, 进程使用go-srv来管理, Main里启动一个http sever和一个tcp server, 这里可以参考下之前文章的进程模型小节, 不过在nsqd中会启动另外的两个goroutine queueScanLoop和lookupLoop。下面是一个具体的进程模型。后面的分析都是基于这个进程模型。NSQD的启动启动时序这块儿大体上和lookupd中的一致, 我们下面来看看lookupLoop和queueScanLoop.lookupLoop代码见nsqd/lookup.go中 主要做以下几件事情:和lookupd建立连接(这里是一个长连接)每隔15s ping一下lookupd新增或者删除topic的时候通知到lookupd新增或者删除channel的时候通知到lookupd动态的更新options由于设计到了nsq里的in-flight/deferred message, 我们把queueScanLoop放到最后来看.一条message的LifeLine下面我们就通过一条message的生命周期来看下nsqd的工作原理. 根据官方的QuickStart, 我们可以通过curl来pub一条消息.curl -d ‘hello world 1’ ‘http://127.0.0.1:4151/pub?topic=test’http handler我们就跟着代码看一下, 首先是http对此的处理:// nsq/nsqd/http.gofunc (s *httpServer) doPUB(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) { … reqParams, topic, err := s.getTopicFromQuery(req) // 从http query中拿到topic信息 …}// nsq/nsqd/http.gofunc (s *httpServer) getTopicFromQuery(req *http.Request) (url.Values, *Topic, error) { reqParams, err := url.ParseQuery(req.URL.RawQuery) topicNames, ok := reqParams[“topic”] return reqParams, s.ctx.nsqd.GetTopic(topicName), nil}// nsq/nsqd/nsqd.go// GetTopic performs a thread safe operation// to return a pointer to a Topic object (potentially new)func (n *NSQD) GetTopic(topicName string) *Topic { // 1. 首先查看n.topicMap,确认该topic是否已经存在(存在直接返回) t, ok := n.topicMap[topicName] // 2. 否则将新建一个topic t = NewTopic(topicName, &context{n}, deleteCallback) n.topicMap[topicName] = t // 3. 查看该nsqd是否设置了lookupd, 从lookupd获取该tpoic的channel信息 // 这个topic/channel已经通过nsqlookupd的api添加上去的, 但是nsqd的本地 // 还没有, 针对这种情况我们需要创建该channel对应的deffer queue和inFlight // queue. lookupdHTTPAddrs := n.lookupdHTTPAddrs() if len(lookupdHTTPAddrs) > 0 { channelNames, err := n.ci.GetLookupdTopicChannels(t.name, lookupdHTTPAddrs) } // now that all channels are added, start topic messagePump // 对该topic的初始化已经完成下面就是message t.Start() return t}topic messagePump在上面消息初始化完成之后就启动了tpoic对应的messagePump// nsq/nsqd/topic.go// messagePump selects over the in-memory and backend queue and// writes messages to every channel for this topicfunc (t *Topic) messagePump() { // 1. do not pass messages before Start(), but avoid blocking Pause() // or GetChannel() // 等待channel相关的初始化完成,GetTopic中最后的t.Start()才正式启动该Pump // 2. main message loop // 开始从Memory chan或者disk读取消息 // 如果topic对应的channel发生了变化,则更新channel信息 // 3. 往该tpoic对应的每个channel写入message(如果是deffermessage // 的话放到对应的deffer queue中 // 否则放到该channel对应的memoryMsgChan中)。}至此也就完成了从tpoic memoryMsgChan收到消息投递到channel memoryMsgChan的投递, 我们先看下http收到消息到通知pump处理的过程。// nsq/nsqd/http.gofunc (s *httpServer) doPUB(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) { … msg := NewMessage(topic.GenerateID(), body) msg.deferred = deferred err = topic.PutMessage(msg) if err != nil { return nil, http_api.Err{503, “EXITING”} } return “OK”, nil}// nsq/nsqd/topic.go// PutMessage writes a Message to the queuefunc (t *Topic) PutMessage(m *Message) error { t.RLock() defer t.RUnlock() if atomic.LoadInt32(&t.exitFlag) == 1 { return errors.New(“exiting”) } err := t.put(m) if err != nil { return err } atomic.AddUint64(&t.messageCount, 1) return nil}func (t *Topic) put(m *Message) error { select { case t.memoryMsgChan <- m: default: b := bufferPoolGet() err := writeMessageToBackend(b, m, t.backend) bufferPoolPut(b) t.ctx.nsqd.SetHealth(err) if err != nil { t.ctx.nsqd.logf(LOG_ERROR, “TOPIC(%s) ERROR: failed to write message to backend - %s”, t.name, err) return err } } return nil}这里memoryMsgChan的大小我们可以通过–mem-queue-size参数来设置,上面这段代码的流程是如果memoryMsgChan还没有满的话就把消息放到memoryMsgChan中,否则就放到backend(disk)中。topic的mesasgePump检测到有新的消息写入的时候就开始工作了,从memoryMsgChan/backend(disk)读取消息投递到channel对应的chan中。 还有一点请注意就是messagePump中 if len(chans) > 0 && !t.IsPaused() { memoryMsgChan = t.memoryMsgChan backendChan = t.backend.ReadChan() }这段代码只有channel(此channel非golang里的channel而是nsq的channel类似nsq_to_file)存在的时候才会去投递。上面部分就是msg从producer生产消息到吧消息写到memoryChan/Disk的过程,下面我们来看下consumer消费消息的过程。首先是consumer从nsqlookupd查询到自己所感兴趣的topic/channel的nsqd信息, 然后就是来连接了。tcp handler对新的client的处理//nsq/internal/protocol/tcp_server.gofunc TCPServer(listener net.Listener, handler TCPHandler, logf lg.AppLogFunc) { go handler.Handle(clientConn)}//nsq/nsqd/tcp.gofunc (p *tcpServer) Handle(clientConn net.Conn) { prot.IOLoop(clientConn)}针对每个client起一个messagePump吧msg从上面channel对应的chan 写入到consumer侧//nsq/nsqd/protocol_v2.gofunc (p *protocolV2) IOLoop(conn net.Conn) error { client := newClientV2(clientID, conn, p.ctx) p.ctx.nsqd.AddClient(client.ID, client) messagePumpStartedChan := make(chan bool) go p.messagePump(client, messagePumpStartedChan) // read the request line, err = client.Reader.ReadSlice(’\n’) response, err = p.Exec(client, params) p.Send(client, frameTypeResponse, response)}//nsq/nsqd/protocol_v2.gofunc (p *protocolV2) Exec(client *clientV2, params [][]byte) ([]byte, error) { switch { case bytes.Equal(params[0], []byte(“FIN”)): return p.FIN(client, params) case bytes.Equal(params[0], []byte(“RDY”)): return p.RDY(client, params) case bytes.Equal(params[0], []byte(“REQ”)): return p.REQ(client, params) case bytes.Equal(params[0], []byte(“PUB”)): return p.PUB(client, params) case bytes.Equal(params[0], []byte(“MPUB”)): return p.MPUB(client, params) case bytes.Equal(params[0], []byte(“DPUB”)): return p.DPUB(client, params) case bytes.Equal(params[0], []byte(“NOP”)): return p.NOP(client, params) case bytes.Equal(params[0], []byte(“TOUCH”)): return p.TOUCH(client, params) case bytes.Equal(params[0], []byte(“SUB”)): return p.SUB(client, params) case bytes.Equal(params[0], []byte(“CLS”)): return p.CLS(client, params) case bytes.Equal(params[0], []byte(“AUTH”)): return p.AUTH(client, params) }}//nsq/nsqd/protocol_v2.gofunc (p *protocolV2) SUB(client *clientV2, params [][]byte) ([]byte, error) { var channel *Channel topic := p.ctx.nsqd.GetTopic(topicName) channel = topic.GetChannel(channelName) channel.AddClient(client.ID, client) // 通知messagePump开始工作 client.SubEventChan <- channel通知topic的messagePump开始工作func (t *Topic) GetChannel(channelName string) *Channel { t.Lock() channel, isNew := t.getOrCreateChannel(channelName) t.Unlock() if isNew { // update messagePump state select { case t.channelUpdateChan <- 1: case <-t.exitChan: } } return channel}message 对应的Pumpfunc (p *protocolV2) messagePump(client *clientV2, startedChan chan bool) { for { if subChannel == nil || !client.IsReadyForMessages() { // the client is not ready to receive messages… // 等待client ready,并且channel的初始化完成 flushed = true } else if flushed { // last iteration we flushed… // do not select on the flusher ticker channel memoryMsgChan = subChannel.memoryMsgChan backendMsgChan = subChannel.backend.ReadChan() flusherChan = nil } else { // we’re buffered (if there isn’t any more data we should flush)… // select on the flusher ticker channel, too memoryMsgChan = subChannel.memoryMsgChan backendMsgChan = subChannel.backend.ReadChan() flusherChan = outputBufferTicker.C } select { case <-flusherChan: // if this case wins, we’re either starved // or we won the race between other channels… // in either case, force flush case <-client.ReadyStateChan: case subChannel = <-subEventChan: // you can’t SUB anymore // channel初始化完成,pump开始工作 subEventChan = nil case identifyData := <-identifyEventChan: // you can’t IDENTIFY anymore case <-heartbeatChan: // heartbeat的处理 case b := <-backendMsgChan: // 1. decode msg // 2. 把msg push到Flight Queue里 // 3. send msg to client case msg := <-memoryMsgChan: // 1. 把msg push到Flight Queue里 // 2. send msg to client case <-client.ExitChan: // exit the routine } }至此我们看的代码就是一条消息从pub到nsqd中到被消费者处理的过程。不过得注意一点,我们在上面的代码分析中,创建topic/channel的部分放到了message Pub的链上, 如果是没有lookupd的模式的话这部分是在client SUB链上的。topic/hannel的管理在NSQ内部通过type NSQD struct { topicMap map[string]*Topic}和type Topic struct { channelMap map[string]*Channel}来维护一个内部的topic/channel状态,然后在提供了如下的接口来管理topic和channel/topic/create - create a new topic/topic/delete - delete a topic/topic/empty - empty a topic/topic/pause - pause message flow for a topic/topic/unpause - unpause message flow for a topic/channel/create - create a new channel/channel/delete - delete a channel/channel/empty - empty a channel/channel/pause - pause message flow for a channel/channel/unpause - unpause message flow for a channelcreate topic/channel的话我们在之前的代码看过了,这里可以重点看下topic/channel delete的时候怎样保证数据优雅的删除的,以及messagePump的退出机制。queueScanLoop的工作// queueScanLoop runs in a single goroutine to process in-flight and deferred// priority queues. It manages a pool of queueScanWorker (configurable max of// QueueScanWorkerPoolMax (default: 4)) that process channels concurrently.//// It copies Redis’s probabilistic expiration algorithm: it wakes up every// QueueScanInterval (default: 100ms) to select a random QueueScanSelectionCount// (default: 20) channels from a locally cached list (refreshed every// QueueScanRefreshInterval (default: 5s)).//// If either of the queues had work to do the channel is considered “dirty”.//// If QueueScanDirtyPercent (default: 25%) of the selected channels were dirty,// the loop continues without sleep.这里的注释已经说的很明白了,queueScanLoop就是通过动态的调整queueScanWorker的数目来处理in-flight和deffered queue的。在具体的算法上的话参考了redis的随机过期算法。总结阅读源码就是走走停停的过程,从一开始的无从下手到后面的一点点的把它啃透。一开始都觉得很困难,无从下手。以前也是尝试着去看一些经典的开源代码,但都没能坚持下来,有时候人大概是会高估自己的能力的,好多东西自以为看个一两遍就能看懂,其实不然,好多知识只有不断的去研究你才能参透其中的原理。 一定要持续的读,不然过几天之后就忘了前面读的内容 一定要多总结, 总结就是在不断的读的过程,从第一遍读通到你把它表述出来至少需要再读5-10次 多思考,这段时间在地铁上/跑步的时候我会回向一下其中的流程 分享(读懂是一个层面,写出来是一个层面,讲给别人听是另外一个层面)后面我会先看下go-nsqd部分的代码,之后会研究下gnatsd, 两个都是cloud native的消息系统,看下有啥区别。 ...

December 1, 2018 · 5 min · jiezi

Laravel核心解读--ENV的加载和读取

Laravel在启动时会加载项目中的.env文件。对于应用程序运行的环境来说,不同的环境有不同的配置通常是很有用的。 例如,你可能希望在本地使用测试的Mysql数据库而在上线后希望项目能够自动切换到生产Mysql数据库。本文将会详细介绍 env 文件的使用与源码的分析。Env文件的使用多环境env的设置项目中env文件的数量往往是跟项目的环境数量相同,假如一个项目有开发、测试、生产三套环境那么在项目中应该有三个.env.dev、.env.test、.env.prod三个环境配置文件与环境相对应。三个文件中的配置项应该完全一样,而具体配置的值应该根据每个环境的需要来设置。接下来就是让项目能够根据环境加载不同的env文件了。具体有三种方法,可以按照使用习惯来选择使用:在环境的nginx配置文件里设置APP_ENV环境变量fastcgi_param APP_ENV dev;设置服务器上运行PHP的用户的环境变量,比如在www用户的/home/www/.bashrc中添加export APP_ENV dev在部署项目的持续集成任务或者部署脚本里执行cp .env.dev .env 针对前两种方法,Laravel会根据env(‘APP_ENV’)加载到的变量值去加载对应的文件.env.dev、.env.test这些。 具体在后面源码里会说,第三种比较好理解就是在部署项目时将环境的配置文件覆盖到.env文件里这样就不需要在环境的系统和nginx里做额外的设置了。自定义env文件的路径与文件名env文件默认放在项目的根目录中,laravel 为用户提供了自定义 ENV 文件路径或文件名的函数,例如,若想要自定义 env 路径,可以在 bootstrap 文件夹中 app.php 中使用Application实例的useEnvironmentPath方法:$app = new Illuminate\Foundation\Application( realpath(DIR.’/../’));$app->useEnvironmentPath(’/customer/path’)若想要自定义 env 文件名称,就可以在 bootstrap 文件夹中 app.php 中使用Application实例的loadEnvironmentFrom方法:$app = new Illuminate\Foundation\Application( realpath(DIR.’/../’));$app->loadEnvironmentFrom(‘customer.env’)Laravel 加载ENV配置Laravel加载ENV的是在框架处理请求之前,bootstrap过程中的LoadEnvironmentVariables阶段中完成的。我们来看一下\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables的源码来分析下Laravel是怎么加载env中的配置的。<?phpnamespace Illuminate\Foundation\Bootstrap;use Dotenv\Dotenv;use Dotenv\Exception\InvalidPathException;use Symfony\Component\Console\Input\ArgvInput;use Illuminate\Contracts\Foundation\Application;class LoadEnvironmentVariables{ /** * Bootstrap the given application. * * @param \Illuminate\Contracts\Foundation\Application $app * @return void / public function bootstrap(Application $app) { if ($app->configurationIsCached()) { return; } $this->checkForSpecificEnvironmentFile($app); try { (new Dotenv($app->environmentPath(), $app->environmentFile()))->load(); } catch (InvalidPathException $e) { // } } /* * Detect if a custom environment file matching the APP_ENV exists. * * @param \Illuminate\Contracts\Foundation\Application $app * @return void / protected function checkForSpecificEnvironmentFile($app) { if ($app->runningInConsole() && ($input = new ArgvInput)->hasParameterOption(’–env’)) { if ($this->setEnvironmentFilePath( $app, $app->environmentFile().’.’.$input->getParameterOption(’–env’) )) { return; } } if (! env(‘APP_ENV’)) { return; } $this->setEnvironmentFilePath( $app, $app->environmentFile().’.’.env(‘APP_ENV’) ); } /* * Load a custom environment file. * * @param \Illuminate\Contracts\Foundation\Application $app * @param string $file * @return bool */ protected function setEnvironmentFilePath($app, $file) { if (file_exists($app->environmentPath().’/’.$file)) { $app->loadEnvironmentFrom($file); return true; } return false; }}在他的启动方法bootstrap中,Laravel会检查配置是否缓存过以及判断应该应用那个env文件,针对上面说的根据环境加载配置文件的三种方法中的头两种,因为系统或者nginx环境变量中设置了APP_ENV,所以Laravel会在checkForSpecificEnvironmentFile方法里根据 APP_ENV的值设置正确的配置文件的具体路径, 比如.env.dev或者.env.test,而针对第三中情况则是默认的.env, 具体可以参看下面的checkForSpecificEnvironmentFile还有相关的Application里的两个方法的源码:protected function checkForSpecificEnvironmentFile($app){ if ($app->runningInConsole() && ($input = new ArgvInput)->hasParameterOption(’–env’)) { if ($this->setEnvironmentFilePath( $app, $app->environmentFile().’.’.$input->getParameterOption(’–env’) )) { return; } } if (! env(‘APP_ENV’)) { return; } $this->setEnvironmentFilePath( $app, $app->environmentFile().’.’.env(‘APP_ENV’) );}namespace Illuminate\Foundation;class Application ….{ public function environmentPath() { return $this->environmentPath ?: $this->basePath; } public function environmentFile() { return $this->environmentFile ?: ‘.env’; }}判断好后要读取的配置文件的路径后,接下来就是加载env里的配置了。(new Dotenv($app->environmentPath(), $app->environmentFile()))->load();Laravel使用的是Dotenv的PHP版本vlucas/phpdotenvclass Dotenv{ public function __construct($path, $file = ‘.env’) { $this->filePath = $this->getFilePath($path, $file); $this->loader = new Loader($this->filePath, true); } public function load() { return $this->loadData(); } protected function loadData($overload = false) { $this->loader = new Loader($this->filePath, !$overload); return $this->loader->load(); }}它依赖/Dotenv/Loader来加载数据:class Loader{ public function load() { $this->ensureFileIsReadable(); $filePath = $this->filePath; $lines = $this->readLinesFromFile($filePath); foreach ($lines as $line) { if (!$this->isComment($line) && $this->looksLikeSetter($line)) { $this->setEnvironmentVariable($line); } } return $lines; }}Loader读取配置时readLinesFromFile函数会用file函数将配置从文件中一行行地读取到数组中去,然后排除以#开头的注释,针对内容中包含=的行去调用setEnvironmentVariable方法去把文件行中的环境变量配置到项目中去:namespace Dotenv;class Loader{ public function setEnvironmentVariable($name, $value = null) { list($name, $value) = $this->normaliseEnvironmentVariable($name, $value); $this->variableNames[] = $name; // Don’t overwrite existing environment variables if we’re immutable // Ruby’s dotenv does this with ENV[key] ||= value. if ($this->immutable && $this->getEnvironmentVariable($name) !== null) { return; } // If PHP is running as an Apache module and an existing // Apache environment variable exists, overwrite it if (function_exists(‘apache_getenv’) && function_exists(‘apache_setenv’) && apache_getenv($name)) { apache_setenv($name, $value); } if (function_exists(‘putenv’)) { putenv("$name=$value"); } $_ENV[$name] = $value; $_SERVER[$name] = $value; } public function getEnvironmentVariable($name) { switch (true) { case array_key_exists($name, $_ENV): return $_ENV[$name]; case array_key_exists($name, $_SERVER): return $_SERVER[$name]; default: $value = getenv($name); return $value === false ? null : $value; // switch getenv default to null } }}Dotenv实例化Loader的时候把Loader对象的$immutable属性设置成了false,Loader设置变量的时候如果通过getEnvironmentVariable方法读取到了变量值,那么就会跳过该环境变量的设置。所以Dotenv默认情况下不会覆盖已经存在的环境变量,这个很关键,比如说在docker的容器编排文件里,我们会给PHP应用容器设置关于Mysql容器的两个环境变量 environment: - “DB_PORT=3306” - “DB_HOST=database"这样在容器里设置好环境变量后,即使env文件里的DB_HOST为homestead用env函数读取出来的也还是容器里之前设置的DB_HOST环境变量的值database(docker中容器链接默认使用服务名称,在编排文件中我把mysql容器的服务名称设置成了database, 所以php容器要通过database这个host来连接mysql容器)。因为用我们在持续集成中做自动化测试的时候通常都是在容器里进行测试,所以Dotenv不会覆盖已存在环境变量这个行为就相当重要这样我就可以只设置容器里环境变量的值完成测试而不用更改项目里的env文件,等到测试完成后直接去将项目部署到环境上就可以了。如果检查环境变量不存在那么接着Dotenv就会把环境变量通过PHP内建函数putenv设置到环境中去,同时也会存储到$_ENV和$_SERVER这两个全局变量中。在项目中读取env配置在Laravel应用程序中可以使用env()函数去读取环境变量的值,比如获取数据库的HOST:env(‘DB_HOST`, ’localhost’);传递给 env 函数的第二个值是「默认值」。如果给定的键不存在环境变量,则会使用该值。我们来看看env函数的源码:function env($key, $default = null){ $value = getenv($key); if ($value === false) { return value($default); } switch (strtolower($value)) { case ’true’: case ‘(true)’: return true; case ‘false’: case ‘(false)’: return false; case ’empty’: case ‘(empty)’: return ‘’; case ’null’: case ‘(null)’: return; } if (strlen($value) > 1 && Str::startsWith($value, ‘”’) && Str::endsWith($value, ‘"’)) { return substr($value, 1, -1); } return $value;}它直接通过PHP内建函数getenv读取环境变量。我们看到了在加载配置和读取配置的时候,使用了putenv和getenv两个函数。putenv设置的环境变量只在请求期间存活,请求结束后会恢复环境之前的设置。因为如果php.ini中的variables_order配置项成了 GPCS不包含E的话,那么php程序中是无法通过$_ENV读取环境变量的,所以使用putenv动态地设置环境变量让开发人员不用去关注服务器上的配置。而且在服务器上给运行用户配置的环境变量会共享给用户启动的所有进程,这就不能很好的保护比如DB_PASSWORD、API_KEY这种私密的环境变量,所以这种配置用putenv设置能更好的保护这些配置信息,getenv方法能获取到系统的环境变量和putenv动态设置的环境变量。本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。 ...

October 23, 2018 · 3 min · jiezi

Laravel源码解析之从入口开始

前言提升能力的方法并非使用更多工具,而是解刨自己所使用的工具。今天我们从Laravel启动的第一步开始讲起。入口文件laravel是单入口框架,所有请求必将经过index.phpdefine(‘LARAVEL_START’, microtime(true)); // 获取启动时间使用composer是现代PHP的标志require DIR.’/../vendor/autoload.php’; // 加载composer -> autoload.php加载启动文件$app = require_once DIR.’/../bootstrap/app.php’;获取$app是laravel启动的关键,也可以说$app是用于启动laravel内核的钥匙????。随后就是加载内核,载入服务提供者、门面所映射的实体类,中间件,最后到接收http请求并返回结果。$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); // 加载核心类$response = $kernel->handle( $request = Illuminate\Http\Request::capture());$response->send();$kernel->terminate($request, $response);看似短短的4行代码,这则是laravel的优雅之处。我们开始深层次解刨。bootstrap\app.php这个启动文件也可以看作是一个服务提供者,不过他并没有boot,register方法。因为入口文件直接加载他,所有这些没必要的方法就不存在了。作为启动文件,首页则是加载框架所有必须的要要件,例如registerBaseBindingsregisterBaseServiceProvidersregisterCoreContainerAliases,这其中包括了很多基础性的方法和类,例如db [\Illuminate\Database\DatabaseManager::class]auth [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class] log [\Illuminate\Log\LogManager::class, \Psr\Log\LoggerInterface::class] queue [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class] redis [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class] 等等 … 而$app这个在服务提供者的核心变量则就是Application实例化所得,而你在服务提供者内使用的make,bind,singleton来自他的父类Container,都说容器是laravel的核心概念。这块的概念后续我们会详细的讲解。$app = new Illuminate\Foundation\Application( realpath(DIR.’/../’));上面我们已经获得$app的实例化了,现在通过$app来注册核心类、异常类,并将$app返回给index.php$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class);$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class);App\Http\Kernel核心类了所有的系统中间件群组中间件路由中间件当然你需要使用中间件也是在这个类中加载,是经常被使用的一个文件。protected $middleware = [ \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \App\Http\Middleware\TrustProxies::class, ]; /** * The application’s route middleware groups. * * @var array */ protected $middlewareGroups = [ ‘web’ => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ‘api’ => [ ’throttle:60,1’, ‘bindings’, ], ];这个核心类继承自他的父类Illuminate\Foundation\Http\Kernel::class,核心类做了很多事情,它会将所有的中间件全部存储到一个指定的数组,方便内核调用及其他类调用。namespace App\Http; use App\Api\Middleware\VerifyApiToken;use Illuminate\Foundation\Http\Kernel as HttpKernel; class Kernel extends HttpKernel回到起点Laravel的启动经历了很繁琐的一个过程。这也是Laravel优雅的关键点。$response = $kernel->handle( $request = Illuminate\Http\Request::capture());$response->send();$kernel->terminate($request, $response);将请求传入则完成了整个laravel的启动,至于结果的返回则有开发者自行通过控制器或其他可访问类返回。致谢感谢你看到这里,本篇文章源码解析靠个人理解。如有出入请拍砖。希望本篇文章可以帮到你。谢谢 ...

September 27, 2018 · 1 min · jiezi

Laravel核心解读--Cookie源码分析

Laravel Cookie源码分析使用Cookie的方法为了安全起见,Laravel 框架创建的所有 Cookie 都经过加密并使用一个认证码进行签名,这意味着如果客户端修改了它们则需要对其进行有效性验证。我们使用 IlluminateHttpRequest 实例的 cookie 方法从请求中获取 Cookie 的值:$value = $request->cookie(’name’);也可以使用Facade Cookie来读取Cookie的值:Cookie::get(’name’, ‘’);//第二个参数的意思是读取不到name的cookie值的话,返回空字符串添加Cookie到响应可以使用 响应对象的cookie 方法将一个 Cookie 添加到返回的 IlluminateHttpResponse 实例中,你需要传递 Cookie 的名称、值、以及有效期(分钟)到这个方法:return response(‘Learn Laravel Kernel’)->cookie( ‘cookie-name’, ‘cookie-value’, $minutes);响应对象的cookie 方法接收的参数和 PHP 原生函数 setcookie 的参数一致:return response(‘Learn Laravel Kernel’)->cookie( ‘cookie-name’, ‘cookie-value’, $minutes, $path, $domain, $secure, $httpOnly);还可使用Facade Cookie的queue方法以队列的形式将Cookie添加到响应:Cookie::queue(‘cookie-name’, ‘cookie-value’);queue 方法接收 Cookie 实例或创建 Cookie 所必要的参数作为参数,这些 Cookie 会在响应被发送到浏览器之前添加到响应中。接下来我们来分析一下Laravel中Cookie服务的实现原理。Cookie服务注册之前在讲服务提供器的文章里我们提到过,Laravel在BootStrap阶段会通过服务提供器将框架中涉及到的所有服务注册到服务容器里,这样在用到具体某个服务时才能从服务容器中解析出服务来,所以Cookie服务的注册也不例外,在config/app.php中我们能找到Cookie对应的服务提供器和门面。‘providers’ => [ /* * Laravel Framework Service Providers… / …… IlluminateCookieCookieServiceProvider::class, ……] ‘aliases’ => [ …… ‘Cookie’ => IlluminateSupportFacadesCookie::class, ……]Cookie服务的服务提供器是 IlluminateCookieCookieServiceProvider ,其源码如下:<?phpnamespace IlluminateCookie;use IlluminateSupportServiceProvider;class CookieServiceProvider extends ServiceProvider{ /* * Register the service provider. * * @return void / public function register() { $this->app->singleton(‘cookie’, function ($app) { $config = $app->make(‘config’)->get(‘session’); return (new CookieJar)->setDefaultPathAndDomain( $config[‘path’], $config[‘domain’], $config[‘secure’], $config[‘same_site’] ?? null ); }); }}在CookieServiceProvider里将IlluminateCookieCookieJar类的对象注册为Cookie服务,在实例化时会从Laravel的config/session.php配置中读取出path、domain、secure这些参数来设置Cookie服务用的默认路径和域名等参数,我们来看一下CookieJar里setDefaultPathAndDomain的实现:namespace IlluminateCookie;class CookieJar implements JarContract{ /* * 设置Cookie的默认路径和Domain * * @param string $path * @param string $domain * @param bool $secure * @param string $sameSite * @return $this / public function setDefaultPathAndDomain($path, $domain, $secure = false, $sameSite = null) { list($this->path, $this->domain, $this->secure, $this->sameSite) = [$path, $domain, $secure, $sameSite]; return $this; }}它只是把这些默认参数保存到CookieJar对象的属性中,等到make生成SymfonyComponentHttpFoundationCookie对象时才会使用它们。生成Cookie上面说了生成Cookie用的是Response对象的cookie方法,Response的是利用Laravel的全局函数cookie来生成Cookie对象然后设置到响应头里的,有点乱我们来看一下源码class Response extends BaseResponse{ /* * Add a cookie to the response. * * @param SymfonyComponentHttpFoundationCookie|mixed $cookie * @return $this / public function cookie($cookie) { return call_user_func_array([$this, ‘withCookie’], func_get_args()); } /* * Add a cookie to the response. * * @param SymfonyComponentHttpFoundationCookie|mixed $cookie * @return $this / public function withCookie($cookie) { if (is_string($cookie) && function_exists(‘cookie’)) { $cookie = call_user_func_array(‘cookie’, func_get_args()); } $this->headers->setCookie($cookie); return $this; }}看一下全局函数cookie的实现:/* * Create a new cookie instance. * * @param string $name * @param string $value * @param int $minutes * @param string $path * @param string $domain * @param bool $secure * @param bool $httpOnly * @param bool $raw * @param string|null $sameSite * @return IlluminateCookieCookieJar|SymfonyComponentHttpFoundationCookie /function cookie($name = null, $value = null, $minutes = 0, $path = null, $domain = null, $secure = false, $httpOnly = true, $raw = false, $sameSite = null){ $cookie = app(CookieFactory::class); if (is_null($name)) { return $cookie; } return $cookie->make($name, $value, $minutes, $path, $domain, $secure, $httpOnly, $raw, $sameSite);}通过cookie函数的@return标注我们能知道它返回的是一个IlluminateCookieCookieJar对象或者是SymfonyComponentHttpFoundationCookie对象。既cookie函数在无接受参数时返回一个CookieJar对象,在有Cookie参数时调用了CookieJar的make方法返回一个SymfonyComponentHttpFoundationCookie对象。拿到Cookie对象后程序接着流程往下走把Cookie设置到Response对象的headers属性里,`headers`属性引用了SymfonyComponentHttpFoundationResponseHeaderBag对象class ResponseHeaderBag extends HeaderBag{ public function setCookie(Cookie $cookie) { $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; $this->headerNames[‘set-cookie’] = ‘Set-Cookie’; }}我们可以看到这里只是把Cookie对象暂存到了headers对象里,真正把Cookie发送到浏览器是在Laravel返回响应时发生的,在Laravel的public/index.php里:$response->send();Laravel的Response继承自Symfony的Response,send方法定义在Symfony的Response里namespace SymfonyComponentHttpFoundation;class Response{ /* * Sends HTTP headers and content. * * @return $this / public function send() { $this->sendHeaders(); $this->sendContent(); if (function_exists(‘fastcgi_finish_request’)) { fastcgi_finish_request(); } elseif (!in_array(PHP_SAPI, array(‘cli’, ‘phpdbg’), true)) { static::closeOutputBuffers(0, true); } return $this; } public function sendHeaders() { // headers have already been sent by the developer if (headers_sent()) { return $this; } // headers foreach ($this->headers->allPreserveCase() as $name => $values) { foreach ($values as $value) { header($name.’: ‘.$value, false, $this->statusCode); } } // status header(sprintf(‘HTTP/%s %s %s’, $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); return $this; } /* * Returns the headers, with original capitalizations. * * @return array An array of headers / public function allPreserveCase() { $headers = array(); foreach ($this->all() as $name => $value) { $headers[isset($this->headerNames[$name]) ? $this->headerNames[$name] : $name] = $value; } return $headers; } public function all() { $headers = parent::all(); foreach ($this->getCookies() as $cookie) { $headers[‘set-cookie’][] = (string) $cookie; } return $headers; }} 在Response的send方法里发送响应头时将Cookie数据设置到了Http响应首部的Set-Cookie字段里,这样当响应发送给浏览器后浏览器就能保存这些Cookie数据了。至于用门面Cookie::queue以队列的形式设置Cookie其实也是将Cookie暂存到了CookieJar对象的queued属性里namespace IlluminateCookie;class CookieJar implements JarContract{ public function queue(…$parameters) { if (head($parameters) instanceof Cookie) { $cookie = head($parameters); } else { $cookie = call_user_func_array([$this, ‘make’], $parameters); } $this->queued[$cookie->getName()] = $cookie; } public function queued($key, $default = null) { return Arr::get($this->queued, $key, $default); }}然后在web中间件组里边有一个IlluminateCookieMiddlewareAddQueuedCookiesToResponse中间件,它在响应返回给客户端之前将暂存在queued属性里的Cookie设置到了响应的headers对象里:namespace IlluminateCookieMiddleware;use Closure;use IlluminateContractsCookieQueueingFactory as CookieJar;class AddQueuedCookiesToResponse{ /* * The cookie jar instance. * * @var IlluminateContractsCookieQueueingFactory / protected $cookies; /* * Create a new CookieQueue instance. * * @param IlluminateContractsCookieQueueingFactory $cookies * @return void / public function __construct(CookieJar $cookies) { $this->cookies = $cookies; } /* * Handle an incoming request. * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed / public function handle($request, Closure $next) { $response = $next($request); foreach ($this->cookies->getQueuedCookies() as $cookie) { $response->headers->setCookie($cookie); } return $response; }这样在Response对象调用send方法时也会把通过Cookie::queue()设置的Cookie数据设置到Set-Cookie响应首部中去了。读取CookieLaravel读取请求中的Cookie值$value = $request->cookie(’name’); 其实是Laravel的Request对象直接去读取Symfony请求对象的cookies来实现的, 我们在写Laravel Request对象的文章里有提到它依赖于Symfony的Request, Symfony的Request在实例化时会把PHP里那些$_POST、$_COOKIE全局变量抽象成了具体对象存储在了对应的属性中。namespace IlluminateHttp;class Request extends SymfonyRequest implements Arrayable, ArrayAccess{ public function cookie($key = null, $default = null) { return $this->retrieveItem(‘cookies’, $key, $default); } protected function retrieveItem($source, $key, $default) { if (is_null($key)) { return $this->$source->all(); } //从Request的cookies属性中获取数据 return $this->$source->get($key, $default); }}关于通过门面Cookie::get()读取Cookie的实现我们可以看下Cookie门面源码的实现,通过源码我们知道门面Cookie除了通过外观模式代理Cookie服务外自己也定义了两个方法:<?phpnamespace IlluminateSupportFacades;/* * @see IlluminateCookieCookieJar /class Cookie extends Facade{ /* * Determine if a cookie exists on the request. * * @param string $key * @return bool / public static function has($key) { return ! is_null(static::$app[‘request’]->cookie($key, null)); } /* * Retrieve a cookie from the request. * * @param string $key * @param mixed $default * @return string / public static function get($key = null, $default = null) { return static::$app[‘request’]->cookie($key, $default); } /* * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return ‘cookie’; }}Cookie::get()和Cookie::has()是门面直接读取Request对象cookies属性里的Cookie数据。Cookie加密关于对Cookie的加密可以看一下IlluminateCookieMiddlewareEncryptCookies中间件的源码,它的子类AppHttpMiddlewareEncryptCookies是Laravelweb中间件组里的一个中间件,如果想让客户端的Javascript程序能够读Laravel设置的Cookie则需要在AppHttpMiddlewareEncryptCookies的$exception里对Cookie名称进行声明。Laravel中Cookie模块大致的实现原理就梳理完了,希望大家看了我的源码分析后能够清楚Laravel Cookie实现的基本流程这样在遇到困惑或者无法通过文档找到解决方案时可以通过阅读源码看看它的实现机制再相应的设计解决方案。本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。 ...

September 2, 2018 · 4 min · jiezi

消息队列二三事

最近在看kafka的代码,就免不了想看看消息队列的一些要点:服务质量(QOS)、性能、扩展性等等,下面一一探索这些概念,并谈谈在特定的消息队列如kafka或者mosquito中是如何具体实现这些概念的。服务质量服务语义服务质量一般可以分为三个级别,下面说明它们不同语义。At most once至多一次,消息可能丢失,但绝不会重复传输。生产者:完全依赖底层TCP/IP的传输可靠性,不做特殊处理,所谓“发送即忘”。kafka中设置acks=0。消费者:先保存消费进度,再处理消息。kafka中设置消费者自动提交偏移量并设置较短的提交时间间隔。At least once至少一次,消息绝不会丢,但是可能会重复。生产者:要做消息防丢失的保证。kafka中设置acks=1 或 all并设置retries>0。消费者:先处理消息,再保存消费进度。kafka中设置消费者自动提交偏移量并设置很长的提交时间间隔,或者直接关闭自动提交偏移量,处理消息后手动调用同步模式的偏移量提交。Exactly once精确一次,每条消息肯定会被传输一次且仅一次。这个级别光靠消息队列本身并不好保证,有可能要依赖外部组件。生产者:要做消息防丢失的保证。kafka中设置acks=1 或 all并设置retries>0。mosquito中通过四步握手与DUP、MessageID等标识来实现单次语义。消费者:要做消息防重复的保证,有多种方案,如:在保存消费进度和处理消息这两个操作中引入两阶段提交协议;让消息幂等;让消费处理与进度保存处于一个事务中来保证原子性。kafka中关闭自动提交偏移量,并设置自定义的再平衡监听器,监听到分区发生变化时从外部组件读取或者存储偏移量,保证自己或者其他消费者在更换分区时能读到最新的偏移量从而避免重复。总之就是结合ConsumerRebalanceListener、seek和一个外部系统(如支持事务的数据库)共同来实现单次语义。此外,kafka还提供了GUID以便用户自行实现去重。kafka 0.11版本通过3个大的改动支持EOS:1.幂等的producer;2. 支持事务;3. 支持EOS的流式处理(保证读-处理-写全链路的EOS)。这三个级别可靠性依次增加,但是延迟和带宽占用也会增加,所以实际情况中,要依据业务类型做出权衡。可靠性上面的三个语义不仅需要生产者和消费者的配合实现,还要broker本身的可靠性来进行保证。可靠性就是只要broker向producer发出确认,就一定要保证这个消息可以被consumer获取。kafka 中一个topic有多个partition,每个partition又有多个replica,所有replica中有一个leader,ISR是一定要同步leader后才能返回提交成功的replica集,OSR内的replica尽力的去同步leader,可能数据版本会落后。在kafka工作的过程中,如果某个replica同步速度慢于replica.lag.time.max.ms指定的阈值,则被踢出ISR存入OSR,如果后续速度恢复可以回到ISR中。可以配置min.insync.replicas指定ISR中的replica最小数量,默认该值为1。LEO是分区的最新数据的offset,当数据写入leader后,LEO就立即执行该最新数据,相当于最新数据标识位。HW是当写入的数据被同步到所有的ISR中的副本后,数据才认为已提交,HW更新到该位置,HW之前的数据才可以被消费者访问,保证没有同步完成的数据不会被消费者访问到,相当于所有副本同步数据标识位。每个partition的所有replica需要进行leader选举(依赖ZooKeeper)。在leader宕机后,只能从ISR列表中选取新的leader,无论ISR中哪个副本被选为新的leader,它都知道HW之前的数据,可以保证在切换了leader后,消费者可以继续看到HW之前已经提交的数据。当ISR中所有replica都宕机该partition就不可用了,可以设置unclean.leader.election.enable=true,该选项使得kafka选择任何一个活的replica成为leader然后继续工作,此replica可能不在ISR中,就可能导致数据丢失。所以实际使用中需要进行可用性与可靠性的权衡。kafka建议数据可靠存储不依赖于数据强制刷盘(会影响整体性能),而是依赖于replica。顺序消费顺序消费是指消费者处理消息的顺序与生产者投放消息的顺序一致。主要可能破坏顺序的场景是生产者投放两条消息AB,然后A失败重投递导致消费者拿到的消息是BA。kafka中能保证分区内部消息的有序性,其做法是设置max.in.flight.requests.per.connection=1,也就是说生产者在未得到broker对消息A的确认情况下是不会发送消息B的,这样就能保证broker存储的消息有序,自然消费者请求到的消息也是有序的。但是我们明显能感觉到这会降低吞吐量,因为消息不能并行投递了,而且会阻塞等待,也没法发挥 batch 的威力。如果想要整个topic有序,那就只能一个topic一个partition了,一个consumer group也就只有一个consumer了。这样就违背了kafka高吞吐的初衷。重复消费重复消费是指一个消息被消费者重复消费了。 这个问题也是上面第三个语义需要解决的。一般的消息系统如kafka或者类似的rocketmq都不能也不提倡在系统内部解决,而是配合第三方组件,让用户自己去解决。究其原因还是解决问题的成本与解决问题后获得的价值不匹配,所以干脆不解决,就像操作系统对待死锁一样,采取“鸵鸟政策”。但是kafka 0.11还是处理了这个问题,见发行说明,维护者是想让用户无可挑剔嘛 [笑cry]。性能衡量一个消息系统的性能有许多方面,最常见的就是下面几个指标。连接数是指系统在同一时刻能支持多少个生产者或者消费者的连接总数。连接数和broker采用的网络IO模型直接相关,常见模型有:单线程、连接每线程、Reactor、Proactor等。单线程一时刻只能处理一个连接,连接每线程受制于server的线程数量,Reactor是目前主流的高性能网络IO模型,Proactor由于操作系统对真异步的支持不太行所以尚未流行。kafka的broker采用了类似于Netty的Reactor模型:1(1个Acceptor线程)+N(N个Processor线程)+M(M个Work线程)。其中Acceptor负责监听新的连接请求,同时注册OPACCEPT事件,将新的连接按照RoundRobin的方式交给某个Processor线程处理。每个Processor都有一个NIO selector,向 Acceptor分配的 SocketChannel 注册 OPREAD、OPWRITE事件,对socket进行读写。N由num.networker.threads决定。Worker负责具体的业务逻辑如:从requestQueue中读取请求、数据存储到磁盘、把响应放进responseQueue中等等。M的大小由num.io.threads决定。Reactor模型一般基于IO多路复用(如select,epoll),是非阻塞的,所以少量的线程能处理大量的连接。如果大量的连接都是idle的,那么Reactor使用epoll的效率是杠杠的,如果大量的连接都是活跃的,此时如果没有Proactor的支持就最好把epoll换成select或者poll。具体做法是-Djava.nio.channels.spi.SelectorProvider把sun.nio.ch包下面的EPollSelectorProvider换成PollSelectorProvider。QPS是指系统每秒能处理的请求数量。QPS通常可以体现吞吐量(该术语很广,可以用TPS/QPS、PV、UV、业务数/小时等单位体现)的大小。kafka中由于可以采用 batch 的方式(还可以压缩),所以每秒钟可以处理的请求很多(因为减少了解析量、网络往复次数、磁盘IO次数等)。另一方面,kafka每一个topic都有多个partition,所以同一个topic下可以并行(注意不是并发哟)服务多个生产者和消费者,这也提高了吞吐量。平均响应时间平均响应时间是指每个请求获得响应需要的等待时间。kafka中处理请求的瓶颈(也就是最影响响应时间的因素)最有可能出现在哪些地方呢?网络? 有可能,但是这个因素总体而言不是kafka能控制的,kafka可以对消息进行编码压缩并批量提交,减少带宽占用;磁盘? 很有可能,所以kafka从分利用OS的pagecache,并且对磁盘采用顺序写,这样能大大提升磁盘的写入速度。同时kafka还使用了零拷贝技术,把普通的拷贝过程:disk->read buffer->app buffer->socket buffer->NIC buffer 中,内核buffer到用户buffer的拷贝过程省略了,加快了处理速度。此外还有文件分段技术,每个partition都分为多个segment,避免了大文件操作的同时提高了并行度。CPU? 不大可能,因为消息队列的使用并不涉及大量的计算,常见消耗有线程切换、编解码、压缩解压、内存拷贝等,这些在大数据处理中一般不是瓶颈。并发数是指系统同时能处理的请求数量数。一般而言,QPS = 并发数/平均响应时间 或者说 并发数 = QPS*平均响应时间。 这个参数一般只能估计或者计算,没法直接测。顾名思义,机器性能越好当然并发数越高咯。此外注意用上多线程技术并且提高代码的并行度、优化IO模型、减少减少内存分配和释放等手段都是可以提高并发数的。扩展性消息系统的可扩展性是指要为系统组件添加的新的成员的时候比较容易。kafka中扩展性的基石就是topic采用的partition机制。第一,Kafka允许Partition在cluster中的Broker之间移动,以此来解决数据倾斜问题。第二,支持自定义的Partition算法,比如你可以将同一个Key的所有消息都路由到同一个Partition上去(来获得顺序)。第三,partition的所有replica通过ZooKeeper来进行集群管理,可以动态增减副本。第四,partition也支持动态增减。对于producer,不存在扩展问题,只要broker还够你连接就行。对于consumer,一个consumer group中的consumer可以增减,但是最好不要超过一个topic的partition数量,因为多余的consumer并不能提升处理速度,一个partition在同一时刻只能被一个consumer group中的一个consumer消费代码上的可扩展性就属于设计模式的领域了,这里不谈。参考《kafka技术内幕》Kafka的存储机制以及可靠性Kafka 0.11.0.0 是如何实现 Exactly-once 语义的查看原文,来自mageekchiu。总结不到位的地方请不吝赐教。

August 30, 2018 · 1 min · jiezi