精讀《用160行js程式碼實現一個React》

  • 2019 年 11 月 20 日
  • 筆記

現在網上有很多react原理解析這樣的文章,但是往往這樣的文章我看完過後卻沒有什麼收穫,因為行文思路太快,大部分就是寫了幾句話簡單介紹下這段程式碼是用來幹嘛的,然後就貼上源碼讓你自己看,有可能作者本人是真的看懂了,但是對於大部分閱讀這篇文章的人來說,確是雲里霧裡。

講解一個框架的源碼,最好的方式就是實現一個簡易版的,這樣在你實現的過程中,讀者就能了解到你整體的思路,也就能站在更高的層面上對框架有一個整體的認知,而不是陷在一些具體的技術細節上。

這篇文章就非常棒的實現了一個簡單的react框架,接下來屬於對原文的翻譯加上一些自己在使用過程中的理解。

首先先整體介紹通過這篇文章你能學到什麼–我們將實現一個簡單的React,包括簡單的組件級api和虛擬dom,文章也將分為以下四個部分

  • Elements:在這一章我們將學習JSX是如何被處理成虛擬DOM的
  • Rendering: 在這一小節我們將想你展示如何將虛擬dom變成真實的DOM的
  • Patching: 在這一章我們將向你展示為什麼key如此重要,並且如何利用虛擬DOM對已存在的DOM進行批量更新
  • Components :最後一小節將告訴你React組件和他的生命周期

Element

元素攜帶者很多重要的資訊,比如節點的type,props,children list,根據這些屬性,能渲染出我們需要的元素,它的樹形結構如下

{      "type": "ul",      "props": {          "className": "some-list"      },      "children": [          {              "type": "li",              "props": {                  "className": "some-list__item"              },              "children": [                  "One"              ]          },          {              "type": "li",              "props": {                  "className": "some-list__item"              },              "children": [                  "Two"              ]          }      ]  }

但是如果我們日常寫程式碼如果要寫成這個樣子,那我們應該要瘋了,所以一般我們會寫jsx的語法

/** @jsx createElement */  const list = <ul className="some-list">      <li className="some-list__item">One</li>      <li className="some-list__item">Two</li>  </ul>;

為了能夠讓他被編譯成常規的方法,我們需要加上注釋來定義用哪個函數,最終定義的函數被執行,最後會返回給一個虛擬DOM

const createElement = (type, props, ...children) => {      props = props != null ? props : {};      return {type, props, children};  };

我為什麼這個地方要加註釋呢,因為我在用babel打包jsx的語法的時候,貌似默認用的React里提供的CreateElement,所以當時我配置了.babelrc以後 發現它報了一個React is not defined錯誤,但是我安裝的是作者這個簡易的類React包,後來才知道在jsx前要加一段注釋來告訴babel編譯的時候用哪個函數

/** @jsx Gooact.createElement */

Rendering

這一節是將vdom渲染真實dom

上一節我們已經得到了根據jsx語法得出的虛擬dom樹形結構,那麼就該將這個虛擬dom結構渲染成真實dom

那麼我們在拿到一個樹形結構的時候,如何判斷這個節點應該渲染成真實dom的什麼樣子呢,這裡就會有3種情況,第一種就是直接會返回一個字元串,那我們就直接生成一個文本節點,如果返回的是一個我們自定義的組件,那麼我們就在調用這個方法,如果是一個常規的dom組件,我們就創建這樣的一個dom元素,然後接著繼續遍歷它的子節點。

setAttribute就是將我們設置在虛擬dom上的屬性設置在真實dom上

const render = (vdom, parent=null) => {      if (parent) parent.textContent = '';      const mount = parent ? (el => parent.appendChild(el)) : (el => el);      if (typeof vdom == 'string' || typeof vdom == 'number') {          return mount(document.createTextNode(vdom));      } else if (typeof vdom == 'boolean' || vdom === null) {          return mount(document.createTextNode(''));      } else if (typeof vdom == 'object' && typeof vdom.type == 'function') {          return mount(Component.render(vdom));      } else if (typeof vdom == 'object' && typeof vdom.type == 'string') {          const dom = document.createElement(vdom.type);          for (const child of [].concat(...vdom.children)) // flatten              dom.appendChild(render(child));          for (const prop in vdom.props)              setAttribute(dom, prop, vdom.props[prop]);          return mount(dom);      } else {          throw new Error(`Invalid VDOM: ${vdom}.`);      }  };    const setAttribute = (dom, key, value) => {      if (typeof value == 'function' && key.startsWith('on')) {          const eventType = key.slice(2).toLowerCase();          dom.__gooactHandlers = dom.__gooactHandlers || {};          dom.removeEventListener(eventType, dom.__gooactHandlers[eventType]);          dom.__gooactHandlers[eventType] = value;          dom.addEventListener(eventType, dom.__gooactHandlers[eventType]);      } else if (key == 'checked' || key == 'value' || key == 'id') {          dom[key] = value;      } else if (key == 'key') {          dom.__gooactKey = value;      } else if (typeof value != 'object' && typeof value != 'function') {          dom.setAttribute(key, value);      }  };

Patching

想像一個你有一個很深的結構,而且你還需要頻繁的更新你的虛擬dom,如果你改變了一些,那麼全部都要渲染,這無疑會消耗很多時間。

但是如果我們有一個演算法能夠比較出新的虛擬dom和已有dom的差異,然後只更新那些改變的地方,這個地方就是經常說的React團隊做了一些經過實踐後的約定,將本來o(n)^3的時間複雜度降低到了o(n),主要就是下面兩種主要的約定

  • 兩個元素如果有不同的類型那麼就會產生兩種不同的樹
  • 當我們給了一個key屬性後,他就會根據它去判斷
const patch = (dom, vdom, parent=dom.parentNode) => {      const replace = parent ? el => (parent.replaceChild(el, dom) && el) : (el => el);      if (typeof vdom == 'object' && typeof vdom.type == 'function') {          return Component.patch(dom, vdom, parent);      } else if (typeof vdom != 'object' && dom instanceof Text) {          return dom.textContent != vdom ? replace(render(vdom)) : dom;      } else if (typeof vdom == 'object' && dom instanceof Text) {          return replace(render(vdom));      } else if (typeof vdom == 'object' && dom.nodeName != vdom.type.toUpperCase()) {          return replace(render(vdom));      } else if (typeof vdom == 'object' && dom.nodeName == vdom.type.toUpperCase()) {          const pool = {};          const active = document.activeElement;          for (const index in Array.from(dom.childNodes)) {              const child = dom.childNodes[index];              const key = child.__gooactKey || index;              pool[key] = child;          }          const vchildren = [].concat(...vdom.children); // flatten          for (const index in vchildren) {              const child = vchildren[index];              const key = child.props && child.props.key || index;              dom.appendChild(pool[key] ? patch(pool[key], child) : render(child));              delete pool[key];          }          for (const key in pool) {              if (pool[key].__gooactInstance)                  pool[key].__gooactInstance.componentWillUnmount();              pool[key].remove();          }          for (const attr of dom.attributes) dom.removeAttribute(attr.name);          for (const prop in vdom.props) setAttribute(dom, prop, vdom.props[prop]);          active.focus();          return dom;      }  };

Component

組件是最像js中函數的概念了,我們通過它能夠展示出什麼應該展示在螢幕上,它可以被定義成一個無狀態的函數,或者是一個有生命周期的組件。

class Component {      constructor(props) {          this.props = props || {};          this.state = null;      }        static render(vdom, parent=null) {          const props = Object.assign({}, vdom.props, {children: vdom.children});          if (Component.isPrototypeOf(vdom.type)) {              const instance = new (vdom.type)(props);              instance.componentWillMount();              instance.base = render(instance.render(), parent);              instance.base.__gooactInstance = instance;              instance.base.__gooactKey = vdom.props.key;              instance.componentDidMount();              return instance.base;          } else {              return render(vdom.type(props), parent);          }      }        static patch(dom, vdom, parent=dom.parentNode) {          const props = Object.assign({}, vdom.props, {children: vdom.children});          if (dom.__gooactInstance && dom.__gooactInstance.constructor == vdom.type) {              dom.__gooactInstance.componentWillReceiveProps(props);              dom.__gooactInstance.props = props;              return patch(dom, dom.__gooactInstance.render());          } else if (Component.isPrototypeOf(vdom.type)) {              const ndom = Component.render(vdom);              return parent ? (parent.replaceChild(ndom, dom) && ndom) : (ndom);          } else if (!Component.isPrototypeOf(vdom.type)) {              return patch(dom, vdom.type(props));          }      }        setState(nextState) {          if (this.base && this.shouldComponentUpdate(this.props, nextState)) {              const prevState = this.state;              this.componentWillUpdate(this.props, nextState);              this.state = nextState;              patch(this.base, this.render());              this.componentDidUpdate(this.props, prevState);          } else {              this.state = nextState;          }      }        shouldComponentUpdate(nextProps, nextState) {          return nextProps != this.props || nextState != this.state;      }        componentWillReceiveProps(nextProps) {          return undefined;      }        componentWillUpdate(nextProps, nextState) {          return undefined;      }        componentDidUpdate(prevProps, prevState) {          return undefined;      }        componentWillMount() {          return undefined;      }        componentDidMount() {          return undefined;      }        componentWillUnmount() {          return undefined;      }  }

本次文章中新開發的gooact輪子就結束了,讓我們看看他有什麼功能

  • 它能夠高效的更新複雜的dom結構
  • 支援函數式和狀態式兩種組件

那它距離一個完整的React應用還差什麼呢?

  • 他還不支援fragments,portals這樣的新版本的特性
  • 因為React Fiber太複雜了,目前還沒有支援
  • 如果你寫了重複的key,可能會有bug
  • 對於一些方法,還少了一些回調函數 但是這篇文章是不是給你帶來一個全新的視角看React框架,讓你對這個框架做的事情有了一個全局的了解呢? 反正筆者看了原文對React框架思路又更加清晰了,最後獻上使用這個框架的用例demo