Explorar o código

optimize markdown rendering

Yuki Takei %!s(int64=6) %!d(string=hai) anos
pai
achega
4efe3c2c7f

+ 1 - 1
CHANGES.md

@@ -2,7 +2,7 @@
 
 ## v3.6.8-RC
 
-*
+* Improvement: Optimize markdown rendering
 
 ## v3.6.7
 

+ 1 - 1
src/client/js/components/Page/RevisionBody.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 
 import { debounce } from 'throttle-debounce';
 
-export default class RevisionBody extends React.Component {
+export default class RevisionBody extends React.PureComponent {
 
   constructor(props) {
     super(props);

+ 48 - 41
src/client/js/components/Page/RevisionRenderer.jsx

@@ -8,7 +8,7 @@ import GrowiRenderer from '../../util/GrowiRenderer';
 
 import RevisionBody from './RevisionBody';
 
-class RevisionRenderer extends React.Component {
+class RevisionRenderer extends React.PureComponent {
 
   constructor(props) {
     super(props);
@@ -21,12 +21,31 @@ class RevisionRenderer extends React.Component {
     this.getHighlightedBody = this.getHighlightedBody.bind(this);
   }
 
-  componentWillMount() {
-    this.renderHtml(this.props.markdown, this.props.highlightKeywords);
+  initCurrentRenderingContext() {
+    this.currentRenderingContext = {
+      markdown: this.props.markdown,
+      currentPagePath: this.props.pageContainer.state.path,
+    };
+  }
+
+  componentDidMount() {
+    this.initCurrentRenderingContext();
+    this.renderHtml();
   }
 
-  componentWillReceiveProps(nextProps) {
-    this.renderHtml(nextProps.markdown, this.props.highlightKeywords);
+  componentDidUpdate(prevProps) {
+    const { markdown: prevMarkdown, highlightKeywords: prevHighlightKeywords } = prevProps;
+    const { markdown, highlightKeywords } = this.props;
+
+    if (markdown !== prevMarkdown || highlightKeywords !== prevHighlightKeywords) {
+      this.initCurrentRenderingContext();
+      this.renderHtml();
+      return;
+    }
+
+    const { interceptorManager } = this.props.appContainer;
+
+    interceptorManager.process('postRenderHtml', this.currentRenderingContext);
   }
 
   /**
@@ -51,42 +70,30 @@ class RevisionRenderer extends React.Component {
     return returnBody;
   }
 
-  renderHtml(markdown) {
-    const { pageContainer } = this.props;
-
-    const context = {
-      markdown,
-      currentPagePath: pageContainer.state.path,
-    };
-
-    const growiRenderer = this.props.growiRenderer;
-    const interceptorManager = this.props.appContainer.interceptorManager;
-    interceptorManager.process('preRender', context)
-      .then(() => { return interceptorManager.process('prePreProcess', context) })
-      .then(() => {
-        context.markdown = growiRenderer.preProcess(context.markdown);
-      })
-      .then(() => { return interceptorManager.process('postPreProcess', context) })
-      .then(() => {
-        context.parsedHTML = growiRenderer.process(context.markdown);
-      })
-      .then(() => { return interceptorManager.process('prePostProcess', context) })
-      .then(() => {
-        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
-
-        // highlight
-        if (this.props.highlightKeywords != null) {
-          context.parsedHTML = this.getHighlightedBody(context.parsedHTML, this.props.highlightKeywords);
-        }
-      })
-      .then(() => { return interceptorManager.process('postPostProcess', context) })
-      .then(() => { return interceptorManager.process('preRenderHtml', context) })
-      .then(() => {
-        this.setState({ html: context.parsedHTML });
-      })
-      // process interceptors for post rendering
-      .then(() => { return interceptorManager.process('postRenderHtml', context) });
-
+  async renderHtml() {
+    const {
+      appContainer, growiRenderer,
+      highlightKeywords,
+    } = this.props;
+
+    const { interceptorManager } = appContainer;
+    const context = this.currentRenderingContext;
+
+    await interceptorManager.process('preRender', context);
+    await interceptorManager.process('prePreProcess', context);
+    context.markdown = growiRenderer.preProcess(context.markdown);
+    await interceptorManager.process('postPreProcess', context);
+    context.parsedHTML = growiRenderer.process(context.markdown);
+    await interceptorManager.process('prePostProcess', context);
+    context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
+
+    if (this.props.highlightKeywords != null) {
+      context.parsedHTML = this.getHighlightedBody(context.parsedHTML, highlightKeywords);
+    }
+    await interceptorManager.process('postPostProcess', context);
+    await interceptorManager.process('preRenderHtml', context);
+
+    this.setState({ html: context.parsedHTML });
   }
 
   render() {

+ 5 - 44
src/client/js/components/PageEditor.jsx

@@ -44,9 +44,6 @@ class PageEditor extends React.Component {
     this.saveDraft = this.saveDraft.bind(this);
     this.clearDraft = this.clearDraft.bind(this);
 
-    // get renderer
-    this.growiRenderer = this.props.appContainer.getRenderer('editor');
-
     // for scrolling
     this.lastScrolledDateWithCursor = null;
     this.isOriginOfScrollSyncEditor = false;
@@ -56,15 +53,14 @@ class PageEditor extends React.Component {
     this.scrollPreviewByEditorLineWithThrottle = throttle(20, this.scrollPreviewByEditorLine);
     this.scrollPreviewByCursorMovingWithThrottle = throttle(20, this.scrollPreviewByCursorMoving);
     this.scrollEditorByPreviewScrollWithThrottle = throttle(20, this.scrollEditorByPreviewScroll);
-    this.renderPreviewWithDebounce = debounce(50, throttle(100, this.renderPreview));
+    this.setMarkdownStateWithDebounce = debounce(50, throttle(100, (value) => {
+      this.setState({ markdown: value });
+    }));
     this.saveDraftWithDebounce = debounce(800, this.saveDraft);
   }
 
   componentWillMount() {
     this.props.appContainer.registerComponentInstance('PageEditor', this);
-
-    // initial rendering
-    this.renderPreview(this.state.markdown);
   }
 
   getMarkdown() {
@@ -93,7 +89,7 @@ class PageEditor extends React.Component {
    * @param {string} value
    */
   onMarkdownChanged(value) {
-    this.renderPreviewWithDebounce(value);
+    this.setMarkdownStateWithDebounce(value);
     this.saveDraftWithDebounce();
   }
 
@@ -285,41 +281,6 @@ class PageEditor extends React.Component {
     this.props.editorContainer.clearDraft(this.props.pageContainer.state.path);
   }
 
-  renderPreview(value) {
-    this.setState({ markdown: value });
-
-    // render html
-    const context = {
-      markdown: this.state.markdown,
-      currentPagePath: decodeURIComponent(window.location.pathname),
-    };
-
-    const growiRenderer = this.growiRenderer;
-    const interceptorManager = this.props.appContainer.interceptorManager;
-    interceptorManager.process('preRenderPreview', context)
-      .then(() => { return interceptorManager.process('prePreProcess', context) })
-      .then(() => {
-        context.markdown = growiRenderer.preProcess(context.markdown);
-      })
-      .then(() => { return interceptorManager.process('postPreProcess', context) })
-      .then(() => {
-        const parsedHTML = growiRenderer.process(context.markdown);
-        context.parsedHTML = parsedHTML;
-      })
-      .then(() => { return interceptorManager.process('prePostProcess', context) })
-      .then(() => {
-        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
-      })
-      .then(() => { return interceptorManager.process('postPostProcess', context) })
-      .then(() => { return interceptorManager.process('preRenderPreviewHtml', context) })
-      .then(() => {
-        this.setState({ html: context.parsedHTML });
-      })
-      // process interceptors for post rendering
-      .then(() => { return interceptorManager.process('postRenderPreviewHtml', context) });
-
-  }
-
   render() {
     const config = this.props.appContainer.getConfig();
     const noCdn = envUtils.toBoolean(config.env.NO_CDN);
@@ -345,7 +306,7 @@ class PageEditor extends React.Component {
         </div>
         <div className="col-md-6 hidden-sm hidden-xs page-editor-preview-container">
           <Preview
-            html={this.state.html}
+            markdown={this.state.markdown}
             // eslint-disable-next-line no-return-assign
             inputRef={(el) => { return this.previewElement = el }}
             isMathJaxEnabled={this.state.isMathJaxEnabled}

+ 74 - 2
src/client/js/components/PageEditor/Preview.jsx

@@ -2,15 +2,75 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { Subscribe } from 'unstated';
+import { createSubscribedElement } from '../UnstatedUtils';
 
 import RevisionBody from '../Page/RevisionBody';
 
+import AppContainer from '../../services/AppContainer';
 import EditorContainer from '../../services/EditorContainer';
 
 /**
  * Wrapper component for Page/RevisionBody
  */
-export default class Preview extends React.PureComponent {
+class Preview extends React.PureComponent {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      html: '',
+    };
+
+    // get renderer
+    this.growiRenderer = props.appContainer.getRenderer('editor');
+  }
+
+  componentDidMount() {
+    this.initCurrentRenderingContext();
+    this.renderPreview();
+  }
+
+  componentDidUpdate(prevProps) {
+    const { markdown: prevMarkdown } = prevProps;
+    const { markdown } = this.props;
+
+    if (markdown !== prevMarkdown) {
+      this.initCurrentRenderingContext();
+      this.renderPreview();
+      return;
+    }
+
+    const { interceptorManager } = this.props.appContainer;
+
+    interceptorManager.process('postRenderPreviewHtml', this.currentRenderingContext);
+  }
+
+  initCurrentRenderingContext() {
+    this.currentRenderingContext = {
+      markdown: this.props.markdown,
+      currentPagePath: decodeURIComponent(window.location.pathname),
+    };
+  }
+
+  async renderPreview() {
+    const { appContainer } = this.props;
+    const { growiRenderer } = this;
+
+    const { interceptorManager } = appContainer;
+    const context = this.currentRenderingContext;
+
+    await interceptorManager.process('preRenderPreview', context);
+    await interceptorManager.process('prePreProcess', context);
+    context.markdown = growiRenderer.preProcess(context.markdown);
+    await interceptorManager.process('postPreProcess', context);
+    context.parsedHTML = growiRenderer.process(context.markdown);
+    await interceptorManager.process('prePostProcess', context);
+    context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
+    await interceptorManager.process('postPostProcess', context);
+    await interceptorManager.process('preRenderPreviewHtml', context);
+
+    this.setState({ html: context.parsedHTML });
+  }
 
   render() {
     return (
@@ -31,6 +91,7 @@ export default class Preview extends React.PureComponent {
           >
             <RevisionBody
               {...this.props}
+              html={this.state.html}
               renderMathJaxInRealtime={editorContainer.state.previewOptions.renderMathJaxInRealtime}
             />
           </div>
@@ -41,10 +102,21 @@ export default class Preview extends React.PureComponent {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const PreviewWrapper = (props) => {
+  return createSubscribedElement(Preview, props, [AppContainer]);
+};
+
 Preview.propTypes = {
-  html: PropTypes.string,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  markdown: PropTypes.string,
   inputRef: PropTypes.func.isRequired, // for getting div element
   isMathJaxEnabled: PropTypes.bool,
   renderMathJaxOnInit: PropTypes.bool,
   onScroll: PropTypes.func,
 };
+
+export default PreviewWrapper;