Wscats/react-tutorial

React虚拟DOM和DIFF算法

Wscats opened this issue · 0 comments

VDOM

VDOM,也叫虚拟 DOM,它是仅存于内存中的 DOM,因为还未展示到页面中,所以称为 VDOM

var vdom = document.createElement("div");

上面这一句就是最简单的虚拟 DOM

var vdom = document.createElement("div");
document.body.append(vdom);

上面这两句就是把虚拟 DOM 转化为 真实 DOM,其实就是把节点 append 到页面中

常见DOM操作

常见DOM操作,就三类:增、删、改。对应的DOM操作如下:

DOM操作 DOM方法
增加一个节点 appendChild
删除一个节点 removeChild
更改一个节点 replaceChild

以前我们写代码经常会拼接完模板,简单粗暴的用$(el).html(template)整块节点替换

这样做最大的问题在于性能,如果页面比较小,问题不大,但如果页面庞大,这样会出现卡顿,用户体验会很差,所以解决办法就是差量更新

差量更新

差量更新就是只对局面的 HTML 片段进行更新。比如你加了一个节点,那么我就只更新这个节点,我无需整个模板替换。这样做效率就会提高。但问题在于,不知道哪个节点更新了,哪个节点删除了,哪个节点替换了,所以我们需要对 DOM 建模

DOM 建模,简单点说就是用一个 JS 对象来表示 VDOM。

如果我们可以用一个JS对象来表示 VDOM,那么这个对象上多一个属性(增加节点),少一个属性(删除节点),或者属性值变了(更改节点),就很清醒了

DOM 也叫 DOM 树,是一个树形结构,DOM 树上有很多元素节点,要对 VDOM 进行建模,本质上就是对一个个元素节点进行建模,然后再把节点放回 DOM 树的指定位置

JSX建模

每个节点都是由以下三部分组成

  • type : 元素类型
  • props : 元素属性
  • children : 子元素集合
{type:"div",props: null, children:[
       {type:"img",props:{"src":"avatar.png", "className":"profile"},children:[],
       {type:"h3",props: null, children:[{[user.firstName, user.lastName].join(' ')}],
]}

上面 VDOM 建模是用下面的 HTML 结构转出来的

var profile = <div>
  <img src="avatar.png" className="profile" />
  <h3>{[user.firstName, user.lastName].join(' ')}</h3>
</div>;

但这段代码并不是合法的 js 代码,它是一种被称为 jsx 的语法扩展,通过它我们就可以很方便的在 js 代码中书写 html 片段

本质上,jsx 是语法糖,上面这段代码会被 babel 转换成如下代码

pig("div", null, pig("img", {
    src: "avatar.png",
    className: "profile"
}), pig("h3", null, [user.firstName, user.lastName].join(" ")))

而上面的这段被转化的代码是 将我们的 VDOM 配合pig(一般应该是createElement函数)转化为真实 DOM

注意,如果是自定义组件<App />会转化为pig(App, null),因为组件是class App extends React.Component {}这样定义的,所以App进入createElement函数里面就会变成是一个对象

这里我们可以把这个函数放进createElement()里面生成一个 VDOM 对象,然后用生成的 VDOM 对象,配合render()生成一个 DOM 插入页面,从而转变成真实 DOM 结构

createElement()

补充createElement()方法的源代码

function createElement(type, props, ...childrens) {
    return {
        // 父标签类型,比如dev,ul等
        type: type,
        // 属性值
        props: {
            ...props,
        },
        // 子节点,比如li,字符串等
        children: childrens.length <= 1 ? childrens[0] : childrens
    };
}

render()

补充render()方法的源代码

//=>DOM的动态创建
function render(jsxObj, container, callback) {
    let {
        type,
        props,
        children
    } = jsxObj;
    let newElement = document.createElement(type);
    //=>属性和子元素的处理
    for (let attr in props) {
        if (!props.hasOwnProperty(attr)) break;
        switch (attr) {
            case 'className':
                newElement.setAttribute('class', props[attr]);
                break;
            case 'style':
                let styleOBJ = props['style'];
                for (let key in styleOBJ) {
                    if (styleOBJ.hasOwnProperty(key)) {
                        newElement['style'][key] = styleOBJ[key];
                    }
                }
                break;
                // =>CHILDREN
            case 'children':
                // 如果children放在props里面的话,这句才会有意义
                // renderChildren()
            default:
                newElement.setAttribute(attr, props[attr]);
        }
    }
    renderChildren()

    function renderChildren() {
        let childrenAry = children;
        childrenAry = childrenAry instanceof Array ? childrenAry : (childrenAry ? [childrenAry] : []);
        childrenAry.forEach(item => {
            // 如果子节点直接是字符串,进入这个分支
            if (typeof item === 'string') {
                // =>字符串:文本节点,直接增加到元素中
                newElement.appendChild(document.createTextNode(item));
            } else {
                // 如果是标签节点比如<span><img />这些都进入这个分支
                // =>字符串:新的JSX元素,递归调用RENDER,只不过此时的容器是当前新创建的newElement
                render(item, newElement);
            }
        });
    }
    console.log(newElement);
    container.appendChild(newElement);
    callback && callback();
}

transform-react-jsx

安装transform-react-jsx来实现 jsx 和 js 之间的转换

npm install babel-loader@8.0.0-beta.0 @babel/core @babel/preset-env webpack // 首先安装好 babel 环境
npm install --save-dev babel-plugin-transform-react-jsx //再安装 transform-react-jsx 插件

配置对应的 webpack 参数,如果这里把注释的那条 plugins 打开,那就不需要写另外配置 .babelrc 文件,这里默认会先使用 plugins 的配置的

const path = require('path');

const config = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /(node_modules|bower_components)/,
            use: {
                loader: 'babel-loader',
                options: {
                    // "plugins": ["transform-react-jsx"]// 如果需要配置参数注释这条,在 .babelrc上面配置
                }
            }
        }]
    }
};

module.exports = config;

配置 .babelrc 文件,如果需要对应的自定义的函数名,可以设置 pragma 的参数,不设置默认返回 React.createElement

{
  "plugins": [
    ["transform-react-jsx", {
      "pragma": "pig.yao" // default pragma is React.createElement
    }]
  ]
}

源码

参考文章