创建你自己的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使用这个系列的所有代码。

原文地址:https://www.cnblogs.com/johnzhu/p/9199363.html