渲染原生组件

  • 该文介绍前提,已经通过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
3
4
5
6

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

1.3. src/react/constants.js

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

1.4. src/react/vdom.js

export function ReactElement($$typeof, type, key, ref, props) {
  let element = {
    $$typeof, type, key, ref, props
  };
  return element;
}
1
2
3
4
5
6

1.5 测试效果

  • 官方结构如下图: ./images/element.png
  • 改写结构如下图: ./images/element-1.png
  • 当阅读到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')
);
1
2
3
4
5
6
7
8
9
10
11

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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

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);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

2.4. 测试效果

  • 页面渲染效果如下图: ./images/dom-render.png

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')
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

3.4. 测试效果

3.4.1. 不调用persist(),控制台效果如下图: ./images/none-persist.png 3.4.2. 调用persist(),控制台效果如下图: 修改src/index.js的第8行,将注释放开

syntheticEvent.persist();
1

控制台效果如下图: ./images/persist.png

3.5. 浏览器的捕获冒泡

  • 首先要理解捕获冒泡的浏览器事件,如下图: ./images/window-event.png
  • 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);
1

将事件挂载到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;
}
1
2
3
4
5
6
7
8

当一个元素事件调用完毕,继续冒泡到parentNode,如果有事件继续执行。

3.7. 程序调用过程分解

  • React模拟冒泡,执行流程如下图: ./images/run-progress.jpeg
  • 过程分解:
  • 一,执行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,利用池的思路去实现.
Last Updated: 4/27/2020, 9:58:13 PM