從零本身編寫一個React框架 【中高級前端殺手鐗級別技能】

圖片描述

想要本身實現一個 React簡易版框架,並非很是難。可是你須要先了解下面這些知識點
若是你能閱讀如下的文章,那麼會更輕鬆的閱讀本文章:

爲了下降本文難度,構建工具選擇了parcel,歡迎加入咱們的前端交流羣~ gitHub倉庫源碼地址和二維碼都會在最後放出來~css

什麼是虛擬DOM

其實就是一個個的具備固定格式的JS對象,例如:html

const obj = {
    tag:'div',
    attrs:{
        className:"test"
    },
    children:[
    tag:'span',
    attrs:{
        className:"text"
    },
    tag:'p',
    attrs:{
        className:"p"
    },
    ]
    
}

怎麼生成對應的虛擬DOM對象?

  • 先把代碼變成抽象語法樹(AST
  • 而後進行對應的處理
  • 輸出成瀏覽器能夠識別的代碼-即js對象
這一切都是基於 Babel作的 babel在線編譯測試
class App extends React.Component{
    render(){
    return <div>123</div>
    }
}

上面這段代碼 會被編譯成:前端

...
  _createClass(App, [{
    key: "render",
    value: function render() {
      return React.createElement("div", null, "123");
    }
  }]);

//省略掉一部分代碼

最核心的一段jsx代碼, return <div>123</div>被轉換成了:return React.createElement("div", null, "123");node

最重要的開始點:

咱們寫的jsx代碼,都會被轉換成React.createElement這種形式react

那咱們只要本身一個React全局對象,給它掛載這個React.createElement方法就能夠進行接下來的處理:webpack

const React = {};
React.createElement = function(tag, attrs, ...children) {
  return {
    tag,
    attrs,
    children
  };
};
export default React;

咱們定義的React.createElement方法也很簡單,只是把對應的參數集中變成一個特定格式的對象,而後返回,再接下來進行處理~。Babel的配置會幫咱們自動把jsx轉換成React.creatElement的代碼,參數都會默認幫咱們傳好~git

構建工具咱們使用零配置的 parcel ,相比 webpack來講,更容易上手,固然對於一個把 webpack玩透了的人來講,其實用什麼都同樣~
  • npm install -g parcel-bundler
  • parcel index.html便可運行項目
// .babelrc 配置
{
    "presets": ["env"],
    "plugins": [
        ["transform-react-jsx", {
            "pragma": "React.createElement"
        }]
    ]
}

處理好了jsx代碼,咱們入口開始寫起:

  • ReactDOM.render方法是咱們的入口
  • 先定義ReactDOM對象,以及它的render方法~
const ReactDom = {};
//vnode 虛擬dom,即js對象 
//container 即對應的根標籤 包裹元素
const render = function(vnode, container) {
  return container.appendChild(_render(vnode));
};
ReactDom.render = render;
思路: 先把虛擬 dom對象- js對象變成真實 dom對象,而後插入到根標籤內。

_render方法,接受虛擬dom對象,返回真實dom對象:github

若是傳入的是null,字符串或者數字 那麼直接轉換成真實dom而後返回就能夠了~web

if (vnode === undefined || vnode === null || typeof vnode === 'boolean')
    vnode = '';

  if (typeof vnode === 'number') vnode = String(vnode);

  if (typeof vnode === 'string') {
    let textNode = document.createTextNode(vnode);
    return textNode;
  }
 const dom = document.createElement(vnode.tag);
 return dom

可是有可能傳入的是個div標籤,並且它有屬性。那麼須要處理屬性,因爲這個處理屬性的函數須要大量複用,咱們單獨定義成一個函數:算法

if (vnode.attrs) {
    Object.keys(vnode.attrs).forEach(key => {
      const value = vnode.attrs[key];
      handleAttrs(dom, key, value);
    });
  }
  
  function setAttribute(dom, name, value) {
  if (name === 'className') name = 'class';
  if (/on\w+/.test(name)) {
    name = name.toLowerCase();
    dom[name] = value || '';
  } else if (name === 'style') {
    if (!value || typeof value === 'string') {
      dom.style.cssText = value || '';
    } else if (value && typeof value === 'object') {
      for (let name in value) {
        dom.style[name] =
          typeof value[name] === 'number' ? value[name] + 'px' : value[name];
      }
    }
  } else {
    if (name in dom) {
      dom[name] = value || '';
    }
    if (value) {
      dom.setAttribute(name, value);
    } else {
      dom.removeAttribute(name);
    }
  }
}

可是可能有子節點的嵌套,因而要用到遞歸:

vnode.children && vnode.children.forEach(child => render(child, dom)); 
  // 遞歸渲染子節點
上面沒有考慮到組件,只考慮到了 div或者字符串數字之類的虛擬 dom.

其實加入組件也很簡單:加入新一個新的處理方式:

咱們先定義好Component這個類,而且掛載到全局React的對象上

export class Component {
  constuctor(props = {}) {
    this.state = {};
    this.props = props;
  }
  setState(stateChange) {
    // 將修改合併到state
    console.log('setstate');
    const newState = Object.assign(this.state, stateChange);
    console.log('state:', newState);
    renderComponent(this);
  }
}
....

//掛載Component類到全局React上
React.Component = Component

若是是組件,Babel會幫咱們把第一個參數變成function

if (typeof vnode.tag === 'function') {
    //先建立組件
    const component = createComponent(vnode.tag, vnode.attrs);
    //設置屬性
    setComponentProps(component, vnode.attrs)
    //返回的是真實dom對象
    return component.base;
  }

createComponentsetComponentProps都是咱們本身定義的方法~後期大量複用

export function createComponent(component, props) {
  let inst;
  // 若是是類定義組件,則直接返回實例
  if (component.prototype && component.prototype.render) {
    inst = new component(props);
    // 若是是函數定義組件,則將其擴展爲類定義組件
  } else {
    inst = new Component(props);
    inst.constructor = component;
    inst.render = function() {
      return this.constructor(props);
    };
  }

  return inst;
}
export function setComponentProps(component, props) {
  if (!component.base) {
    if (component.componentWillMount) component.componentWillMount();
  } else if (component.base && component.componentWillReceiveProps) {
    component.componentWillReceiveProps(props);
  }

  component.props = props;

  renderComponent(component);
}

renderComponent也是咱們本身定義的方法,用來渲染組件:

export function renderComponent(component) {
  console.log('renderComponent');
  let base;

  const renderer = component.render();

  if (component.base && component.componentWillUpdate) {
    component.componentWillUpdate();
  }

  base = _render(renderer);

  if (component.base) {
    if (component.componentDidUpdate) component.componentDidUpdate();
  } else {
    component.base = base;
    component.componentDidMount && component.componentDidMount();
    if (component.base && component.base.parentNode) {
      component.base.parentNode.replaceChild(base, component.base);
    }
    return;
  }
  if (component.base && component.base.parentNode) {
    component.base.parentNode.replaceChild(base, component.base);
  }
  //base是真實dom對象
  //component.base是將本次渲染好的dom對象掛載到組件上,方便判斷是否首次掛載
  component.base = base;
  //互相飲用,方便後期的隊列處理
  base._component = component;
}

圖片描述

最簡單的版本已經完成,對應的生命簡單週期作了粗糙處理,可是沒有加入 diff算法和異步 setState,歡迎移步 gitHub點個 star

最簡單版React-無diff算法和異步state,選擇master分支

加入diff算法和shouldComponentUpdate生命週期優化:

圖片描述

沒有diff算法,更新state後是全部的節點都要更新,這樣性能損耗很是大。如今咱們開始加入Reactdiff算法

首先改造renderComponent方法

function renderComponent(component, newState = {}) {

  console.log('renderComponent');
  //真實dom對象
  let base;
  //虛擬dom對象
  const renderer = component.render();
  //component.base是爲了表示是否通過初次渲染,好進行生命週期函數調用
  if (component.base && component.componentWillUpdate) {
    component.componentWillUpdate();
  }

  if (component.base && component.shouldComponentUpdate) {
    //若是組件通過了初次渲染,是更新階段,那麼能夠根據這個生命週期判斷是否更新
    let result = true;
    result =
      component.shouldComponentUpdate &&
      component.shouldComponentUpdate((component.props = {}), newState);
    if (!result) {
      return;
    }
  }
  
  //獲得diff算法對比後的真實dom對象
  base = diffNode(component.base, renderer);

  if (component.base) {
    if (component.componentDidUpdate) component.componentDidUpdate();
  } else {
  //爲了防止死循環,調用完`didMount`函數就結束。
    component.base = base;
    base._component = component;
    component.componentDidMount && component.componentDidMount();
    return;
  }
  component.base = base;
  base._component = component;
}
注意,咱們是跟 preact同樣,將真實 dom對象和虛擬 dom對象進行對比:

分爲下面幾種diff:

  • Node節點diff
  • Component組件diff
  • 屬性diff
  • 純文本或者數字的diff...
  • 子節點的diff(這個最複雜)

純文本或者數字的diff:

純文本和數字之類的直接替換掉 dom節點的 textContent便可
diffNode(dom, vnode) {
  let out = dom;

  if (vnode === undefined || vnode === null || typeof vnode === 'boolean')
    vnode = '';

  if (typeof vnode === 'number') vnode = String(vnode);

  // diff text node
  if (typeof vnode === 'string') {
    // 若是當前的DOM就是文本節點,則直接更新內容
    if (dom && dom.nodeType === 3) {
      // nodeType: https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType
      if (dom.textContent !== vnode) {
        dom.textContent = vnode;
      }
      // 若是DOM不是文本節點,則新建一個文本節點DOM,並移除掉原來的
    } else {
      out = document.createTextNode(vnode);
      if (dom && dom.parentNode) {
        dom.parentNode.replaceChild(out, dom);
      }
    }

    return out;
  }

Component組件diff

若是不是一個類型組件直接替換掉,不然只更新屬性便可

function diffComponent(dom, vnode) {
  let c = dom && dom._component;
  let oldDom = dom;

  // 若是組件類型沒有變化,則從新set props
  if (c && c.constructor === vnode.tag) {
    setComponentProps(c, vnode.attrs);
    dom = c.base;
    // 若是組件類型變化,則移除掉原來組件,並渲染新的組件
  } else {
    if (c) {
      unmountComponent(c);
      oldDom = null;
    }

    c = createComponent(vnode.tag, vnode.attrs);

    setComponentProps(c, vnode.attrs);
    dom = c.base;

    if (oldDom && dom !== oldDom) {
      oldDom._component = null;
      removeNode(oldDom);
    }
  }

  return dom;
}

屬性的diff

export function diffAttributes(dom, vnode) {
  const old = {}; // 當前DOM的屬性
  const attrs = vnode.attrs; // 虛擬DOM的屬性

  for (let i = 0; i < dom.attributes.length; i++) {
    const attr = dom.attributes[i];
    old[attr.name] = attr.value;
  }

  // 若是原來的屬性不在新的屬性當中,則將其移除掉(屬性值設爲undefined)
  for (let name in old) {
    if (!(name in attrs)) {
      handleAttrs(dom, name, undefined);
    }
  }

  // 更新新的屬性值
  for (let name in attrs) {
    if (old[name] !== attrs[name]) {
      handleAttrs(dom, name, attrs[name]);
    }
  }
}

childrendiff

function diffChildren(dom, vchildren) {
  const domChildren = dom.childNodes;
  //沒有key值的真實dom集合
  const children = [];
  //有key值的集合 
  const keyed = {};

  if (domChildren.length > 0) {
    for (let i = 0; i < domChildren.length; i++) {
      const child = domChildren[i];
      const key = child.key;
      if (key) {
        keyed[key] = child;
      } else {
        children.push(child);
      }
    }
  }

  if (vchildren && vchildren.length > 0) {
    let min = 0;
    let childrenLen = children.length;

    for (let i = 0; i < vchildren.length; i++) {
      const vchild = vchildren[i];
      const key = vchild.key;
      let child;

      if (key) {
        if (keyed[key]) {
          child = keyed[key];
          keyed[key] = undefined;
        }
      } else if (min < childrenLen) {
        for (let j = min; j < childrenLen; j++) {
          let c = children[j];

          if (c && isSameNodeType(c, vchild)) {
            child = c;
            children[j] = undefined;

            if (j === childrenLen - 1) childrenLen--;
            if (j === min) min++;
            break;
          }
        }
      }

      child = diffNode(child, vchild);

      const f = domChildren[i];
      if (child && child !== dom && child !== f) {
        if (!f) {
          dom.appendChild(child);
        } else if (child === f.nextSibling) {
          removeNode(f);
        } else {
          dom.insertBefore(child, f);
        }
      }
    }
  }
}

childrendiff這段,確實看起來不那麼簡單,總結兩點精髓:

  • 利用key值將節點分紅兩個隊列
  • 先對比有key值的節點,而後對比相同類型的節點,而後進行dom操做

shouldComponentUpdate的對比優化:

shouldComponentUpdate(nextProps, nextState) {
    if (nextState.test > 5) {
      console.log('shouldComponentUpdate中限制了更新')
      alert('shouldComponentUpdate中限制了更新')
      return false;
    }
    return true;
  }

效果:
圖片描述

建議去倉庫看完整源碼認真斟酌:
帶diff算法版mini-React,選擇diff分支

看加入了diff算法後的效果

圖片描述

固然state更新後,只是更新了對應的節點,所謂的diff算法,就是將真實dom和虛擬dom對比後,直接dom操做。操做那些有更新的節點~ 固然也有直接對比兩個虛擬dom對象,而後打補丁上去~咱們這種方式若是作SSR同構就不行,由於咱們服務端沒dom對象這個說法,沒法運行~

這段 diff是有點硬核,可是去倉庫認真看看,本身嘗試寫寫,也是能夠啃下來的。

異步合併更新state

圖片描述

上面的版本,每次setState都會更新組件,這樣很不友好,由於有可能一個操做會帶來不少個setState,並且極可能會頻繁更新state。爲了優化性能,咱們把這些操做都放在一幀內去操做~

圖片描述
這裏咱們使用requestAnimationFrame,去執行合併操做~

首先更新setState入口,不要直接從新渲染組件:

import { _render } from '../reactDom/index';
import { enqueueSetState } from './setState';
export class Component {
  constuctor(props = {}) {
    this.state = {};
    this.props = props;
  }
  setState(stateChange) {
    // 將修改合併到state
    console.log('setstate');
    const newState = Object.assign(this.state, stateChange);
    console.log('state:', newState);
    this.newState = newState;
    enqueueSetState(newState, this);
  }
}

enqueueSetState是咱們的一個入口函數:

function enqueueSetState(stateChange, component) {
  if (setStateQueue.length === 0) {
    //清空隊列的辦法是異步執行,下面都是同步執行的一些計算
    defer(flush);
  }

  //向隊列中添加對象 key:stateChange value:component
  setStateQueue.push({
    stateChange,
    component
  });

  //若是渲染隊列中沒有這個組件 那麼添加進去
  if (!renderQueue.some(item => item === component)) {
    renderQueue.push(component);
  }
}

上面代碼的精髓:

  • 先執行同步代碼
  • 首次setState調用進入if (setStateQueue.length === 0) 的判斷
  • 異步在下一幀執行flush函數
  • 同步執行setStateQueue.push
  • 同步執行 renderQueue.push(component)
  • 最後執行defer函數

defer函數

function defer(fn) {
  //requestIdleCallback的兼容性很差,對於用戶交互頻繁屢次合併更新來講
  ,requestAnimation更有及時性高優先級,requestIdleCallback則適合處理能夠延遲渲染的任務~
  //   if (window.requestIdleCallback) {
  //     console.log('requestIdleCallback');
  //     return requestIdleCallback(fn);
  //   }
  //高優先級任務
  return requestAnimationFrame(fn);
}

思考了好久,決定仍是用requestAnimationFrame,爲了體現界面交互的及時性

flush清空隊列的函數:

function flush() {
  let item, component;
  //依次取出對象,執行
  while ((item = setStateQueue.shift())) {
    const { stateChange, component } = item;

    // 若是沒有prevState,則將當前的state做爲初始的prevState
    if (!component.prevState) {
      component.prevState = Object.assign({}, component.state);
    }

    // 若是stateChange是一個方法,也就是setState的第二種形式
    if (typeof stateChange === 'function') {
      Object.assign(
        component.state,
        stateChange(component.prevState, component.props)
      );
    } else {
      // 若是stateChange是一個對象,則直接合併到setState中
      Object.assign(component.state, stateChange);
    }

    component.prevState = component.state;
  }

  //依次取出組件,執行更新邏輯,渲染
  while ((component = renderQueue.shift())) {
    renderComponent(component);
  }
}

flush函數的精髓:

  • 抽象隊列,一個是對應的改變state和組件的隊列, 一個是須要更新的組件隊列
  • 每一幀就清空當前setState隊列的須要更新的組件,一次性合併清空

完整代碼倉庫地址,歡迎star
帶diff算法和異步state的minj-react

上面是V15版本的stack遞歸diff版本的React實現:

當咱們有100個節點須要更新的時候,咱們正在遞歸對比節點,此時用戶點擊界面須要彈框,那麼可能會形成延遲彈出窗口,根據RAID,超過100ms,用戶就會感受明顯卡頓。爲了防止出現這種狀況,咱們須要改變總體的diff策略。把遞歸的對比,改爲能夠暫停執行的循環對比,這樣若是即時咱們在對比階段,有用戶點擊須要交互的時候,咱們能夠暫停對比,處理用戶交互。

上面這段話,說的就是 stack版本和 Fiber架構的區別。

stack版本就是咱們上面的版本

Fiber版本:

思路:

  • 將對比階段分割成一個個小任務
  • 採用兩個虛擬dom對象的去diff對比方式,單鏈表結構,三根指針,return children sibling
  • 每幀完成一個小任務,而後去執行requestAnimationFrame,若是還有時間,那麼就去執行requestIdleCallback.
這個版本暫時就結束了哦~ 歡迎加入咱們的前端交流羣,還有前往 gitHub給個 star

本人蔘考:
hujiulong的博客,感謝這些大佬的無私開源

前端交流羣:
如今人數超過了100人,因此只能加我,而後拉大家進羣!!

clipboard.png

另外深圳招收跨平臺開發 Electron+React的即時通信產品前端工程師

歡迎投遞: 453089136@qq.com - Peter

招收中級和高級各一名~團隊氛圍nice 不加班

相關文章
相關標籤/搜索