├── .babelrc ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── test-img │ └── Mini-React测试图.PNG └── src ├── index.js ├── react-dom ├── diff.jsx ├── dom.jsx ├── index.jsx └── render.jsx └── react ├── component.jsx ├── create-element.jsx ├── index.jsx └── set-state-queue.jsx /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": [ 4 | [ 5 | "transform-react-jsx", 6 | { 7 | "pragma": "React.createElement" 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | node_modules/ 4 | dist/ 5 | npm-debug.log 6 | yarn-error.log 7 | 8 | # Editor directories and files 9 | .idea 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mini-React 2 | 3 | 一个简易版的 React 框架,用于说明 React 的实现原理。 4 | 5 | ## 实现核心功能 6 | 7 | 实现了 React 的 JSX,虚拟 DOM,组件,生命周期,diff 算法,异步的 setState 等核心功能。 8 | 9 | ## 运行 10 | 11 | > 1. git clone 12 | > 2. cd Mini-React 13 | > 3. npm install 14 | > 4. npm start 15 | > 5. 浏览器打开 localhost:1234 16 | 17 | ## 测试图 18 | 19 | ![测试图](https://github.com/xingpengchao/Mini-React/blob/master/public/test-img/Mini-React%E6%B5%8B%E8%AF%95%E5%9B%BE.PNG) 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Mini-React 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-react", 3 | "version": "1.0.0", 4 | "description": "mini-react", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "parcel index.html" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/xingpengchao/Mini-React.git" 13 | }, 14 | "author": "Xingpc", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/xingpengchao/Mini-React/issues" 18 | }, 19 | "homepage": "https://github.com/xingpengchao/Mini-React#readme", 20 | "devDependencies": { 21 | "babel-core": "^6.26.3", 22 | "babel-plugin-transform-react-jsx": "^6.24.1", 23 | "babel-preset-env": "^1.7.0", 24 | "parcel-bundler": "^1.12.4" 25 | }, 26 | "dependencies": { 27 | "npm": "^6.13.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/test-img/Mini-React测试图.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xingpengchao/Mini-React/55265224507975df5b5b1e4b5cb5ee2331bb4009/public/test-img/Mini-React测试图.PNG -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // 关于为什么要导入React? 2 | // 1. 因为要继承React.Component父类 3 | // 2. 因为编译JSX片段并返回JS对象(虚拟Dom),需要用到React.createElement()方法,需要导入React 4 | // React暴露了createElement和Component 5 | import React from './react'; 6 | 7 | // 需要用到ReactDOM.render()方法 8 | import ReactDOM from './react-dom'; 9 | 10 | class App extends React.Component{ 11 | render(){ 12 | return( 13 |

Hello, World!

14 | ) 15 | } 16 | } 17 | 18 | // ReactDOM.render()方法接收两个参数,第一个参数是虚拟DOM,第二个参数是所要挂载的容器 19 | // ReactDOM.render()方法的作用就是将虚拟DOM渲染成真实的DOM,并挂载在容器上 20 | ReactDOM.render( 21 | , 22 | document.getElementById('root') 23 | ) 24 | -------------------------------------------------------------------------------- /src/react-dom/diff.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from "../react"; 2 | import { setAttribute } from "./dom"; 3 | 4 | /** 5 | * @param {HTMLElement} dom 真实DOM 6 | * @param {vnode} vnode 虚拟DOM 7 | * @param {HTMLElement} container 容器 8 | * @returns {HTMLElement} 更新后的DOM 9 | */ 10 | 11 | export function diff(dom, vnode, container) { 12 | const ret = diffNode(dom, vnode); 13 | 14 | if (container && ret.parentNode !== container) { 15 | container.appendChild(ret); 16 | } 17 | 18 | return ret; 19 | } 20 | 21 | function diffNode(dom, vnode) { 22 | let out = dom; 23 | 24 | if (vnode == undefined || vnode == null || typeof vnode === "boolean") 25 | vnode = ""; 26 | 27 | if (typeof vnode === "number") vnode = String(vnode); 28 | 29 | // 对比文本节点 30 | if (typeof vnode === "string") { 31 | // 如果当前的DOM就是文本节点,则直接更新内容 32 | if (dom && dom.nodeType === 3) { 33 | // nodeType: https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType 34 | if (dom.textContent !== vnode) { 35 | dom.textContent = vnode; 36 | } 37 | // 如果DOM不是文本节点,则新建一个文本节点DOM,并移除掉原来的 38 | } else { 39 | out = document.createTextNode(vnode); 40 | if (dom && dom.parentNode) { 41 | dom.parentNode.replaceChild(out, dom); 42 | } 43 | } 44 | 45 | return out; 46 | } 47 | 48 | // 如果虚拟DOM是函数,调用对比组件方法 49 | if (typeof vnode.tag === "function") { 50 | return diffComponent(dom, vnode); 51 | } 52 | // 对比DOM节点 53 | // 对比元素节点 54 | if (!dom || !isSameNodeType(dom, vnode)) { 55 | out = document.createElement(vnode.tag); 56 | 57 | if (dom) { 58 | [...dom.childNodes].map(out.appendChild); // 将原来的子节点移到新节点下 59 | 60 | if (dom.parentNode) { 61 | dom.parentNode.replaceChild(out, dom); // 移除掉原来的DOM对象 62 | } 63 | } 64 | } 65 | 66 | // 对比子节点 67 | if ( 68 | (vnode.children && vnode.children.length > 0) || 69 | (out.childNodes && out.childNodes.length > 0) 70 | ) { 71 | diffChildren(out, vnode.children); 72 | } 73 | 74 | // 对比属性节点 75 | diffAttributes(out, vnode); 76 | 77 | return out; 78 | } 79 | 80 | function diffChildren(dom, vchildren) { 81 | const domChildren = dom.childNodes; 82 | const children = []; 83 | 84 | const keyed = {}; 85 | 86 | if (domChildren.length > 0) { 87 | for (let i = 0; i < domChildren.length; i++) { 88 | const child = domChildren[i]; 89 | const key = child.key; 90 | if (key) { 91 | keyedLen++; 92 | keyed[key] = child; 93 | } else { 94 | children.push(child); 95 | } 96 | } 97 | } 98 | 99 | if (vchildren && vchildren.length > 0) { 100 | let min = 0; 101 | let childrenLen = children.length; 102 | 103 | for (let i = 0; i < vchildren.length; i++) { 104 | const vchild = vchildren[i]; 105 | const key = vchild.key; 106 | let child; 107 | 108 | if (key) { 109 | if (keyed[key]) { 110 | child = keyed[key]; 111 | keyed[key] = undefined; 112 | } 113 | } else if (min < childrenLen) { 114 | for (let j = min; j < childrenLen; j++) { 115 | let c = children[j]; 116 | 117 | if (c && isSameNodeType(c, vchild)) { 118 | child = c; 119 | children[j] = undefined; 120 | 121 | if (j === childrenLen - 1) childrenLen--; 122 | if (j === min) min++; 123 | break; 124 | } 125 | } 126 | } 127 | 128 | child = diffNode(child, vchild); 129 | 130 | const f = domChildren[i]; 131 | if (child && child !== dom && child !== f) { 132 | if (!f) { 133 | dom.appendChild(child); 134 | } else if (child === f.nextSibling) { 135 | removeNode(f); 136 | } else { 137 | dom.insertBefore(child, f); 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | function diffComponent(dom, vnode) { 145 | let c = dom && dom._component; 146 | let oldDom = dom; 147 | 148 | // 如果组件类型没有发生变化,则进行set props操作 149 | if (c && c.constructor === vnode.tag) { 150 | setComponentProps(c, vnode.attrs); 151 | dom = c.base; 152 | // 如果组件类型发生变化,则移除原来组件,并渲染新的组件 153 | } else { 154 | if (c) { 155 | unmountComponent(c); 156 | oldDom = null; 157 | } 158 | 159 | c = createComponent(vnode.tag, vnode.attrs); 160 | 161 | setComponentProps(c, vnode.attrs); 162 | dom = c.base; 163 | 164 | if (oldDom && dom !== oldDom) { 165 | oldDom._component = null; 166 | removeNode(oldDom); 167 | } 168 | } 169 | 170 | return dom; 171 | } 172 | 173 | // set props方法 174 | function setComponentProps(component, props) { 175 | if (!component.base) { 176 | if (component.componentWillMount) component.componentWillMount(); 177 | } else if (component.componentWillReceiveProps) { 178 | component.componentWillReceiveProps(props); 179 | } 180 | 181 | component.props = props; 182 | 183 | renderComponent(component); 184 | } 185 | 186 | // 导出渲染组件方法,供异步的setState中清空队列时,渲染组件使用 187 | export function renderComponent(component) { 188 | let base; 189 | 190 | const renderer = component.render(); 191 | 192 | if (component.base && component.componentWillUpdate) { 193 | component.componentWillUpdate(); 194 | } 195 | 196 | base = diffNode(component.base, renderer); 197 | 198 | if (component.base) { 199 | if (component.componentDidUpdate) component.componentDidUpdate(); 200 | } else if (component.componentDidMount) { 201 | component.componentDidMount(); 202 | } 203 | 204 | component.base = base; 205 | base._component = component; 206 | } 207 | 208 | // 创建新组件 209 | function createComponent(component, props) { 210 | let inst; 211 | 212 | if (component.prototype && component.prototype.render) { 213 | inst = new component(props); 214 | } else { 215 | inst = new component(props); 216 | inst.constructor = component; 217 | inst.render = function () { 218 | return this.constructor(props); 219 | }; 220 | } 221 | 222 | return inst; 223 | } 224 | 225 | // 移除旧组件 226 | function unmountComponent(component) { 227 | if (component.componentWillUnmount) component.componentWillUnmount(); 228 | removeNode(component.base); 229 | } 230 | 231 | // 判断节点类型是否相同 232 | function isSameNodeType(dom, vnode) { 233 | if (typeof vnode === "string" || typeof vnode === "number") { 234 | return dom.nodeType === 3; 235 | } 236 | 237 | if (typeof vnode.tag === "string") { 238 | return dom.nodeName.toLowerCase() === vnode.tag.toLowerCase(); 239 | } 240 | 241 | return dom && dom._component && dom._component.constructor === vnode.tag; 242 | } 243 | 244 | // 对比属性节点 245 | function diffAttributes(dom, vnode) { 246 | const old = {}; // 当前DOM的属性 247 | const attrs = vnode.attrs; // 虚拟DOM的属性 248 | 249 | for (let i = 0; i < dom.attributes.length; i++) { 250 | const attr = dom.attributes[i]; 251 | old[attr.name] = attr.value; 252 | } 253 | 254 | // 如果原来的属性不在新的属性当中,则将其移除掉(属性值设为undefined) 255 | for (let name in old) { 256 | if (!(name in attrs)) { 257 | setAttribute(dom, name, undefined); 258 | } 259 | } 260 | 261 | // 更新新的属性值 262 | for (let name in attrs) { 263 | if (old[name] !== attrs[name]) { 264 | setAttribute(dom, name, attrs[name]); 265 | } 266 | } 267 | } 268 | 269 | // 移除dom节点 270 | function removeNode(dom) { 271 | if (dom && dom.parentNode) { 272 | dom.parentNode.removeChild(dom); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/react-dom/dom.jsx: -------------------------------------------------------------------------------- 1 | // setAttribute方法用于设置属性集合的属性 2 | // setAttribute方法接收三个参数,分别是虚拟DOM对应的真实DOM元素标签、该标签的属性名、属性值 3 | const setAttribute = (dom, name, value) => { 4 | // 如果属性名是className,则改回class 5 | if (name === "className") name = "class"; 6 | 7 | // 如果属性名是onXXX,则是一个时间监听方法onClick 8 | if (/on\w+/.test(name)) { 9 | // 在html中事件绑定方法是小写,需将驼峰属性名转换为小写 10 | name = name.toLowerCase(); 11 | dom[name] = value || ""; 12 | // 如果属性名是style,则更新style对象 13 | } else if (name === "style") { 14 | // 这种形式:style="red" 15 | if (!value || typeof value === "string") { 16 | dom.style.cssText = value || ""; 17 | } 18 | // 这种形式: style={ color:red } 19 | else if (value && typeof value === "object") { 20 | for (let name in value) { 21 | // 可以通过style={ width: 20 }这种形式来设置样式,可以省略掉单位px 22 | dom.style[name] = 23 | typeof value[name] === "number" ? value[name] + "px" : value[name]; 24 | } 25 | } 26 | // 普通属性则直接更新属性 27 | } else { 28 | if (name in dom) { 29 | dom[name] = value || ""; 30 | } 31 | if (value) { 32 | dom.setAttribute(name, value); // 属性值存在,设置属性值 33 | } else { 34 | dom.removeAttribute(name, value); // 属性值不存在,移除属性 35 | } 36 | } 37 | }; 38 | 39 | export default setAttribute; 40 | -------------------------------------------------------------------------------- /src/react-dom/index.jsx: -------------------------------------------------------------------------------- 1 | import render from "./render"; 2 | 3 | const ReactDOM = { 4 | render, 5 | }; 6 | export default ReactDOM; 7 | -------------------------------------------------------------------------------- /src/react-dom/render.jsx: -------------------------------------------------------------------------------- 1 | import { diff } from "./diff.jsx"; 2 | 3 | // render方法的作用是将虚拟DOM渲染成真实DOM,并挂载到容器上 4 | // render方法接收两个参数,第一参数为虚拟DOM,第二个参数为所要挂载的容器 5 | const render = (vnode, container) => { 6 | // 将更新后的DOM挂载到容器上 7 | let dom = diff(dom, vnode, container); 8 | return container.appendChild(dom); 9 | }; 10 | 11 | const _render = (vnode, container) => { 12 | // 当虚拟DOM为空,终止函数 13 | if (vnode === undefined) return; 14 | 15 | // 当虚拟DOM是组件 16 | if (vnode.isReactComponent) { 17 | const component = vnode; 18 | // 判断组件是否已挂载在DOM上 19 | if (component._container) { 20 | // 组件已挂载且有componentWillUpdate()方法,调用方法 21 | if (component.componentWillUpdate) { 22 | component.componentWillUpdate(); 23 | } 24 | // 组件未挂载且有componentWillMount()方法,调用方法 25 | } else if (component.componentWillMount) { 26 | component.componentWillMount(); 27 | } 28 | 29 | // 保存父容器信息,用于更新 30 | component._container = container; 31 | 32 | vnode = component.render(); 33 | } 34 | // 当vnode为字符串时,渲染结果是一段文本 35 | if (typeof vnode === "string") { 36 | // 利用原生DOM操作创建文本节点 37 | const textNode = document.createTextNode(vnode); 38 | // 给真实DOM节点添加子元素 39 | return container.appendChild(textNode); 40 | } 41 | // 创建虚拟DOM对应真实DOM的具体元素 42 | const dom = document.createElement(vnode.tag); 43 | 44 | //如果虚拟DOM的属性集合不为空,对属性集合的属性进行操作 45 | if (vnode.attrs) { 46 | Object.keys(vnode.attrs).forEach((key) => { 47 | const value = vnode.attrs[key]; 48 | // 如果是属性集合的属性名是类名,直接设置其属性值 49 | if (key === "className") key = "class"; 50 | 51 | // 如果属性值是事件监听函数,则直接附加到dom上 52 | if (typeof value === "function") { 53 | dom[key.toLowerCase()] = value; 54 | } else { 55 | // 其他情况调用setAttribute方法设置属性 56 | dom.setAttribute(key, vnode.attrs[key]); 57 | } 58 | }); 59 | } 60 | 61 | // 递归渲染子节点 62 | if (vnode.children) { 63 | vnode.children.forEach((child) => _render(child, dom)); 64 | } 65 | 66 | // 将真实DOM挂载到容器上 67 | return container.appendChild(dom); 68 | }; 69 | 70 | export default render; 71 | -------------------------------------------------------------------------------- /src/react/component.jsx: -------------------------------------------------------------------------------- 1 | import { enqueueSetState } from "./set-state-queue.jsx"; 2 | 3 | // 定义React组件基类/父类 4 | class Component { 5 | constructor(props = {}) { 6 | // 用于判断是否为类组件 7 | this.isReactComponent = true; 8 | // 初始化实例属性props和state 9 | this.props = props; 10 | this.state = {}; 11 | } 12 | 13 | // 定义原型方法setState,用于改变状态state 14 | // stateChange可以是对象,也可以是函数 15 | setState(stateChange) { 16 | enqueueSetState(stateChange, this); 17 | } 18 | } 19 | 20 | export default Component; 21 | -------------------------------------------------------------------------------- /src/react/create-element.jsx: -------------------------------------------------------------------------------- 1 | import Component from "./component.jsx"; 2 | 3 | /*createElement方法用于编译JSX并返回JS对象,返回的JS对象记录了该标签的所有DOM信息,也就是虚拟DOM。 4 | createElement方法接收三个参数: 5 | 第一个参数是DOM节点标签,比如div、h1、span等等; 6 | 第二个参数是一个对象,表示属性集合,包含了标签的所有属性,比如className、id、style等等; 7 | 第三个参数是一个数组,使用扩展运算符将child1、child2一些列子节点合并成的一个数组children;*/ 8 | 9 | function createElement(tag, attrs, ...children) { 10 | attrs = attrs || {}; 11 | // 返回一个JS对象,就是虚拟Dom 12 | return { 13 | tag, 14 | attrs, 15 | children, 16 | key: attrs.key || null, 17 | }; 18 | } 19 | 20 | export default createElement; 21 | -------------------------------------------------------------------------------- /src/react/index.jsx: -------------------------------------------------------------------------------- 1 | import createElement from "./create-element.jsx"; 2 | import Component from "./component.jsx"; 3 | 4 | const React = { 5 | Component, 6 | createElement, 7 | }; 8 | 9 | export default React; 10 | -------------------------------------------------------------------------------- /src/react/set-state-queue.jsx: -------------------------------------------------------------------------------- 1 | import { renderComponent } from "../react-dom/diff"; 2 | 3 | const setStateQueue = []; // 存储setState的队列 4 | const renderQueue = []; // 渲染组件队列 5 | 6 | // 异步清空队列,使用JS事件循环机制实现,具体使用Promise.resolve().then()包裹清空队列的方法,生成微任务, 7 | // 执行步骤是同步任务执行完成后执行微任务,达到异步清空队列的效果,这是实现异步setState的关键 8 | function defer(fn) { 9 | return Promise.resolve().then(fn); 10 | } 11 | 12 | export function enqueueSetState(stateChange, component) { 13 | // setState存储队列为空,异步清空存储队列并渲染组件 14 | if (setStateQueue.length === 0) { 15 | defer(flush); 16 | } 17 | // 否则,将每次setState数据push进队列 18 | setStateQueue.push({ 19 | stateChange, 20 | component, 21 | }); 22 | 23 | // 与渲染组件队列中组件不重复,将组件push进队列 24 | if (!renderQueue.some((item) => item === component)) { 25 | renderQueue.push(component); 26 | } 27 | } 28 | 29 | // 清空队列方法 30 | function flush() { 31 | let item, component; 32 | 33 | // 清空setState存储队列,直至队列为空 34 | while ((item = setStateQueue.shift())) { 35 | const { stateChange, component } = item; 36 | 37 | // 如果没有prevState,则将当前的state作为初始的prevState 38 | if (!component.prevState) { 39 | component.prevState = Object.assign({}, component.state); 40 | } 41 | 42 | // 如果stateChange是一个函数,也就是setState的第二种形式 43 | if (typeof stateChange === "function") { 44 | Object.assign( 45 | component.state, 46 | stateChange(component.prevState, component.props) 47 | ); 48 | } else { 49 | // 如果stateChange是一个对象,则直接合并到setState中 50 | Object.assign(component.state, stateChange); 51 | } 52 | 53 | component.prevState = component.state; 54 | } 55 | 56 | // 清空渲染组件队列,直至队列为空 57 | while ((component = renderQueue.shift())) { 58 | // 渲染组件方法 59 | renderComponent(component); 60 | } 61 | } 62 | --------------------------------------------------------------------------------