渲染原生組件
- 該文介紹前提,已經通過
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
開始的屬性key
和value
,比如onClick
,賦值給eventStore
的onclick
屬性.
這樣,每個DOM都有eventStore
,如果eventStore
具有onclick
屬性,當觸發時,就可以執行事件回調. -
二,執行瀏覽器行爲
由於document
綁定監聽函數第3個參數爲false
,因此瀏覽器會監聽冒泡事件.
瀏覽器執行冒泡事件,會找到最底層元素span
,span
的父類理論上來講,如果它本來就綁定了事件,那該事件也會執行,比如jquery
給button
綁定了事件.
但是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,利用池的思路去實現.
知識產權
- 核心知識產權來自《珠峯架構》,轉載請註明《珠峯架構》
- 小編對知識內容進行了拆分和優化。