美文网首页
【译】了解React源代码-初始渲染(类组件)5

【译】了解React源代码-初始渲染(类组件)5

作者: nextChallenger | 来源:发表于2019-10-15 16:14 被阅读0次

    【译】了解React源代码-初始渲染(简单组件)1
    【译】了解React源代码-初始渲染(简单组件)2
    【译】了解React源代码-初始渲染(简单组件)3
    【译】了解React源代码-初始渲染(类组件)4
    【译】了解React源代码-初始渲染(类组件)5
    【译】了解React源代码-UI更新(事务)6
    【译】了解React源代码-UI更新(事务)7
    【译】了解React源代码-UI更新(单个DOM)8
    【译】了解React源代码-UI更新(DOM树)9


    上一次,我们完成了类组件渲染逻辑的上半部分,尽管在以下几个方面与简单组件渲染有所不同,但它们相似:1)实例化一个附加的ReactCompositeComponent来表示类组件(App); 2)调用App.render(),触发级联的React.createElement()来建立ReactElement树。

    这次,我们将通过检查树中的ReactElement如何转换为它们各自的ReactDOMComponent,以及最终转换为实际DOM对象,来探索下半部分的更多分支。

    本文中使用的文件:

    renderers / dom / shared / ReactDOMComponent.js
    提供了本文重点介绍的方法之一_createInitialChildren

    renderers / dom / client / utils / setTextContent.js
    DOM操作,设置文本

    renderers / dom / client / utils / DOMLazyTree.js
    DOM操作,附加子级

    renderers / shared / stack / reconciler / ReactMultiChild.js
    traverseAllChildren的中间方法以及焦点中的另一个方法mountChildren()

    shared / utils / traverseAllChildren.js
    迭代直接子ReactElement并实例化它们各自的ReactDOMComponent的方法

    调用堆栈中使用的符号: 循环

    I use {} to reference the previous post that is relevant to the methods (or logic process) being discussed.
    我使用{}来引用与所讨论的方法(或逻辑过程)相关的先前文章。

    本文讨论的过程主要发生在ReactDOMComponent [6] .mountComponent()中。 该方法的主要任务是从ReactDOMComponent [6]派生一个DOM对象,在{第三篇}中介绍。 该任务编号为0),以供以后参考。

    在本文中,我们将解决上次有意忽略的方法之一_createInitialChildren,该方法用于将新引入的ReactElement树作为类组件的子代处理。 在{第三篇 *7}的一个小众案例中,它被用作文本子对象,并且仅触发了一个分支。 此分支以及整个方法将在本文中详细讨论。

    _createInitialChildren is our protagonist today; please search *7 in post three if you want to check its role in the simple component rendering. The other overlooked method _updateDOMProperties in {post three *6} will be discussed in later articles.
    _createInitialChildren是我们今天的主角; 如果要检查其在简单组件渲染中的作用,请在第三篇文章中搜索 *7。 {第三篇 *6}中另一个被忽略的方法_updateDOMProperties将在以后的文章中讨论。

    更具体地说,此方法1)将ReactElement转换为其相应的ReactDOMComonents; 2)(递归)调用ReactDOMComponent [*]。mountComponent()创建DOM对象; 和3)将它们附加到在0)中创建的根DOM节点。

    因此,首先让我们回顾一下类组件的上下文中的步骤0)。

    ReactDOMComponent [6] .mountComponent()(在_createInitialChildren之前)—创建DOM元素[6]

    time-saving hint: this paragraph is here to keep the post self-contained, the detail of the ReactDOMComponent creation process has been covered in {post three}
    节省时间的提示:此段是为了保持帖子自成体系,ReactDOMComponent创建过程的详细信息已在{第三篇}中进行了介绍

    数据结构:

    调用栈:

    ...
    |~mountComponentIntoNode()                                    |
      |-ReactReconciler.mountComponent()                          |
        |-ReactCompositeComponent[T].mountComponent()             |
          |-ReactCompositeComponent[T].performInitialMount()  upper half
            |-ReactReconciler.mountComponent()                    |
              |-ReactCompositeComponent[ins].mountComponent()     |
                |-this.performInitialMount()                      |
                  |-this._renderValidatedComponent()              |
                  |-instantiateReactComponent()                  _|_ 
                    (we are here)                                 |
                  |-ReactDOMComponent[6].mountComponent(          |
                      transaction, // scr: -----> not of interest |
                      hostParent,  // scr: -----> null            |
                      hostContainerInfo,// scr:---------------------> ReactDOMContainerInfo[ins]                                lower half
                      context      // scr: -----> not of interest |
                    )                                             |
    ...
    

    此步骤使用ReactDOMComponent [6]创建一个DOM对象,并设置其属性。

    回顾一下:1)初始化ReactDOMComponent [6]的属性; 2)使用document.createElement()创建一个div DOM元素; 3)在ReactDOMComponent [6]和DOM对象之间创建一个双向链接; 4)和5)设置新创建的DOM对象的属性和属性; 和6)将DOM对象嵌入DOMLazyTree [1]

    mountComponent: function (
      transaction,
      hostParent,
      hostContainerInfo,
      context
    ) {
    
    // scr: --------------------------------------------------------> 1)
      this._rootNodeID = globalIdCounter++;
      this._domID = hostContainerInfo._idCounter++;
      this._hostParent = hostParent;
      this._hostContainerInfo = hostContainerInfo; // scr: ------------> ReactDOMContainerInfo[ins]
    
      var props = this._currentElement.props;
    
      switch (this._tag) { // scr: ---> no condition is met here
    ...
      }
    
    ... // scr: -----> sanity check
    
    // We create tags in the namespace of their parent container, except HTML
    // tags get no namespace.
      var namespaceURI;
      var parentTag;
    
      if (hostParent != null) { // scr: -----> it is null
    ...
      } else if (hostContainerInfo._tag) {
        namespaceURI = hostContainerInfo._namespaceURI; // scr: -------> "http://www.w3.org/1999/xhtml"
        parentTag = hostContainerInfo._tag;        // scr: ------> "div"
      }
      if (namespaceURI == null || 
          namespaceURI === DOMNamespaces.svg && 
          parentTag === 'foreignobject'
      ) { // scr: -----> no
    ...
      }
    
      if (namespaceURI === DOMNamespaces.html) {
        if (this._tag === 'svg') {               // scr: -----> no
    ...
        } else if (this._tag === 'math') {       // scr: -----> no
    ...
        }
      }
    
      this._namespaceURI = namespaceURI;  // scr: ---------------------> "http://www.w3.org/1999/xhtml"
    
    ... // scr: ------> DEV code
    
      var mountImage;
    
      if (transaction.useCreateElement) { // scr: ---------------------> transaction related logic, we assume it is true
        var ownerDocument = hostContainerInfo._ownerDocument;
        var el;
    
        if (namespaceURI === DOMNamespaces.html) {
          if (this._tag === 'script') {         // scr: -----> no
    ...
          } else if (props.is) {                // scr: -----> no
    ...
          } else {
            // Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug.
            // See discussion in https://github.com/facebook/react/pull/6896
            // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
    
    // scr: --------------------------------------------------------> 2)
            // scr: ---------> HTML DOM API
            el = ownerDocument.createElement(this._currentElement.type);
          }
        } else { // scr: ------> no
    ...
        }
    
    // scr: --------------------------------------------------------> 3)
        ReactDOMComponentTree.precacheNode(this, el); // scr: --------> doubly link (._hostNode & .internalInstanceKey)
        this._flags |= Flags.hasCachedChildNodes; // scr: ------------>
    bit wise its flags
    
    // scr: --------------------------------------------------------> 4)
        if (!this._hostParent) { // scr: ------> it is the root element
          DOMPropertyOperations.setAttributeForRoot(el); // scr: -----> data-reactroot
        }
    
    // scr: --------------------------------------------------------> 5)
        this._updateDOMProperties( //*6
          null,
          props,
          transaction
        ); // scr: --------------------------> style:{ “color”: “blue” }
    
    // scr: --------------------------------------------------------> 6)
        var lazyTree = DOMLazyTree(el); // scr: ------> DOMLazyTree[ins]
        this._createInitialChildren(transaction, props, context, lazyTree);
    ...
      } // if (transaction.useCreateElement)
    
      return mountImage;
    }
    
    ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js
    

    ReactDOMComponent[6] gets its DOM node, its children are next in the line
    ReactDOMComponent [6]得到它的DOM节点,它的孩子在该行的下一个

    ReactDOMComponent [6] ._ createInitialChildren()-创建DOM元素[2-5]

    指定的数据结构:

    如前所述,此方法用于在{第三篇}中创建字符串子节点(“ hello world”)。 渲染相似的节点(即[3]和[5])时,我们将在本文中重用该分支,并将其命名为分支{1}。

    对于类组件,第一次访问此方法时,路由{2}被命中。 具体来说,此分支处理ReactElement树。 如前所述,它1)将ReactElements转换为ReactDOMComponents(a),生成具有ReactDOMComponents的DOM节点(b),以及2)在最后一步中将DOM节点插入由ReactDOMComponent [6]生成的根节点。

    _createInitialChildren: function (
      transaction, // scr: not of interest
      props,       // scr: -------------------> ReactElement[6].props
      context,     // scr: not of interest
      lazyTree     // scr: -------------------> DOMLazyTree[ins]
    ) {
      // Intentional use of != to avoid catching zero/false.
      // scr: it is named as 'dangerous', let's avoid touching it
      var innerHTML = props.dangerouslySetInnerHTML;
      if (innerHTML != null) { // scr: so no innerHTML
    ...
      } else {
        var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
    
        var childrenToUse = contentToUse != null ? null : props.children;
        // scr: some comments
        if (contentToUse != null) {
          // scr: some comments
          if (contentToUse !== '') { // scr: ----------------> route {1}
    ...// scr: DEV code
            DOMLazyTree.queueText(lazyTree, contentToUse);
          }
        } else if (childrenToUse != null) { // scr: ---------> route {2}
          var mountImages = this.mountChildren(childrenToUse, transaction, context);  // scr: --------------------------------> 1)
          for (var i = 0; i < mountImages.length; i++) { scr: ------> 2)
            DOMLazyTree.queueChild(lazyTree, mountImages[i]);
          }
        }
      }
    },
    
    ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js
    

    The call hierarchy and iteration is a bit complex from now on, so this time I’ll first establish an overview of the big picture before diving into any detail.
    从现在开始,呼叫的层次结构和迭代有点复杂,因此这次我将首先对总体情况进行概述,然后再探讨任何细节。

    调用栈:

    ...                                            (outer recursion)
    ReactDOMComponent[6].mountComponent()    <-------------------------|
        (we are here)                                                  |
      |-this._createInitialChildren()                                  |
      ?{1}                                                             |
        |-DOMLazyTree.queueText()                                      |
      ?{2}                                                             |
        |-this.mountChildren()        // scr: ---------------> 1)(a)   |
          |-this._reconcilerInstantiateChildren()                      |
            |-ReactChildReconciler.instantiateChildren()               |
              |-traverseAllChildren()                                  |
                |-traverseAllChildrenImpl()  <------|inner             |
                  |↻traverseAllChildrenImpl() ------|recursion         |
                    |-instantiateChild()                               |
                      |-instantiateReactComponent()                    |
          |↻ReactDOMComponent.mountComponent()      // scr: -> 1)(b)---|
        |↻DOMLazyTree.queueChild()    // scr: ---------------> 2)
    ...
    

    首先,我们检查在DOM级别上运行的(复杂)堆栈的底部(通过了解最终目的,我们可以在面对复杂调用图时有所放心)。

    DOMLazyTree.queueText()和DOMLazyTree.queueChild()

    在此演练中,DOMLazyTree.queueText()只有一行有效:

    function queueText(tree, text) {
      if (enableLazy) { // scr: NO, I mean, false
    ...
      } else {
        setTextContent(tree.node, text);
      }
    }
    
    queueText@renderers/dom/client/utils/DOMLazyTree.js
    
    var setTextContent = function (node, text) {
      if (text) {
        var firstChild = node.firstChild;
    
      if (firstChild && firstChild === node.lastChild && firstChild.nodeType === 3) { // scr: false
    ...
        }
      }
      node.textContent = text; // scr: the only effective line
    };
    
    setTextContent@renderers/dom/client/utils/setTextContent.js
    

    Node.textContent是一个DOM标准属性,可以很好地表示节点的文本内容。 显然,这是路线{1}的最终目的。

    DOMLazyTree.queueChild()也有一行:

    function queueChild(parentTree, childTree) {
      if (enableLazy) { // scr: again, false
    ...
      } else {
        parentTree.node.appendChild(childTree.node);
      }
    }
    
    queueChild@renderers/dom/client/utils/DOMLazyTree.js
    

    这里Node.appendChild()是另一个DOM标准API,它将一个节点作为子节点插入另一个节点。 显然,这是路线{2}的最后一站。

    现在,我们可以将这两种方法替换为它们各自的本质线。

    ...                                          (outer recursion)
    ReactDOMComponent[6].mountComponent()    <-------------------------|
      |-this._createInitialChildren()                                  |
      ?{1}                                                             |
        |-node.textContent = text;                                     |
      ?{2}                                                             |
        |-this.mountChildren()        // scr: ---------------> 1)(a)   |
          |-this._reconcilerInstantiateChildren()                      |
            |-ReactChildReconciler.instantiateChildren()               |
              |-traverseAllChildren()                                  |
                |-traverseAllChildrenImpl()  <------|inner             |
                  |↻traverseAllChildrenImpl() ------|recursion         |
                    |-instantiateChild()                               |
                      |-instantiateReactComponent()                    |
          |↻ReactDOMComponent.mountComponent() // scr: ------> 1)(b)---|
        |↻node.appendChild()                   // scr: ------> 2)
    ...
    

    推断大局

    为此,我们从已知的方法开始。

    首先,instantiateReactComponent()ReactElement实例化一个ReactDOMComponent(现在在树中没有任何“复合” ReactElement,因此所有创建的组件都是ReactDOMComponents),这也是深度嵌套调用层次结构的结尾{第二篇}

    其次,ReactDOMComponent.mountComponent()初始化在上一步中创建的ReactDOMComponent,并基于它们创建相应的DOM节点。 {第三篇}&{开始}

    考虑到以上两个操作是{OG}(operation group),现在更容易解释如何处理其余ReactElement树。

    这是一个高级解释:

    1. 当为非叶子节点调用ReactDOMComponent.mountComponent()的外部递归时,将使用分支{2}来触发每个组件子节点的{OG};

    2. 当对包含文本的叶节点调用ReactDOMComponent.mountComponent()的外部递归时,分支{1}将起作用,这将设置node.textContent;。

    3. 当为不包含文本的叶节点调用ReactDOMComponent.mountComponent()的外部递归时,将根本不会调用_createInitialChildren()

    请注意,在此过程中,将反复使用ReactDOMComponent.mountComponent()为相应的ReactDOMComponent实例创建DOM节点,因此,如果它不在(大脑)缓存中,则可能需要在本文开头检查其实现。

    现在该使调用堆栈发挥作用了:

    ...
    ReactDOMComponent[6].mountComponent()
      |-this._createInitialChildren()
        |-this.mountChildren() 
    ...           |↻instantiateReactComponent()[4,5]
          |-ReactDOMComponent[5].mountComponent()
            |-this._createInitialChildren()
              |-node.textContent = text; // scr: [5] done
          |-ReactDOMComponent[4].mountComponent()
            |-this._createInitialChildren()
              |-this.mountChildren() 
    ...                 |↻instantiateReactComponent()[2,3]
              |-ReactDOMComponent[2].mountComponent() // scr: [2] done
              |-ReactDOMComponent[3].mountComponent()
                |-this._createInitialChildren()
                  |-node.textContent = text; // scr: [3] done
            |↻node[4].appendChild()[2,3] // scr: [4] done
    
        |↻node[6].appendChild()[4,5] // scr: [6] done
    ...
    

    在此调用堆栈中,我省略了用于在ReactElement实例化ReactDOMComponent时使用的深层嵌套调用层次结构,接下来我们将对其进行检查。

    InstantiateReactComponent()的深层嵌套循环

    特别是,我们需要注意参数,以便在涉及递归和回调的相当深而复杂的链中跟踪输入和输出。

    ReactDOMComponent._createInitialChildren()内部开始:

    ...
    var mountImages = this.mountChildren(
      childrenToUse, // scr:----------> ReactElement[6].props.children
      transaction,   // scr: not of interest
      context        // scr: not of interest
    );
    ...
    

    接下来,我们看一下ReactDOMComponent.mountChildren()的实现。 如前所述,它1)实例化ReactDOMComponents的所有子级; 和2)通过调用ReactDOMComponent.mountComponent()初始化那些ReactDOMComponent

    mountChildren: function (
      nestedChildren, // scr:----------> ReactElement[6].props.children
      transaction,    // scr: not of interest
      context         // scr: not of interest
    ) {
      
      // scr: ------------------------------------------------------> 1)
      var children = this._reconcilerInstantiateChildren(nestedChildren, transaction, context);
      this._renderedChildren = children;
      var mountImages = [];
      var index = 0;
      for (var name in children) {
        if (children.hasOwnProperty(name)) {
          var child = children[name];
          var selfDebugID = 0;
    ...// scr: DEV code
                                                       (outer recursion)      
          // scr: --------------------------------------------------> 2)
          var mountImage = ReactReconciler.mountComponent(child, transaction, this, this._hostContainerInfo, context, selfDebugID);
          
          child._mountIndex = index++;
          mountImages.push(mountImage);
        }
      }
    ...// scr: DEV code
      return mountImages;
    },
    
    ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js
    

    2)在之前被称为“外部递归”,并且是另一个ReactReconciler.mountComponent(){第二篇},所以我们关注1)

    _reconcilerInstantiateChildren: function (
      nestedChildren, // scr:----------> ReactElement[6].props.children
      transaction,    // scr: not of interest
      context         // scr: not of interest
    ) {
    ...// scr: DEV code
      return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
    },
    
    ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.js
    

    这是直接调用

    instantiateChildren: function (
      nestedChildNodes, // scr: --------> ReactElement[6].props.children
      transaction,      // scr: not of interest
      context,          // scr: not of interest
      selfDebugID
    ) // 0 in production and for roots {
      if (nestedChildNodes == null) {
        return null;
      }
      var childInstances = {};
      if (process.env.NODE_ENV !== 'production') {
    ...// scr: DEV code
      } else {
        traverseAllChildren(nestedChildNodes, instantiateChild, childInstances);
      }
      return childInstances;
    },
    
    instantiateChildren@renderers/shared/stack/reconciler/ReactChildReconciler.js
    

    再次,这是对traverseAllChildren()的直接调用。 请注意,instantiateChild是每个子级调用的回调方法。

    function instantiateChild(
      childInstances, // scr: ---> the output parameter childInstances is passed all the way down here
      child,      // scr: --> a ReactElement
      name,       // scr: --> unique name for indexing in childInstances
      selfDebugID // scr: --> undefined
    ) {
    ... // scr: DEV code
      }
      if (child != null && keyUnique) {
        childInstances[name] = instantiateReactComponent(child, true);
      }
    }
    
    instantiateChild@renderers/shared/stack/reconciler/ReactChildReconciler.js
    

    反过来,它直接调用InstantiateReactComponent(){第一篇}。

    我们继续traverseAllChildren()

    function traverseAllChildren(
      children, // scr: ---------> ReactElement[6].props.children
      callback, // scr: ---------> instantiateChild
      traverseContext // scr: ---> output parameter, initialized as {}
    ) {
      if (children == null) {
        return 0;
      }
      return traverseAllChildrenImpl(children, '', callback, traverseContext);
    }
    
    traverseAllChildren@shared/utils/traverseAllChildren.js
    

    另一个直接调用traverseAllChildrenImpl()

    function traverseAllChildrenImpl(
      children,  // scr: ---------> ReactElement[6].props.children
      nameSoFar, // scr: ---------> ''
      callback,  // scr: ---------> instantiateChild
      traverseContext // scr: ---> output parameter, initialized as {}
    ) {
      var type = typeof children;
      if (type === 'undefined' || type === 'boolean') {
        // All of the above are perceived as null.
        children = null;
      }
    // scr: -------------------------------------------------------> {a}
      if (children === null || type === 'string' || type === 'number' || type === 'object' && children.$$typeof === REACT_ELEMENT_TYPE) {
        callback(traverseContext, children,
        // If it's the only child, treat the name as if it was wrapped in an array
        // so that it's consistent if the number of children grows.
        nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);
        return 1;
      }
      var child;
      var nextName;
      var subtreeCount = 0; // Count of children found in the current subtree.
      var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
    // scr: -------------------------------------------------------> {b}
      if (Array.isArray(children)) {
        for (var i = 0; i < children.length; i++) {
          child = children[i];
          nextName = nextNamePrefix + getComponentKey(child, i);
          subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
        }
      } else {
    ... // scr: this branch will not be called here
      }
      return subtreeCount;
    }
    
    traverseAllChildrenImpl@shared/utils/traverseAllChildren.js
    

    在直接对另一种方法进行单行调用之后,traverseAllChildrenImpl()是真正的工作场所。 此方法在不久之前也被称为“内部递归”。

    traverseAllChildrenImpl()的逻辑很简单:第一次调用时(children参数的typearray),它会为数组中的每个ReactElement进行调用; 当它被连续调用时(childrenReactElement),它调用内部依赖于InstantiateReactComponent(){第一篇}的上述回调,以将ReactElement转换为一个空的且未初始化的ReactDOMComonent

    Note that “inner recursion” works on DIRECT children only while the “outer recursion” traverse the ENTIRE ReactElement tree.
    请注意,“内部递归”仅在“外部递归”遍历ENTIRE ReactElement树时适用于DIRECT子代。

    将所有ReactElement转换为ReactDOMComonents之后,输出将一直返回到ReactDOMComponent.mountChildren()并完成循环。

    To better understand the full circle, you might need to refer to different pieces of the puzzle back and forth, for example, the beginning of this text where ReactDOMComponent.mountComponent() is discussed; the two DOM operations (Node.appendChild, Node.textContent) that define the stack bottom; the discussion of the big picture; as well as this section.
    为了更好地理解整个循环,您可能需要前后来回指一下难题的不同部分,例如,本文的开头讨论了ReactDOMComponent.mountComponent()。 定义栈底部的两个DOM操作( (Node.appendChild, Node.textContent)); 全局的讨论; 以及本节。

    最后,在生成所有DOM节点之后,逻辑返回到ReactReconciler.mountComponent()并将新的节点树插入到指定的div容器中。 {第三篇}

    ...
    |~mountComponentIntoNode()                                    |
      |-ReactReconciler.mountComponent()                          |
        |-ReactCompositeComponent[T].mountComponent()             |
          |-ReactCompositeComponent[T].performInitialMount()  upper half
            |-ReactReconciler.mountComponent()                    |
              |-ReactCompositeComponent[ins].mountComponent()     |
                |-this.performInitialMount()                      |
                  |-this._renderValidatedComponent()              |
                  |-instantiateReactComponent()                  _|_ 
                  |-ReactDOMComponent[6].mountComponent(          |
                      transaction, // scr: -----> not of interest |
                      hostParent,  // scr: -----> null            |
                      hostContainerInfo,// scr:---------------------> ReactDOMContainerInfo[ins]                                    | 
                      context      // scr: -----> not of interest |
                    )                                             |
                                                                  |
    ... // the content of this section                        lower half
            |-_mountImageIntoNode()                  (HTML DOM specific)
                markup,    // scr: --> DOMLazyTree[ins]           |
                container, // scr: --> document.getElementById(‘root’)
                wrapperInstance, // scr:----> same                |
                shouldReuseMarkup, // scr:--> same                |
                transaction, // scr: -------> same                |
              )                                                  _|_
    

    (原文地址)Understanding The React Source Code - Initial Rendering (Class Component) V

    (上一篇)【译】了解React源代码-初始渲染(类组件)4

    (下一篇)【译】了解React源代码-UI更新(事务)6

    相关文章

      网友评论

          本文标题:【译】了解React源代码-初始渲染(类组件)5

          本文链接:https://www.haomeiwen.com/subject/bflsmctx.html