前提
React 17版本之前,都是利用createElement函数生成虚拟dom对象
jsx语法由babel解析后,然后通过createElement得到一个虚拟dom对象例如
babel解析是在webpack打包编译的时候解析的,然后将解析的代码在浏览器执行createElement函数的时候生成虚拟dom对象
<div className="test">zzzz<span>dddd</span></div>
---> babel解析 ----------------->
React.createElement("div", {
className: "test"
}, "zzzz", /*#__PURE__*/React.createElement("span", null, "dddd"));
----> React.createElement转为虚拟dom对象
const vdom = {
props: {
className: 'test',
children: "zzzzs",
// child可能是字符串也可能是数组
// children: ["zzzzs",{
// type: 'span',
// props: {
// 属性
// }
// }]
},
type: 'div'
}
React 17
实现react.CreateElement
function createElement (type, config, children) {
if(config) {
delete config.__self
delete config.__source
}
const props = {...config};
// 有多个子节点的时候
if (arguments.length > 3) {
// children 是一个数组
children = Array.prototype.slice.call(arguments, 2)
}
props.children = children;
return {
type,
props,
}
}
const React = {
createElement,
}
export default React;
react
react-dom
有一个render方法,将虚拟dom转为真实dom,然后挂载到容器上
render方法实现
根据虚拟dom创建真实dom
把虚拟dom的属性挂在到真实dom上
把虚拟dom的子节点也变成真实dom挂在到自己的dom上
将dom挂载到容器上
// 传入一个虚拟dom对象.和一个容器进行渲染
function render(vdom, container) {
// 将虚拟dom转为真实dom,进行渲染
const dom = createDom(vdom)
container.appendChild(dom)
}
// 将虚拟dom转为真实dom
function createDom(vdom) {
// 如果是数字或者字符串,直接返回一个真实到文本节点
if(typeof vdom === 'string' || typeof vdom === 'number') {
return document.createTextNode(vdom)
}
const { type, props } = vdom;
// 创建真实dom
const dom = document.createElement(type);
// 把虚拟dom属性更新到真实dom上,
updateProps(dom, props)
// 把虚拟dom的儿子也变成真实dom挂在到自己的dom上。
if (typeof props.children === 'string' || typeof props.children === 'number') {
dom.textContent = props.children
} else if (typeof props.children === 'object' && props.children.type) {
// 只是有一个子元素,并且儿子是虚拟dom,把儿子变成真实dom插入到dom上
render(props.children, dom)
} else if (Array.isArray(props.children)) {
// 如果children是一个数组的时候
updateArryChild(props.children, dom)
} else {
document.textContent = props.children ? props.children.toString() : ""
}
// 把真实dom作为一个dom属性放到虚拟dom上,为以后更新准备
// vdom.dom = dom;
return dom;
}
// 将虚拟dom的props属性挂在真实dom 上
/**
*
* @param {*} dom 真实dom
* @param {*} props 虚拟dom的属性
*/
function updateProps(dom, props) {
for (let key in props) {
if (key === 'children') continue;
if (key === 'style') {
let styleObj = props.style;
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr]
}
} else {
dom[key] = props[key]
}
}
}
/**
*
* @param {*} childVdom 虚拟节点的儿子 child是一个数组的时候
* @param {*} parantdom 父节点
*/
function updateArryChild(childVdom, parantdom) {
for (let i = 0; i < childVdom.length; i++) {
let childVdoms = childVdom[i]
// 将虚拟节点挂在父节点上
render(childVdoms, parantdom)
}
}
const ReactDOM = {
render,
}
export default ReactDOM;
实现Component
子元素是组件
所以要在获取到真实dom的时候做一下判断,然后走不同的逻辑去获取真实dom
- // 创建真实dom
- const dom = document.createElement(type);
+ // 创建真实dom
+ let dom;
+ if (typeof type === 'function') {
+ if (type.isReactComponent) {
+. // 如果是类组件
+ return createDomClassComponent(vdom)
+ } else {
+ return createDomFunctionComponent(vdom) // 函数组件
+ }
+ } else {
+ dom = document.createElement(type); // 普通的
+ }
函数组件
创建真实dom react-dom.js
/**
*
* @param {*} vdom 将函数组件虚拟dom转为真实dom
*/
function createDomFunctionComponent(vdom) {
const {type: functionType, props} = vdom;
// 函数组件本质上是一个函数,执行拿到虚拟dom
const renderDom = functionType(props)
// 将其转换为真实dom
return createDom(renderDom)
}
类组件
类组件继承Component, 新建一个component.js文件,也可以写到react文件上
import ReactDOM from './React-dom'
class Component {
static isReactComponent = true; // 用来判断是不是类组件
constructor(props) {
this.props = props;
this.state = {}
}
}
export default Component
创建真实dom react-dom.js
/**
*
* @param {*} vdom 将类组件虚拟dom转为真实dom
*/
function createDomClassComponent(vdom) {
// 解构类的定义和类的属性对象
const { type, props } = vdom;
// 创建类的实例
const classInstance = new type(props)
// 调用实例的render方法返回要渲染的虚拟dom
const renderVdom = classInstance.render()
// 根据虚拟dom对象创建真实的dom对象
const dom = createDom(renderVdom)
// 为了类组件更新,把真实dom挂在实例上
classInstance.dom = dom;
return dom
}
更新类组件
将事件先挂到组件上 react-dom 文件
// 将虚拟dom的props属性挂在真实dom 上
/**
*
* @param {*} dom 真实dom
* @param {*} props 虚拟dom的属性
*/
function updateProps(dom, props) {
for (let key in props) {
if (key === 'children') continue;
if (key === 'style') {
let styleObj = props.style;
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr]
}
+ } else if (key.startsWith('on')) {
+ // 处理事件
+ dom[key.toLocaleLowerCase()] = props[key];
} else {
dom[key] = props[key]
}
}
}
在componetn.js文件上新增setState方法
import ReactDOM from './React-dom'
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.state = {}
}
+ setState(prevState) {
+ let state = this.state;
+ this.state = {...state, ...prevState}
+ // 重新生成虚拟dom
+ const newVdom = this.render()
+ // 更新
+ updateClassComponent(this, newVdom);
+ }
}
/**
*
* @param {*} instance 实例
* @param {*} vdom 虚拟dom
*/
// 这里是直接渲染了整个dom结构,没有用diff,后面再比较
function updateClassComponent(instance, vdom) {
// 取出类组件上次渲染的真实dom
const oldDom = instance.dom;
const newDom = ReactDOM.createDom(vdom);
// 用新的指向老的
oldDom.parentNode.replaceChild(newDom, oldDom);
instance.dom = newDom;
}
export default Component
我们写一个类组件
class ClassCounter extends Component {
constructor(props) {
super(props)
this.state = { number: 0 }
}
render() {
const handleClick = () => {
console.log('xxx')
this.setState({number: this.state.number + 1})
}
return (
<div onClick={handleClick}>
<span>类组件</span>
{this.state.number}
</div>
)
}
}
最后就可以顺利更新啦
批量更新
在react里,事件的更新可能是异步的,是批量的,不是同步的 setState, 比如调两次相同的setState只会执行一次调用setstate之后并没有呀立刻更新,而是缓存起来,等事件函数处理完之后,再进行批量更新,一次更新并重新渲染,(这样就不用频繁刷新组件)
比如
const handleClick = () => {
this.setState({number: this.state.number + 1})
console.log(this.state.number) // 0 异步更新
this.setState({number: this.state.number + 1})
console.log(this.state.number) // 0 异步更新
this.setState({number: this.state.number + 1})
console.log(this.state.number) // 0 异步更新,当函数执行玩,state变为1
// 调用三次setState ,其实没有变化,相当于只调用一次
setTimeout(() => {
console.log(this.state.number, '---?') // 1
this.setState({number: this.state.number + 1})
console.log(this.state.number) //2 同步更新
this.setState({number: this.state.number + 1})
console.log(this.state.number) // 3 同步更新
this.setState({number: this.state.number + 1})
console.log(this.state.number) //4 同步更新
} ,0)
// 所以不管是宏任务还是微任务都是一样的,
queueMicrotask(() => {
console.log(this.state.number, '---????') // 1
this.setState({number: this.state.number + 1})
console.log(this.state.number) //2
this.setState({number: this.state.number + 1})
console.log(this.state.number) // 3
this.setState({number: this.state.number + 1})
console.log(this.state.number) //4
})
}
setState返回一个函数也是一样的
const handleClick = () => {
this.setState((lastState) => ({number: lastState.number + 1}))
console.log(this.state.number) // 0
this.setState((lastState) => ({number: lastState.number + 1}))
console.log(this.state.number) // 0
this.setState((lastState) => ({number: lastState.number + 1}))
console.log(this.state.number) // 0
setTimeout(() => {
console.log(this.state.number) // 3
this.setState((lastState) => ({number: lastState.number + 1}))
console.log(this.state.number) // 4
this.setState((lastState) => ({number: lastState.number + 1}))
console.log(this.state.number) // 5
this.setState((lastState) => ({number: lastState.number + 1}))
console.log(this.state.number) // 6
},0)
}
setState也可以传第二个参数为一个回调函数,这个回调函数会在等函数执行完的时候才执行
结论: 事件处理函数只要归react管就是异步,比如在handlCkick里面,(事件处理函数,生命周期函数), 只要不归react管就是同步
设置一个批量更新的标志,以及存储每一个组件的updaters,当组件是批量更新的时候,将state存储起来,当标志变成为false的时候,直接更新组件
import ReactDOM from './React-dom'
export const updateQueue = {
isBatchUpdate: false, // 当前是否处于批量更新模式,默认false
updaters: new Set()
}
class Updater {
constructor(classInstance) {
this.instance = classInstance; // 类组件的实例
this.pendingState = []; // 等待生效的状态,可能是一个对象,也可能是一个函数
this.callBacks = [];
}
addState(prevState, callback) {
// 等待更新或者生效的状态
this.pendingState.push(prevState)
// 状态更新后的回调
if (typeof callback === 'function') {
this.callBacks.push(callback)
}
if (updateQueue.isBatchUpdate) {
// this 是updater的实例
updateQueue.updaters.add(this) // 如果是批量更新,缓存updater
} else {
// 否则就直接更新组件
this.updateComponent()
}
}
// isBatchUpdate 状态为false 下 直接更新组件
updateComponent() {
const { instance, pendingState, callBacks } = this;
// 如果有等待更新的状态的话,先计算出新状态
if (pendingState.length > 0) {
// 计算新状态
instance.state = this.computedState();
instance.updateStateComponet()
callBacks.forEach(cb => cb())
}
}
// 计算新state
computedState() {
const { instance, pendingState, callBacks } = this;
let { state } = instance;
pendingState.forEach((nextState) => {
// 判断等待更新的状态里面是对象还是函数
if (typeof nextState === 'function') {
nextState = nextState.call(instance, state);
}
state = {...state, ...nextState}
})
pendingState.length = 0;
callBakcs.length = 0;
return state;
}
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.state = {};
// 每一个组件都有一个,一个组件对应一个updater的实例
this.updater = new Updater(this)
}
setState(prevState, callback) {
// 将state的状态和callback保存起来
this.updater.addState(prevState, callback);
}
updateStateComponet() {
// 重新生成虚拟dom
const newVdom = this.render();
// 更新
updateClassComponent(this, newVdom);
}
}
/**
*
* @param {*} instance 实例
* @param {*} vdom 虚拟dom
*/
function updateClassComponent(instance, vdom) {
// 取出类组件上次渲染的真实dom
const oldDom = instance.dom;
const newDom = ReactDOM.createDom(vdom);
// 用新的指向老的
oldDom.parentNode.replaceChild(newDom, oldDom);
instance.dom = newDom;
}
export default Component
合成事件
使用事件代理,将event事件进行包装,可以做兼容处理,兼容不同的浏览器,也可以在事件处理函数之前和之后做一些事情,比如react事件的批量更新
我们在将虚拟dom上的事件挂载到真实dom上的时候,进行事件代理,将在要挂载的事件进行处理
// React-dom.js
import { CompositeEvent } from './CompositeEvent'
function updateProps(dom, props) {
for (let key in props) {
if (key === 'children') continue;
if (key === 'style') {
let styleObj = props.style;
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr]
}
} else if (key.startsWith('on')) {
- // 处理事件
- // dom[key.toLocaleLowerCase()] = props[key];
+ // 合成事件
+ CompositeEvent(dom, key.toLocaleLowerCase(), props[key])
} else {
dom[key] = props[key]
}
}
}
给updateQueue中添加一个方法,用来绑定到事件里面去
// component.js
export const updateQueue = {
isBatchUpdate: false, // 当前是否处于批量更新模式,默认false
updaters: new Set(),
+ batchUpdate() {
+ // 批量更新,在react事件里面刚开始的时候将isBatchUpate设置为true,执行完设置为false
+ for (let updater of this.updaters) {
+ updater.updateComponent()
+ }
+ this.isBatchUpdate = false;
+ }
}
新建一个 CompositeEvent.js 文件用来实现事件代理,不管给哪一个dom元素绑定事件,最后都要统一代理到document事件上面去,重写event事件
import { updateQueue } from "./Component";
/**
*
* @param {*} dom 真实dom
* @param {*} eventType 事件类型
* @param {*} handleEvent 事件处理函数
*/
export function CompositeEvent(dom, eventType, handleEvent) {
// 给dom绑定一个onClick事件回调函数,,handleEvent 就是于handlclick
// let store;
// if(dom.store) {
// store = dom.store
// } else {
// dom.store = {}
// store = dom.store
// }
let store = dom.store || (dom.store = {})
store[eventType] = handleEvent; // store.onclick = handleClick
if (!document[eventType]) { //事件委托 不管给哪个dom元素上绑定事件,最后统一代理到document事件上去
document[eventType] = dispatchEvent; // document.onclick = dispatchEvent
}
}
// 存储创建出来的合成事件
let eventStore = {
bubbling: false,
stopBubbling() {
this.bubbling = true;
console.log('阻止冒泡')
}
}
// event 是原生事件
function dispatchEvent(event) {
let { target, type } = event; // 事件源 button哪个dom元素,类型 type= click
const eventType = `on${type}`;
//React控制事件里面,批量更新
updateQueue.isBatchUpdate = true;
// 根据原生事件对象创建出一个合成事件对象
eventStore = createCompositeEvent(event);
// 事件冒泡
while(target) {
const { store } = target;
const handleEvent = store && store[eventType];
// 执行事件处理函数
handleEvent && handleEvent.call(target, eventStore);
if (eventStore.bubbling)
break;
target = target.parentNode;
}
// 让每一次都用eventStore这个对象,形成单例模式
for(let key in eventStore) {
eventStore[key] = null;
}
updateQueue.batchUpdate()
}
function createCompositeEvent(event) {
for (let key in event) {
eventStore[key] = event[key]
}
return eventStore
}
生命周期 15版本
- 实现 componentWillMount
- 实现 componentDidMount (要在组件渲染到页面上之后才执行,所以先保存到真实dom上,当真实dom挂载完成执行)
// react-dom
// 传入一个虚拟dom对象.和一个容器进行渲染
function render(vdom, container) {
// 将虚拟dom转为真实dom,进行渲染
const dom = createDom(vdom)
container.appendChild(dom)
+ // 组件渲染之后执行
+ dom.componentDidMount&& dom.componentDidMount()
}
/**
*
* @param {*} vdom 将类组件虚拟dom转为真实dom
*/
function createDomClassComponent(vdom) {
// 解构类的定义和类的属性对象
const { type, props } = vdom;
// 创建类的实例
const classInstance = new type(props)
// 生命周期
+ if (classInstance.componentWillMount) {
+ classInstance.componentWillMount();
+ }
// 调用实例的render方法返回要渲染的虚拟dom
const renderVdom = classInstance.render()
// 根据虚拟dom对象创建真实的dom对象
const dom = createDom(renderVdom)
// 为了类组件更新,把真实dom挂在实例上
+ if (classInstance.componentDidMount) {
+ // 组件渲染之后执行,所以在将组件挂载到root节点上的时候才执行
+ dom.componentDidMount = classInstance.componentDidMount.bind(classInstance)
+ }
classInstance.dom = dom;
return dom
}
shouldComponentUpdate
// component.js
addState(prevState, callback) {
// 等待更新或者生效的状态
this.pendingState.push(prevState)
// 状态更新后的回调
this.callBacks.push(callback)
- // 状态改变, state改变
- // if (updateQueue.isBatchUpdate) {
- // // this 是updater的实例
- // updateQueue.updaters.add(this) // 如果是批量更新,缓存updater
- // } else {
- // // 否则就直接更新组件
- // this.updateComponent()
- // }
+ this.emitUpdate()
}
// 不管组件属性变了还是状态变,都会更新 发射更新
emitUpdate (newprops) { // 传一个参数,属性改变,也就是props改变
if (updateQueue.isBatchUpdate) {
// this 是Updater的实例
updateQueue.updaters.add(this) // 如果当前的批量更新模式,先缓存updater
} else {
this.updateComponent(); // 直接更新组件
}
}
// isBatchUpdate 状态为false 下 直接更新组件
updateComponent() {
const { instance, pendingState, callBacks } = this;
// 如果有等待更新的状态的话,先计算出新状态
if (pendingState.length > 0) {
// 计算新状态
- // instance.state = this.computedState();
- // instance.updateStateComponet()
callBacks.forEach(cb => cb())
+ shouldUpdate(instance, this.computedState())
}
}
function shouldUpdate(instance, nextState) {
// 不管组件要不要刷新,其实组件的state属性一定会改变
instance.state = nextState;
// 如果有这个方法,并且方法返回值为false,则不需要哦继续更新
if (instance.shouldComponentUpdate&&!instance.shouldComponentUpdate(instance.props,instance.state)) {
return;
}
instance.updateStateComponet();
}
componentWillUpdate
componentDidUpdate
updateStateComponet() {
+ if (this.componentWillUpdate) {
+ this.componentWillUpdate()
+ }
// 重新生成虚拟dom
const newVdom = this.render();
// 更新
updateClassComponent(this, newVdom);
+ if (this.componentDidUpdate) {
+ this.componentDidUpdate()
+ }
}
生命周期 16版本
diff
------------------------------------>