- 在 React 代码执行之前,Babel 会将 JSX 转为
React.createElement
的方法调用. 如下<div className="container"> <h3>Hello React</h3> <p>React is great </p> </div>
React.createElement( "div", { className: "container" }, React.createElement("h3", null, "Hello React"), React.createElement("p", null, "React is great") );
- React.createElement将JSX转换成一个
virtualDOM
.树中有children props type 属性export default function createElement(type, props, ...children) { const childElements = [].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: childElements }, props), children: childElements } }
- 然后调用React.render去进行渲染.它的第一个参数就是要渲染的组件,它会被转为virtualDOM. 第二个参数是根节点,渲染的过程就是diff的过程.
React.render(<KeyDemo />, root)
export default function render( virtualDOM, container, oldDOM = container.firstChild ) { diff(virtualDOM, container, oldDOM) }
- ①diff对比开始,判断 oldDOM 是否存在(旧节点就是root根节点的第一个子节点root.firstChild)
- 如果diff的时候oldDOM不存在,那么根据virtualDOM.type判断是组件还是标签,并分别进行渲染
- ②如果type是函数那说明是组件,通过判断它原型上是否有render属性来区分是函数式组件还是类组件
- 如果是函数式组件,调用该函数并传入props参数,得到一个返回的virtualDOM
- 如果是类组件,用new实例化它并传入props参数,然后执行render函数,同样得到一个返回的virtualDOM并缓存到该virtualDOM的component中. 同时缓存在当前作用域中的component中.
- 这里要注意的是类组件继承了
React.Component
,子类会继承父类的所有方法 - 在React.Component中去接收props并且定义了setState函数,setDOM函数,getDOM函数,updateProps函数以及类组件的所有声明周期. setState的过程其实就是渲染的过程也就是diff对比的过程
setState(state) { this.state = Object.assign({}, this.state, state) // 获取最新的要渲染的 virtualDOM 对象 let virtualDOM = this.render() // 获取旧的 virtualDOM 对象 进行比对 let oldDOM = this.getDOM() // 获取容器 let container = oldDOM.parentNode // 实现对象 diff(virtualDOM, container, oldDOM) }
- 这里要注意的是类组件继承了
- 得到新的virtualDOM之后,又去判断是组件还是标签,如果是组件那递归第②步,否则的话进行dom的操作,也就是进入第③步
- 所有的组件最后都是由html标签组成,递归的过程就是一个洋葱代码,当到最深一层之后,再一层层往上走,这个往上走的过程其实就是创建dom到对应容器container的过程,创建完成之后去判断缓存的component是否存在,如果存在说明是类组件,调用
componentDidMount
生命周期(已经挂载).并判断类组件中props是否存在ref属性,存在的话执行component.props.ref(component)
,洋葱代码向上执行.
- ③如果是html标签
- 首先根据type创建dom
- 判断是文本节点还是元素节点,如果是文本节点用
newElement = document.createTextNode(virtualDOM.props.textContent)
创建,如果是元素节点用newElement = document.createElement(virtualDOM.type)
创建 - 并将当前虚拟dom缓存在
newElement._virtualDOM = virtualDOM
中 - 如果virtualDOM还有子结点children,那么遍历这个children,拿到子结点的virtualDOM又去判断是组件还是标签,如果是组件那递归第②步,否则的话进入第③步对dom的操作
- 知道以上循环结束之后,去判断virtualDOM的props中是否存在ref属性,有的话调用它
virtualDOM.props.ref(newElement)
- 然后返回
newElement
- 判断是文本节点还是元素节点,如果是文本节点用
- 拿到返回的newElement之后,就可以放到对应的地方了
- 如果oldDom存在
- 那插入到旧的dom之前
container.insertBefore(newElement, oldDOM)
- ④ 然后调用
unmountNode(oldDom)
进行删除旧节点操作- type为"text"的文本节点可以直接删除
oldDom.remove()
- 看一下节点是否是由组件生成的,其实就是判断
virtualDOM.component
是否存在- 如果 component 存在 就说明节点是由组件生成的,调用声明周期
component.componentWillUnmount()
- 如果 component 存在 就说明节点是由组件生成的,调用声明周期
- 看一下节点身上是否有ref属性,如果有调用
virtualDOM.props.ref(null)
- 遍历
Object.keys(virtualDOM.props)
看一下节点的属性中是否有’on’开头的事件属性- 如果有删除他们
oldDom.removeEventListener(eventName, eventHandler)
- 如果有删除他们
- 最后看oldDOM有没有childNodes,有的话遍历它并且进入深度递归执行步骤④
- 递归执行到最里层之后,往上逐步删除节点
oldDom.remove()
- type为"text"的文本节点可以直接删除
- 那插入到旧的dom之前
- 如果oldDom不存在,那么添加子节点
container.appendChild(newElement)
- 如果oldDom存在
- 如果有
virtualDOM.component
说明是类组件,那么将DOM对象存储在类组件实例对象中component.setDOM(newElement)
用于类组件setStae获取旧的 virtualDOM 对象 进行比对
- 首先根据type创建dom
- ②如果type是函数那说明是组件,通过判断它原型上是否有render属性来区分是函数式组件还是类组件
- 如果diff的时候oldDOM存在
- 如果不是组件且新旧节点type不同,那么不需要对比,直接进入第③步骤得到一个newElement并替换旧的dom对象
oldDOM.parentNode.replaceChild(newElement, oldDOM)
- 如果
typeof virtualDOM.type === "function"
是组件,那么进入组件的diff对比- 判断新旧类组件是否是同一个,如果是那么类组件已经被创建好了 直接更新就行
virtualDOM.type === oldComponent.constructor
,做类组件更新操作,涉及声明周期和diff对比- 调用
oldComponent.componentWillReceiveProps(virtualDOM.props)
执行生命周期(当props发生变化时也就是组件更新时) - 判断
oldComponent.shouldComponentUpdate(virtualDOM.props)
是否为真(指定 React 是否应该继续渲染,默认值是 true) - 为真的话调用
oldComponent.componentWillUpdate(virtualDOM.props)
生命周期(指定组件将要更新,这个时候给的是新的props,但是组件中的this.props还是旧的) - 更新组件中的props
oldComponent.updateProps(virtualDOM.props)
- 调用render函数获取组件返回的最新的 virtualDOM
nextVirtualDOM = oldComponent.render()
- 更新 component 组件实例对象
nextVirtualDOM.component = oldComponent
- 此时拿到了新旧节点可以进行diff对比了,也就是递归第①步进行对比
diff(nextVirtualDOM, container, oldDOM)
- 对比完成之后,说明当前组件更新完成调用
oldComponent.componentDidUpdate(prevProps)
生命周期
- 调用
- 如果不是类组件,根据virtualDOM.type判断是组件还是标签,并分别进行渲染(②③步)
- 判断新旧类组件是否是同一个,如果是那么类组件已经被创建好了 直接更新就行
- 如果不是组件且旧节点存在且新旧节点的type相同,那么进入html的节点对比
- 如果是文本节点
virtualDOM.type === "text"
,更新内容- 如果新旧文本内容不同那么更新内容
oldDOM.textContent = virtualDOM.props.textContent
.并且保存虚拟domoldDOM._virtualDOM = virtualDOM
- 如果新旧文本内容不同那么更新内容
- 如果是元素节点,更新元素
- 获取新旧节点对应的属性对象
- 先遍历新节点的属性对象,以新节点的属性为基准作对比
Object.keys(newProps).forEach(propName)=>{...}
- 根据propName获取新旧节点的属性值,属性值全等不做操作.属性值不等的话继续执行
- 判断属性是否是否事件属性 onClick -> click
propName.slice(0, 2) === "on"
- 获取事件名称
const eventName = propName.toLowerCase().slice(2)
- 为元素添加事件
newElement.addEventListener(eventName, newPropsValue)
- 如果有原有的事件,删除原有的事件的事件处理函数
newElement.removeEventListener(eventName, oldPropsValue)
- 获取事件名称
- 判断是否有表单元素
propName === "value" || propName === "checked"
- 如果有就直接设置它
newElement[propName] = newPropsValue
- 如果有就直接设置它
- 判断其它不为children的情况
propName !== "children"
- 如果是className
propName === "className"
- 创建它
newElement.setAttribute("class", newPropsValue)
- 创建它
- 如果是其它属性
- 直接定义它
newElement.setAttribute(propName, newPropsValue)
- 直接定义它
- 如果是className
- 判断属性是否是否事件属性 onClick -> click
- 根据propName获取新旧节点的属性值,属性值全等不做操作.属性值不等的话继续执行
- 再遍历旧节点的属性对象,判断属性被删除的情况
Object.keys(oldProps).forEach(propName)=>{...}
- 根据propName获取新节点的属性值,如果新节点属性值不存在,那么说明属性被删除了,然后分别作判断
- 判断属性是否是否事件属性 onClick -> click
propName.slice(0, 2) === "on"
- 获取事件名称
const eventName = propName.toLowerCase().slice(2)
- 删除原有的事件的事件处理函数
newElement.removeEventListener(eventName, oldPropsValue)
- 获取事件名称
- 判断其它不为children的情况
propName !== "children"
- 删除属性
newElement.removeAttribute(propName)
- 删除属性
- 判断属性是否是否事件属性 onClick -> click
- 根据propName获取新节点的属性值,如果新节点属性值不存在,那么说明属性被删除了,然后分别作判断
- 更新完当前dom节点之后,开始对它的子结点进行更新
- 遍历oldDOM的childNodes子结点将拥有key属性的子元素放置在一个单独的对象中
keyedElements
let keyedElements = {} for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) { let domElement = oldDOM.childNodes[i] if (domElement.nodeType === 1) { let key = domElement.getAttribute("key") if (key) { keyedElements[key] = domElement } } }```
- 如果keyedElements为空
let hasNoKey = Object.keys(keyedElements).length === 0
- 循环当前虚拟dom的子元素,然后通过索引值直接取旧的虚拟dom中拿值对比子节点
virtualDOM.children.forEach((child, i) => { diff(child, oldDOM, oldDOM.childNodes[i]) })
- 遍历oldDOM的childNodes子结点将拥有key属性的子元素放置在一个单独的对象中
- 如果keyedElements不为空,先循环 virtualDOM 的子元素 获取子元素的 key 属性,并去keyedElements中去进行匹配
- 如果匹配到了看看当前位置的元素是不是期望的元素,如果是的话就不做操作了,如果不是就在旧节点前插入新节点.
- 如果没匹配到那么根据virtualDOM.type判断是组件还是标签,并分别进行渲染.执行③④步virtualDOM.children.forEach((child, i) => { let key = child.props.key if (key) { let domElement = keyedElements[key] if (domElement) { // 3. 看看当前位置的元素是不是我们期望的元素 if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) { oldDOM.insertBefore(domElement, oldDOM.childNodes[i]) } } else { // 新增元素 mountElement(child, oldDOM, oldDOM.childNodes[i]) } } })
- 如果旧节点要比新节点多那么要做多余节点删除操作
- 如果没有key, 从后往前删,直到新旧节点长度一致
- 如果有key,用两个for循环,先遍历旧节点oldChildNodes,再遍历新节点virtualDOM.children,然后拿旧节点的key去新节点里面找,如果找到了就不作操作,如果没找到就执行步骤④去递归删除该元素与其子元素
// 获取旧节点 let oldChildNodes = oldDOM.childNodes // 判断旧节点的数量 if (oldChildNodes.length > virtualDOM.children.length) { if (hasNoKey) { // 有节点需要被删除 for ( let i = oldChildNodes.length - 1; i > virtualDOM.children.length - 1; i-- ) { unmountNode(oldChildNodes[i]) } } else { // 通过key属性删除节点 for (let i = 0; i < oldChildNodes.length; i++) { let oldChild = oldChildNodes[i] let oldChildKey = oldChild._virtualDOM.props.key let found = false for (let n = 0; n < virtualDOM.children.length; n++) { if (oldChildKey === virtualDOM.children[n].props.key) { found = true break } } if (!found) { unmountNode(oldChild) } } } }
- 如果旧节点要比新节点多那么要做多余节点删除操作
- 如果是文本节点
- 如果不是组件且新旧节点type不同,那么不需要对比,直接进入第③步骤得到一个newElement并替换旧的dom对象
- 如果diff的时候oldDOM不存在,那么根据virtualDOM.type判断是组件还是标签,并分别进行渲染