之前自学了一阵子React
源码(文章),感觉自己对Component
和setState
,所以这里决定写一个React-Like
项目加深一下对React
的理解
项目使用了
transform-react-jsx
来进行JSX
和JS
的转换
既然是写React
,那就先定义一下React
基本结构
// src/react/index.js
import Component from './Component.js'
import createElement from './CreateReactElement.js'
const React = {
Component,
createElement
}
export default React;
其中Component
为基本组件作为父类,createElement
来创建组件
// src/react/Component.js
import { enqueueSetState } from './StateQueue'
class Component {
constructor(props){
this.isComponent = true // 是否为组件
this.isReplace = false // 是否是更新的组件
this.props = props
this.state = {}
}
setState(partialState){
enqueueSetState(partialState, this)
}
}
export default Component;
这里进行了一些基本的初始化, 还定义了setState
方法,其中调用了enqueueSetState
(后话)进行组件更新
// src/react/CreateReactElement.js
function createElement(tag, attrs, children){
var props = {}
var attrs = attrs || {}
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
return {
tag,
attrs,
props,
key: attrs.key || null
}
}
export default createElement;
这里同样进行一些初始化操作,但是对传进来的children
进行了特殊的处理,利用arguments
获得children
长度,之后决定是转化成数组还是直接写到porps
上去,最后将所有属性作为对象返回,当用户创建React
对象时会自动调用这个函数
同样我们先定义一个ReactDom
对象
// src/react-dom/index.js
import render from './Render'
const ReactDOM = {
render: ( nextElement, container ) => {
return render( nextElement, container );
}
}
export default ReactDOM;
在这里定义了一个大名鼎鼎的render
函数,传入两个参数分别为当前的元素和要插入的容器,然后调用Render
文件中的render
方法
// src/react-dom/Render.js
import {
createComponent,
setComponentProps
} from './Diff'
import setAttribute from './Dom'
/**入口render方法
* @param {ReactElement} nextElement 要插入到DOM中的组件
* @param {DOMElement} container 要插入到的容器
*/
export function render(nextElement, container){
if(nextElement == null || container == null) return;
if(nextElement.isComponent){
const component = nextElement;
if (component._container) {
if (component.componentWillUpdate){
component.componentWillUpdate();
} else if (component.componentWillMount) {
component.componentWillMount();
}
}
component._container = container;
nextElement = component.render()
}
const type = typeof nextElement
if(type === 'string' || type === 'number'){
let textNode = document.createTextNode(nextElement);
return container.appendChild(textNode);
}
if(typeof nextElement.tag === 'function'){
let component = createComponent(nextElement.tag, nextElement.attrs)
setComponentProps(component,nextElement.attrs, container)
return render(component.base, container);
}
const dom = document.createElement(nextElement.tag)
if(nextElement.attrs){
Object.keys(nextElement.attrs).map(key => {
setAttribute(key, nextElement.attrs[key], dom)
})
}
if(nextElement.props){
if(typeof nextElement.props.children == 'object'){
nextElement.props.children.forEach(item => {
render(item, dom)
})
}else{
render(nextElement.props.children, dom)
}
}
if(nextElement._component){
if(nextElement._component.isReplace){
var arr = Array.from(nextElement._component.parentNode.childNodes)
arr.map((item,index) => {
if(isSameDom(item,dom)){
return container.replaceChild(dom, nextElement._component.parentNode.children[index])
}
})
}
}
return container.appendChild(dom)
}
function isSameDom(item, dom){
return (item.nodeName == dom.nodeName && item.nodeType == dom.nodeType && item.nextSibling == dom.nextSibling)
}
export default render;
代码比较长,我们这里分段分析一下
const type = typeof nextElement
if(type === 'string' || type === 'number'){
let textNode = document.createTextNode(nextElement);
return container.appendChild(textNode);
}
如果元素类型为string
或number
则直接创建TextNode
并直接append
到container
中里
if(typeof nextElement.tag === 'function'){
let component = createComponent(nextElement.tag, nextElement.attrs)
setComponentProps(component,nextElement.attrs, container)
return render(component.base, container);
}
如果元素的tag
类型为function
即为React
组件,则调用Diff
中的方法来创建组件(后话)
const dom = document.createElement(nextElement.tag)
if(nextElement.attrs){
Object.keys(nextElement.attrs).map(key => {
setAttribute(key, nextElement.attrs[key], dom)
})
}
如果都不是的话即为普通元素,则直接调用document.createElement
创建Dom
,之后遍历attrs
调用setAttribute
来设置属性,Object.keys
将对象转化成数组方便遍历,接下来我们看一下setAttribute
方法
function setAttribute(key, value, dom){
if(key === 'className'){
key = 'class'
}
if(typeof value === 'function'){
dom[key.toLowerCase()] = value || '';
}else if(key === 'style'){
if(typeof value === 'string'){
dom.style.cssText = value || '';
}else if(typeof value === 'object'){
for (let name in value) {
dom.style[name] = typeof value[name] === 'number' ? value[name] + 'px' : value[name];
}
}
}else{
if(value){
dom.setAttribute(key, value);
}else{
dom.removeAttribute(key, value);
}
}
}
export default setAttribute;
- 先将
className
转化为class
- 若绑定的类型为
function
则转化成小写后写入dom
属性 - 若
key
为style
,则分类讨论,若属性为string
则写入cssText
,若为object
则判断其是否为number
,若是则自动在后面添加px
,然后写入style
- 若为其他则直接调用原生
setAttribute
方法 - 若属性值为空则在
dom
上删除该属性
if(nextElement.props){
if(typeof nextElement.props.children == 'object'){
nextElement.props.children.forEach(item => {
render(item, dom)
})
}else{
render(nextElement.props.children, dom)
}
}
顺着render
往下看,这里遍历元素的子元素递归渲染
if(nextElement._component){
if(nextElement._component.isReplace){
var arr = Array.from(nextElement._component.parentNode.childNodes)
arr.map((item,index) => {
if(isSameDom(item,dom)){
return container.replaceChild(dom, nextElement._component.parentNode.children[index])
}
})
}
}
return container.appendChild(dom)
最后判断两次render
的组件是否为同一个,若为同一个则调用replaceChild
方法进行替换,否则appendChild
到容器中
回到上面nextElement.tag === 'function'
中,其中有两个函数createComponent
和setComponentProps
// src/react-dom/Diff.js
export function createComponent(component, props){
let instance;
if(component.prototype && component.prototype.render){
instance = new component(props)
}else{
instance = new component(props)
instance.constructor = component
instance.render = function() {
return this.constructor(props)
}
}
return instance;
}
第一个if
判断是不是class
创建的组件,若是则直接new
一个,若不是则为函数返回组件,调整一下constructor
以及render
方法,然后将新组件返回
// src/react-dom/Diff.js
export function setComponentProps(component, props, container){
if (!component.base){
if (component.componentWillMount)
component.componentWillMount();
}else if(component.componentWillReceiveProps){
component.componentWillReceiveProps(props);
}
component.props = props;
component.parentNode = container
renderComponent(component, container)
}
首先判断组件的base
是否存在,若存在则判断是否为初次挂载,否则判断是否为接受新的props
,然后将props
即render
中的attrs
和container
作为成员添加到component
上,parentNode
用来定位父元素方便更新,然后调用renderComponent
进行组件挂载或者更新
// src/react-dom/Diff.js
export function renderComponent(component, container){
let base;
if ( component.base && component.componentWillUpdate ) {
component.componentWillUpdate();
}
base = component.render()
if (component.base) {
if (component.componentDidUpdate){
component.componentDidUpdate();
}
}else if(component.componentDidMount) {
component.componentDidMount();
}
component.base = base;
base._component = component;
if(!container){
component.isReplace = true
render(base, component.parentNode)
}
}
base
为createComponent
中的component
渲染后结果,然后进行一下简单的生命周期判断,最后判断container
是否为空,若为空则为更新组件,把component.parentNode
作为container
传回render
在文章开始提到过,Component
中的setState
方法调用了enqueueSetState
// src/react/StateQueue.js
const batchingUpdates = [] // 需要更新的状态
const dirtyComponent = [] // 需要更新的组件
var isbatchingUpdates = false // 是否处于更新状态
function callbackQueue(fn){
return Promise.resolve().then(fn);
}
export function enqueueSetState(partialState, component){
if(!isbatchingUpdates){
callbackQueue(flushBatchedUpdates)
}
isbatchingUpdates = true
batchingUpdates.push({
partialState,
component
})
if(!dirtyComponent.some(item => item === component)){
dirtyComponent.push(component)
}
}
isbatchingUpdates
判断事务是否处于更新状态(初始值为false
),若不为更新则调用callbackQueue
来执行flushBatchedUpdates
函数来更新组件,然后设置更新状态为true
,将当前状态和组件添加到batchingUpdates
中,最后判断dirtyComponent
中是否有当前组件,若无则添加进去
callbackQueue
使用了Promise
来达到延时模拟setState
的功能
// src/react/StateQueue.js
function flushBatchedUpdates(){
let queueItem, componentItem;
while(queueItem = batchingUpdates.shift()){
const { partialState, component } = queueItem;
if(!component.prevState){
component.prevState = Object.assign({}, partialState)
}
if(typeof partialState == 'function'){
Object.assign(component.state, partialState(component.prevState, component.props))
}else{
Object.assign(component.state, partialState)
}
component.prevState = component.state
}
while(componentItem = dirtyComponent.shift()){
renderComponent(componentItem)
}
isbatchingUpdates = false
}
遍历batchingUpdates
数组排头(shift
自查),获取其中组件和状态,判断组件的前一个状态,若无之前的状态,则将空对象和当前状态合并设为该组件的初始状态,若前一状态为function
,则调用该函数并将返回值和之前状态合并,若不为函数则直接合并,然后设置组件的上一状态为其之前的状态,最后遍历dirtyComponent
更新组件,完成后设置isbatchingUpdates
为false
入口在为元素设置属性的setAttribute中
// 若绑定的类型为function则挂载到事件委托上
if(typeof value === 'function'){
setFuncBus(key, value, dom);
}
我们看一下setFuncBus这个函数
/**
* @msg: 事件代理函数
* @param {string} key 属性的key
* @param {any} value 属性的值
* @param {dom} dom 被设置属性的元素
* @return: null
*/
function setFuncBus(key, value, dom) {
let funcKey = key.toLowerCase();
let domKey = dom.key;
if(document.eventBus[funcKey]) {
document.eventBus[funcKey][domKey] = value || '';
} else {
document.eventBus[funcKey] = {};
document.eventBus[funcKey][domKey] = value || '';
addWindowEventListener(funcKey);
}
}
对于key, 我简单的在在render函数中生成dom时为其赋值了key,其为一个8位随机数(随便啦
eventBus是一个普通对象。先将函数名转为小写,然后判断eventBus中是否已经委托了该函数,若没有则初始化后赋值,这里简单的以key作为触发元素的唯一标识,最后将funcKey添加到全局的事件监听中。
/**
* @msg: 添加全局事件委托
* @param {string} funcKey 委托事件名
* @return: null
*/
export function addWindowEventListener(funcKey) {
let listenName = funcKey.replace('on', '');
funcKey = funcKey.toLowerCase();
// 根据eventbus避免全局事件重复注册
(!document.eventBus[funcKey] || Object.keys(document.eventBus[funcKey]).length < 2 ) ?
window.addEventListener(listenName, function(e){
// 判断当前元素是否为被委托事件
let func = document.eventBus[funcKey][e.target.key];
// 如果当前元素被委托则执行
if(func) {
func();
} else {
// 向上冒泡寻找是否有适合条件的委托函数
// e.path为层级数组,索引从低到高为 子---->父
e.path.forEach(item => {
document.eventBus[funcKey][item.key] ?
document.eventBus[funcKey][item.key]() : '';
});
}
}) : null;
}
基本的事件委托到这里结束
代码请移步GitHub仓库