創(chuàng)建你自己的React

1.(Didact)一個DIY教程:創(chuàng)建你自己的react

[更新]這個系列從老的react架構(gòu)寫起,你可以跳過前面,直接看使用新的fiber架構(gòu)重寫的:文章

[更新2]聽Dan的沒錯,我是認真的?

這篇深入fiber架構(gòu)的文章真的很棒。
—?@dan_abramov

1.1 引言

很久以前,當學數(shù)據(jù)結(jié)構(gòu)和算法時,我有個作業(yè)就是實現(xiàn)自己的數(shù)組,鏈表,隊列,和棧(用Modula-2語言)。那之后,我再也沒有過要自己來實現(xiàn)鏈表的需求。總會有庫讓我不需要自己重造輪子。

所以,那個作業(yè)還有意義嗎?當然,我從中學到了很多,知道如何合理使用各種數(shù)據(jù)結(jié)構(gòu),并知道根據(jù)場景合理選用它們。

這個系列文章以及對應的(倉庫)的目的也是一樣,不過要實現(xiàn)的是一個,我們比鏈表使用更多的東西:React

我好奇如果不考慮性能和設備兼容性,POSIX(可移植操作系統(tǒng)接口)核心可以實現(xiàn)得多么小而簡單。
—?@ID_AA_Carmack

??????我對react也這么好奇

幸運的是,如果不考慮性能,調(diào)試,平臺兼容性等等,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的調(diào)和過程(這里只講根據(jù)子節(jié)點原順序的調(diào)和)
  • 其他渲染引擎 (只支持DOM)
  • 舊瀏覽器支持

你可以從react實現(xiàn)筆記Paul O’Shannessy的這個youtube演講視頻,或者react倉庫地址,找到更多關于如何實現(xiàn)react的細節(jié).

2.渲染dom元素

2.1 什么是DOM

開始之前,讓我們回想一下,我們經(jīng)常使用的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);

注意到我們設置元素的屬性而不是特性屬性和特性的區(qū)別,只有合法的屬性才可以設置。

2.2 Didact元素

我們用js對象來描述渲染過程,這些js對象我們稱之為Didact元素.這些元素有2個屬性,type和props。type可以是一個字符串或者方法。在后面講到組件之前,我們先用字符串。props是一個可以為空的對象(不過不能為null)。props可能有children屬性,這個children屬性是一個Didact元素的數(shù)組。

我們將多次使用Didact元素,目前我們先稱之為元素。不要和html元素混淆,在變量命名的時候,我們稱它們?yōu)镈OM元素或者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,創(chuàng)建元素就和創(chuàng)建js對象一樣.Didatc我們也這么做,不過在后面章節(jié)我們再加上create元素的代碼

2.3 渲染dom元素

下一步是渲染一個元素以及它的children到dom里。我們將寫一個render方法(對應于react的ReactDOM.render),它接受一個元素和一個dom 容器。然后根據(jù)元素的定義生成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);
}

我們?nèi)匀粵]有對其添加屬性和事件監(jiān)聽。現(xiàn)在讓我們使用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文本節(jié)點

現(xiàn)在render函數(shù)不支持的就是文本節(jié)點,首先我們定義文本元素什么樣子,比如,在react中描述<span>Foo</span>:

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

注意到子節(jié)點,只是一個字符串,并不是其他元素對象。這就讓我們的Didact元素定義不合適了:children元素應該是一個數(shù)組,數(shù)組里的元素都有type和props屬性。如果我們遵守這個規(guī)則,后面將減少不必要的if判斷.所以,Didact文本元素應該有一個“TEXT ELEMENT”的類型,并且有在對應的節(jié)點有文本的值。比如:

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

現(xiàn)在我們來定義文本元素應該如何渲染。不同的是,文本元素不使用createElement方法,而用createTextNode代替。節(jié)點值就和其他屬性一樣被設置上去。

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 總結(jié)

我們現(xiàn)在創(chuàng)建了一個可以渲染元素以及子元素的render方法。后面我們需要實現(xiàn)如何創(chuàng)建元素。我們將在下節(jié)講到如何使JSX和Didact很好地融合。

3.JSX和創(chuàng)建元素

3.1 JSX

我們之前講到了Didact元素,講到如何渲染到DOM,用一種很繁瑣的方式.這一節(jié)我們來看看如何使用JSX簡化創(chuàng)建元素的過程。

JSX提供了一些創(chuàng)建元素的語法糖,不用使用下面的代碼:

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" } }]
        }
      }
    ]
  }
};

我們現(xiàn)在可以這么寫:

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會把上面的代碼轉(zhuǎn)成下面這樣:

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方法。其他事的交給預處理器去做。這個方法的第一個參數(shù)是元素的類型type,第二個是含有props屬性的對象,剩下的參數(shù)都是子節(jié)點children。createElement方法需要創(chuàng)建一個對象,并把第二個參數(shù)上所有的值賦給它,把第二個參數(shù)后面的所有參數(shù)放到一個數(shù)組,并設置到children屬性上,最后返回一個有type和props的對象。用代碼實現(xiàn)很容易:

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元素的參數(shù)都轉(zhuǎn)成一個'文本元素'

 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參數(shù)。我們不需要把它們加到props.children上因為我們根本不會去渲染它們。

3.2總結(jié)

到這里我們并沒有為Didact加特殊的功能.但是我們有了更好的開發(fā)體驗,因為我們可以使用JSX來定義元素。我已經(jīng)更新了codepen上的代碼。因為codepen用babel轉(zhuǎn)譯JSX,所以以/** @jsx createElement */開頭的注釋都是為了讓babel知道使用哪個函數(shù)。

你同樣可以查看github提交

下面我們將介紹Didact用來更新dom的虛擬dom和所謂的調(diào)和算法.

4.虛擬DOM和調(diào)和過程

到目前為止,我們基于JSX的描述方式實現(xiàn)了dom元素的創(chuàng)建機制。這里開始,我們專注于怎么更新DOM.

在下面介紹setState之前,我們之前更新DOM的方式只有再次調(diào)用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);

我們現(xiàn)在的render方法還做不到這個。它不會為每個tick更新之前同一個的div,相反它會新添一個新的div.第一種解決辦法是每一次更新,替換掉div.在render方法的最下面,我們檢查父元素是否有子元素,如果有,我們就用新元素生產(chǎn)的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);    
 }
}  

在這個小列子里,這個辦法很有效。但在復雜情況下,這種重復創(chuàng)建所有子節(jié)點的方式并不可取。所以我們需要一種方式,來對比當前和之前的元素樹之間的區(qū)別。最后只更新不同的地方。

4.1 虛擬DOM和調(diào)和過程

React把這種diff過程稱之為調(diào)和過程,我們現(xiàn)在也這么稱呼它。首先我們要保存之前的渲染樹,從而可以和新的樹對比。換句話說,我們將實現(xiàn)自己的DOM,虛擬dom.

這種虛擬dom的‘節(jié)點’應該是什么樣的呢?首先考慮使用我們的Didact元素。它們已經(jīng)有一個props.children屬性,我們可以根據(jù)它來創(chuàng)建樹。但是這依然有兩個問題,一個是為了是調(diào)和過程容易些,我們必須為每個虛擬dom保存一個對真實dom的引用,并且我們更希望元素都不可變(imumutable).第二個問問題是后面我們要支持組件,組件有自己的狀態(tài)(state),我們的元素還不能處理那種。

4.2 實例(instance)

所以我們要介紹一個新的名詞:實例。實例代表的已經(jīng)渲染到DOM中的元素。它其實是一個有著,element,dom,chilInstances屬性的JS普通對象。childInstances是有著該元素所以子元素實例的數(shù)組。

注意我們這里提到的實例和Dan Abramovreact組件,元素和實列這篇文章提到實例不是一個東西。他指的是React調(diào)用繼承于React.component的那些類的構(gòu)造函數(shù)所獲得的‘公共實例’(public instances)。我們會在以后把公共實例加上。

每一個DOM節(jié)點都有一個相應的實例。調(diào)和算法的一個目標就是盡量避免創(chuàng)建和刪除實例。創(chuàng)建刪除實例意味著我們在修改DOM,所以重復利用實例就是越少地修改dom樹。

4.3 重構(gòu)

我們來重寫render方法,保留同樣健壯的調(diào)和算法,但添加一個實例化方法來根據(jù)給定的元素生成一個實例(包括其子元素)

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;
}

這段代碼和之前一樣,不過我們對上一次調(diào)用render方法保存了實例,我們也把調(diào)和方法和實例化方法分開了。

為了復用dom節(jié)點而不需要重新創(chuàng)建dom節(jié)點,我們需要一種更新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節(jié)點

我們說過調(diào)和算法會盡量復用dom節(jié)點.現(xiàn)在我們?yōu)檎{(diào)和(reconcile)方法添加一個校驗,檢查是否之前渲染的元素和現(xiàn)在渲染的元素有一樣的類型(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 子元素的調(diào)和

現(xiàn)在調(diào)和算法少了重要的一步,忽略了子元素。子元素調(diào)和是react的關鍵。它需要元素上一個額外的key屬性來匹配之前和現(xiàn)在渲染樹上的子元素.我們將實現(xiàn)一個該算法的簡單版。這個算法只會匹配子元素數(shù)組同一位置的子元素。它的弊端就是當兩次渲染時改變了子元素的排序,我們將不能復用dom節(jié)點。

實現(xiàn)這個簡單版,我們將匹配之前的子實例 instance.childInstances 和元素子元素 element.props.children,并一個個的遞歸調(diào)用調(diào)和方法(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節(jié)點

如果nextChildElements數(shù)組比childInstances數(shù)組長度長,reconcileChildren將為所有子元素調(diào)用reconcile方法,并傳入一個undefined實例。這沒什么問題,因為我們的reconcile方法里if (instance == null)語句已經(jīng)處理了并創(chuàng)建新的實例。但是另一種情況呢?如果childInstances數(shù)組比nextChildElements數(shù)組長呢,因為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 總結(jié)

這一章我們增強了Didact使其支持更新dom.我們也通過重用dom節(jié)點避免大范圍dom樹的變更,使didact性能更好。另外也使管理一些dom內(nèi)部的狀態(tài)更方便,比如滾動位置和焦點。

這里我更新了codepen,在每個狀態(tài)改變時調(diào)用render方法,你可以在devtools里查看我們是否重建dom節(jié)點。

demo2.gif

因為我們是在根節(jié)點調(diào)用render方法,調(diào)和算法是作用在整個樹上。下面我們將介紹組件,組件將允許我們只把調(diào)和算法作用于其子樹上。

5.組件和狀態(tài)(state)

5.1 回顧

我們上一章的代碼有幾個問題:

  • 每一次變更觸發(fā)整個虛擬樹的調(diào)和算法
  • 狀態(tài)是全局的
  • 當狀態(tài)變更時,我們需要顯示地調(diào)用render方法

組件解決了這些問題,我們可以:

  • 為jsx定義我們自己的‘標簽’
  • 生命周期的鉤子(我們這章不講這個)

5.2 組件類

首先我們要提供一個供組件繼承的Component的基類。我們還需要提供一個含props參數(shù)的構(gòu)造方法,一個setState方法,setState接收一個partialState參數(shù)來更新組件狀態(tài):

class Component {
  constructor(props) {
    this.props = props;
    this.state = this.state || {};
  }

  setState(partialState) {
    this.state = Object.assign({}, this.state, partialState);
  }
}

我們的應用里將和其他元素類型(div或者span)一樣繼承這個類再這樣使用:<Mycomponent/>。注意到我們的createElement方法不需要改變?nèi)魏螙|西,createElement會把組件類作為元素的type,并正常的處理props屬性。我們真正需要的是一個根據(jù)所給元素來創(chuàng)建組件實例(我們稱之為公共實例)的方法。

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

除了創(chuàng)建公共實例外,我們保留了對觸發(fā)組件實例化的內(nèi)部實例(從虛擬dom)引用,我們需要當公共實例狀態(tài)發(fā)生變化時,能夠只更新該實例的子樹。

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);
}

我們也需要更新實例化方法。對組件而言,我們需要創(chuàng)建公共實例,然后調(diào)用組件的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;
  }
}

組件的內(nèi)部實例和dom元素的內(nèi)部實例不同,組件內(nèi)部實例只能有一個子元素(從render函數(shù)返回),所以組件內(nèi)部只有childInstance屬性,而dom元素有childInstances數(shù)組。另外,組件內(nèi)部實例需要有對公共實例的引用,這樣在調(diào)和期間,才可以調(diào)用render方法。

唯一缺失的是處理組件實例調(diào)和,所以我們將為調(diào)和算法添加一些處理。如果組件實例只能有一個子元素,我們就不需要處理子元素的調(diào)和,我們只需要更新公共實例的props屬性,重新渲染子元素并調(diào)和算法它:

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;
  }
}

這就是全部代碼了,我們現(xiàn)在支持組件,我更新了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"));

使用組件使我們可以創(chuàng)建自己的'JSX標簽',封裝組件狀態(tài),并且只在子樹上進行調(diào)和算法

demo3.gif

最后的codepen使用這個系列的所有代碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 原教程內(nèi)容詳見精益 React 學習指南,這只是我在學習過程中的一些閱讀筆記,個人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,953評論 1 18
  • 以下內(nèi)容是我在學習和研究React時,對React的特性、重點和注意事項的提取、精練和總結(jié),可以做為React特性...
    科研者閱讀 8,423評論 2 21
  • 本筆記基于React官方文檔,當前React版本號為15.4.0。 1. 安裝 1.1 嘗試 開始之前可以先去co...
    Awey閱讀 7,934評論 14 128
  • 說在前面 關于 react 的總結(jié)過去半年就一直碎碎念著要搞起來,各(wo)種(tai)原(lan)因(le)。心...
    陳嘻嘻啊閱讀 7,038評論 7 41
  • 一個好朋友買了一套房子,讓我?guī)退鲈O計,說是讓幫忙隨便懟懟,但是既然答應做了,就不能隨便懟,還開始找了一些意向圖給...
    楊pure閱讀 114評論 0 0

友情鏈接更多精彩內(nèi)容