JSX
是一种看起来非常像 HTML 的 JavaScript 语法的扩展,React 使用它来描述用户界面长成什么样子。 在 React 代码执行之前,Babel 会对将 JSX 编译为 React API.
- JSX
<div className="container">
<h3>Hello React</h3>
<p>React is great </p>
</div>
- React API
React.createElement(
"div",
{
className: "container"
},
React.createElement("h3", null, "Hello React"),
React.createElement("p", null, "React is great")
);
基本概念
- 出现目的
大多数 JavaScript 框架对于 DOM 的更新效率非常低下。 例如假设你有包含十个项目的列表,你仅仅更改了列表中的第一项,大多数 JavaScript 框架会重建整个列表,这比必要的工作要多十倍。 为了解决这个问题,React 普及了一种叫做 Virtual DOM 的东西,Virtual DOM 出现的目的就是为了提高 JavaScript 操作 DOM 对象的效率。
- 是什么
在 React 中,每个 DOM 对象都有一个对应的 Virtual DOM 对象,它是 DOM 对象的 JavaScript 对象表现形式,其实就是使用 JavaScript 对象来描述 DOM 对象信息,比如 DOM 对象的类型是什么,它身上有哪些属性,它拥有哪些子元素。 可以把 Virtual DOM 对象理解为 DOM 对象的副本,但是它不能直接显示在屏幕上。
{
type: "div",
props: { className: "container" },
children: [
{
type: "h3",
props: null,
children: [
{
type: "text",
props: {
textContent: "Hello React"
}
}
]
},
{
type: "p",
props: null,
children: [
{
type: "text",
props: {
textContent: "React is great"
}
}
]
}
]
}
- 如何提升效率
精准找出发生变化的 DOM 对象,只更新发生变化的部分。 在 React 第一次创建 DOM 对象后,会为每个 DOM 对象创建其对应的 Virtual DOM 对象,在 DOM 对象发生更新之前,React 会先更新所有的 Virtual DOM 对象,然后 React 会将更新后的 Virtual DOM 和 更新前的 Virtual DOM 进行比较,从而找出发生变化的部分,React 会将发生变化的部分更新到真实的 DOM 对象中,React 仅更新必要更新的部分。 Virtual DOM 对象的更新和比较仅发生在内存中,不会在视图中渲染任何内容,所以这一部分的性能损耗成本非常小。
执行流程
- 创建 Virtual DOM
在 React 代码执行前,JSX 会被 Babel 转换为 React.createElement 方法的调用,在调用 createElement 方法时会传入元素的类型,元素的属性,以及元素的子元素,createElement 方法的返回值为构建好的 Virtual DOM 对象。
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
}
}
- 渲染 Virtual DOM 对象为 DOM 对象
通过调用 render 方法可以将 Virtual DOM 对象更新为真实 DOM 对象。 在更新之前需要确定是否存在旧的 Virtual DOM,如果存在需要比对差异,如果不存在可以直接将 Virtual DOM 转换为 DOM 对象。
// 1
function render(virtualDOM, container, oldDOM){
diff(virtualDOM, container, oldDOM)
}
function diff(virtualDOM, container, oldDOM){
if(!oldDOM){
mountElement(virtualDOM, container)
}
}
function mountElement(virtualDOM, container){
if(isFunction(virtualDOM)){
mountComponent(virtualDOM, container)
}else{
mountNativeElement(virtualDOM, container)
}
}
// 2、如果是组件
function mountComponent(virtualDOM, container){
let nextVirtualDOM = null;
// 函数组件
if(isFunctionComponent(virtualDOM)){
nextVirtualDOM = buildFunctionComponent(virtualDOM)
}else{
// 类组件
nextVirtualDOM = buildClassComponent(virtualDOM)
}
if(isFunction(nextVirtualDOM)){
mountComponent(nextVirtualDOM, container)
}else{
mountNativeElement(nextVirtualDOM, container)
}
// 处理函数组件
function buildFunctionComponent(virtualDOM){
return virtualDOM.type(virtualDOM.props || {})
}
// 处理类组件
function buildClassComponent(virtualDOM){
const component = new virtualDOM.type(virtualDOM.props || {});
const nextVirtualDOM = component.render();
return nextVirtualDOM
}
}
// 3、如果是普通DOM元素
function mountNativeElement(virtualDOM, container){
let newElement = createDOMElement(virtualDOM)
container.appendChild(newElement)
}
// 4、创建DOM元素
function createDOMElement(virtualDOM){
let newElement = null;
if(virtualDOM.type == 'text'){
newElement = document.createTextNode(virtualDOM.props.textContent)
}else{
newElement = document.createElement(virtualDOM.type)
()(newElement, virtualDOM)
}
// 保存老的虚拟DOM
newElement._virtualDOM = virtualDOM;
virtualDOM.children.forEach(child => {
mountElement(child, newElement)
});
return newElement
// 为元素添加属性
function ()(newElement, virtualDOM){
const newProps = virtualDOM.props;
Object.keys(newProps).forEach(propName => {
const newPropsValue = newProps[propName]
if(propName.slice(0, 2) === 'on'){
const eventName = propName.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)
}
}
})
}
}
- Virtual DOM 比对
在进行 Virtual DOM 比对时,需要用到更新后的 Virtual DOM 和更新前的 Virtual DOM,更新后的 Virtual DOM 目前我们可以通过 render 方法进行传递。 对于更新前的 Virtual DOM,对应的其实就是已经在页面中显示的真实 DOM 对象。其实就是通过render方法的第三个参数获取的,container.firstChild。
- 深度优先比对、忽略跨层级
function render(virtualDOM, container, oldDOM = container.firstChild) {
diff(virtualDOM, container, oldDOM)
}
function diff(virtualDOM, container, oldDOM){
const oldVirtualDOM = oldDOM && oldDOM._virtualDOM
if(!oldDOM){
mountElement(virtualDOM, container)
// 1、节点类型相同
}else if(oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type){
if(virtualDOM.type === 'text'){
updateTextNode(virtualDOM, oldVirtualDOM, oldDOM)
}else {
updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)
}
virtualDOM.children.forEach((child, i)=>{
diff(child, oldDOM.childNodes[i])
})
// 删除节点
let oldChildNodes = oldDOM.childNodes;
if(oldChildNodes.length > virtualDOM.childNodes.length){
for(let i=oldChildNodes.length; i>virtualDOM.childNodes.length; i--){
oldDOM.removeChild(oldChildNodes[i])
}
}
}else{
// 2、节点类型不同
const newElement = createDOMElement(virtualDOM)
oldDOM.parentNode.removeChild(newElement, oldDOM)
}
// 更新文本节点
function updateTextNode(virtualDOM, oldVirtualDOM, oldDOM){
if(virtualDOM.props.textContent !== oldVirtualDOM.props.textContent){
oldDOM.textContent = virtualDOM.props.textContent;
oldDOM._virtualDOM = virtualDOM;
}
}
}
- 组件更新
在 diff 方法中判断要更新的 Virtual DOM 是否是组件。 如果是组件再判断要更新的组件和未更新前的组件是否是同一个组件,如果不是同一个组件就不需要做组件更新操作。 如果是同一个组件,就执行更新组件操作,其实就是将最新的 props 传递到组件中,再调用组件的render方法获取组件返回的最新的 Virtual DOM 对象,再将 Virtual DOM 对象传递给 diff 方法,让 diff 方法找出差异,从而将差异更新到真实 DOM 对象中。
setState(state){
this.state = 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
}