参考Git项目:[Diact] DIY react
React的高效性在于它尽量只修改更新的dom,尽量减少操作dom的次数。方法就是利用虚拟dom,每次render之后生成一个新的vdom,把新旧vdom进行比较,只更新变化的的部分。
React里我们使用jsx语法描述组件内容,jsx代码需要先转换成vdom,再转换成dom(html元素)。第一步由React.createElement-创建React元素-完成,第二步由ReactDom.render-把React实例渲染到真实dom节点-完成。
将使用的原生js方法概览:
document.createElement(tagName) : 创建html节点
var textNode = document.createTextNode("hello"):创建文本节点,内容为"hello"
textNode.nodeValue = "world" :设置或者获取文本jsx和元素创建-MyReact.createElement
1.使用babel将jsx转换成js,替代函数是MyReact.createElement。
2.MyReact.createElement通过插件自动获取参数(type, props, ...children),创建由jsx定义的element对象。
const obj = <div id='div1'> <span class='cl1'> hello </span> world</div> 对应
{
type: 'div',
props: {
id: 'div1',
children: [
{
type: 'span',
props: {
class: 'cl1',
children:[ { type: 'TEXT_ELEMENT', props: { nodeValue: 'hello'}]
}
},
{ type: 'TEXT_ELEMENT', props: { nodeValue: 'world' }}
]
}
}//生成element,每个element必符合{type, props}的数据结构
function createElement(type, props, ...children){
props = Object.assign({}, props);
const hasChildren = children.length > 0? true: false
if(hasChildren){
props.children = [].concat(...children)
.filter(child => child != null && child !== false)
.map(child => typeof child === 'string'? createTextElement(child): child)
}
return {type, props};
}
//若为文本节点,返回值如{type:'TEXT_ELEMENT', props: {nodevalue: 'hello world'}}
function createTextElement(text){
return createElement('TEXT_ELEMENT',{nodevalue: text})
}
const MyReact = { createElement}
const test = (<div>hello<span>world!</span></div>);
console.log(test)生成的element如下:

至此我们可以顺利创建element,下一步是把element渲染到dom容器中。
渲染元素-MyReact.render
MyReact.render接收一个element和一个dom容器,创建由element定义的dom子树并将其附加到容器中。
从jsx创建element时分为文本节点和其他节点,type分别对应"TEXT_ELEMENT"和tagName (如 "div" ); MyReact.render同样需要对以上两种情况创建文本节点-document.createTextNode('')-和其他节点-document.createElement(type)
import { TEXT_ELEMENT } from './common'import MyReact from './index'
function render(element, parentDom){
const { type, props } = element
const isTextElement = type === TEXT_ELEMENT
const isListener = propName => propName.startsWith('on')
const isAttribute = propName => !isListener(propName) && propName !== 'children'
const dom = isTextElement ? document.createTextNode(''): document.createElement(type)
Object.keys(props).filter(isListener).forEach(propName => { //设置事件监听器
dom.addEventListener(propName.substring(2).toLowerCase(), props.propName)
})
Object.keys(props).filter(isAttribute).forEach(propName => { //设置属性
dom[propName] = props[propName]
}
const children = props.children || []
children.forEach(child => render(child, dom)) //递归render孩子节点,完成后追加到dom上
parentDom.appendChild(dom) ////把dom追加到parentDom上
}const test = (<div>hello<span>world!</span></div>)
console.log(test) // 测试转译能否成功
render(test, document.getElementById('root')) //能否将element渲染成dom
export default render问:为什么要导入MyReact这个“没用”的组件呢?
答:jsx转译时需要用到MyReact.createElement,须通过导入MyReact找到createElement
结果:

对比新旧虚拟dom-reconcile
目前MyReact.render可以将element对应的dom子树渲染到指定的dom容器,我们再通过浏览器将其绘制出来。但这只是一帧内容,怎么更新dom呢?简单的办法是重新调用render,并传入新的element,将parentDom中的dom子树替换成新子树。
可是重新创建所有子节点的性能成本是不可接受的。这种做法和React的核心理念不符,React高效的理由就是尽可能少更新dom。所以我们的MyReact也需要一种方法来比较当前和前一次调用生成的元素树->render,并只更新差异。
实例对象instance的结构: { element, dom, childInstances }
MyReact.render:接收element, parentDom,保留前一帧的实例,把前一帧和新的element传入对比函数reconcile,获得当前帧的实例,保存当前帧为前一帧-为了下一次render做准备。
reconcile:接收newElement, preInstance, parentDom, 当前帧实例是在前一帧实例-preInstance-上修改了属性,还是新建而来的,由此函数决定。如果当前节点的类型未变,就重用此节点,只更新属性,对于其preInstance的子实例们,只比较相同位置的-childInstances-数组中childInstance和-children-数组中的element,递归处理,最后返回前一帧实例;否则根据newElement新建实例,返回新实例。无论返回新/旧实例,都要保证parentDom的内容及时更新。
instantiate:接收element,实例化后返回实例。 结构是{ element, dom, childInstances }
Debug心得记录
1. 访问对象的属性用点(.)还是方括号([])?
错误回顾:
Object.keys(newProps).filter(isListener).forEach(propName => {
//propName是一个字符串,不可以用newProps.propName获取
dom.addEventListener(propName.toLowerCase().substring(2), newProps[propName])
})解释:
const obj = {
"name": "zwh",
"443": "443", //数字开头,非法标识符
"哈 哈": "哈 哈" //有空格,非法标识符
}
let aVar = "name";
console.log(obj[aVar]); //"zwh"console.log(obj["443"]); //"443"
console.log(obj["哈 哈"]); //"哈 哈"- 点使用特点
- 必须跟一个合法的标识符—> obj.name
- 方括号使用特点:
- 可以用变量动态访问对象属性—> obj[aVar]
- 运算元是一个字符串即可,不必是合法的变量命名 —> obj["name"]或者obj["4~3"]