react15-源碼演化-渲染原生組件(一)

渲染原生組件

  • 該文介紹前提,已經通過create-react-app初始化項目。
  • 由於React本身也是不斷演化出來的產品,因此該文的源碼跟官方並不完全一致。
  • 意義在於核心思想跟react大體一致,以便於理解分析React源碼。

1. React.createElement

1.1. src/index.js

import React from './react';
let element = React.createElement('button',
  { id: 'sayHello' },
  'say', React.createElement('span', { style: { color: 'red' } }, 'Hello')
);
console.log(element);

1.2. src/react/index.js

import { ELEMENT } from './constants';
import { ReactElement } from './vdom';

function createElement(type, config = {}, children) {
  delete config.__source;//dev環境下變量,不考慮該變量
  delete config.__self;//dev環境下變量,不考慮該變量
  let { key, ref, ...props } = config;
  let $$typeof = null;
  if (typeof type === 'string') {//span div button
    $$typeof = ELEMENT;//是一個原生的DOM類型
    /**
     * 這裏要注意,用真實React在測試時,你會發現:
     * let e1 = React.createElement('abc');
     * console.log(e1); // $$typeof 仍然是 react.element類型,type: 'abc'
     */
  } else {
    console.error('Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: number.');
  }
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {//children是一個對象或字符串
    props.children = children;
  } else if (childrenLength > 1) {//children是一個數組
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }
  return ReactElement($$typeof, type, key, ref, props);
}
const React = {
  createElement
}
export default React;

1.3. src/react/constants.js

export const TEXT = Symbol.for('TEXT');// 文本類型
export const ELEMENT = Symbol.for('ELEMENT');//React元素類型 button div span 等等

1.4. src/react/vdom.js

export function ReactElement($$typeof, type, key, ref, props) {
  let element = {
    $$typeof, type, key, ref, props
  };
  return element;
}

1.5 測試效果

  • 官方結構如下圖:
    在這裏插入圖片描述
  • 改寫結構如下圖:
    在這裏插入圖片描述
  • 當閱讀到dom-diff,設計的結構跟官方會不一樣,爲了方便dom比對

2. ReactDom.render

2.1. src/index.js

import React from './react';
import ReactDOM from './react-dom';
let element = React.createElement('button',
  { id: 'sayHello' },
  'say', React.createElement('span', { style: { color: 'red' } }, 'Hello')
);
//console.log(element);
ReactDOM.render(
  element,
  document.getElementById('root')
);

2.2. src/react/vdom.js

import { ELEMENT } from './constants';
import { setProps } from './utils';

export function createDOM(element) {
  let dom = null;
  if (element == null) { // null or undefined
    return dom; // appendChild時,如果爲null,則不掛載到parent上
  } else if (typeof element === 'object') { // 如果是對象類型
    let { $$typeof } = element;
    if (!$$typeof) { // 字符串或者數字
      dom = document.createTextNode(element);
    } else if ($$typeof == ELEMENT) { // 原生DOM節點
      dom = createNativeDOM(element);
    }
  } else { // 如果非對象類型,數字,字符串
    dom = document.createTextNode(element);
  }
  return dom;
}
/**
let element = React.createElement('button',
  { id: 'sayHello', onClick },
  'say', React.createElement('span', { onClick: spanClick, style: { color: 'red' } }, 'Hello')
);
 */
function createNativeDOM(element) {
  let {type, props} = element; // div button span
  let dom = document.createElement(type); //真實DOM對象
  //1,創建虛擬dom的子節點
  createNativeDOMChildren(dom, element.props.children);
  //2,給DOM元素添加屬性
  setProps(dom, props);
  return dom;
}
function createNativeDOMChildren(parentNode, ...children) {
  let childrenNodeArr = children && children.reduce((prev,curr) => prev.concat(curr),[]);
  if (childrenNodeArr) {
    for (let i = 0; i < childrenNodeArr.length; i++) {
      let childDOM = createDOM(childrenNodeArr[i]);
      childDOM && parentNode.appendChild(childDOM);
    }
  }
}

export function ReactElement($$typeof, type, key, ref, props) {
  let element = {
    $$typeof, type, key, ref, props
  };
  return element;
}

2.3. src/react/utils

export function setProps(dom, props) {
  for (let key in props) {
    if (key != 'children') {
      let value = props[key];
      setProp(dom, key, value);
    }
  }
}
function setProp(dom, key, value) {
  if (/^on/.test(key)) {
    // TODO 綁定事件
  } else if (key === 'style') {
    for (const styleName in value) {
      dom.style[styleName] = value[styleName];
    }
  } else {
    dom.setAttribute(key, value);
  }
}

2.4. 測試效果

  • 頁面渲染效果如下圖
    在這裏插入圖片描述

3. event事件

3.1. src/index.js

import React from './react';
import ReactDOM from './react-dom';
let onClick = (syntheticEvent) => {
  console.log('buttonClick', syntheticEvent);
}
let spanClick = (syntheticEvent) => {
  console.log('spanClick', syntheticEvent);
  // syntheticEvent.persist();
  setTimeout(() => {
    console.log('spanClick', syntheticEvent);
  }, 1000);
}
let element = React.createElement('button',
  { id: 'sayHello', onClick },
  'say', React.createElement('span', { onClick: spanClick, style: { color: 'red' } }, 'Hello')
);
// console.log(element);
ReactDOM.render(
  element,
  document.getElementById('root')
);

3.2. src/react/util.js

import { addEvent } from './event';

export function setProps(dom, props) {
  for (let key in props) {
    if (key != 'children') {
      let value = props[key];
      setProp(dom, key, value);
    }
  }
}
function setProp(dom, key, value) {
  if (/^on/.test(key)) {
    addEvent(dom, key, value);
  } else if (key === 'style') {
    for (const styleName in value) {
      dom.style[styleName] = value[styleName];
    }
  } else {
    dom.setAttribute(key, value);
  }
}

3.3. src/react/event.js

/**
 * React通過,類似於`事件委託`機制,將事件綁定到document上;
 * 並把事件回調函數,以`eventStore`的形式,掛載到對應的真實DOM上
 * @param {*} dom 要綁定事件的DOM節點
 * @param {*} eventType 事件類型 onClick
 * @param {*} listener 事件處理函數
 */
export function addEvent(dom, eventType, listener) {
  eventType = eventType.toLowerCase(); // onClick 作爲key,轉換成 onclick
  //在要綁定的DOM節點上掛載一個對象,準備存放監聽函數
  let eventStore = dom.eventStore || (dom.eventStore = {});
  //eventStore.onClick = () => {console.log('this is onClick')}
  eventStore[eventType] = listener;
  /**
   * 這裏可以做兼容處理,比如兼容IE、Chrome、Firefox等等
   */
  // true是捕獲階段,處理事件; false是冒泡階段,處理事件
  document.addEventListener(eventType.slice(2), dispatchEvent, false);
}

let syntheticEvent;//合成對象,可以複用,減少垃圾回收,提高性能
function dispatchEvent(event) {
  let { type, target } = event;//type->click target->button
  let eventType = 'on' + type; //onclick
  syntheticEvent = getSyntheticEvent(event);
  // 模擬冒泡過程
  while(target) {
    let {eventStore} = target;
    let listener = eventStore && eventStore[eventType];//onClick
    if (listener) {
      listener.call(target, syntheticEvent);
    }
    target = target.parentNode;
  }
  //所有監聽函數執行完畢,清掉所有屬性
  for (const key in syntheticEvent) {
    if (syntheticEvent.hasOwnProperty(key)) {
      delete syntheticEvent[key];
    }
  }
}
//如果執行了persist,就讓syntheticEvent指向新對象
function persist() {
  syntheticEvent = {};
  syntheticEvent.__proto__.persist = persist;
}
function getSyntheticEvent(nativeEvent){
  if (!syntheticEvent) {
    syntheticEvent = {};
    syntheticEvent.__proto__.persist = persist;
  }
  syntheticEvent.nativeEvent = nativeEvent;
  syntheticEvent.currentTarget = nativeEvent.target;
  //把原生事件對象上的方法和屬性都拷貝到合成對象上
  for (let key in nativeEvent) {
    if (typeof nativeEvent[key] === 'function') {
      syntheticEvent[key] = nativeEvent[key].bind(nativeEvent); //綁定this
    } else {
      syntheticEvent[key] = nativeEvent[key];
    }
  }
  return syntheticEvent;
}

3.4. 測試效果

3.4.1. 不調用persist(),控制檯效果如下圖
在這裏插入圖片描述

3.4.2. 調用persist()

修改src/index.js的第8行,將註釋放開

syntheticEvent.persist();

控制檯效果如下圖
在這裏插入圖片描述

3.5. 瀏覽器的捕獲冒泡

  • 首先要理解捕獲冒泡的瀏覽器事件,如下圖
    在這裏插入圖片描述

  • MouseEvent點擊事件舉例說明:

    1,目標元素是div,先走捕獲,再走冒泡

    2,捕獲事件依次從(1)走到(4)

    3,冒泡事件已從(5)走到(8)

阻止捕獲和冒泡事件event.stopPropagation();

3.6. React模擬冒泡事件

  • src/react/event.js第18行
document.addEventListener(eventType.slice(2), dispatchEvent, false);

將事件掛載到document

  • src/react/event.js第27至34行
while(target) {
  let {eventStore} = target;
  let listener = eventStore && eventStore[eventType];//onClick
  if (listener) {
    listener.call(target, syntheticEvent);
  }
  target = target.parentNode;
}

當一個元素事件調用完畢,繼續冒泡到parentNode,如果有事件繼續執行。

3.7. 程序調用過程分解

  • React模擬冒泡,執行流程如下圖:
    在這裏插入圖片描述

  • 過程分解:

  • 一,執行addEvent方法

    在執行addEvent方法時,將事件綁定到document上,並在每個DOM上添加eventStore.

    並將src/index.js中的以on開始的屬性keyvalue,比如onClick,賦值給eventStoreonclick屬性.

    這樣,每個DOM都有eventStore,如果eventStore具有onclick屬性,當觸發時,就可以執行事件回調.

  • 二,執行瀏覽器行爲

    由於document綁定監聽函數第3個參數爲false,因此瀏覽器會監聽冒泡事件.

    瀏覽器執行冒泡事件,會找到最底層元素spanspan的父類理論上來講,如果它本來就綁定了事件,那該事件也會執行,比如jquerybutton綁定了事件.

    但是react聲明的事件並不是原生事件,因此需要代碼中觸發,那麼while循環中使用target = target.parentNode;向上查找,並觸發listener回調,就模擬出了瀏覽器的冒泡行爲.

  • 三,觸發階段過程分解(圖例中的過程)

    1, spanClick 事件首先觸發

    2, spanClick 回調,將 syntheticEventA 變量賦值給 syntheticEventB 變量

    3, 調用 persist 持久化方法,調用後 syntheticEventA 指向了新的 {} 空對象

    4, 程序繼續向下執行,找到父類 button

    5, 觸發 button 的 onClick 事件

    6, button 的 onClick 回調,將 syntheticEventA 變量賦值給 syntheticEventC 變量,

    注意此時的 syntheticEventA 已經是 {} 空對象

    7, setTimeout 執行,此時 syntheticEventB 來自 spanClick 回調,並不是空對象.

3.8. 改進思路

  • 仔細觀察 3.4.2 的結果,會發現很奇怪嗎?我們並不希望 buttonClick 回調 syntheticEvent 變成空對象.
  • 改進思路: 改造src/react/event.js中的全局syntheticEvent,變成一個 Array 或 Map,利用池的思路去實現.

知識產權

  • 核心知識產權來自《珠峯架構》,轉載請註明《珠峯架構》
  • 小編對知識內容進行了拆分和優化。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章