MyReact(一)创建和渲染元素

208 阅读4分钟

参考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["哈 哈"]);     //"哈 哈"

  • 点使用特点
  1. 必须跟一个合法的标识符—>   obj.name
  • 方括号使用特点:
  1. 可以用变量动态访问对象属性—>   obj[aVar]
  2. 运算元是一个字符串即可,不必是合法的变量命名  —> obj["name"]或者obj["4~3"]