目录
本文将分以下四个部分进行:
1、实现 React.createElement && ReactDOM.render 方法
2、实现组件Component 及生命周期函数
3、实现 setState 的异步更新
4、实现DOM Diff 算法
1、实现 React.createElement && ReactDOM.render 方法
- 关于JSX
JSX本身是一种语法糖,会被 Babel 的 transform-react-jsx 插件转换为 JS 代码
// 转换前
const profile = (
{[user.firstName, user.lastName].join(' ')}
);
// 转换后
const profile = React.createElement("div", null,
React.createElement("img", { src: "avatar.png", className: "profile" }),
React.createElement("h3", null, [user.firstName, user.lastName].join(" "))
);
这个转换后的React.createElement是可以通过babel配置的:
{
"plugins": [
["@babel/plugin-transform-react-jsx", {
"pragma": " React.createElement",
}]
]
}
默认的 pragma 就是 React.createElement,所以一般不需要额外配置。这也是为什么在写React组件时,明明没有用到React,但还是要将其引入到文件中的原因。(你写的代码是编译前的,但是实际运行的代码是编译后的,而编译后的文件需要 React,所以必须在编译前就引入,否则就是报 React 找不到的错误)
- 关于虚拟DOM(称之为 vdom)
虚拟DOM本质上就是一个JS对象,只不过是一个真实DOM的映射,能反应出真实DOM的 tag , attributes , children等信息。结合diff 算法,用虚拟DOM描述真实DOM有利于在DOM更新时提升更新效率。
// 虚拟 DOM
{
tag,
attrs,
children
}
- 实现 React.createElement
createElement 的主要作用就是创建虚拟DOM,这里简单实现为返回上述的虚拟DOM
React.createElement = function(tag,attrs,...children){ // 第三个参数开始是子元素,用扩展运算符接收得到子元素数组
return {
tag,
attrs,
children
}
}
注意这里参数顺序是跟Babel转换后的代码一致的,也就是说参数的值由 Babel 负责传递
- 实现 ReactDOM.render
ReactDOM.render方法接受一个JSX参数(转化后就是vdom)和一个挂载元素
ReactDOM.render = function(vdom,container){
container.innerHTML = '' // 先清空
container.appendChild(render(vdom) ) // render 负责将 vdom 编译成 dom 片段
}
要实现 render,这里要说下 vdom 中 children 元素的类型,不是所有 children 元素都是 vdom 类型:对于文本, children 元素是一个字符串;对于非文本, children 元素才是vdom,且对于自定义组件,vdom的tag是function类型,html标签的tag是string类型
function render(vdom,container){
if(typeof vdom === 'string'){ // 文本节点单独对待
return document.createTextNode(vdom)
}
const dom = document.createElement(vdom.tag)
for(let key in vdom.attrs){
if(vdom.attrs.hasOwnProperty(key)){
setAttributes(dom,key,vdom.attrs[key]) // 设置属性,如事件、样式、自定义属性等
}
}
vdom.children.forEach((item,key)=>render(item,dom)) // 遍历子节点,递归调用 render 完成渲染
return dom
}
接下来就是属性的设置了,主要是区分事件、样式和其它属性的设置
// 转换前
const b=a
// 转换后
var b = React.createElement("p", {
onClick: test,
style: {
color: 'red'
},
className: 'class',
custom: 123
}, "a");
对着上述转换结果,可写出 setAttributes 函数
function setAttributes(dom,key,value){
// 设置事件,React中事件以 onXXX 开头,这里用正则判断
if(/on[A-Z]/.test(key)){
dom.addEventListener(key.slice(2).toLowerCase(),value,false)
}else if(key==='className'){
dom.classList.add(value)
}else if(key==='style'){
for(let i in style){
dom.style.cssText+=`${i}:${style[i]};`
}
}else{
dom.setAttribute(key,value)
}
}
2、实现类组件Component,函数式组件及各生命周期函数
函数式组件只是类组件的一个特例,将函数式组件作为类组件的render函数基本就可以将函数式组件扩展为类式组件,所以本质上二者是一样的。以下专注于处理类式组件。
- 实现 React.Component
Component 作为组件基类,最基本的需要提供构造函数以及组件更新的 setState 函数
class Component{
constructor(props){
this.props = props
this.state = {}
}
setState(state){
Object.assign(this.state,state)
renderComponent(this)
}
forceUpdate(){ // 强制更新
renderComponent(this)
}
}
对于类式组件,JSX被Babel编译后产生的vnode中的tag是函数类型。所以要渲染类式组件,需要调整上述render函数的实现:
// 新增 tag 为 function 的情况(组件)
if(typeof vdom.tag === 'function'){
const comp = createComponent(vdom.tag,vdom.attrs)
setComponentProps(comp,vdom.attrs)
renderComponent(comp)
return comp.base // 将渲染的真实dom存在base属性上
}
createComponent 负责创建组件的实例,也就是创建一个JS对象,要注意区分函数组件和类组件,这步操作实际上已经屏蔽了 类式组件和函数式组件的区别。
function createComponent(fun,props){
let comp;
if(fun.prototype.render){
comp = new fun(props)
}else{
comp = new Component(props)
comp.constructor = fun
comp.render = function(){
return fun(props)
}
}
return comp
}
setComponentProps 负责更新属性,并调用相关生命周期函数
function setComponentProps(comp,props){
if(comp.base){
// dom已存在,做更新操作
if(comp.componentWillReceiveProps){
comp.componentWillReceiveProps(props)
}
}else{
// dom 还未生成,做初始操作
if(comp.componentWillMount){
comp.componentWillMount(props)
}
}
comp.props = props
}
renderComponent 负责将comp转化为dom片段
function renderComponent(comp){
const vdom = comp.render()
if(comp.base){
// dom 已存在,做更新
if(comp.componentWillUpdate){
comp.componentWillUpdate(comp.props)
}
}
const base = render(vdom)
if(comp.base){
// dom 已存在,做更新
if(comp.componentDidUpdate){
comp.componentDidUpdate()
}
}else{
// dom 不存在,做创建操作
if(comp.componentDidMount){
comp.componentDidMount()
}
}
if(comp.base.parentNode){ // 第一次挂载后,通过setState更新组件时,要替换老节点
comp.base.parentNode.replaceChild(base,comp.base)
}
comp.base =base // 覆盖老的 dom
}
3、实现 setState 的异步更新
上述 setState 的实现是同步的,每次调用 setState 都会触发 renderComponent,当setState 调用频繁时会是一个性能瓶颈,以下将实现 setState 的异步更新,基本原理是利用 微任务队列控制 renderComponent 的执行时机
setState(state){
enQueueState(state,this)
}
// 辅助函数-队列保存历史state
const queue=[]
function enQueueState(state,comp){
if(queue.length===0){
return new Promise().then(flush)
}
queue.push({
state,
comp
})
}
// 辅助函数-清空队列,合并state并渲染组件
function flush(){
let item;
while(item=queue.shift()){
const {state,comp} = item
Object.assign(comp.state,state)
if(queue.length===0){
renderComponent(comp)
}
}
}
4、实现DOM Diff 算法
diff 算法有两种实现,一种是用新旧vdom对比,得出变化部分再做更新操作;另一种是用真实旧dom和新的vdom对比,边对比边更新,这里采用后者来实现,主要说下思路:
diff 函数接受旧dom和新vdom为参数,返回更新后的真实dom
function diff(dom,vdom){
const ret = diffNode( dom, vnode );
if ( container && ret.parentNode !== container ) {
container.appendChild( ret );
}
return ret;
}
diffNode 分五种情况:
1、diff 文本
2、diff 组件
3、diff 不同类型原始标签
4、同类型原始标签时,diff 属性
5、diff children 里递归调用 diffNode
diff 的调用时机在 renderComponent 函数里:
function renderComponent(comp){
const vdom = comp.render()
if(comp.base){
// dom 已存在,做更新
if(comp.componentWillUpdate){
comp.componentWillUpdate(comp.props)
}
}
const base = diff(comp.base,vdom)
if(comp.base){
// dom 已存在,做更新
if(comp.componentDidUpdate){
comp.componentDidUpdate()
}
}else{
// dom 不存在,做创建操作
if(comp.componentDidMount){
comp.componentDidMount()
}
}
if(comp.base.parentNode){ // 第一次挂载后,通过setState更新组件时,要替换老节点
comp.base.parentNode.replaceChild(base,comp.base)
}
comp.base =base // 覆盖老的 dom
}
参考
- 从零开始实现一个React