前言
现在我们完成了配置,可以在js文件中开始实现我们自己的react了,首先要了解的是react是如何进行渲染的?
我的理解是:根据jsx模板生成虚拟dom -> 虚拟dom根据属性渲染为真实dom -> 将真实dom挂载到页面的dom节点下 -> 页面呈现出想要的效果
生成虚拟dom
React提供了createElement这个方法来创建虚拟dom元素,具体格式在1中提到过了,所以我们需要自己实现一个createElement方法,来返回我们需要的标签名、属性和子节点。
/**
* createElement(tag, attrs, children)
* @param {string} tag 代表标签名 - div/span/p等
* @param {object} attrs 代表标签属性 - value/style等
* @param {array} children 代表标签下的子节点
* createElement的主要作用就是创建虚拟dom对象,所以在这里将接受到的参数返回
*/
MyReact.prototype.createElement = function(tag, attrs, ...children) {
return {
tag,
attrs,
children // children可能不止一个,所以使用了...运算符
};
}
虚拟dom转化为真实dom
为了将虚拟dom转化成真实dom,定义了一个名为mountComponent的方法来接受虚拟dom,并返回真实dom。其中需要考虑到的问题是: 如果接受到的虚拟dom不是对象怎么办? 如果虚拟dom还有子节点怎么办?
function mountComponent(vDom) {
/**
* <div>{null}</div> | <div>{undefined}</div> | <div>{true}</div> | <div>{false}</div> | <div/>
* 以上的输出结果可以尝试一下?
*/
if (vDom === null || vDom === undefined || typeof vDom === 'boolean') {
vDom = "";
return vDom;
} else if (typeof vDom === 'string' || typeof vDom === 'number') {
// 文本节点
vDom = vDom.toString();
const textDom = document.createTextNode(vDom);
return textDom;
}
const dom = document.createElement(vDom.tag);
setAttributes(dom, vDom.attrs);
// 递归调用直到最后的子节点
vDom.children.forEach(element => {
dom.appendChild(mountComponent(element));
});
return dom;
}
增加节点属性
上面将虚拟dom渲染成真实dom的过程中,我们还需要将vDom里的attrs属性赋予到生成的真实dom上,于是在mountComponent()中,去使用了setAttribute()方法
/**
* 将attrs里的属性赋到dom上,例如
* attrs: { style: {{ color: 'red' }}, value: 'value', onClick: fn() }
*/
function setAttributes(dom, attrs) {
if (typeof attrs !== 'object') {
return dom;
} else {
Object.keys(attrs).forEach((key) => {
const value = attrs[key];
if (key === 'style') {
// 样式属性
for (let styleName in value) {
dom.style[styleName] = value[styleName];
}
} else if (/on[A-Z]/.test(key)) {
// 将例如onClick转换为click
dom.addEventListener(key.slice(2).toLowerCase(), value, false);
} else {
dom.setAttribute(key, value);
}
});
}
}
挂载到render容器
现在我们通过mountComponent()方法,最后成功得到了要放到页面内的真实dom,因为在平时的项目中,会在入口文件使用诸如React.render(<App/>, document.getElementById("root"))的代码,所以我们实现的render方法应该接受一个由虚拟dom转换而成的真实dom,和一个页面内挂载真实dom的容器container,所以我们在html中写入了一个id为root的div标签。
/**
* render方法:将虚拟dom渲染到真实页面的过程
* @param {object} vDom
* @param {*} container
*/
MyReact.prototype.render = function(vDom, container) {
// 清空上一次挂载的内容,准备挂载新的内容
container.innerHTML = "";
container && container.appendChild(mountComponent(vDom));
}
运行结果
现在我们已经完成了最基础的虚拟dom和render方法,写一段jsx语法来实验一下吧!
const React = new MyReact();
const number = 1; // 实验jsx的{}内可以使用变量
const el = (
<div id="test">
<p style={{ color: "red" }}>这是staru的jsx语法</p>
<button onClick={() => { console.log('click') }}>按钮</button>
<input value={number} onChange={(e) => { console.log(e.target.value) }} />
</div>
);
React.render(el, document.getElementById("root"));
然后使用package.json里的脚本npm run start启动(上一篇里有具体内容)
打包成功了!让我们去html的网页里看看吧
结果是毫无疑问地报错了……
通过控制台我们可以很方便地调试代码
处理了一通代码里的bug后……运行成功,是忘记把vDom.children生成的dom节点挂载到父节点下面了,然后再次npm run start,页面出现了我们想要的内容!上面贴的代码有错误的地方也更新啦。
接下来,我将去探索如何实现react的组件、props、state和生命周期。