React 进阶 ----- Virtual Dom与Diff 算法

一、什么叫做的JSX

JSX实际是一个javascript 语法扩展,在react 代码执行之前,babel先将JSX编译为React API,即Virtual Dom,再将Virtual Dom 转换为真实Dom对象,最后将真实Dom对象显示到页面上。所以他实际是javascript,目的是为了开发者编写页面更加轻松。

<div className = 'box'>
    <h1>你好</h1>
    <p>世界</p>
</div>

<--------------------------------------->

React.createElement(
    "div",
    {
        className:"box"
    },
    React.createElement(
        "h1",
        null,
        "你好"
    ),
    React.createElement(
        "p",
        null,
        "世界"
    ),
)

二、什么是Virtual Dom

在React中,每个Dom对象都有一个对应的Virtual DOM 对象,他是DOM对象的javaScript对象表现形式,使用javascript 对象来描述DOM对象的信息。

<div className = 'box'>
    <h1>你好</h1>
    <p>世界</p>
</div>
<---------------------------------------->

{
    type:'div',
    props:{className:"box"},
    children :[
        {
            type:'h1',
            props:null,
            children:[
                {
                    type:'text',
                    props:{
                        textContent:'你好'
                    }
                }
            ]
        },

        {
            type:'p',
            props:null,
            children:[
                {
                    type:'text',
                    props:{
                        textContent:'世界'
                    }
                }
            ]
        },
    ]
}

那么为什么使用Virtual Dom 能够提高更新的效率呢?

因为第一次生成的Dom对象就会有一个对应的Virtual Dom,在代码更新后,实际Dom更新之前,又会生成一个新的Virtual Dom,此时,将最新的Virtual Dom和旧的Virtual Dom作出对比,只把有变化的部分再更新到实际的Dom对象。

三、实现一个VirtualDom 对象

现在就模拟react创建VirtualDom 对象的过程。
首先,babel 会默认将react代码转换成React.createElement ,我们需要更改babel的配置,让jsx代码转换自己写的TinyReact.createElement 。在根目录中创建.babelrc 文件,添加如下配置

{
    "presets":[
        "@babel/preset-env",
        [
            "@babel/preset-react",
            {
                "pragma":"TinyReact.createElement"
            }
        ]
    ]
}

建一个createElement 文件

export default function createElement (type,props,...children){
    // 判断是否为普通文本,剔除无效的dom结构(if语句等)
    const childElement = [].concat(...children).reduce((result,child )=>{
        if(child !== false && child !== true && child !== null){
            if(child instanceof Object) {
                result.push(child)
            }else {
                result.push(createElement("text",{textContent:child}))
            }
        } 
        return result    
        
    },[])
    return {
        type,
        props:Object.assign({children:childElement},props),
        childElement
    }
}

四、普通VirturlDom 对象转换成真实的Dom对象

//区分是否是普通VirturlDom ,还是组件. form ---mountElement.js
import mountNativeElement from './mountNativeElement.js'
import isFunction from './isFunction.js'
import mountComponent from './mountComponent.js'
export default function   mountElement (virtualDom,container){
    // Component or NativeElement(普通jsx 元素)
    if(isFunction(virtualDom)){
    //渲染组件
        mountComponent(virtualDom,container)
    }else{
    //渲染virtual Dom 节点
        mountNativeElement (virtualDom,container)
    }   
}
//附注:
export default function isFunction(virtualDom){
    return virtualDom && typeof  virtualDom.type === "function"
}

// 如果是virtual Dom 节点 组件,则渲染  from---createDomElement.js
import mountElement from './mountElement.js'
import updateNodeElement from './updateNodeElement.js'
export default function  createDomElement (virtualDom){
    let newElement  = null
    if(virtualDom.type === "text"){
        // 文本节点
        newElement = document.createTextNode (virtualDom.props.textContent)
    }else {
        // 元素节点
        newElement = document.createElement (virtualDom.type)
        // 添加属性
        updateNodeElement (newElement,virtualDom)
    }
    // 递归创建子节点
    virtualDom.childElement.forEach(child =>{
        mountElement(child,newElement)
    })
    return newElement
}

// form ---- mountNativeElement.js
import createDomElement from './createDomElement.js'
export default function mountNativeElement (virtualDom , container){
    let newElement = createDomElement(virtualDom)
    // 将转换之后的Dom对象放置在页面中
    container.appendChild(newElement)
}

五、为Dom对象添加属性

export default function updateNodeElement (newElement,virtualDom){
    const newProps = virtualDom.props
    Object.keys(newProps).forEach(propName =>{
        // 获取属性值
        const newPropsValue = newProps[propName]
        // 判断是否是事件属性
        if(propName.slice(0,2) === "on"){
            // 事件名称
            const eventName = propsName.toLowerCase().slice(2)
            newElement.addEventListener(eventName,newPropsValue)
        }else if (propName === "value" || propName === "checked"){
            newElement[propName] = newPropsValue
        }else if(propName !== "children"){
            if(propName === "className"){
                newElement.setAttribute('class',newPropsValue)
            }else {
                newElement.setAttribute(propName,newPropsValue)
            }
        }
    })
}

六、区分组件函数式还是类组件

import isFunction from './isFunction.js'
export default function isFunctionComponent (virtualDom){
    const type  = virtualDom.type
    return (
        type &&  isFunction(virtualDom) && !(type.prototype && type.prototype.render )
        )

七、渲染函数式组件

import isFunctionComponent from './isFunctionComponent.js'
import mountNativeElement from './mountNativeElement.js'
import isFunction from './isFunction'
export default function mountComponent (virtualDom,container){
    let nextVirtualDom = null
    // 判断组件组件是函数组件还是组件
    if(isFunctionComponent(virtualDom)){
        // 函数组件
        nextVirtualDom  =  buildFunctionComponent(virtualDom)
    }
    // 判断是否返回的就是普通virtral Dom,如果不是,则继续调用
    if(isFunction (nextVirtualDom)) {
        mountComponent(nextVirtualDom,container)
    } else{
        mountNativeElement(nextVirtualDom,container)  
    }

}
function buildFunctionComponent (virtualDom) {
    // 调用virtualDom.type 的方法
    return virtualDom.type(virtualDom.props)
}

八、函数组件的prop处理

function buildFunctionComponent (virtualDom) {
    // 调用virtualDom.type 的方法
    return virtualDom.type(virtualDom.props)
}

九、类组件prop处理及渲染

function buildClassComponent (virtualDom){
    const component = new virtualDom.type(virtualDom.props || {})
    const nextVirtualDom = component.render()
    return nextVirtualDom
}
// from ---- index.js
class Alert extends TinyReact.Componet {
    constructor(props){
        super(props)
    }
    render(){
        return <div>{this.props.name}</div>
    }
}
TinyReact.render(<Alert name = "张三" />,root)
// from -----component.js

十、对比更新Dom元素

(1)元素节点相同

	// 无元素节点的时候
	if(!oldDom){
        console.log(33); //不打印
        mountElement (virtualDom, container)
   //当Dom元素都相同的时候
    }else if(oldVirtualDom && virtualDom.type === oldVirtualDom.type){
        // 当元素是text 的时候
        if(virtualDom.type === "text"){
            //更新内容
            updateTextElement(virtualDom, container, oldDom)
        } else {
        // 更新元素属性
            updateNodeElement(oldDom, virtualDom, oldVirtualDom)
        }
        // 循环内部的元素进行更新
        virtualDom.children.forEach((child,i) => {
            diff(child,oldDom,oldDom.children[i])
        })

    }
    //更新文字方法
    export default function updateTextElement(virtualDom, container, oldDom){
    if(virtualDom.props.textContent !==  oldDom.props.textContent){
        oldDom.textContent = virtualDom.props.textContent
        oldDom._virtualDom = virtualDom
    }
}
	//更新 元素方法
	export default function updateNodeElement (newElement,virtualDom,oldVirtualDom = {}){
    const newProps = virtualDom.props || {}
    const oldProps = oldVirtualDom.props  || {}
    // 替换属性的值
    Object.keys(newProps).forEach(propName =>{
        // 获取属性值
        const newPropsValue = newProps[propName] 
        const oldPropsValue = oldProps[propName] 
        if(newPropsValue !== oldPropsValue){
            // 判断是否是事件属性
            if(propName.slice(0,2) === "on"){
                // 事件名称
                const eventName = propsName.toLowerCase().slice(2)
                newElement.addEventListener(eventName,newPropsValue)
                // 移除原来的事件处理函数
                if(oldPropsValue){
                    newElement.removeEventListener(eventName,oldPropsValue)
                }
            }else if (propName === "value" || propName === "checked"){
                newElement[propName] = newPropsValue
            }else if(propName !== "children"){
                if(propName === "className"){
                    newElement.setAttribute('class',newPropsValue)
                }else {
                    newElement.setAttribute(propName,newPropsValue)
                }
            }
        }    
    })
    
    // 如果属性有被删除
    Object.keys(oldProps).forEach(propName =>{
        const newPropsValue = newProps[propName]
        const oldPropsValue = oldProps[propName]
        // 如果新的属性上没有值,则表示被删除
        if(!newPropsValue){
            if(propName.slice(0,2) === "on"){
                const EventName = propName.toLocaleLowerCase().slice(2)
                newElement.removeEventListener(EventName,oldPropsValue)
            } else if (propName !== "children"){
                newElement.removeAttribute(propName)
            }
        }
    })
}

(2)元素节点不相同(同级节点比较,深度优先)

else if ( virtualDom.type !== oldVirtualDom.type && typeof virtualDom !== "function" ){
        const newDom = createDomElement(virtualDom)
        oldDom.parentNode.replaceChild(newDom,oldDom)  
    }

十一、 删除节点

 // 如果属性有被删除
    Object.keys(oldProps).forEach(propName =>{
        const newPropsValue = newProps[propName]
        const oldPropsValue = oldProps[propName]
        // 如果新的属性上没有值,则表示被删除
        if(!newPropsValue){
            if(propName.slice(0,2) === "on"){
                const EventName = propName.toLocaleLowerCase().slice(2)
                newElement.removeEventListener(EventName,oldPropsValue)
            } else if (propName !== "children"){
                newElement.removeAttribute(propName)
            }
        }
    })

十二、setState 实现类组件更新,并添加生命周期函数
关键代码

class Alert extends TinyReact.Componet {
    constructor(props){
        super(props)
        this.state = {
            title:'title'
        }
        this.handleClick = this.handleClick.bind(this)
    }
    handleClick (){
        this.setState({title:"change title"})
    }
    render(){
        return <div>
        {this.props.name}
        {this.state.title}
        <button onClick = {this.handleClick()}>点击</button>
        </div>
    }
}
TinyReact.render(Alert ,root)

<---------------------------------------------------------------->
//from ---- component.js
export default class Componet {
    constructor(props) {
        this.props = props    
    }
    setState(state){
        this.setState = Object.assign({}, this.state, state)
        let virtualDom = this.render()
        let oldDom = this.getDom()
        let container = oldDom.parentNode
        diff(virtualDom,container,oldDom)

    }
    setDom(dom) {
        this._dom = dom
    }
    getDom(){
        return this._dom
    }
    updateProps(props){
        this.props = props
    }
    // 生命周期函数
    componentWillMount ()
    componentDidMount()
    componentWillReceiveProps()
    shouldComponentUpdate(nextProps,nextState){
        return nextProps != this.props || nextState != this.state
    }
    componentWillUpdate(nextProps,nextState){}
    componentDidUpdate(nextProps,nextState){}
    componentWillUmount(){}
}

<------------------------------------------------------ >
//form ----- diff.js
    // setState 更新类组件关键代码
    }else if ( typeof virtualDom === "function"){
        // 是组件
        diffComponent(virtualDom, oldComponent, oldDom, container)
    }

<------------------------------------------------------>
//from ------diffComponent.js

import mountElement from './mountElement'
import updateComponent from './updateComponent.js'
export default  function diffComponent (
     virtualDom,
     oldComponent, 
     oldDom, 
     container
){
    if(isSameComponent(virtualDom, oldComponent)){
        // 同一个组件,做组件更新工作,替换props
        updateComponent(virtualDom, oldComponent, oldDom, container)
    }else{
        // 不是同一个组件,直接替换组件
        mountElement(virtualDom, container, oldComponent, oldDom)

    }
}
// 判断是否是同一个组件
function isSameComponent(virtualDom,oldComponent){
    return oldComponent && virtualDom.type() === oldComponent.constructor
}
<--------------------------------->
//from -------updateComponent.js。如果是同一个组件
import diff from "./diff";

export default function updateComponent (
    virtualDom, 
    oldComponent, 
    oldDom, 
    container
){
    oldComponent.componentWillReceiveProps(virtualDom.props)
    if(oldComponent.shouldComponentUpdate(virtualDom.props)){
        let prevProps = oldComponent.props
        oldComponent.componentWillUpdate(virtualDom.props)
        // 组件更新
        oldComponent.updateProps(virtualDom.props)
        // 获取组件返回最新的 virtualDom
        let nextVirtualDom = oldComponent.render() 
        // 更新 component 组件实例对象
        nextVirtualDom.component = oldComponent
        // 比对
        diff(nextVirtualDom, container, oldDom)
        oldComponent.componentDidUpdate(prevProps)
    }   
}
<------------------------------------->
//from mountElement.js  不是同一个组件,替换删除
import mountNativeElement from './mountNativeElement.js'
import isFunction from './isFunction.js'
import mountComponent from './mountComponent.js'
//添加oldDom以替换
export default function   mountElement(virtualDom, container, oldDom) {
    // Component or NativeElement(普通jsx 元素)
    if(isFunction(virtualDom)){
        mountComponent(virtualDom, container, oldDom)
    }else{
        mountNativeElement (virtualDom, container, oldDom)
    }   
}
//from  mountNativeElement.js
import createDomElement from './createDomElement.js'
import unmountNode from './unmountNode'
export default function mountNativeElement (virtualDom , container, oldDom){
    let newElement = createDomElement(virtualDom)
    // 判断旧的Dom是否存在,存在删除
    if(oldDom){
        unmountNode(oldDom)
    }
    // 将转换之后的Dom对象放置在页面中
    container.appendChild(newElement)
    let component = virtualDom.component
    if(component){
        component.setDom(newElement)
    }
}

十三、ref 获取dom 和组件

//from index.js
  handleClick (){
        // this.setState({title:"change title"})
        console.log(this.input.value);
        // 获取sh
        console.log(this.alert);
        
    }
    render(){
        return <div>
        {this.props.name}
        {this.state.title}
        <input type="text" ref={input => {this.input = input}}></input>
        <Alert ref={alert => this.alert = alert }></Alert>
        <button onClick = {this.handleClick()} >点击</button>

        </div>
    }
<------------------------------------------>
//获取 最新Dom 元素
//from  ----createDomElement.js
if(virtualDom.props && virtualDom.props.ref){
        virtualDom.props.ref(newElement)
    }

//获取组件实例对象
//from -----mountComponent
 let nextVirtualDom = null
    let component = null 
    // 判断组件组件是函数组件还是组件
    if(isFunctionComponent(virtualDom)){
        // 函数组件
        nextVirtualDom = buildFunctionComponent(virtualDom)
    }else{
        // 类组件‘
        nextVirtualDom = buildClassComponent(virtualDom)
        component = nextVirtualDom.component
    }
    if(component){
        if(component.props && component.props.ref){
            component.props.ref(component)
        }
    }

你可能感兴趣的:(react,进阶)