为什么在JS文件中用那么几行神奇的代码, 就能够把虚拟DOM转换成真实DOM?并且还实实在在的插入了页面中?
为了搞明白这一切, 我自己写了两个方法, 也就是对React.createElementReactDOM.render方法的重构, 来揭露其到底是怎么实现的!
方法实现
-
实现ReactDOM.render
/** * 简单实现 React.createElement(type, props, ...children) 方法 * 该方法会创建一个虚拟DOM树, 通过ReactDOM.render()可以把虚拟DOM树实体化成DOM并插入页面中 * * @param {创建的DOM的类型} type * @param {创建的DOM的一些属性, 比如id, className, style等等} props * @param {创建的DOM的子元素以及子标签(字符串|子DOM)} children * @returns 虚拟DOM树 * * 虚拟DOM树结构: * { * type: 'div', * props: { * id: 'one', * className: 'two', * style: { * color: 'red', * fontSize: '50px' * ... * }, * children: ['aaa', [Object], [Object]...] * } * __proto__: Object * } */ function createElement(type, props, ...children) { props = props || {} let obj = { type: type, ref: props.ref || null, key: props.key || null, props: { ...props, } } props.ref ? delete obj.props.ref : null props.key ? delete obj.props.ref : null children && Array.isArray(obj.props.children) ? obj.props.children.append(children) : obj.props.children = children return obj }
-
实现React.render方法
// 1.1版本 /** * 简单实现ReactDOM.render(obj, container, callback): * 该方法会 * 1. 把obj[虚拟DOM树]进行DOM实体化, * 2. 把实体化DOM插入container(页面中的元素), * 3. 最后触发回调函数callback * * @param {虚拟DOM树} [obj={}] * @param {DOM被插入的位置} container * @param {回调函数} callback * @returns 根据虚拟DOM树生成真实DOM并插入页面中 */ function render(obj = {}, container, callback) { let { type, // 生成的DOM类型[function|字符串] props // 各种属性 {style:{color: 'red'...}, children: ['我是div标签', {}]} } = obj, elem function vir2DOM(VirDOMObj) { if (!VirDOMObj) { return } /** * handleChildren: 把child(字符串|虚拟DOM对象)都转换成XML字符串, 并返回 * * @param {children遍历传入的单个child对象} child * @param {XML字符串} XMLString * @returns */ function handleChildren(child, XMLString) { if (typeof child === 'string' || typeof child === 'number') { // 1. 如果是 String|NUmber, 为html中文本对象 XMLString += child } else if (Object.prototype.toString.call(child) === '[object Object]') { // 2. children为抽象DOM对象 /** * 把DOM对象转换成字符: * 1. 创建一个div标签 * 2. 把想要转换的DOM对象插入div中 * 3. 获取div的innerHTML => 就是字符串的DOM对象 */ const div = document.createElement('div') div.appendChild(vir2DOM(child)) XMLString += div.innerHTML } else if (Object.prototype.toString.call(child) === '[object Array]') { // 3. children为数组(里面是children子元素) ['我是div标签', {type:'div', props: {...}}] XMLString += child.map(item => handleChildren(item, '')).join('') } return XMLString } // 把虚拟DOM树转换成真实DOM let { type, // 生成的DOM类型[组件function|字符串(div|p|span...)] props // 生成的DOM各种属性 {style:{color: 'red'...}, children: ['我是div标签', {}]} } = VirDOMObj let elem = document.createElement(type) for (let k in props) { if (k === 'style') { // 1. pops.style {color: 'red', fontSize:'50px', ...} const style = props[k] for (let styleName in style) { elem.style[styleName] = style[styleName] } } else if (k === 'children') { // 2. props.children {'aaa', {type: div': style:{...}, children:{...}}} const children = props[k] let resInnerHtml = `` // XML字符串, 把所有子元素转换成XML字符串,最后绑定到innerHTML中 for (let child in children) { // 遍历Children对象, 并交给handleChildren处理子元素 resInnerHtml = handleChildren(children[child], resInnerHtml) } elem.innerHTML = resInnerHtml } else { // 3. 挂载在DOM上的属性, 如id, className, ref, key这些等等 elem[k] = props[k] } } return elem } // 1. 把obj[虚拟DOM树]进行DOM实体化, if (typeof type === 'function') { // 1. type 如果是函数组件 elem = vir2DOM(type(props)) // 1. 执行传入的组件函数获取虚拟DOM树 2. 转换成真实DOM } else { // 2. type是字符串(div|p|span这些) elem = vir2DOM(obj) // 虚拟DOM对象转换成真实DOM } // 2. 把实体化DOM插入container(页面中的元素) if (elem) { container ? container.appendChild(elem) : document.querySelector('#root').appendChild(elem) } // 3. 最后触发回调函数callback callback && setTimeout(callback) return elem }
方法测试
- 准备两个文件
-
index.js代码
function Login(props) { var type = props.type, content = props.content, children = props.children; // console.log(content); // => 自己写的样式 var myCss = { width: '50%', margin: '0 auto 10px', padding: '0' }; // => 传入的类型处理 var typeValue = type || '系统提示'; if (typeof type === 'number') { switch (type) { case 0: { typeValue = '系统提示'; break; } case 1: { typeValue = '系统错误'; break; } case 2: { typeValue = '系统警告'; break; } default: { typeValue = '真香'; break; } } } return createElement("section", { className: "panel panel-default col-lg-4", style: myCss }, createElement("div", { className: "panel-heading" }, createElement("h3", { className: "panel-title" }, typeValue)), createElement("div", { className: "panel-body" }, content // children.map(item => item) ), children ? createElement("div", { className: "panel-footer" }, Array.prototype.map.call(children, function (item) { return item; })) : null); } function fn() { return createElement(Login, { type: '请登录', content: createElement("div", null, createElement("div", { className: "input-group" }, createElement("span", { className: "input-group-btn" }, createElement("button", { className: "btn btn-default", type: "button" }, "User")), createElement("input", { type: "text", className: "form-control", placeholder: "\u8BF7\u8F93\u5165\u7528\u6237\u540D" })), createElement("p", { style: { height: '2px' } }), createElement("div", { className: "input-group" }, createElement("span", { className: "input-group-btn" }, createElement("button", { className: "btn btn-default", type: "button" }, "Pswd")), createElement("input", { type: "text", className: "form-control", placeholder: "\u8BF7\u8F93\u5165\u5BC6\u7801" }))) }, createElement("button", { className: "btn btn-info", }, "\u767B\u5F55"), createElement("button", { className: "btn btn-danger", style: { marginLeft : '10px' } }, "\u53D6\u6D88")); } let res = fn() render(res, document.querySelector('body'), () => {console.log('ok')})
-
html.js代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>测试</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> </head> <body> <div id="root"></div> <script src="./index.js"></script> </body> </html>