手写实现react系列,实现react的常用api;参照源码简化实现,抽取核心部分,相关函数命名与源码一致。
传送
前言
还是那句话:
网上那些八股文,都是别人对某个知识点掌握后整理的描述,背下来没有任何意义!代码是数学题,不是背课文! 自己写一遍,彻底搞清楚它的实现过程原理,这样收获的才是自己的!用自己的理解总结出来的,才是真的掌握了!知其然,知其所以然!
导图:
接着上一篇继续
一、实现完整的生命周期
1.复习
先回顾一下生命周期函数有哪些,还是看图比较直接点
详情可以参考React之类组件核心及原理这里不再赘述
旧版:
新版:
上一篇我们实际上已经把旧版第一部分的initialization阶段的setup props and state已经实现了,其他很多钩子函数实际也进行了处理,这里再做一次汇总!所以直接从挂载阶段开始写!
2.实现componentWillMount
上一篇代码其实已经写了,这里回顾一下!
打开react-dom.js,定位到类组件挂载函数mountClassComponent
function mountClassComponent(vdom) {
// 挂载前……
// -------------------关键代码位置---------------------
/* 记录vdom的实例,后面需要从该属性上拿到生命周期钩子,并执行钩子*/
vdom.classInstance = classInstance;
// -------------------关键代码位置---------------------
// 挂载中……
// -------------------关键代码位置---------------------
/* 暂时把didMount方法暂存到dom上,前面的mount方法中会调用该钩子 */
if (classInstance.componentDidMount) {
/* 用bind确保其this指向始终是当前实例 */
dom.componentDidMount = classInstance.componentDidMount.bind(classInstance);
}
// -------------------关键代码位置---------------------
return dom; // 返回真实dom
}
3.实现componentDidMount
在上面的mountClassComponent中,将componentDidMount暂存到了真实dom的属性上,在插入到容器中后,直接调用该方法即可!
定位到mount方法中
function mount(vdom, container) {
// 调用createDom方法,传入虚拟dom,根据虚拟dom,创建出真实dom
let newDOM = createDOM(vdom);
// 将得到的真实dom, 插入容器中(父元素)
container.appendChild(newDOM);
// -------------------关键代码位置---------------------
// 此时如果真实dom上,存在componentDidMount,就调用该生命周期函数!
if (newDOM.componentDidMount) newDOM.componentDidMount();
// -------------------关键代码位置---------------------
}
这样只实现了第一次加载的组件,此后更新时新增的还需要继续处理
再定位到compareTwoVdom方法中,在更新时,如果是新增的组件,渲染完毕后也要触发钩子
export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
// ……
} else if (!oldVdom && newVdom) {
//如果老的没有,新的有,就根据新的组件创建新的DOM并且添加到父DOM容器中
let newDOM = createDOM(newVdom); // 创建真实dom
if (nextDOM) {
// 插入前看一下有没有位置标记,如果有,就放入指定的位置
parentDOM.insertBefore(newDOM, nextDOM);
} else {
parentDOM.appendChild(newDOM); // 没有标记位置时,直接放到最后
}
// -------------------关键代码位置---------------------
/* 新增的组件,需要触发生命周期钩子函数componentDidMount */
if (newDOM.componentDidMount) newDOM.componentDidMount();
// -------------------关键代码位置---------------------
} else if (oldVdom && newVdom && oldVdom.type !== newVdom.type) {
//新老都有,但是type不同(例如新的是div 旧的是p)也不能复用,则需要删除老的,添加新的
let oldDOM = findDOM(oldVdom); // 先获取 老的真实DOM
let newDOM = createDOM(newVdom); // 创建新的真实DOM
/* 在卸载旧的组件前,需要执行生命周期钩子函数componentWillUnmount */
if (oldVdom.classInstance && oldVdom.classInstance.componentWillUnmount) {
oldVdom.classInstance.componentWillUnmount(); // 执行组件卸载方法
}
/* 通过老的父节点,用新的把旧的替换掉 */
oldDOM.parentNode.replaceChild(newDOM, oldDOM);
// -------------------关键代码位置---------------------
/* 新的挂载完成后,需要执行生命周期钩子函数componentDidMount */
if (newDOM.componentDidMount) newDOM.componentDidMount();
// -------------------关键代码位置---------------------
} else {
// 老的有,新的也有,且类型也一样,需要复用老节点,进行深度的递归dom diff了
updateElement(oldVdom, newVdom);
}
}
4.实现componentWillReceiveProps
组件更新前对props的拦截处理,在updateClassComponent中触发该钩子
定位到updateClassComponent方法中
function updateClassComponent(oldVdom, newVdom) {
let classInstance = (newVdom.classInstance = oldVdom.classInstance);
newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
// -------------------关键代码位置---------------------
/*
触发组件的生命周期钩子componentWillReceiveProps
+ 此更新可能是由于父组件更新引起的,父组件在重新渲染的时候,给子组件传递新的属性
*/
if (classInstance.componentWillReceiveProps) {
classInstance.componentWillReceiveProps();
}
// -------------------关键代码位置---------------------
// 调用组件实例的updater方法,将新的props传递过去,递归继续更新!
classInstance.updater.emitUpdate(newVdom.props);
}
5.实现shouldComponentUpdate
是否应该更新组件
这个钩子是shouldUpdate方法中触发的,通过组件实例调用此方法,再判断是否需要更新
定位到Component.js中的shouldUpdate方法
function shouldUpdate(classInstance, nextProps, nextState) {
let willUpdate = true; // 是否要更新的标杆,默认值是true
// -------------------关键代码位置---------------------
if (classInstance.shouldComponentUpdate) {
/*
shouldComponentUpdate钩子函数的处理:
+ 如果有此方法,就将此方法的返回值,作为是否更新的标杆
+ 传入nextProps 和 nextState
*/
willUpdate = classInstance.shouldComponentUpdate(nextProps, nextState);
}
// -------------------关键代码位置---------------------
// -------------------关键代码位置---------------------
if (willUpdate && classInstance.componentWillUpdate) {
/*
componentWillUpdate钩子函数的处理
+ 如果上面允许更新,且存在该钩子函数,就执行此函数
*/
classInstance.componentWillUpdate();
}
// -------------------关键代码位置---------------------
// ……
if (willUpdate) {
/* 经过上面的判断,如果依旧可以更新,则触发实例上的forceUpdate */
classInstance.forceUpdate();
}
}
6.实现componentWillUpdate
组件将要更新的钩子
细心的你其实已经发现,上面的代码中已经实现了componentWillUpdate
7.实现componentDidUpdate
组件更新完成的钩子
这个方法是在Component构造类的forceUpdate中调用的,也就是组件实例.forceUpdate中
定位到Component构造类 的 forceUpdate中
export class Component {
// ……
forceUpdate() {
// ……更新处理
/* 拿到新的虚拟dom后,开始走更新逻辑,调用compareTwoVdom进行vdom比对 */
compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom);
/* 重置旧的vdom,将新的作为下一次的旧的,用于下一次的更新 */
this.oldRenderVdom = newRenderVdom;
// -------------------关键代码位置---------------------
if (this.componentDidUpdate) {
/*
触发componentDidUpdate生命周期钩子函数
+ 将最新的props和state,以及上面的更新快照传递过去
*/
this.componentDidUpdate(this.props, this.state, extraArgs);
}
// -------------------关键代码位置---------------------
}
}
到此,旧版的生命周期已经实现完,接下来开始实现新版生命周期
8.实现getDerivedStateFromProps
componentWillReceiveProps的替代品!作用主要是将新的props更新到state上!
替换原因:因为以有很多人在使用componentWillReceiveProps会调用this.setState经常引起死循环!
所以:这个钩子函数被设计为静态函数,它属于构造函数,避免在这个钩子函数中访问到this!
触发该钩子时,需要通过this.constructor.getDerivedStateFromProps调用
该钩子在shouldUpdate方法中调用,定位到Compondnt.js 中的 shouldUpdate方法
function shouldUpdate(classInstance, nextProps, nextState) {
// ……更新判断及处理,参考前面代码
// -------------------关键代码位置---------------------
if (classInstance.constructor.getDerivedStateFromProps) {
/*
getDerivedStateFromProps钩子函数的处理
+ 传入最新props和实例state,通过该钩子得到最新的 state
+ 如果state有状态,就作为实例的state
*/
let nextState = classInstance.constructor.getDerivedStateFromProps(
nextProps,
classInstance.state
);
if (nextState) {
classInstance.state = nextState;
}
} else {
/* 默认赋值最新的 永远指向最新的状态 */
classInstance.state = nextState;
}
// -------------------关键代码位置---------------------
if (willUpdate) {
/* 经过上面的判断,如果依旧可以更新,则触发实例上的forceUpdate */
classInstance.forceUpdate();
}
}
9.实现getSnapshotBeforeUpdate
获取更新前的快照,更新前获取dom元素等信息,一般用于处理滚动条,例如在线聊天的消息位置定位
该钩子在Component的forceUpdate方法中触发,定位到forceUpdate
export class Component {
// ……
forceUpdate() {
// ……
// -------------------关键代码位置---------------------
let extraArgs; // 快照的返回值
if (this.getSnapshotBeforeUpdate) {
/*
在更新前,调用getSnapshotBeforeUpdate生命周期钩子函数
+ 如果存在getSnapshotBeforeUpdate,就调用该钩子函数,将返回值赋值给extraArgs
*/
extraArgs = this.getSnapshotBeforeUpdate();
}
// -------------------关键代码位置---------------------
/* 拿到新的虚拟dom后,开始走更新逻辑,调用compareTwoVdom进行vdom比对 */
compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom);
/* 重置旧的vdom,将新的作为下一次的旧的,用于下一次的更新 */
this.oldRenderVdom = newRenderVdom;
if (this.componentDidUpdate) {
/*
触发componentDidUpdate生命周期钩子函数
+ 将最新的props和state,以及上面的更新快照传递过去
*/
// -------------------关键代码位置---------------------
this.componentDidUpdate(this.props, this.state, extraArgs);
// -------------------关键代码位置---------------------
}
}
}
10.实现componentWillMount
组件将要销毁时的钩子,在mountClassComponent中,组件卸载时触发
定位到react-dom.js 中的 mountClassComponent方法中
function mountClassComponent(vdom) {
// ……
/* 记录vdom的实例,后面需要从该属性上拿到生命周期钩子,并执行钩子*/
vdom.classInstance = classInstance;
// -------------------关键代码位置---------------------
/* 如果实例上,有componentWillMount,就执行该钩子函数! */
if (classInstance.componentWillMount) classInstance.componentWillMount();
// -------------------关键代码位置---------------------
/* 调用实例的render方法,jsx会编译成虚拟dom对象 */
let renderVdom = classInstance.render();
// ……
return dom; // 返回真实dom
}
二、实现ref
1.复习
先回顾一下ref的几种写法:
1) 类组件ref的3种写法
- 字符串赋值+refs取值
- 回调函数赋值
- createRef + ref赋值
class Demo extends React.Component {
ref1 = React.createRef()
getRef = () => {
console.log(this.ref1.current)
console.log(this.ref2)
console.log(this.refs.ref3) // 这种在react严格模式下会报错了
}
render () {
return <>
<div ref={this.ref1}></div>
<div ref={(ref) => { this.ref2 = ref }}></div>
<div ref="ref3"></div>
<button onClick={this.getRef}>查看ref</button>
</>
}
}
2) 函数组件ref的3种写法
- 自定义变量 + ref赋值
- createRef + ref赋值
- useRef
let ref2; // 闭包
const Demo = () => {
// createRef:每次函数更新都会重新创建,性能差
// 写在类组件中不会:因为更新的是render,不会重复new组件
const ref1 = React.createRef()
// useRef:有缓存,更新时不会重新创建
const ref3 = React.useRef()
const getRef = () => {
console.log(ref1.current)
console.log(ref2)
console.log(ref3.current)
}
return <>
<div ref={ref1}></div>
<div ref={(ref) => { ref2 = ref }}></div>
<div ref={ref3}></div>
<button onClick={getRef}>查看ref</button>
</>
}
3) 获取类组件ref
class Child extends React.Component {
inputRef = React.createRef();
inputFocus = () => {
this.inputRef.current.focus();
}
render () {
return <input ref={this.inputRef} />
}
}
class Parent extends React.Component {
childRef = React.createRef();
getChildFocus = () => {
this.childRef.current.inputFocus();
}
render () {
return (
<div>
<Child ref={this.childRef} />
<button onClick={this.getChildFocus}>获得焦点</button>
</div>
)
}
}
4) forwardRef
类组件能直接获取ref,是因为类组件创建时,会new,会产生实例!
而函数组件没有实例!每次函数执行完就销毁了!所以需要搭配辅助函数获取!
forwardRef
const Child = React.forwardRef((props, ref) => {
return <input ref={ref} />
})
class Parent extends React.Component {
childRef = React.createRef();
getChildFocus = () => {
this.childRef.current.focus();
}
render () {
return (
<div>
<Child ref={this.childRef} />
<button onClick={this.getChildFocus}>获得焦点</button>
</div>
)
}
}
5) forwardRef搭配useImperativeHandle
const Child = React.forwardRef((props, ref) => {
const childRef = React.useRef()
const inputFocus = () => {
childRef.current.focus()
}
React.useImperativeHandle(ref, () => ({
inputFocus
}))
return <input ref={childRef} />
})
class Parent extends React.Component {
childRef = React.createRef();
getChildFocus = () => {
this.childRef.current.inputFocus()
}
render () {
return (
<div>
<Child ref={this.childRef} />
<button onClick={this.getChildFocus}>获得焦点</button>
</div>
)
}
}
2.实现类组件ref
这里只实现两种ref:createRef 和 回调函数ref,不去实现淘汰掉的字符串赋值写法
1) 实现createRef
打开react.js文件,新建createRef函数
/**
* @description: 创建一个ref对象
* @return {*} 啥也没做,就是创建一个对象而已。。。
*/
function createRef () {
return { current: null }
}
// ……
// 记得导出!
const React = {
createElement,
Component,
createRef,
forwardRef
};
2) ref赋值处理
创建好的ref会被编译到props中,随后我们通过createElement方法将其放到了vdom上(和props平级)
上面通过createRef创建好了ref对象,接下来只需要对其赋值处理即可!
在createDOM挂载后赋值、在mountClassComponent更新组件后赋值
定位到createDOM方法中
function createDOM(vdom) {
let { type, props, ref } = vdom;
let dom; // 真实DOM元素
// ……
vdom.dom = dom; // 让虚拟DOM的dom属生指向它的真实DOM
// -------------------关键代码位置---------------------
if (ref) ref.current = dom; // 让ref.current属性指向真实DOM的实例
// -------------------关键代码位置---------------------
return dom; // 最后返回创建好的dom
}
定位到mountClassComponent方法中
function mountClassComponent(vdom) {
/* 取出关键属性,此时type是构造函数 */
let { type, props, ref } = vdom;
// 初始化defaultProps,类组件中的默认props
let defaultProps = type.defaultProps || {};
// new 类组件构造函数,传入props,得到类组件实例
let classInstance = new type({ ...defaultProps, ...props });
// ……
// -------------------关键代码位置---------------------
/* ref.current指向类组件的实例 */
if (ref) ref.current = classInstance;
// -------------------关键代码位置---------------------
// ……
return dom; // 返回真实dom
}
三、实现forwardRef
定位到react.js中,创建forwardRef函数
1.创建forwardRef组件
/**
* 函数组件的ref转发函数
* @param {*} render 函数组件本身
*/
function forwardRef(render) {
return {
// 一个标记为Symbol("react.forward_ref")的组件
$$typeof: REACT_FORWARD_REF_TYPE,
render, // 原来那个函数组件
};
}
2.mountForwardComponent
现在需要对forwardRef组件进行挂载处理,上一篇已经创建好了相关函数,并且做了调用判断!
这里直接实现mountForwardComponent即可!
/**
* 挂载forward_ref组件
* @param {*} vdom
*/
function mountForwardComponent(vdom) {
/* 同样先取出关键属性 */
let { type, props, ref } = vdom;
/*
type上的render函数,就是函数组件自身!调用该函数,jsx就能编译出虚拟dom!
+ 将ref属性,作为第二个参数,传递给函数组件!
+ 基于对象的堆内存原理,函数组件中对ref的修改,就能同步到父组件!
*/
let renderVdom = type.render(props, ref);
/* 这一步和之前一样,需要记录旧的虚拟dom */
vdom.oldRenderVdom = renderVdom;
/* 最后调用createDOM渲染真实dom即可! */
return createDOM(renderVdom);
}
四、高阶组件
高阶组件的概念来自高阶函数
高阶函数:函数的参数是函数,或者函数的返回值是函数,就可以说它是一个高阶函数!(其他很多语言,例如java是不能把函数作为参数的)
高阶组件的两大用途:属性代理、反向继承
1.属性代理
核心:抽离公共状态、复用逻辑、通过props传递给下级组件!
使组件拥有强大的逻辑复用能力!
示例1
import React from "./react";
import ReactDOM from "./react-dom";
/*
+ 一个显示loading状态的复用组件
+ 提供 show 和 hide 两个方法
*/
const withLoading = (OldComponent) => {
return class extends React.Component {
show = () => {
let loading = document.createElement("div");
loading.innerHTML = `<p id="loading"
style="position:absolute;top:100px;left:50%;z-index:10;background-color:gray">loading</p>`;
document.body.appendChild(loading);
};
hide = () => {
document.getElementById("loading").remove();
};
render() {
return <OldComponent {...this.props} show={this.show} hide={this.hide} />;
}
};
};
@withLoading // 可通过类的装饰器实现
class Panel extends React.Component {
render() {
return (
<div>
{this.props.title}
{/* 这里用到的show 和hide 都是从withLoading传递过来的 */}
<button onClick={this.props.show}>显示</button>
<button onClick={this.props.hide}>隐藏</button>
</div>
);
}
}
//let LoadingPanel = withLoading(Panel); // 也可直接调用函数
ReactDOM.render(<Panel title="这是标题" />, document.getElementById("root"));
实例2
import React from "./react";
import ReactDOM from "./react-dom";
function withTracker(OldComponent) {
return class extends React.Component {
state = {
x: 0,
y: 0,
};
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY,
});
};
render() {
return (
<div onMouseMove={this.handleMouseMove}>
<OldComponent {...this.state} />
</div>
);
}
};
}
function Welcome(props) {
return (
<div>
<h1>移动鼠标</h1>
<p>
当前的鼠标位置是x={props.x},y={props.y}
</p>
</div>
);
}
let Tracker = withTracker(Welcome);
ReactDOM.render(<Tracker />, document.getElementById("root"));
2.反向继承
在不修改组件代码的前提下,对组件进行改写!
需求:渲染某个别人的组件时,我想在不修改其代码的基础上,修改其内部的属性和children,请问该怎么做?
// 假如这是一个antd的按钮组件,理论上无法对其进行修改
class AntdButton extends React.Component {
state = { name: "default" };
render() {
return <button className={this.state.name}>{this.props.title}</button>;
}
}
const wapper = (component) => {
return class extends component {
state = { number: 0 };
handlerClick = () => {
this.setState({ number: this.state.number + 1 });
};
render() {
// 通过super,调用被继承组件的render,被反向继承组件的虚拟dom
const renderElement = super.render();
/* 此时element是被冻结的!你无法对其进行任何修改!需要搭配cloneElement使用! */
const newProps = {
...renderElement.props,
onClick: this.handlerClick,
};
// 借助cloneElement,将新的props和children(第三个及之后的参数)传过去,创建一个新的虚拟dom
const cloneElement = React.cloneElement(
renderElement,
newProps,
this.state.number
);
return cloneElement;
}
};
};
const WapperAntdBtn = wapper(AntdButton);
ReactDOM.render(
<WapperAntdBtn title="这是标题" />,
document.getElementById("root")
);
3.实现cloneElement
在上面的高阶组件实例中有说到,基于安全考虑,jsx编译出来的虚拟dom是无法被修改的!
所以我们在实现反向继承时,需要用到cloneElement,现在开始实现这个api(其实很简单)
/**
* 根据一个老的元素,克隆出一个新的元素
* @param {*} oldElement 老元素
* @param {*} newProps 新属性
* @param {*} children 新的儿子们
*/
function cloneElement(oldElement, newProps, children) {
if (arguments.length > 3) {
children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
children = wrapToVdom(children);
}
let props = { ...oldElement.props, ...newProps, children };
return { ...oldElement, props };
}
五、实现context
前面我们已经预留了context组件的挂载函数,以及调用判断,现在开始实现context
1.实现createContext
定位到react.js中
/**
* 创建上下文组件
* @param {*} render 函数组件本身
*/
function createContext() {
/* 标记他是一个上下文组件 */
let context = { $$typeof: REACT_CONTEXT };
/* 标记Provider组件 */
context.Provider = { $$typeof: REACT_PROVIDER, _context: context };
/* 标记Consumer属性, Consumer的type 就是 REACT_CONTEXT! */
context.Consumer = { $$typeof: REACT_CONTEXT, _context: context };
return context;
}
2.完善mountProviderComponent
上下文组件创建好后,需要完善其挂载方法
/**
* 挂载上下文的provider组件
* @param {*} vdom
*/
function mountProviderComponent(vdom) {
/* 取出关键属性 */
let { type, props } = vdom;
// 在渲染Provider组件的时候,拿到属性中的value,赋给context._currentValue
type._context._currentValue = props.value;
/* props.children就是renderDom! */
let renderVdom = props.children;
/* 同样需要记住旧的,后面比对更新会用 */
vdom.oldRenderVdom = renderVdom;
/* 调用createDOM创建真实dom */
return createDOM(renderVdom);
}
3.完善mountClassComponent
前面实际已经把代码写好了,但还是看一眼
function mountClassComponent(vdom) {
/* 取出关键属性,此时type是构造函数 */
let { type, props, ref } = vdom;
// ……
if (type.contextType) {
/*
对类组件context的处理:
+ 如果构造函数中有contextType属性,就将其_currentValue赋值给实例的context
+ contextType必须加static的原因就在这里,它是类组件自身的,而不是实例的!
*/
classInstance.context = type.contextType._currentValue;
}
// ……
return dom; // 返回真实dom
}
4.完善mountContextComponent
Consumer组件的挂载逻辑
/**
* 挂载上下文组件
* @param {*} vdom
*/
function mountContextComponent(vdom) {
/* 取出关键属性 */
let { type, props } = vdom;
/*
props返回值中的children属性,就是写在Consumer组件内的子组件
+ Consumer内的组件是一个函数!
+ 例如 <Context.Consumer value={}> {
(props)=> <Children ...props/>
}</Context.Consumer>
*/
let renderVdom = props.children(type._context._currentValue);
// 同样保留旧的虚拟dom用于下一次更新比对
vdom.oldRenderVdom = renderVdom;
/* 调用createDOM创建真实dom */
return createDOM(renderVdom);
}
5.完善updateProviderComponent
provider组件的更新逻辑
function updateProviderComponent(oldVdom, newVdom) {
/* 找到父节点的真实dom */
let parentDOM = findDOM(oldVdom).parentNode;
let { type, props } = newVdom;
/* 重新对_context._currentValue赋值 */
type._context._currentValue = props.value;
let renderVdom = props.children;
/* 比对更新 */
compareTwoVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
/* 重置oldRenderVdom */
newVdom.oldRenderVdom = renderVdom;
}
6.完善updateContextComponent
consumer组件的更新逻辑
function updateContextComponent(oldVdom, newVdom) {
/* 找到父节点的真实dom */
let parentDOM = findDOM(oldVdom).parentNode;
let { type, props } = newVdom;
/* 和挂载时一样,调用children传入新的上下文数据 */
let renderVdom = props.children(type._context._currentValue);
/* 比对更新渲染真实dom */
compareTwoVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
/* 重置oldRenderVdom */
newVdom.oldRenderVdom = renderVdom;
}
六、实现PureComponent
PureComponent就是帮我们加了一个shouldComponentUpdate钩子函数,并且在里面做了浅比较
为什么是浅比较?为了性能!如果有很深的数据,做深比较,太浪费性能!再加上react没有依赖收集!每次都是从root更新!就是慢上加慢!
export class PureComponent extends Component {
shouldComponentUpdate(nextProps, nextState) {
// utils中的浅比较方法排上用场了
return (
!shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState)
);
}
}
七、实现memo
memo和PureComponent的作用差不多
1.创建memo组件
/**
* memo组件
* @param {*} type 原组件
* @param {*} compare compare
* 第二个参数是函数 是自定义的比对逻辑,根据该返回值决定是否更新
* 如果不传,默认是浅比较!
*/
function memo(type, compare = shallowEqual) {
return {
$$typeof: REACT_MEMO,
type, //原来那个真正的函数组件
compare,
};
}
2.挂载memo组件
完善前面创建好的mountMemoComponent函数
/**
* 挂载memo组件
* @param {*} vdom
*/
function mountMemoComponent(vdom) {
/* 同样取出关键参数 */
let { type, props } = vdom;
/* type就是memo接收的函数组件,调用函数并传入props */
let renderVdom = type.type(props);
/* 记录一下老的属性对象,在更新的时候会用到 */
vdom.prevProps = props;
vdom.oldRenderVdom = renderVdom;
/* 创建真实dom */
return createDOM(renderVdom);
}
3.更新memo组件
/**
* 更新memo组件
* @param {*} oldVdom 旧的虚拟dom
* @param {*} newVdom 新的虚拟dom
*/
function updateMemoComponent(oldVdom, newVdom) {
let { type, prevProps } = oldVdom;
//比较老的属性对象和新的属性对象是否相等
// 就算未渲染该组件也必须执行render 保证hook的hookIndex能下移
let renderVdom = newVdom.type.type(newVdom.props);
/* 取出useReducer时,缓存在当前挂载实例mountingComponent(vdom) 上面的hooks */
let hookKeys = Object.keys(oldVdom.hooks);
if (
type.compare(prevProps, newVdom.props) &&
hookKeys.every((key) => hookState[key] === oldVdom.hooks[key])
) {
/*
调用compare方法,传入新旧props和state进行浅比较,如果相同,就不能更新!
同时需要通知基于useReducer绑定的所有hooks执行!!
*/
/* oldRenderVdom传递 不做更改 */
newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
/* props传递 不做更改 */
newVdom.prevProps = oldVdom.props;
/* 执行全部关联的hook */
hookKeys.forEach((key) => {
oldVdom.hooks[key] = hookState[key];
});
} else {
/* 需要更新 找到父节点的真实dom */
let parentDOM = findDOM(oldVdom).parentNode;
/* 比对更新 */
compareTwoVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
/* 重置props和oldRenderVdom */
newVdom.prevProps = newVdom.props;
newVdom.oldRenderVdom = renderVdom;
}
newVdom.hooks = oldVdom.hooks; // 将新hooks 缓存下来,下一次使用
}
接下来开始实现react中的几个常用hooks
八、实现useReducer
先看上一篇创建的几个状态
// 缓存当前挂载的实例,reducer需要用到这个属性
let mountingComponent = null;
// 这里存放着所有的状态,源码时fiber链表,这里用数组简单实现
let hookState = [];
// 当前的执行的hook的索引
let hookIndex = 0;
// 调度更新方法,数据变化后 能找到组件对应的此方法更新视图
关键点 都在注释上
/**
* 实现useReducer
* @param {*} reducer 需要执行的操作
* @param {*} initialState 初始状态
*/
export function useReducer(reducer, initialState) {
/*
从hookState中取出对应的数据,源码时fiber链表,这里用数组替代
+ 如果hookState中没有,就新增,有救取值
*/
if (!hookState[hookIndex]) {
/* 新增hook,将初始状态存进去 */
hookState[hookIndex] = initialState;
// 将对应的hooks 下标和值缓存到当前挂载的实例
if (mountingComponent && !mountingComponent.hooks) {
mountingComponent.hooks = {};
mountingComponent.hooks[hookIndex] = hookState[hookIndex];
}
}
/* 闭包缓存当前hook对应的索引! */
let currentIndex = hookIndex;
/* 改变状态的方法 */
function dispatch(action) {
/*
如果初始时传了reducer,就用reducer的返回值作为新的state
如果没传,就用传进来的直接覆盖原状态!
+ useState是useReducer的语法糖!
+ 这玩意就是redux的作者 被谷歌挖过去后写的!
*/
hookState[currentIndex] = reducer
? reducer(hookState[currentIndex], action)
: action;
/*
触发前面保存的更新函数,再补充:
+ react的每一次更新都是从根元素开始!没有做依赖收集!
+ 所以它的性能真的不如vue!别杠!
*/
scheduleUpdate();
}
/*
注意这里!!!hookIndex++ !有新的hook进来时,保证其能往数组里面加!
每次触发scheduleUpdate更新完后,hookIndex会重置!
*/
return [hookState[hookIndex++], dispatch];
}
九、实现useState
useState是useReducer的语法糖!!上面useReducer代码中的dispatch中做的处理,就是给useState的!
export function useState(initialState) {
// 调用useReducer,reducer传null,disPatch时就直接用新的state覆盖旧的
return useReducer(null, initialState);
}
十、实现useMemo
export function useMemo(factory, deps) {
/* 这一块的处理和前面的useReducer差不多 */
if (hookState[hookIndex]) {
// 如果有值,说明不是第一次是更新
// 取出上一次的memo函数,以及上一次收集的依赖
let [lastMemo, lastDeps] = hookState[hookIndex];
// 遍历比对新的依赖和旧的依赖是否相等!
let everySame = deps.every((item, index) => item === lastDeps[index]);
if (everySame) {
// 如果相等,不做任何处理,同时让hookIndex递增,确保下一个hook能正常执行
hookIndex++;
return lastMemo; // 用上一个memo作为返回值
} else {
/* 依赖发生变化,重新执行factory函数,获取最新数据 */
let newMemo = factory();
// 赋值的同时,让hookIndex++ 确保下一个hook正常实行
hookState[hookIndex++] = [newMemo, deps];
return newMemo; // 返回最新的数据
}
} else {
/* 新增hook 调用 factory的返回值作为数据*/
let newMemo = factory();
// 赋值的同时,让hookIndex++ 确保下一个hook正常实行
hookState[hookIndex++] = [newMemo, deps];
return newMemo; // 返回最新的数据
}
}
十一、实现useRef
这个可能是最简单的hook,看完上面的,这里应该不用写注释了吧。。。
这个和createRef的区别就在于,useRef有缓存,所以哪怕你能在函数组件中使用createRef,也不建议你用!
export function useRef() {
if (hookState[hookIndex]) {
return hookState[hookIndex++];
} else {
hookState[hookIndex] = { current: null };
return hookState[hookIndex++];
}
}
十二、实现useEffect
这个应该是最难的一个hook,它的特性、原理、实现、都需要掌握大量js知识!
/**
* @param {*} callback 当前渲染完成之后下一个宏任务
* @param {*} deps 依赖数组,
*/
export function useEffect(callback, deps) {
if (hookState[hookIndex]) {
/* 如果hook存在 则取出关键属性 */
let [destroy, lastDeps] = hookState[hookIndex];
/* 比对收集的依赖是否发生变化,和useMemo大致差不多 */
let everySame = deps.every((item, index) => item === lastDeps[index]);
if (everySame) {
hookIndex++; // 如果一样,就不需要做任何处理
} else {
// 销毁函数每次都是在下一次执行的时候才会触发执行
destroy && destroy(); //先执行销毁函数
setTimeout(() => {
// 在下一个宏任务中,重新调用callback将返回值作为destroy
let destroy = callback();
// 重新给hook赋值
hookState[hookIndex++] = [destroy, deps];
});
}
} else {
//初次渲染的时候,开启一个宏任务,在宏任务里执行callback,保存销毁函数和依赖数组
setTimeout(() => {
// 调用callback 如果传入的函数有返回值,就作为销毁函数存起来
let destroy = callback();
hookState[hookIndex++] = [destroy, deps];
});
}
}
十三、实现useLayoutEffect
这个钩子和useEffect的区别:
- useEffect是在宏任务中执行,会在dom渲染完成后执行!
- useLayoutEffect是在微任务中执行,会在在dom渲染时同步执行
- 其实现原理和useEffect几乎一样,只是setTimeout变成了queueMicrotask
export function useLayoutEffect(callback, deps) {
if (hookState[hookIndex]) {
let [destroy, lastDeps] = hookState[hookIndex];
let everySame = deps.every((item, index) => item === lastDeps[index]);
if (everySame) {
hookIndex++;
} else {
destroy && destroy(); //先执行销毁函数
queueMicrotask(() => {
let destroy = callback();
hookState[hookIndex++] = [destroy, deps];
});
}
} else {
//初次渲染的时候,开启一个宏任务,在宏任务里执行callback,保存销毁函数和依赖数组
queueMicrotask(() => {
let destroy = callback();
hookState[hookIndex++] = [destroy, deps];
});
}
}
十四、实现useCallback
看过上面的实现原理,相信这里已经可以不加注释了吧~
export function useCallback(callback, deps) {
if (hookState[hookIndex]) {
let [lastCallback, lastDeps] = hookState[hookIndex];
let everySame = deps.every((item, index) => item === lastDeps[index]);
if (everySame) {
hookIndex++;
return lastCallback;
} else {
hookState[hookIndex++] = [callback, deps];
return callback;
}
} else {
hookState[hookIndex++] = [callback, deps];
return callback;
}
}
十五、实现useContext
这个超级简单将传入的context上的_currentValue属性取出来就行了
function useContext(context) {
return context._currentValue;
}
十六、实现useImperativeHandle
这个也超级简单,将接收的第二个参数(函数)的返回值,赋值给第一个参数ref就行了。
function useImperativeHandle(ref, factory) {
ref.current = factory();
}
结语
到此,基本api已经全部实现,都是超级精简的代码!注重其实现原理!
如果需要代码笔记的,可以私信我~