• 创建你自己的React


    1.(Didact)一个DIY教程:创建你自己的react

    [更新]这个系列从老的react架构写起,你可以跳过前面,直接看使用新的fiber架构重写的文章

    [更新2]听Dan的没错,我是认真的☺

    这篇深入fiber架构的文章真的很棒。
    — @dan_abramov

    1.1 引言

    很久以前,当学数据结构和算法时,我有个作业就是实现自己的数组,链表,队列,和栈(用Modula-2语言)。那之后,我再也没有过要自己来实现链表的需求。总会有库让我不需要自己重造轮子。

    所以,那个作业还有意义吗?当然,我从中学到了很多,知道如何合理使用各种数据结构,并知道根据场景合理选用它们。

    这个系列文章以及对应的(仓库)的目的也是一样,不过要实现的是一个,我们比链表使用更多的东西:React

    我好奇如果不考虑性能和设备兼容性,POSIX(可移植操作系统接口)核心可以实现得多么小而简单。
    — @ID_AA_Carmack

    我对react也这么好奇

    幸运的是,如果不考虑性能,调试,平台兼容性等等,react的主要3,4个特性重写并不难。事实上,它们很简单,甚至只要不足200行代码

    这就是我们接下来要做的事,用不到200行代码写一个有一样的API,能跑的React。因为这个库的说教性(didactic)特点,我们打算就称之为Didact

    用Didact写的应用如下:

        const stories = [
      { name: "Didact introduction", url: "http://bit.ly/2pX7HNn" },
      { name: "Rendering DOM elements ", url: "http://bit.ly/2qCOejH" },
      { name: "Element creation and JSX", url: "http://bit.ly/2qGbw8S" },
      { name: "Instances and reconciliation", url: "http://bit.ly/2q4A746" },
      { name: "Components and state", url: "http://bit.ly/2rE16nh" }
    ];
    
    class App extends Didact.Component {
      render() {
        return (
          <div>
            <h1>Didact Stories</h1>
            <ul>
              {this.props.stories.map(story => {
                return <Story name={story.name} url={story.url} />;
              })}
            </ul>
          </div>
        );
      }
    }
    
    class Story extends Didact.Component {
      constructor(props) {
        super(props);
        this.state = { likes: Math.ceil(Math.random() * 100) };
      }
      like() {
        this.setState({
          likes: this.state.likes + 1
        });
      }
      render() {
        const { name, url } = this.props;
        const { likes } = this.state;
        const likesElement = <span />;
        return (
          <li>
            <button onClick={e => this.like()}>{likes}<b>❤️</b></button>
            <a href={url}>{name}</a>
          </li>
        );
      }
    }
    
    Didact.render(<App stories={stories} />, document.getElementById("root"));
    

    这就是我们在这个系列文章里要使用的例子。效果如下
    demo.gif

    我们将会从下面几点来一步步添加Didact的功能:

    这个系列暂时不讲的地方:

    • Functional components
    • Context(上下文)
    • 生命周期方法
    • ref属性
    • 通过key的调和过程(这里只讲根据子节点原顺序的调和)
    • 其他渲染引擎 (只支持DOM)
    • 旧浏览器支持

    你可以从react实现笔记Paul O’Shannessy的这个youtube演讲视频,或者react仓库地址,找到更多关于如何实现react的细节.

    2.渲染dom元素

    2.1 什么是DOM

    开始之前,让我们回想一下,我们经常使用的DOM API

     // Get an element by id
    const domRoot = document.getElementById("root");
    // Create a new element given a tag name
    const domInput = document.createElement("input");
    // Set properties
    domInput["type"] = "text";
    domInput["value"] = "Hi world";
    domInput["className"] = "my-class";
    // Listen to events
    domInput.addEventListener("change", e => alert(e.target.value));
    // Create a text node
    const domText = document.createTextNode("");
    // Set text node content
    domText["nodeValue"] = "Foo";
    // Append an element
    domRoot.appendChild(domInput);
    // Append a text node (same as previous)
    domRoot.appendChild(domText);
    

    注意到我们设置元素的属性而不是特性属性和特性的区别,只有合法的属性才可以设置。

    2.2 Didact元素

    我们用js对象来描述渲染过程,这些js对象我们称之为Didact元素.这些元素有2个属性,type和props。type可以是一个字符串或者方法。在后面讲到组件之前,我们先用字符串。props是一个可以为空的对象(不过不能为null)。props可能有children属性,这个children属性是一个Didact元素的数组。

    我们将多次使用Didact元素,目前我们先称之为元素。不要和html元素混淆,在变量命名的时候,我们称它们为DOM元素或者dom(preact就是这么做的)

    一个元素就像下面这样:

        const element = {
      type: "div",
      props: {
        id: "container",
        children: [
          { type: "input", props: { value: "foo", type: "text" } },
          { type: "a", props: { href: "/bar" } },
          { type: "span", props: {} }
        ]
      }
    };
    

    对应描述下面的dom:

      <div id="container">
      <input value="foo" type="text">
      <a href="/bar"></a>
      <span></span>
      </div>
    

    Didact元素和react元素很像,但是不像react那样,你可能使用JSX或者createElement,创建元素就和创建js对象一样.Didatc我们也这么做,不过在后面章节我们再加上create元素的代码

    2.3 渲染dom元素

    下一步是渲染一个元素以及它的children到dom里。我们将写一个render方法(对应于react的ReactDOM.render),它接受一个元素和一个dom 容器。然后根据元素的定义生成dom树,附加到容器里。

        function render(element, parentDom) {
        const { type, props } = element;
        const dom = document.createElement(type);
        const childElements = props.children || [];
        childElements.forEach(childElement => render(childElement, dom));
        parentDom.appendChild(dom);
      }
    

    我们仍然没有对其添加属性和事件监听。现在让我们使用object.keys来遍历props属性,设置对应的值:

    function render(element, parentDom) {
      const { type, props } = element;
      const dom = document.createElement(type);
    
      const isListener = name => name.startsWith("on");
      Object.keys(props).filter(isListener).forEach(name => {
        const eventType = name.toLowerCase().substring(2);
        dom.addEventListener(eventType, props[name]);
      });
    
      const isAttribute = name => !isListener(name) && name != "children";
      Object.keys(props).filter(isAttribute).forEach(name => {
        dom[name] = props[name];
      });
    
      const childElements = props.children || [];
      childElements.forEach(childElement => render(childElement, dom));
    
      parentDom.appendChild(dom);
    }
    

    2.4 渲染DOM文本节点

    现在render函数不支持的就是文本节点,首先我们定义文本元素什么样子,比如,在react中描述 <span>Foo<span/>:

    const reactElement = {
      type: "span",
      props: {
        children: ["Foo"]
      }
    };
    

    注意到子节点,只是一个字符串,并不是其他元素对象。这就让我们的Didact元素定义不合适了:children元素应该是一个数组,数组里的元素都有type和props属性。如果我们遵守这个规则,后面将减少不必要的if判断.所以,Didact文本元素应该有一个“TEXT ELEMENT”的类型,并且有在对应的节点有文本的值。比如:

    const textElement = {
      type: "span",
      props: {
        children: [
          {
            type: "TEXT ELEMENT",
            props: { nodeValue: "Foo" }
          }
        ]
      }
    };
    

    现在我们来定义文本元素应该如何渲染。不同的是,文本元素不使用createElement方法,而用createTextNode代替。节点值就和其他属性一样被设置上去。

    function render(element, parentDom) {
      const { type, props } = element;
    
      // Create DOM element
      const isTextElement = type === "TEXT ELEMENT";
      const dom = isTextElement
        ? document.createTextNode("")
        : document.createElement(type);
    
      // Add event listeners
      const isListener = name => name.startsWith("on");
      Object.keys(props).filter(isListener).forEach(name => {
        const eventType = name.toLowerCase().substring(2);
        dom.addEventListener(eventType, props[name]);
      });
    
      // Set properties
      const isAttribute = name => !isListener(name) && name != "children";
      Object.keys(props).filter(isAttribute).forEach(name => {
        dom[name] = props[name];
      });
    
      // Render children
      const childElements = props.children || [];
      childElements.forEach(childElement => render(childElement, dom));
    
      // Append to parent
      parentDom.appendChild(dom);
    }
    

    2.5 总结

    我们现在创建了一个可以渲染元素以及子元素的render方法。后面我们需要实现如何创建元素。我们将在下节讲到如何使JSX和Didact很好地融合。

    3.JSX和创建元素

    3.1 JSX

    我们之前讲到了Didact元素,讲到如何渲染到DOM,用一种很繁琐的方式.这一节我们来看看如何使用JSX简化创建元素的过程。

    JSX提供了一些创建元素的语法糖,不用使用下面的代码:

    const element = {
      type: "div",
      props: {
        id: "container",
        children: [
          { type: "input", props: { value: "foo", type: "text" } },
          {
            type: "a",
            props: {
              href: "/bar",
              children: [{ type: "TEXT ELEMENT", props: { nodeValue: "bar" } }]
            }
          },
          {
            type: "span",
            props: {
              onClick: e => alert("Hi"),
              children: [{ type: "TEXT ELEMENT", props: { nodeValue: "click me" } }]
            }
          }
        ]
      }
    };
    

    我们现在可以这么写:

    const element = (
      <div id="container">
        <input value="foo" type="text" />
        <a href="/bar">bar</a>
        <span onClick={e => alert("Hi")}>click me</span>
      </div>
    );
    

    如果你不熟悉JSX的话,你可能怀疑上面的代码是否是合法的js--它确实不是。要让浏览器理解它,上面的代码必须使用预处理工具处理。比如babel.babel会把上面的代码转成下面这样:

    const element = createElement(
      "div",
      { id: "container" },
      createElement("input", { value: "foo", type: "text" }),
      createElement(
        "a",
        { href: "/bar" },
        "bar"
      ),
      createElement(
        "span",
        { onClick: e => alert("Hi") },
        "click me"
      )
    );
    

    支持JSX我们只要在Didact里添加一个createElement方法。其他事的交给预处理器去做。这个方法的第一个参数是元素的类型type,第二个是含有props属性的对象,剩下的参数都是子节点children。createElement方法需要创建一个对象,并把第二个参数上所有的值赋给它,把第二个参数后面的所有参数放到一个数组,并设置到children属性上,最后返回一个有type和props的对象。用代码实现很容易:

    function createElement(type, config, ...args) {
      const props = Object.assign({}, config);
      const hasChildren = args.length > 0;
      props.children = hasChildren ? [].concat(...args) : [];
      return { type, props };
    }
    

    同样,这个方法对文本元素不适用。文本的子元素是作为字符串传给createElement方法的。但是我们的Didact需要文本元素一样有type和props属性。所以我们要把不是didact元素的参数都转成一个'文本元素'

     const TEXT_ELEMENT = "TEXT ELEMENT";
    
    function createElement(type, config, ...args) {
      const props = Object.assign({}, config);
      const hasChildren = args.length > 0;
      const rawChildren = hasChildren ? [].concat(...args) : [];
      props.children = rawChildren
        .filter(c => c != null && c !== false)
        .map(c => c instanceof Object ? c : createTextElement(c));
      return { type, props };
    }
    
    function createTextElement(value) {
      return createElement(TEXT_ELEMENT, { nodeValue: value });
    }
    

    我同样从children列表里过滤了null,undefined,false参数。我们不需要把它们加到props.children上因为我们根本不会去渲染它们。

    3.2总结

    到这里我们并没有为Didact加特殊的功能.但是我们有了更好的开发体验,因为我们可以使用JSX来定义元素。我已经更新了codepen上的代码。因为codepen用babel转译JSX,所以以/** @jsx createElement */开头的注释都是为了让babel知道使用哪个函数。

    你同样可以查看github提交

    下面我们将介绍Didact用来更新dom的虚拟dom和所谓的调和算法.

    4.虚拟DOM和调和过程

    到目前为止,我们基于JSX的描述方式实现了dom元素的创建机制。这里开始,我们专注于怎么更新DOM.

    在下面介绍setState之前,我们之前更新DOM的方式只有再次调用render()方法,传入不同的元素。比如:我们要渲染一个时钟组件,代码是这样的:

       const rootDom = document.getElementById("root");
    
      function tick() {
        const time = new Date().toLocaleTimeString();
        const clockElement = <h1>{time}</h1>;
        render(clockElement, rootDom);
      }
    
      tick();
      setInterval(tick, 1000);
    

    我们现在的render方法还做不到这个。它不会为每个tick更新之前同一个的div,相反它会新添一个新的div.第一种解决办法是每一次更新,替换掉div.在render方法的最下面,我们检查父元素是否有子元素,如果有,我们就用新元素生产的dom替换它:

        function render(element, parentDom) {  
      
      // ...
      // Create dom from element
      // ...
      
      // Append or replace dom
      if (!parentDom.lastChild) {
        parentDom.appendChild(dom);     
      } else {
        parentDom.replaceChild(dom, parentDom.lastChild);    
      }
    }  
    

    在这个小列子里,这个办法很有效。但在复杂情况下,这种重复创建所有子节点的方式并不可取。所以我们需要一种方式,来对比当前和之前的元素树之间的区别。最后只更新不同的地方。

    4.1 虚拟DOM和调和过程

    React把这种diff过程称之为调和过程,我们现在也这么称呼它。首先我们要保存之前的渲染树,从而可以和新的树对比。换句话说,我们将实现自己的DOM,虚拟dom.

    这种虚拟dom的‘节点’应该是什么样的呢?首先考虑使用我们的Didact元素。它们已经有一个props.children属性,我们可以根据它来创建树。但是这依然有两个问题,一个是为了是调和过程容易些,我们必须为每个虚拟dom保存一个对真实dom的引用,并且我们更希望元素都不可变(imumutable).第二个问问题是后面我们要支持组件,组件有自己的状态(state),我们的元素还不能处理那种。

    4.2 实例(instance)

    所以我们要介绍一个新的名词:实例。实例代表的已经渲染到DOM中的元素。它其实是一个有着,element,dom,chilInstances属性的JS普通对象。childInstances是有着该元素所以子元素实例的数组。

    注意我们这里提到的实例和Dan Abramovreact组件,元素和实列这篇文章提到实例不是一个东西。他指的是React调用继承于React.component的那些类的构造函数所获得的‘公共实例’(public instances)。我们会在以后把公共实例加上。

    每一个DOM节点都有一个相应的实例。调和算法的一个目标就是尽量避免创建和删除实例。创建删除实例意味着我们在修改DOM,所以重复利用实例就是越少地修改dom树。

    4.3 重构

    我们来重写render方法,保留同样健壮的调和算法,但添加一个实例化方法来根据给定的元素生成一个实例(包括其子元素)

     let rootInstance = null;
    
    function render(element, container) {
      const prevInstance = rootInstance;
      const nextInstance = reconcile(container, prevInstance, element);
      rootInstance = nextInstance;
    }
    
    function reconcile(parentDom, instance, element) {
      if (instance == null) {
        const newInstance = instantiate(element);
        parentDom.appendChild(newInstance.dom);
        return newInstance;
      } else {
        const newInstance = instantiate(element);
        parentDom.replaceChild(newInstance.dom, instance.dom);
        return newInstance;
      }
    }
    
    function instantiate(element) {
      const { type, props } = element;
    
      // Create DOM element
      const isTextElement = type === "TEXT ELEMENT";
      const dom = isTextElement
        ? document.createTextNode("")
        : document.createElement(type);
    
      // Add event listeners
      const isListener = name => name.startsWith("on");
      Object.keys(props).filter(isListener).forEach(name => {
        const eventType = name.toLowerCase().substring(2);
        dom.addEventListener(eventType, props[name]);
      });
    
      // Set properties
      const isAttribute = name => !isListener(name) && name != "children";
      Object.keys(props).filter(isAttribute).forEach(name => {
        dom[name] = props[name];
      });
    
      // Instantiate and append children
      const childElements = props.children || [];
      const childInstances = childElements.map(instantiate);
      const childDoms = childInstances.map(childInstance => childInstance.dom);
      childDoms.forEach(childDom => dom.appendChild(childDom));
    
      const instance = { dom, element, childInstances };
      return instance;
    }
    

    这段代码和之前一样,不过我们对上一次调用render方法保存了实例,我们也把调和方法和实例化方法分开了。

    为了复用dom节点而不需要重新创建dom节点,我们需要一种更新dom属性(className,style,onClick等等)的方法。所以,我们将把目前用来设置属性的那部分代码抽出来,作为一个更新属性的更通用的方法。

    function instantiate(element) {
      const { type, props } = element;
    
      // Create DOM element
      const isTextElement = type === "TEXT ELEMENT";
      const dom = isTextElement
        ? document.createTextNode("")
        : document.createElement(type);
    
      updateDomProperties(dom, [], props);
    
      // Instantiate and append children
      const childElements = props.children || [];
      const childInstances = childElements.map(instantiate);
      const childDoms = childInstances.map(childInstance => childInstance.dom);
      childDoms.forEach(childDom => dom.appendChild(childDom));
    
      const instance = { dom, element, childInstances };
      return instance;
    }
    
    function updateDomProperties(dom, prevProps, nextProps) {
      const isEvent = name => name.startsWith("on");
      const isAttribute = name => !isEvent(name) && name != "children";
    
      // Remove event listeners
      Object.keys(prevProps).filter(isEvent).forEach(name => {
        const eventType = name.toLowerCase().substring(2);
        dom.removeEventListener(eventType, prevProps[name]);
      });
      // Remove attributes
      Object.keys(prevProps).filter(isAttribute).forEach(name => {
        dom[name] = null;
      });
    
      // Set attributes
      Object.keys(nextProps).filter(isAttribute).forEach(name => {
        dom[name] = nextProps[name];
      });
    
      // Add event listeners
      Object.keys(nextProps).filter(isEvent).forEach(name => {
        const eventType = name.toLowerCase().substring(2);
        dom.addEventListener(eventType, nextProps[name]);
      });
    }
    

    updateDomProperties 方法删除所有旧属性,然后添加上新的属性。如果属性没有变,它还是照做一遍删除添加属性。所以这个方法会做很多无谓的更新,为了简单,目前我们先这样写。

    4.4 复用dom节点

    我们说过调和算法会尽量复用dom节点.现在我们为调和(reconcile)方法添加一个校验,检查是否之前渲染的元素和现在渲染的元素有一样的类型(type),如果类型一致,我们将重用它(更新旧元素的属性来匹配新元素)

    function reconcile(parentDom, instance, element) {
      if (instance == null) {
        // Create instance
        const newInstance = instantiate(element);
        parentDom.appendChild(newInstance.dom);
        return newInstance;
      } else if (instance.element.type === element.type) {
        // Update instance
        updateDomProperties(instance.dom, instance.element.props, element.props);
        instance.element = element;
        return instance;
      } else {
        // Replace instance
        const newInstance = instantiate(element);
        parentDom.replaceChild(newInstance.dom, instance.dom);
        return newInstance;
      }
    }
    

    4.5 子元素的调和

    现在调和算法少了重要的一步,忽略了子元素。子元素调和是react的关键。它需要元素上一个额外的key属性来匹配之前和现在渲染树上的子元素.我们将实现一个该算法的简单版。这个算法只会匹配子元素数组同一位置的子元素。它的弊端就是当两次渲染时改变了子元素的排序,我们将不能复用dom节点。

    实现这个简单版,我们将匹配之前的子实例 instance.childInstances 和元素子元素 element.props.children,并一个个的递归调用调和方法(reconcile)。我们也保存所有reconcile返回的实例来更新childInstances。

    function reconcile(parentDom, instance, element) {
      if (instance == null) {
        // Create instance
        const newInstance = instantiate(element);
        parentDom.appendChild(newInstance.dom);
        return newInstance;
      } else if (instance.element.type === element.type) {
        // Update instance
        updateDomProperties(instance.dom, instance.element.props, element.props);
        instance.childInstances = reconcileChildren(instance, element);
        instance.element = element;
        return instance;
      } else {
        // Replace instance
        const newInstance = instantiate(element);
        parentDom.replaceChild(newInstance.dom, instance.dom);
        return newInstance;
      }
    }
    
    function reconcileChildren(instance, element) {
      const dom = instance.dom;
      const childInstances = instance.childInstances;
      const nextChildElements = element.props.children || [];
      const newChildInstances = [];
      const count = Math.max(childInstances.length, nextChildElements.length);
      for (let i = 0; i < count; i++) {
        const childInstance = childInstances[i];
        const childElement = nextChildElements[i];
        const newChildInstance = reconcile(dom, childInstance, childElement);
        newChildInstances.push(newChildInstance);
      }
      return newChildInstances;
    } 
    

    4.6 删除Dom节点

    如果nextChildElements数组比childInstances数组长度长,reconcileChildren将为所有子元素调用reconcile方法,并传入一个undefined实例。这没什么问题,因为我们的reconcile方法里if (instance == null)语句已经处理了并创建新的实例。但是另一种情况呢?如果childInstances数组比nextChildElements数组长呢,因为element是undefined,这将导致element.type报错。

    这是我们并没有考虑到的,如果我们是从dom中删除一个元素情况。所以,我们要做两件事,在reconcile方法中检查element == null的情况并在reconcileChildren方法里过滤下childInstances

    function reconcile(parentDom, instance, element) {
      if (instance == null) {
        // Create instance
        const newInstance = instantiate(element);
        parentDom.appendChild(newInstance.dom);
        return newInstance;
      } else if (element == null) {
        // Remove instance
        parentDom.removeChild(instance.dom);
        return null;
      } else if (instance.element.type === element.type) {
        // Update instance
        updateDomProperties(instance.dom, instance.element.props, element.props);
        instance.childInstances = reconcileChildren(instance, element);
        instance.element = element;
        return instance;
      } else {
        // Replace instance
        const newInstance = instantiate(element);
        parentDom.replaceChild(newInstance.dom, instance.dom);
        return newInstance;
      }
    }
    
    function reconcileChildren(instance, element) {
      const dom = instance.dom;
      const childInstances = instance.childInstances;
      const nextChildElements = element.props.children || [];
      const newChildInstances = [];
      const count = Math.max(childInstances.length, nextChildElements.length);
      for (let i = 0; i < count; i++) {
        const childInstance = childInstances[i];
        const childElement = nextChildElements[i];
        const newChildInstance = reconcile(dom, childInstance, childElement);
        newChildInstances.push(newChildInstance);
      }
      return newChildInstances.filter(instance => instance != null);
    }
    

    4.7 总结

    这一章我们增强了Didact使其支持更新dom.我们也通过重用dom节点避免大范围dom树的变更,使didact性能更好。另外也使管理一些dom内部的状态更方便,比如滚动位置和焦点。

    这里我更新了codepen,在每个状态改变时调用render方法,你可以在devtools里查看我们是否重建dom节点。

    demo2.gif

    因为我们是在根节点调用render方法,调和算法是作用在整个树上。下面我们将介绍组件,组件将允许我们只把调和算法作用于其子树上。

    5.组件和状态(state)

    5.1 回顾

    我们上一章的代码有几个问题:

    • 每一次变更触发整个虚拟树的调和算法
    • 状态是全局的
    • 当状态变更时,我们需要显示地调用render方法

    组件解决了这些问题,我们可以:

    • 为jsx定义我们自己的‘标签’
    • 生命周期的钩子(我们这章不讲这个)

    5.2 组件类

    首先我们要提供一个供组件继承的Component的基类。我们还需要提供一个含props参数的构造方法,一个setState方法,setState接收一个partialState参数来更新组件状态:

    class Component {
      constructor(props) {
        this.props = props;
        this.state = this.state || {};
      }
    
      setState(partialState) {
        this.state = Object.assign({}, this.state, partialState);
      }
    }
    

    我们的应用里将和其他元素类型(div或者span)一样继承这个类再这样使用:<Mycomponent/>。注意到我们的createElement方法不需要改变任何东西,createElement会把组件类作为元素的type,并正常的处理props属性。我们真正需要的是一个根据所给元素来创建组件实例(我们称之为公共实例)的方法。

    function createPublicInstance(element, internalInstance) {
      const { type, props } = element;
      const publicInstance = new type(props);
      publicInstance.__internalInstance = internalInstance;
      return publicInstance;
    }
    

    除了创建公共实例外,我们保留了对触发组件实例化的内部实例(从虚拟dom)引用,我们需要当公共实例状态发生变化时,能够只更新该实例的子树。

    class Component {
      constructor(props) {
        this.props = props;
        this.state = this.state || {};
      }
    
      setState(partialState) {
        this.state = Object.assign({}, this.state, partialState);
        updateInstance(this.__internalInstance);
      }
    }
    
    function updateInstance(internalInstance) {
      const parentDom = internalInstance.dom.parentNode;
      const element = internalInstance.element;
      reconcile(parentDom, internalInstance, element);
    }
    

    我们也需要更新实例化方法。对组件而言,我们需要创建公共实例,然后调用组件的render方法来获取之后要再次传给实例化方法的子元素:

    function instantiate(element) {
      const { type, props } = element;
      const isDomElement = typeof type === "string";
    
      if (isDomElement) {
        // Instantiate DOM element
        const isTextElement = type === TEXT_ELEMENT;
        const dom = isTextElement
          ? document.createTextNode("")
          : document.createElement(type);
    
        updateDomProperties(dom, [], props);
    
        const childElements = props.children || [];
        const childInstances = childElements.map(instantiate);
        const childDoms = childInstances.map(childInstance => childInstance.dom);
        childDoms.forEach(childDom => dom.appendChild(childDom));
    
        const instance = { dom, element, childInstances };
        return instance;
      } else {
        // Instantiate component element
        const instance = {};
        const publicInstance = createPublicInstance(element, instance);
        const childElement = publicInstance.render();
        const childInstance = instantiate(childElement);
        const dom = childInstance.dom;
    
        Object.assign(instance, { dom, element, childInstance, publicInstance });
        return instance;
      }
    }
    

    组件的内部实例和dom元素的内部实例不同,组件内部实例只能有一个子元素(从render函数返回),所以组件内部只有childInstance属性,而dom元素有childInstances数组。另外,组件内部实例需要有对公共实例的引用,这样在调和期间,才可以调用render方法。

    唯一缺失的是处理组件实例调和,所以我们将为调和算法添加一些处理。如果组件实例只能有一个子元素,我们就不需要处理子元素的调和,我们只需要更新公共实例的props属性,重新渲染子元素并调和算法它:

    function reconcile(parentDom, instance, element) {
      if (instance == null) {
        // Create instance
        const newInstance = instantiate(element);
        parentDom.appendChild(newInstance.dom);
        return newInstance;
      } else if (element == null) {
        // Remove instance
        parentDom.removeChild(instance.dom);
        return null;
      } else if (instance.element.type !== element.type) {
        // Replace instance
        const newInstance = instantiate(element);
        parentDom.replaceChild(newInstance.dom, instance.dom);
        return newInstance;
      } else if (typeof element.type === "string") {
        // Update dom instance
        updateDomProperties(instance.dom, instance.element.props, element.props);
        instance.childInstances = reconcileChildren(instance, element);
        instance.element = element;
        return instance;
      } else {
        //Update composite instance
        instance.publicInstance.props = element.props;
        const childElement = instance.publicInstance.render();
        const oldChildInstance = instance.childInstance;
        const childInstance = reconcile(parentDom, oldChildInstance, childElement);
        instance.dom = childInstance.dom;
        instance.childInstance = childInstance;
        instance.element = element;
        return instance;
      }
    }
    

    这就是全部代码了,我们现在支持组件,我更新了codepen,我们的应用代码就像下面这样:

    const stories = [
      { name: "Didact introduction", url: "http://bit.ly/2pX7HNn" },
      { name: "Rendering DOM elements ", url: "http://bit.ly/2qCOejH" },
      { name: "Element creation and JSX", url: "http://bit.ly/2qGbw8S" },
      { name: "Instances and reconciliation", url: "http://bit.ly/2q4A746" },
      { name: "Components and state", url: "http://bit.ly/2rE16nh" }
    ];
    
    class App extends Didact.Component {
      render() {
        return (
          <div>
            <h1>Didact Stories</h1>
            <ul>
              {this.props.stories.map(story => {
                return <Story name={story.name} url={story.url} />;
              })}
            </ul>
          </div>
        );
      }
    }
    
    class Story extends Didact.Component {
      constructor(props) {
        super(props);
        this.state = { likes: Math.ceil(Math.random() * 100) };
      }
      like() {
        this.setState({
          likes: this.state.likes + 1
        });
      }
      render() {
        const { name, url } = this.props;
        const { likes } = this.state;
        const likesElement = <span />;
        return (
          <li>
            <button onClick={e => this.like()}>{likes}<b>❤️</b></button>
            <a href={url}>{name}</a>
          </li>
        );
      }
    }
    
    Didact.render(<App stories={stories} />, document.getElementById("root"));
    

    使用组件使我们可以创建自己的'JSX标签',封装组件状态,并且只在子树上进行调和算法

    demo3.gif

    最后的codepen使用这个系列的所有代码。

  • 相关阅读:
    grep: Linux基础命令及用法 -- grep
    [功能集锦] 003
    [功能集锦] 002
    [mysql相关集锦] 001
    [eclipse中使用Git插件] 008
    [eclipse相关] 001
    [代码优化集锦]
    [功能集锦] 001
    [java基础] 002
    [java基础] 001
  • 原文地址:https://www.cnblogs.com/johnzhu/p/9199363.html
Copyright © 2020-2023  润新知