前言
實現ref
- 前面在做虛擬dom時,裏面會單獨把ref提取出來,這次就用上了。
- ref時初始化時用createRef造個,vdom上賦上 ,打印時就可以在current裏獲取。
class Counter extends React.Component{
static defaultProps = {name:'yehuozhili'}
constructor(props){
super(props)
this.state={number:0}
this.wrapper=React.createRef()
}
handleClick=()=>{
console.log(this.wrapper)
this.setState((state)=>({number:state.number+1}))
}
render(){
console.log('render')
return (
<div ref={this.wrapper}>
<p>{this.state.number}</p>
{
this.state.number>3?null:<Child number={this.state.number}></Child>
}
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
- 一共有2個地方可以賦ref,一個是類組件實例,一個是原生dom:
function createNativeDOM(element){
let {type,props,ref} =element
let dom = document.createElement(type)
createNativeDOMChildren(dom,element.props.children)
setProps(dom,props)
if(ref){
ref.current=dom
}
return dom
}
function createClassComponetDOM(element){
let {type,props,ref}=element
let componentInstance =new type(props)
if(ref){
ref.current=componentInstance
}
if(componentInstance.componentWillMount){
componentInstance.componentWillMount()
}
if(type.getDerivedStateFromProps){
let newState =type.getDerivedStateFromProps(props,componentInstance.state)
if(newState){
componentInstance.state={...componentInstance,...newState}
}
}
let renderElement = componentInstance.render()
componentInstance.renderElement=renderElement
element.componentInstance=componentInstance
let newDom =createDOM(renderElement)
if(componentInstance.componentDidMount){
componentInstance.componentDidMount()
}
return newDom
}
- 加個判斷就可以了。然後createRef實際就是創建個對象
function createRef(){
return {current :null}
}
實現context
- context應用有以下幾步:
- 使用createContext創建context
let ThemeContext = React.createContext(null);
- 使用context.provider包裹渲染組件,並傳遞value,製作生產者。
class FunctionPage extends Component {
constructor(props) {
super(props);
this.state = { color: 'red' };
}
changeColor = (color) => {
this.setState({ color });
}
render() {
let contextVal = { changeColor: this.changeColor, color: this.state.color };
return (
<ThemeContext.Provider value={contextVal}>
<div style={{ margin: '10px', border: `5px solid ${this.state.color}`, padding: '5px', width: '200px' }}>
page
<FunctionHeader />
<FunctionMain />
</div>
</ThemeContext.Provider>
)
}
}
- 消費者通過context的consumer包裹,即可得到生產者提供參數
class FunctionHeader extends Component {
render() {
return (
<ThemeContext.Consumer>
{
(value) => (
<div style={{ border: `5px solid ${value.color}`, padding: '5px' }}>
header
<FunctionTitle />
</div>
)
}
</ThemeContext.Consumer>
)
}
}
function createContext(defaultValue){
Provider.value=defaultValue
function Provider(props){
Provider.value=props.value
return props.children
}
function Consumer(props){
return props.children(Provider.value)
}
return {Provider,Consumer}
}
- 這樣就完成了,實際上,被這個組件包裹的元素,也就是這個組件的props.children原本是要渲染的元素,相當於做了個代理,組件內的中間件。
- 在類組件裏,如果掛了靜態屬性contextType,還可以通過this.context.xxx來取得context的屬性:
class Header extends Component {
static contextType = ThemeContext;
render() {
return (
<div style={{ border: `5px solid ${this.context.color}`, padding: '5px' }}>
header
<Title />
</div>
)
}
}
- 就是在創建類虛擬dom裏把靜態屬性裏面對象的那個值拿出來,賦給實例的context。
function createClassComponetDOM(element){
let {type,props,ref}=element
let componentInstance =new type(props)
if(type.contextType){
componentInstance.context =type.contextType.Provider.value
}
if(ref){
ref.current=componentInstance
}
if(componentInstance.componentWillMount){
componentInstance.componentWillMount()
}
if(type.getDerivedStateFromProps){
let newState =type.getDerivedStateFromProps(props,componentInstance.state)
if(newState){
componentInstance.state={...componentInstance,...newState}
}
}
let renderElement = componentInstance.render()
componentInstance.renderElement=renderElement
element.componentInstance=componentInstance
let newDom =createDOM(renderElement)
if(componentInstance.componentDidMount){
componentInstance.componentDidMount()
}
return newDom
}
function updateClassComponent(oldelement,newelement){
let componentInstance = oldelement.componentInstance
let updater = componentInstance.updater
let nextProps = newelement.props
if(oldelement.type.contextType){
componentInstance.context=oldelement.type.contextType.Provider.value
}
if(componentInstance.componentWillReceiveProps){
componentInstance.componentWillReceiveProps(nextProps)
}
if(newelement.type.getDerivedStateFromProps){
let newState =newelement.type.getDerivedStateFromProps(nextProps,componentInstance.state)
if(newState){
componentInstance.state={...componentInstance,...newState}
}
}
updater.emitUpdate(nextProps)
}
- 因爲實例上的context還是老狀態,更新了需要用虛擬dom上的新狀態把老狀態換掉。
將生命週期改爲批處理
- 由於前面寫的生命週期並不是批處理更新,所以需要更改下。
- 其中react-dom裏能解構一個方法,叫
unstable_batchedUpdates
。這個就相當於批量更新的開關。
- 先實現這個方法,然後把生命週期裏的函數傳給它就行了:
function unstable_batchedUpdates(fn){
updateQueue.ispending=true
fn()
updateQueue.ispending=false
updateQueue.batchUpdate()
}
if(componentInstance.componentDidMount){
unstable_batchedUpdates(()=>componentInstance.componentDidMount())
}
修復空數組bug
- 前面寫的還有2個bug,就是對於字符串和產生空數組會有點問題。需要改這幾個bug:
- createElement判斷有點問題,另外如果空數組情況,就不傳children了
export function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
if(typeof children==='string'||typeof children==='number'){
children ={$$typeof:REACT_TEXT_TYPE,key:null,content:children,type:REACT_TEXT_TYPE,ref:null,props:null}
}
if(!(children instanceof Array &&children.length===0)){
props.children = children;
}
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
if(typeof arguments[i + 2] === 'string')arguments[i + 2]= {$$typeof:REACT_TEXT_TYPE,key:null,content:children,type:REACT_TEXT_TYPE,ref:null,props:null}
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type,
key,
ref,
ReactCurrentOwner.current,
props,
);
}
- 做Map時的判斷,如果是undefined直接return個空Map:
function getChildrenElementsMap(oldChildrenElements){
let oldChildrenElementsMap={}
if(!oldChildrenElements){
return{}
}
if(!oldChildrenElements.length){
oldChildrenElements=[oldChildrenElements]
}
for(let i=0 ;i<oldChildrenElements.length;i++){
let oldkey = oldChildrenElements[i]?.key ||i.toString()
oldChildrenElementsMap[oldkey]=oldChildrenElements[i]
}
return oldChildrenElementsMap
}
修復渲染時propsbug
- 在render下如果新屬性沒有變動,但是組件渲染了,會導致render下新的屬性是undefined,所以要加個判斷就好了:
function shouldUpdate(componentInstance,nextProps,nextState){
let nextPropsT = nextProps?nextProps:componentInstance.props
let scu=componentInstance.shouldComponentUpdate&&!componentInstance.shouldComponentUpdate(nextPropsT,nextState)
componentInstance.props =nextPropsT
componentInstance.state = nextState
if(scu){
return false
}
componentInstance.forceUpdate()
}
修復更新屬性不正確bug
- 由於在diff時,老虛擬dom需要更新屬性,再進行復用,如果忘記更新屬性,可能導致每次渲染量都在原來基礎上增加。
- 修改updateElement函數:
function updateElement(oldelement,newelement ){
let currentDom =newelement.dom =oldelement.dom
if(oldelement.$$typeof===REACT_TEXT_TYPE&&newelement.$$typeof===REACT_TEXT_TYPE){
if(currentDom.textContent!==newelement.content)currentDom.textContent=newelement.content
}else if(oldelement.$$typeof===REACT_ELEMENT_TYPE){
updateDomProperties(currentDom,oldelement.props,newelement.props)
updateChildrenElements(currentDom,oldelement.props.children,newelement.props.children)
oldelement.props=newelement.props
}else if(oldelement.$$typeof===FUNCTION_COMPONENT){
updateFunctionComponent(oldelement,newelement)
}else if(oldelement.$$typeof===CLASS_COMPONENT){
updateClassComponent(oldelement,newelement)
}
}
- 這樣就基本沒有bug了,可以拿下面案例試一下,能正確添加刪除就ok。
class Todos extends React.Component {
constructor(props) {
super(props);
this.state = { list: [], text: '' };
}
add = () => {
console.log(this.state)
if (this.state.text && this.state.text.length > 0) {
console.log({ list: [...this.state.list, this.state.text] })
this.setState({ list: [...this.state.list, this.state.text] });
}
}
onChange = (event) => {
this.setState({ text: event.target.value });
}
onDel = (index) => {
this.state.list.splice(index, 1);
this.setState({ list: this.state.list });
}
render() {
console.log(this.state.list)
return(
<div>
<input onChange={this.onChange} value={this.state.text}></input><button onClick={this.add}>+++</button>
<ul>
{this.state.list.map((item,index)=>{
return (
<li key={index}>{item}<button onClick={this.onDel}>x</button></li>
)
})}
</ul>
</div>
)
}
}
let element = React.createElement(Todos, {});
ReactDOM.render(
element,
document.getElementById('root')
);