1. 页面初次渲染
JSX语法的转换
-
先看看我们使用React打印HelloWorld
ReactDOM.render(( <h1 style="color:red">Hello World</h1> ),document.getElementById('root'));
-
使用React.createElement包裹
ReactDOM.render(( React.createElement("h1",{style:"color:red"},"Hello World") ), document.getElementById('root'));
-
根据这两个结果,我们查看打包后的代码。我们不难得知,使用JSX语法,在打包时转换成React.createElement函数。
1.1 虚拟dom
- 虚拟dom就是一个对象,用来描述一个元素的具体属性。在React中,我们通过React.createElement得到虚拟dom。接着将虚拟dom通过React.render()转化成真实dom渲染到界面中。
- React.createElement需要传入三个参数,type(元素类型),props(元素的属性),children(元素的孩子)
- 下面是React.createElement()简易版的代码
import Component from './Component'; function createElement(type,config,children){ if(config){ delete config.__source; delete config.__self; } let props = {...config}; if(arguments.length>3){ // 当孩子超过一个时,我们将孩子们放到一个数组中 children=Array.prototype.slice.call(arguments,2); } // 将孩子数组放到属性中。 props.children=children; return { type, props } } const React = {createElement,Component} export default React;
最终的到的虚拟dom有两个属性,type(类型),props(属性,包含孩子的数组),不仅如此,如果当传入的孩子不是数字,字符串时,通过会被React.createElement包裹。如:
ReactDOM.render((
<div>
<h1>
你好呀
</h1>
<h1>
你好呀
</h1>
</div>
), document.getElementById('root'));
// 在打包时,转换成
ReactDOM.render((
React.createElement("div", {}, React.createElement("h1", {}, "你好啊"), React.createElement("h1", {}, "你好啊"))
), document.getElementById('root'));
2 将虚拟dom转换为真实dom
- 上面说到,得到虚拟dom之后,调用React.render()函数将虚拟dom渲染到页面中。
- 那我们很容易就能想到,第一步要将虚拟dom渲染成真实dom,接着将真实的dom放到我们指定的dom元素下(document.getElementById(‘root’))
以下代码模拟以上过程。接着我们需要编写createDOM()来得到真实domfunction render(vdom,container){ const dom = createDOM(vdom); container.appendChild(dom); }
2.1 编写 createDOM
- 直接上代码
function createDOM(vdom){ //如果vdom是数字或者字符串的话,直接返回一个真实的文本节点 if(typeof vdom === 'string'||typeof vdom === 'number'){ return document.createTextNode(vdom); } //否则 它就是一个虚拟DOM对象了,也就是React元素 let {type,props}= vdom; let dom; if(typeof type === 'function'){//自定义的函数组件 if(type.isReactComponent){//说明这是一个类组件 return mountClassComponent(vdom); }else{//否则说明是一个函数组件 return mountFunctionComponent(vdom); } }else{//原生组件 dom = document.createElement(type); } //使用虚拟DOM的属性更新刚创建出来的真实DOM的属性 updateProps(dom,props); //在这处理props.children属性 //如果只有一个儿子,并且这个儿子是一个文本 if(typeof props.children ==='string'||typeof props.children ==='number'){ dom.textContent = props.children; //如果只有一个儿子,并且这个儿子是一个虚拟DOM元素 }else if(typeof props.children ==='object' && props.children.type){ //把儿子变成真实DOM插到自己身上,形成递归! render(props.children,dom); //如果儿子是一个数的话,说明儿子不止一个 }else if(Array.isArray(props.children)){ reconcileChildren(props.children,dom); }else{ document.textContent = props.children?props.children.toString():""; } return dom; }
这段代码的逻辑很简单,拿到虚拟dom,判断是否为字符串跟数字。不是的话就判断是否为自定义的函数组件(分别为函数组件跟类组件)或者是普通的标签,得到一个真实的dom,接着给该dom通过**updateProps(dom,props);**将虚拟dom中的属性放到真实dom中。接着处理它的孩子们即可。
2.2 编写 updateProps(dom,props)
-
迭代传入的 props ,所有的属性绑定到dom中,注意:要跳过children,因为我们不是在这里处理它的孩子。
function updateProps(dom,newProps){ for(let key in newProps){ if(key === 'children') continue;//单独处理,不在此处处理 if(key === 'style'){ let styleObj = newProps.style; for(let attr in styleObj){ dom.style[attr]=styleObj[attr]; } }else if(key.startsWith('on')){ //给真实DOM加属性的话 onclick //dom[key.toLocaleLowerCase()]=newProps[key]; addEvent(dom,key.toLocaleLowerCase(),newProps[key]); }else{//在JS中 dom.className='title' dom[key]= newProps[key]; } } }
处理自定义函数式组件
-
类组件
- 该组件继承React.Component,所以我们需要编写Component作为父类,根据我们平时使用习惯可以推断出它最起码有 props与state属性,并且实例调用render方法能返回一个jsx编写的界面
class Component{ // 该静态属性用于标识该组件为类组件 static isReactComponent = true constructor(props){ // 在被 new 时,props传入 this.props = props; this.state = {}; } }
- 在createDOM()编写处理类组件的函数 mountClassComponent(vdom)
function mountClassComponent(vdom){ //解构类的定义和类的属性对象 let {type,props}= vdom; //创建类的实例 let classInstance = new type(props); //调用实例的render方法返回要渲染的虚拟DOM对象 let renderVdom = classInstance.render(); // 我们在子类编写的render(),返回虚拟dom //根据虚拟DOM对象创建真实DOM对象 let dom = createDOM(renderVdom); //为以后类组件的更新,把真实DOM挂载到了类的实例上 classInstance.dom = dom; return dom; }
-
函数组件
最常见的组件编写方法,我们传入props,执行该函数,直接得到一个虚拟dom,然后调用createDOM()得到真实dom返回。function mountFunctionComponent(vdom){ let {type,props}= vdom; let renderVdom = type(props); return createDOM(renderVdom); }
处理孩子们
- 如果孩子是字符串跟数字,直接放到dom中
- 如果孩子是对象,那么就是被React.createElement返回的虚拟dom对象,那就可以通过 render函数,传入当前孩子和当前dom递归,将孩子的真实dom挂载到当前dom上
- 如果孩子是数组,那就遍历该数组,依次调用render方法
- 如果啥都不是,那就转换成字符串挂载到dom上。
挂载到页面中
回到最初的render函数
function render(vdom,container){
const dom = createDOM(vdom);
container.appendChild(dom);
}
我们好不容易得到真实dom,通过appendChild(JS的原生方法),将dom作为根dom的孩子插入到root上。
综上,初次渲染完成!
感谢张仁阳老师的教导