详解:虚拟dom及dIff算法-一篇就够了(文章比较长,建议收藏)

9,892 阅读1小时+

~前言~~~~~~~~~~~~~~~~~~~~~~~~~

虚拟dom,dom-diff是面试官经常问的问题。流行的react、vue设计原理也是用到了Virtual DOM及dom-diff,即使是新版的react fiber也可看到,且因为使用了Virtual DOM为这两个框架都带来了跨平台的能力(React-Native、React VR 和 Weex),实现ssr等。再次就问大家来由浅入深的捋一捋虚拟dom,dom-diff,也实现了一版本react。。。

*文章比较长,大家耐心查看,竟然写的超了专栏字数~~~~~o(╥﹏╥)o*

在阅读文章之前,下面有几个问题,你先看看能不能答出来,这也是现在面试官经常会问到的问题,如果不知道也没关系,我们会在本文章讲到,你也可以带着这些问题来看看~

  • 1、vdom(Virtual DOM)是什么?为何会存在vdom?
  • 2、vdom如何应用,核心api是什么?
  • 3、vdom和jsx存在必然的关系吗?
  • 4、介绍一下diff算法,
  • 5、diff原理简单实现(核心)

**本文将讲解的内容目录有:

  • 1、介绍vdom(Virtual DOM)
  • 2、简述vdom(Virtual DOM)的实现流程及核心api
  • 3、模拟vdom(Virtual DOM)的实现(不包括diff部分)
  • 4、diff算法实现流程原理 (重点、难点!!!!!!)
  • 5、模拟初步的diff算法实现
  • 6、应用:模拟vdom(Virtual DOM)在react中的实现
  • 7、应用:模拟vdom(Virtual DOM)中diff算法在react的实现
  • 8、总结
  • 9、重点知识的讲解及注释(也是一些面试官经常会问的问题

其中dom-diff算法是虚拟dom的核心,重点,难点。

正文~~~~~~~~~~~~~~~~~~~~~~~

为什么要了解虚拟dom呢?react和vue两个框架为啥都使用虚拟dom呢?是怎么实现的呢?对性能是否有优化呢?为啥又有说虚拟dom已死呢?我们在学习虚拟dom中能借鉴到什么呢?

一开始呢?先不从react或者vue中的虚拟dom、dom-diff算法源码入手,先从浅入深自己写一版,虚拟dom及dom-diff是为什么出现的,慢慢来~~~

1、介绍Virtual DOM

Virtual DOM是对DOM的抽象,本质上是JavaScript对象,这个对象就是更加轻量级的对DOM的描述,提高重绘性能。

1.1)dom是什么?

DOM 全称为“文档对象模型”(Document Object Model),JavaScript 操作网页的接口。它的作用是将网页转为一个 JavaScript 对象,从而可以用脚本进行各种操作(比如增删内容)。

案例:

真实dom:(代码1.1)

<ul id='list'>
  <li class='item'>itemA</li>
  <li class='item'>itemB</li>
</ul>

而我们在js中获取时,所用代码

let ulDom = document.getElementById('list');
console.log(ulDom);

1.2)什么是虚拟DOM

Virtual DOM(虚拟DOM)是对DOM的抽象,本质上是JavaScript对象,这个对象就是更加轻量级的对DOM的描述。简写为vdom。

比如上边的例子:真是DOM(代码1.1)

<ul id='list'>
  <li class='item'>itemA</li>
  <li class='item'>itemB</li>
</ul>

而虚拟DOM是:代码:1.2(比照上边的案例1.1中的真是dom树结构实现如下的js对象)

{  
    tag:'ul',  // 元素的标签类型
    attrs:{  //  表示指定元素身上的属性
        id:'list'
    },
    children:[  // ul元素的子节点
        {
            tag: 'li',
            attrs:{
                className:'item'
            },
            children:['itemA']
        },
        {   tag: 'li',
            attrs:{
                className:'item'
            },
            children:['itemB']
        }
    ]
}

虚拟DOM这个对象(代码:1.2)的参数分析:

  • tag: 指定元素的标签类型,案例为:'ul' (react中用type)
  • attrs: 表示指定元素身上的属性,如id,class, style, 自定义属性等(react中用props)
  • children: 表示指定元素是否有子节点,参数以数组的形式传入,如果是文本就是数组中为字符串

1.3)为啥会存在虚拟dom?

既然我们已经有了DOM,为什么还需要额外加一层抽象?

  • 首先,我们都知道在**前端性能优化**的一个秘诀就是尽可能少地操作DOM,不仅仅是DOM相对较慢,更因为频繁变动DOM会造成浏览器的回流或者重绘(重绘和回流的讲解部分:9.1),这些都是性能的杀手,因此我们需要这一层抽象,在patch过程中尽可能地一次性将差异更新到DOM中,这样保证了DOM不会出现性能很差的情况.
  • 其次,现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率.
  • 打开了函数式UI编程的大门,
  • 最后,也是Virtual DOM最初的目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR(服务端渲染),那么一个方式就是借助Virtual DOM,因为Virtual DOM本身是JavaScript对象. 而且在的ReactNative,React VR、weex都是使用了虚拟dom。

为啥说dom操作是“昂贵”的,js运行效率高?

例如:我们只在页面创建一个简单的div元素,打印出来,我们输出可以看到

(代码:1.3)

var div = document.createElement(div);
var str = '';
for(var key in div){
    str += key+' ';
}
console.log(str)

img

(图1.1)

如图1.1所示,真正的DOM元素是非常庞大的,因为浏览器的标准就把DOM设计的非常复杂。当我们频繁的去做DOM更新,导致页面重排,会产生一定的性能问题。

为了更好的了解虚拟dom,在这之前需要了解浏览器的运行机制(浏览器的运行机制:9.1)

1.4)虚拟dom的缺点

  • 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。虚拟 DOM 需要在内存中的维护一份 DOM 的副本。
  • 如果你的场景是虚拟 DOM 大量更改,这是合适的。但是单一的,频繁的更新的话,虚拟 DOM 将会花费更多的时间处理计算的工作。比如,你有一个 DOM 节点相对较少页面,用虚拟 DOM,它实际上有可能会更慢。但对于大多数单页面应用,这应该都会更快。这也是为啥react和vue中的更新用了异步的方法,频繁更新时,只更新最后一次的。

1.5)总结:

  • 虚拟dom是一个js对象

  • DOM操作是”昂贵“的,js运行效率高

  • 尽量减少DOM操作,而不是”推到重来“

  • 项目越复杂,影响越严重

  • 更好的跨平台

    ——————vdom即可解决这些问题————————

2、简述vdom的实现流程及核心api

前端框架中react和vue均不同程度的使用了虚拟dom的技术,因此通过一个简单的库来学习虚拟dom技术在由浅及深的了解就十分必要了

至于为什么会选择snabbdom.js这个库呢?原因主要有两个:

  • 源码简短。
  • 流行的vue框架的虚拟dom实现也是参考了snabbdom.js的实现。 而react的虚拟dom也是很相似。

我们借用snabbdom库来讲解一下:

api:snabbdomgithub.com/snabbdom/sn…

当然你还可以看库virtual-domgithub.com/Matt-Esch/v…

2.1 snabbdom.js 的虚拟dom实现案例

如果要我们自己去实现一个虚拟dom,可根据snabbdom.js库实现过程的以下三个核心问题处理:

  • compile,如何把真实DOM编译成vnode虚拟节点对象。(通过h函数)
  • diff,通过算法,我们要如何知道oldVnode和newVnode之间有什么变化。(内部diff算法)
  • patch, 如果把这些变化用打补丁的方式更新到真实dom上去。

我看一下是你snabbdom上的案例

图(2.1.1)

比照snabbdom上的案例实现:我们先使用snabbdom库来看看效果,

第一步:新建html文件(demo.html),只有一个空的id为container的div标签

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
     <title>Document</title>
</head>
<body>
    <div id="container"></div>
</body>
</html>

第二步:引入snabbdom库,本篇内容用cdn形式引入,因要引入多个js,需要注意版本的一致

<!--  引入相关snabbdom  需要注意版本一致 开始-->
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script>
<!--  引入相关snabbdom  需要注意版本一致 开始-->

第三步:初始化,定义h函数

<script>
    let snabbdom = window.snabbdom;

    // 定义 h
    let h = snabbdom.h;

   //  h函数返回虚拟节点
    let vnode = h('ul',{id:'list'},[
        h('li',{'className':'item'},'itemA'),
        h('li',{'className':'item'},'itemB')
    ]);

    console.log('h函数返回虚拟dom为',vnode);

</script>

(图2.2.1)

我们从上变的代码可以发现:

h 函数接受是三个参数,分别代表是 DOM 元素的标签名、属性、子节点(children有多个子节点),最终返回一个虚拟 DOM 的对象;我可以看到在返回的虚拟节点中还有key(节点的唯一标识)、text(如果是文本节点时对应的内容)

第四步:定义patch,更新vnode

    //定义 patch
    let patch = snabbdom.init([
        snabbdom_class,
        snabbdom_props,
        snabbdom_style,
        snabbdom_eventlisteners
    ]);
    // 获取container的dom
    let container = document.getElementById('container');
    // 第一次patch 
    patch(container,vnode);

我们在运行浏览器如下图:

(图2.2.2)

ps:我们从图2.2.2中可以看到渲染成功了,需要注意的是在第一次patch的时候vnode是覆盖了原来的真是dom(

),这跟react中的render不同,render是在此dom上增加子节点

第五步:增加按钮,点击触发事件,触发第二次patch方法

<button id="btn-change">Change</button>

1)如果我们的新节点(虚拟节点)没有改变时,

    // 添加事件,触发第二次patch

    let btn = document.getElementById('btn-change');
    document.addEventListener('click',function (params) {
        let newVnode = h('ul#list',{},[
                h('li.item',{},'itemA'),
                h('li.item',{},'itemB')
        ]);
        // 第二次patch
        patch(vnode,newVnode);
    });

因为vnode和newVnode的结构是一样的,这时候我们查看浏览器,点击事件发现没有渲染

2)我们将newVnode改一下

document.addEventListener('click',function (params) {
        let newVnode = h('ul#list',{},[
            h('li.item',{},'itemC'),
            h('li.item',{},'itemB'),
            h('li.item',{},'itemD')
        ]);
        // 第二次patch
        patch(vnode,newVnode);
});

(图2.2.3)

整个demo.html代码如下:(代码2.3.1)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
     <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <button id="btn-change">Change</button>
<!--  引入相关snabbdom  需要注意版本一致 开始-->
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script>
<!--  引入相关snabbdom  需要注意版本一致 开始-->

<script>
    let snabbdom = window.snabbdom;

    // 定义 h
    let h = snabbdom.h;

   //  h函数返回虚拟节点
    let vnode = h('ul#list',{},[
        h('li.item',{},'itemA'),
        h('li.item',{},'itemB')
    ]);

    console.log('h函数返回虚拟dom为',vnode);

    //定义 patch
    let patch = snabbdom.init([
        snabbdom_class,
        snabbdom_props,
        snabbdom_style,
        snabbdom_eventlisteners
    ]);
    // 获取container的dom
    let container = document.getElementById('container');
    // 第一次patch
    patch(container,vnode);

    // 添加事件,触发第二次patch

    let btn = document.getElementById('btn-change');

// newVnode 更改
    document.addEventListener('click',function (params) {
        let newVnode = h('ul#list',{},[
            h('li.item',{},'itemC'),
            h('li.item',{},'itemB'),
            h('li.item',{},'itemD')
        ]);
        // 第二次patch
        patch(vnode,newVnode);
    });
</script>
</body>
</html>

2.2 react中初步虚拟Dom案例效果

不了解React的可以去查看官网地址:facebook.github.io/react/docs/…

react中使用了jsx语法,与snabbdom不同,会先将代码通过babel转换。另外,主要

例子:2.2.1 dom tree定义

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<h1>hello,lee</h1>, document.getElementById('root'));

我们看例子2.2.1时,发现引入了react好像代码中没有用到??我们将代码方法<h1>hello,lee</h1>(在js中这样写一段html语言,这是一个jsx语法9.2)放入 www.babeljs.cn/repl 中解析一下发现代码为:React.createElement("h1", null, "hello,lee");

(图2.3.1)

有子节点的 tree

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
<ul id="list">
  <li class="item">itemA</li> 
  <li class="item">itemB</li> 
</ul>, 
document.getElementById('root'));

编译后

React.createElement("ul", {
  id: "list"
}, React.createElement("li", {
  class: "item"
}, "itemA"), React.createElement("li", {
  class: "item"
}, "itemB"));

ps:

  • react.js 是 React 的核心库
  • react-dom.js 是提供与DOM相关的功能,内部比较重要的方法是render,它用来向浏览器里插入DOM元素

例子:2.2.2 函数组件

import React from 'react';
import ReactDOM from 'react-dom';

function Welcome(props){
   return (

       <h1>hello ,{props.name}</h1>

   )
}

ReactDOM.render( <Welcome name='lee' /> , document.getElementById('root'));

上边的welcome是函数组件,函数组件接收一个单一的props对象并返回了一个React元素,通过babel编译可以看到如下:

function Welcome(props) {
  return React.createElement("h1", null, "hello ,", props.name);
}

例子:2.2.3 类组件

import React from 'react';
import ReactDOM from 'react-dom';

class Welcome1 extends React.Component{
    render(){
       return (
       <h1>hello ,{this.props.name}</h1>
   ) 
    }
}
ReactDOM.render( < Welcome1 name = 'lee' / > , document.getElementById('root'));

welcome1是类组件编译返回的是如下:

class Welcome1 extends React.Component {
  render() {
    return React.createElement("h1", null, "hello ,", this.props.name);
  }

}

上述在没有编译前的写法属于jsx语法(jsx讲解部分:9.2

例子:2.2.4文本

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render( '<h1>hello,lee</h1>' , document.getElementById('root'));

此时将会把'<h1>hello,lee</h1>'作为文本插入到页面。

总结:

1、React.createElement()函数:跟snabbdom中的h函数很类似,

h函数:[类型,属性,子节点]三个参数最后一个如果有子节点时放到数组中;

React.createElement(): 第一个参数也是类型,第二个参数表示属性值,第三个及之后表示的是子节点。

2、ReactDOM.render():就类似patch()函数了,只是参数顺序颠倒了。

ReactDOM.render():第一个参数是vnode,第二个是要挂在的真实dom;

patch()函数:第一个参数vnode虚拟dom,或者是真实dom,第二个参数vnode;

ps:注意:

  • React元素不但可以是DOM标签,还可以是用户自定义的组件

  • 当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)转换为单个对象传递给组件,这个对象被称之为 props

  • 组件名称必须以大写字母开头

  • 组件必须在使用的时候定义或引用它

  • 组件的返回值只能有一个根元素

  • render()时注意

    1、需要注意特殊处理一些属性,如:style、class、事件、children等

    2、定义组件时区分类组件和函数组件及标签组件

3、模拟vdom的实现

我们在这边重新创建一个项目来实现,为了启动服务使用webpack来进行打包,webpack-dev-server启动.

3.1 搭建开发环境,初始化项目

第一步:创建空文件夹lee-vdom,在初始化项目:npm init -y ,如果你让上传git最好创建一个忽略文件来把忽略一些不必要的文件.gitignore

第二步:安装依赖包

npm i webpack webpack-cli webpack-dev-server -D

第三步:配置package.json中scripts部分

"scripts": {   
    "build": "webpack --mode=development",
    "dev": "webpack-dev-server --mode=development --contentBase=./dist"
  },

第四步:在项目根目录下新建一个src目录,在src目录下新建一个index.js文件(ps:webpack默认入口文件为src目录下的index.js,默认输出目录为项目根目录下的dist目录)

我们可以在index.js中输入测试文件输出

console.log("测试vdom src/index.js")

第五步: 执行npm run build 打包输出,此时我们查看项目,会发现在根目录下生成一个dist目录,并在dist目录下打包输出了一个main.js,然后我们在dist目录下,新建一个index.html,器引入打包输出的main.js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vdom dom-diff</title>
</head>
<body>
    <div id="app"></div>
    <script src="./main.js"></script>
</body>
</html>

第六步:执行npm run dev 启动项目,然后在浏览器中输入http://localhost:8080 ,发现浏览器的控制台输出了 测试vdom src/index.js ,表示项目初始化成功。

3.2 实现虚拟dom

我们根据上述2.3.1 的代码发现核心api:h函数和patch函数,本小节主要内容是:手写一般可以生成虚拟节点的h函数,和第一次渲染的patch函数。

回忆代码:如之前的图2.2.1

(图2.2.1)

h('<标签名>',{...属性...},[...子元素...]) //生成vdom节点的
h('<标签名>',{...属性...},'文本结点')
patch(container,vnode) //render //打补丁渲染dom的
第一步:新增src\vdom\index.js统一导出

我们将这些方法都写入到src下的vdom目录中,在通过src\vdom\index.js统一导出。

import h from './h';

// 统一对外暴露的虚拟dom实现的出口
export  {
    h
}
第二步:创建src\vdom\h.js

(图3.2.1)

  • 创建h函数方法,返回的是如图3.2.1的虚拟dom对象,
  • 传入的参数h('<标签名>',{...属性...},[...子元素...])//生成vdom节点的 h('<标签名>',{...属性...},'文本结点')
  • 需要注意要从属性中分离key,它是唯一值,没有的时候undefined

从图3.2.1中我们可以通过h函数方法,返回的虚拟dom对象的参数大概有:

sel,data,children,key,text,elm

h.js初步代码:

import vnode from './vnode';
/**
   * @param {String} sel 'div' 标签名  可以是元素的选择器 可参考jq
   * @param {Object} data {'style': {background:'red'}} 对应的Vnode绑定的数据
   属性集 包括attribute、 eventlistener、 props, style、hook等等 
   * @param {Array} children [ h(), 'text'] 子元素集
   *  text当前的text 文本 itemA
   *  elm 对应的真是的dom 元素的引用
   *  key  唯一 用于不同vnode之前的比对
   */


function h(sel, data, ...children) {
    return vnode(sel,data,children,key,text,elm);
}
export default h;
第三步:创建 src\vdom\vnode.js
// 通过 symbol 保证唯一性,用于检测是不是 vnode
const VNODE_TYPE = Symbol('virtual-node')

/**
   * @param {String} sel 'div' 标签名  可以是元素的选择器 可参考jq
   * @param {Object} data {'style': {background:'red'}} 对应的Vnode绑定的数据
   属性集 包括attribute、 eventlistener、 props, style、hook等等 
   * @param {Array} children [ h(), 'text'] 子元素集
   * @param {String} text当前的text 文本 itemA
   * @param {Element} elm 对应的真是的dom 元素的引用
   * @param {String} key  唯一 用于不同vnode之前的比对
   * @return {Object}  vnode  
   */

function vnode(sel, data = {}, children, key, text, elm) {
    return {
        _type: VNODE_TYPE,
        sel,
        data,
        children,
        key,
        text,
        elm
    }
}
export default vnode;

ps:代码理解注意:

1、构造vnode时内置的_type,值为symbol(symbol:9.3).时利用 symbol 的唯一性来校验 vnode ,判断是不是虚拟节点的一个依据。

2、vnode的children/text 不可共存,例如:但是我们在写的时候还是h('li',{},'itemeA'),我们知道这个子节点itemA是作为children传给h函数的,但是它是文本节点text, 这是为什么呢?其实这只是为了方便处理,text 节点和其它类型的节点处理起来差异很大。 h('p',123) —> <p>123</p> 如:h('p,[h('h1',123),'222']) —> <p><h1>123</h1>222</p>

  • 可以这样理解,有了 text 代表该 vnode 其实是 VTextNode,仅仅是 snabbdom 没有对 vnode 区分而已。
  • elm 用于保存 vnode 对应 DOM 节点。
第四步: 完善h.js
/** 
 * h函数的主要工作就是把传入的参数封装为vnode
 */

import vnode from './vnode';
import {
  hasValidKey,
  isPrimitive, isArray
} from './utils'


const hasOwnProperty = Object.prototype.hasOwnProperty;

/**
 * RESERVED_PROPS 要过滤的属性的字典对象
 * 在react源码中hasValidRef和hasValidKey方法用来校验config中是否存在ref和key属性,
 *  有的话就分别赋值给key和ref变量。 
 * 然后将config.__self和config.__source分别赋值给self和source变量, 如果不存在则为null。
 * 在本代码中先忽略掉ref、 __self、 __source这几个值
 */
const RESERVED_PROPS = {
  key: true,
  __self: true,
  __source: true
}
// 将原来的props通过for in循环重新添加到props对象中,
// 且过滤掉RESERVED_PROPS里边属性值为true的值
function getProps(data) {
  let props = {};
  const keys = Object.keys(data);
  if (keys.length == 0) {
    return data;
  }
  for (let propName in data) {
    if (hasOwnProperty.call(data, propName) && !RESERVED_PROPS[propName]) {
      props[propName] = data[propName]
    }
  }
  return props;
}

/**
 * 
 * @param {String} sel 选择器
 * @param {Object} data  属性对象
 * @param  {...any} children 子节点集合
 * @returns {{
   sel,
   data,
   children,
   key,
   text,
   elm}
 }
 */
function h(sel, data, children) {
  let props = {},c,text,key; 
  // 如果存在子节点 
  if (children !== undefined) {
    // // 那么h的第二个参数就是
    props = data;    
    if (isArray(children)) {
      c = children;
    } else if (isPrimitive(children)) {
      text = children;
    }
    // 如果children
  } else if(data != undefined){ // 如果没有children,data存在,我们认为是省略了属性部分,此时的data是子节点
    // 如果是数组那么存在子节点
    if (isArray(data)) {
      c = data;
    } else if (isPrimitive(data)) {
      text = data;
    }else {
      props = data;
    }
  }
  // 获取key
  key = hasValidKey(props) ? props.key : undefined;
  props = getProps(props);
  if(isArray(c)){
    c.map(child => {
      return isPrimitive(child) ? vnode(undefined, undefined, undefined, undefined, child) : child
    })    
  } 
  // 因为children也可能是一个深层的套了好几层h函数所以需要处理扁平化
  return vnode(sel, props, c, key,text,undefined);
}
export default h;

增加帮助js,src\vdom\utils.js

/**
 * 
 * 一些帮助工具公共方法
 */

// 是否有key,
/**
 * 
 * @param {Object} config 虚拟dom树上的属性对象
 */
function hasValidKey(config) {
    config = config || {};
    return config.key !== undefined;
}
// 是否有ref,
/**
 * 
 * @param {Object} config 虚拟dom树上的属性对象
 */
function hasValidRef(config) {
    config = config || {};
    return config.ref !== undefined;
}

/**
 * 确定是children中的是文本节点
 * @param {*} value 
 */
function isPrimitive(value) {
    const type = typeof value;
    return type === 'number' || type === 'string'
}
/**
 * 判断arr是不是数组
 * @param {Array} arr 
 */
function isArray(arr){
    return Array.isArray(arr);
}

function isFun(fun) {
    return typeof fun === 'function';
}
/**
 * 判断是都是undefined* 
 *
 */
function isUndef(val) {
  return val === undefined;
}

export  {
    hasValidKey,
    hasValidRef,
    isPrimitive,
    isArray,
    isUndef
}
第五步:初步渲染

增加patch,src\vdom\patch.js

// 不考虑hook

import htmlApi from './domUtils';
import {
    isArray, isPrimitive,isUndef
} from './utils';

// 从vdom生成真是dom
function createElement(vnode) {
  let {sel,data,children,text,elm}  = vnode;
  // 如果没有选择器,则说ing这是一个文本节点
  if(isUndef(sel)){
    elm = vnode.elm = htmlApi.createTextNode(text);
  }else{
    elm = vnode.elm = analysisSel(sel);
    // 如果存在子元素节点,递归子元素插入到elm中引用
    if (isArray(children)) {
      // analysisChildrenFun(children, elm);  
      children.forEach(c => {
        htmlApi.appendChild(elm, createElement(c))
      });
    } else if (isPrimitive(text)) {
      // 子元素是文本节点直接插入当前到vnode节点
      htmlApi.appendChild(elm, htmlApi.createTextNode(text));
    }
  }
  
 return vnode.elm;


}
function patch(container, vnode) {
    console.log(container, vnode);
    let elm = createElement( vnode);
    console.log(elm);
    container.appendChild(elm);
};

/**
 * 解析sel 因为有可能是 div# divId.divClass - > id = "divId"
 class = "divClass"
 *
 * @param {String} sel
 * @returns {Element} 元素节点
 */
function analysisSel(sel){
  if(isUndef(sel)) return;
  let elm;
  let idx = sel.indexOf('#');
  let selLength = sel.length;
  let classIdx = sel.indexOf('.', idx);
  let idIndex = idx > 0 ? idx : selLength;
  let classIndex = classIdx > 0 ? classIdx : selLength;
  let tag = (idIndex != -1 || classIndex != -1) ? sel.slice(0, Math.min(idIndex, classIndex)) : sel;
  // 创建一个DOM节点 并且在虚拟dom上elm引用
  elm = htmlApi.createElement(tag);
  // 获取id #divId -> divId
  if (idIndex < classIndex) elm.id = sel.slice(idIndex + 1, classIndex);
  // 如果sel中有多个类名 如 .a.b.c -> a b c
  if (classIdx > 0) elm.className = sel.slice(classIndex + 1).replace(/\./g, ' ');
  return elm;
}
  // 如果存在子元素节点,递归子元素插入到elm中引用
function analysisChildrenFun(children, elm) {
   children.forEach(c => {
       htmlApi.appendChild(elm, createElement(c))
   });
}


export default patch;

我们可以看到增加了一些关于dom操作的方法src\vdom\domUtils.js

/** DOM 操作的方法
 * 元素/节点 的 创建、删除、判断等
 */

 function createElement(tagName){
    return document.createElement(tagName);
 }

 function createTextNode(text) {
     return document.createTextNode(text);
 }
function appendChild(node, child) {
    node.appendChild(child)
}
function isElement(node) {
    return node.nodeType === 1
}

function isText(node) {
    return node.nodeType === 3
}

export const htmlApi = {
    createElement,
    createTextNode,
    appendChild
}
 export default htmlApi;
第六步:我们来一段测试看看:

src\index.js

import { h,patch } from './vdom';
  //  h函数返回虚拟节点
  let vnode = h('ul#list', {}, [
      h('li.item', {}, 'itemA'),
      h('li.item', {}, 'itemB')
  ]);
  let container = document.getElementById('app');
  patch(container, vnode);
  console.log('自己写的h函数返回虚拟dom为', vnode);

(图3.2.2)

如图3.2.2,说明初步渲染成功了。

第七步:处理属性

之前的代码createElement函数中我们看到没有对h函数的data属性处理,因为比较复杂,我们来先看看snabbdom中的data参数都是怎么处理的。

主要包括几类的处理:

  1. class:这里我们可以理解为动态的类名,sel上的类可以理解为静态的,例如上面class:{active:true}我们可以通过控制这个变量来表示此元素是否是当前被点击
  2. style:内联样式
  3. on:绑定的事件类型
  4. dataset:data属性
  5. hook:钩子函数

例子:

vnode = h('div#divId.red', {
    'class': {
        'active': true
    },
    'style': {
        'color': 'red'
    },
    'on': {
        'click': clickFn
    }    
}, [h('p', {}, '文本内容')])
function clickFn() {
    console.log(click')
}
vnode = patch(app, vnode);

新建:src\vdom\updataAttrUtils

import {
    isArray
} from './utils'

/**
 *更新style属性
 *
 * @param {Object} vnode 新的虚拟dom节点对象
 * @param {Object} oldStyle 
 * @returns
 */
function undateStyle(vnode, oldStyle = {}) {
    let doElement = vnode.elm;
    let newStyle = vnode.data.style || {};

    // 删除style
    for(let oldAttr in oldStyle){
        if (!newStyle[oldAttr]) {
            doElement.style[oldAttr] = '';
        }
    }

    for(let newAttr in newStyle){
        doElement.style[newAttr] = newStyle[newAttr];
    }
}
function filterKeys(obj) {
    return Object.keys(obj).filter(k => {
        return k !== 'style' && k !== 'id' && k !== 'class'
    })
}
/**
 *更新props属性
 * 支持 vnode 使用 props 来操作其它属性。
 * @param {Object} vnode 新的虚拟dom节点对象
 * @param {Object} oldProps 
 * @returns
 */
function undateProps(vnode, oldProps = {}) {
    let doElement = vnode.elm;
    let props = vnode.data.props || {};

    filterKeys(oldProps).forEach(key => {
        if (!props[key]) {
            delete doElement[key];
        }
     })

     filterKeys(props).forEach(key => {
         let old = oldProps[key];
         let cur = props[key];
         if (old !== cur && (key !== 'value' || doElement[key] !== cur)) {
            doElement[key] = cur;
         }
     })
}


/**
 *更新className属性 html 中的class
 * 支持 vnode 使用 props 来操作其它属性。
 * @param {Object} vnode 新的虚拟dom节点对象
 * @param {*} oldName 
 * @returns
 */
function updateClassName(vnode, oldName) {
    let doElement = vnode.elm;
    const newName = vnode.data.className;

    if (!oldName && !newName) return
    if (oldName === newName) return

    if (typeof newName === 'string' && newName) {
        doElement.className = newName.toString()
    } else if (isArray(newName)) {
        let oldList = [...doElement.classList];
        oldList.forEach(c => {
            if (!newName.indexOf(c)) {
                doElement.classList.remove(c);
            }
        })
        newName.forEach(v => {
            doElement.classList.add(v)
        })
    } else {
        // 所有不合法的值或者空值,都把 className 设为 ''
        doElement.className = ''
    }
}

function initCreateAttr(vnode) {
    updateClassName(vnode);
    undateProps(vnode);
    undateStyle(vnode);
}
export const styleApis = {
    undateStyle,
    undateProps,
    updateClassName,
    initCreateAttr
};
  export default styleApis;

在patch.js中增加方法:

......
import attr from './updataAttrUtils'
function createElement(vnode) {
 ....
  attr.initCreateAttr(vnode); 
 ....
}
.....

在src\index.js增加测试代码:

import { h,patch } from './vdom';
  //  h函数返回虚拟节点
  let vnode = h('ul#list', {}, [
      h('li.item', {style:{'color':'red'}}, 'itemA'),
      h('li.item.c1', {
        className:['c1','c2']
      }, 'itemB'),
      h('input', {
            props: {
              type: 'radio',
              name: 'test',
              value: '0',
              className:'inputClass'
        }  })
  ]);
  let container = document.getElementById('app');
  patch(container, vnode);

(图3.3.1)

4、diff算法实现流程原理

diff是来比较差异的算法

4.1 什么是diff算法

是用来对比差异的算法,有 linux命令 diff(我们dos命令中执行diff 两个文件可以比较出两个文件的不同)、git命令git diff、可视化diff(github、gitlab...)等各种实现。

4.2 vdom为何用diff算法

我们上边使用snabbdom.js的案例中,patch(vnode,newVnode)就是通过这个diff算法来判断是否有改变两个虚拟dom之间,没有就不用再渲染到真实dom树上了,节约了性能。

vdom使用diff算法是为了找出需要更新的节点。vdom使用diff算法来比对两个虚拟dom的差异,以最小的代价比对2颗树的差异,在前一个颗树的基础上生成最小操作树,但是这个算法的时间复杂度为n的三次方=O(nnn),当树的节点较多时,这个算法的时间代价会导致算法几乎无法工作。

4.3 diff算法的实现规则

diff算法是差异计算,记录差异

4.3.1、同级节点的比较,不能跨级

(网上找的图),如下图

(图4.3.1)

4.3.2、先序深度优化、广度优先:

1、深度优先

(图4.3.2)

2、广度优先

从某个顶点出发,首先访问这个顶点,然后找出这个结点的所有未被访问的邻接点,访问完后再访问这些结点中第一个邻接点的所有结点,重复此方法,直到所有结点都被访问完为止。

4.4、 snabbdom和vue中dom-diff实现原理流程(重点!!!)

4.4.1、在比较之前我们发现snabbdom中是用patch同一个函数来操作的,所以我们需要判断。第一个参数传的是虚拟dom还是 HTML 元素 。

4.4.2、再看源码的时候发现snabbdom中将html元素转换为了虚拟dom在继续操作的。这是为了方便后面的更新,更新完毕后在进行挂载。

4.4.3、通过方法来判断是否是同一个节点

方法:比较新节点(newVnode)和(oldVnode)的sel(其他的库中可能叫type) key两个属性是否相等,不定义key值也没关系,因为不定义则为undefined,而undefined===undefined,如果不同(比如sel从ul改变为了p),直接用通过newVnode的dom元素替换oldVnodedom元素,因为4.3.1中介绍的一样,dom-diff是按照层级分解树的,只有同级别比较,不会跨层移动vnode。不会在比较他们的children。如果不同再具体去比较其差异性,在旧的vnode上进行’打补丁’ 。

(图4.4.3)

ps:其实 在用vue的时候,在没有用v-for渲染的组件的条件下,是不需要定义key值的,也不会影响其比较。

4.4.4、data 属性更新

循环老的节点的data,属性,如果跟新节点data不存在就删除,最后在都新增加到老的节点的elm上;

需要特殊处理style、class、props,其中需要排除key\id,因为会用key来进行diff比较,没有key的时候会用id,都有当前索引。

代码实现可查看----》5.2

4.4.5、children比较(最核心重点)

4.4.5.1、新节点的children是文本节点且oldvnode的text和vnode的text不同,则更新为vnode的text

4.4.5.2、判断双方是只有一方有children,

i 、如果老节点有children,新的没有,老节点children直接都删除

ii、如果老节点的children没有,新的节点的children有,直接创建新的节点的children的dom引用到老的节点children上。

4.4.5.3、 将旧新vnode分别放入两个数组比较(最难点)

以下为了方便理解我们将新老节点两个数组来说明,实现流程。 用的是双指针的方法,头尾同时开始扫描;

重复下面的五种情况的对比过程,直到两个数组中任一数组的头指针(开始的索引)超过尾指针(结束索引),循环结束 :

oldStartIdx:老节点的数组开始索引,
oldEndIdx:老节点的数组结束索引,
newStartIdx:新节点的数组开始索引
newEndIdx:新节点的数组结束索引

oldStartVnode:老的开始节点
oldEndVnode:老的结束节点
newStartVnode:新的开始节点
newEndVnode:新的结束节点

 循环两个数组,循环条件为(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)

(图4.4.5.1)

首尾比较的状况

1、 头头对比:oldStartVnode - > newStartVnode
2、 尾尾对比:oldEndVnode - > newEndVnode
3、 老尾与新头对比: oldEndVnode- > newStartVnode
4、 老头与新尾对比:oldStartVnode- > newEndVnode
5、 利用key对比

情况1: 头头对比:

判断oldStartVnode、newStartVnode是否是同一个vnode: 一样:patch(oldStartVnode,newChildren[newStartIdx]);

++oldStartIdx,++oldStartIdx ,

oldStartVnode = oldChildren[oldStartIdx]、newStartVnode = newChildren[oldStartIdx ];

针对一些dom的操作进行了优化:在尾部增加或者减少了节点;

例子1:节点:ABCD =>ABCDE ABCD => ABC

开始时:

oldChildren:['A','B','C','D']
oldStartIdx:0
oldStartVnode: A虚拟dom
oldEndIdx:3
oldEndVnode:D虚拟dom
newChildren:['A','B','C','D','E']
newStartIdx:0
newStartVnode:A虚拟dom
newEndIdx:4
newEndVnode:E虚拟dom

(图4.4.5.2)

比较过后,

oldChildren:['A','B','C','D']
oldStartIdx:4
oldStartVnode: undefined
oldEndIdx:3
oldEndVnode:D虚拟dom
newChildren:['A','B','C','D','E']
newStartIdx:4
newStartVnode:D虚拟dom
newEndIdx:4
newEndVnode:E虚拟dom

newStartIndex <= newEndIndex :说明循环比较完后,新节点还有数据,这时候需要将这些虚拟节点的创建真是dom新增引用到老的虚拟dom的elm上,且新增位置是老节点的oldStartVnode即末尾;

newStartIndex > newEndIndex :说明newChildren已经全部比较了,不需要处理;

oldStartIdx>oldEndIdx: 说明oldChildren已经全部比较了,不需要处理;

oldStartIdx <= oldEndIdx :说明循环比较完后,老节点还有数据,这时候需要将这些虚拟节点的真是dom删除;

------------------------------------代码的具体实现可查看5.3.3

情况2:尾尾对比:

判断oldEndVnode、newEndVnode是否是同一个vnode:

一样:patch(oldEndVnode、newEndVnode);

--oldEndIdx,--newEndIdx;

oldEndVnode = oldChildren[oldEndIdx];newEndVnode = newChildren[newEndIdx],

针对一些dom的操作进行了优化:在头部增加或者减少了节点;

例子2:节点:ABCD =>EFABCD ABCD => BCD

开始时:

oldChildren:['A','B','C','D']
oldStartIdx:0
oldStartVnode: A虚拟dom
oldEndIdx:3
oldEndVnode:D虚拟dom
newChildren:['E','A','B','C','D']
newStartIdx:0
newStartVnode:E虚拟dom
newEndIdx:4
newEndVnode:D虚拟dom

(图4.4.5.3)

比较过后,

oldChildren:['A','B','C','D']
oldStartIdx:0
oldStartVnode: A虚拟dom
oldEndIdx:-1
oldEndVnode: undefined
newChildren:['E','A','B','C','D']
newStartIdx:0
newStartVnode:E虚拟dom
newEndIdx:1
newEndVnode:A虚拟dom

情况3、老尾与新头对比:

判断oldStartVnode跟newEndVnode比较vnode是否相同:

一样:patch(oldStartVnode、newEndVnode);

将老的oldStartVnode移动到newEndVnode的后边,

++oldStartIdx ;

--newEndIdx;

oldStartVnode = oldChildren[oldStartIdx] ;

newEndVnode = newChildren[newEndIdx];

**针对一些dom的操作进行了优化:**在头部增加或者减少了节点;

例子3:节点:ABCD => BCDA

开始时:

oldChildren:['A','B','C','D']
oldStartIdx:0
oldStartVnode: A虚拟dom
oldEndIdx:3
oldEndVnode:D虚拟dom
newChildren:['B','C','D','A']
newStartIdx:0
newStartVnode:B虚拟dom
newEndIdx:3
newEndVnode:A虚拟dom

(图4.4.5.4)

['A','B','C','D']  -> ['B','C','D','A']
1:老[0] -> 新[0] 不等 
2: 老[3] -> 新[3] 不等  
3:老[0] -> 新[3] 相等  
 移动老[0].elm到老[3].elm后
++oldStartIdx;--newEndIdx;移动索引指针来比较
以下都按照情况一来比较了
4: 老[1] -> 新[0] 相等,
5:老[2] -> 新[1] 相等
6:老[3] -> 新[2] 相等

比较过后,

oldChildren:['A','B','C','D']
oldStartIdx:4
oldStartVnode: undefined
oldEndIdx:3
oldEndVnode:D虚拟dom
newChildren:['B','C','D','A']
newStartIdx:3
newStartVnode:A虚拟dom
newEndIdx:2
newEndVnode:D虚拟dom

情况4、老头与新尾对比

将老的结束节点oldEndVnode 跟新的开始节点newStartVnode 比较,vnode是否一样,一样:

patch(oldEndVnode 、newStartVnode );

将老的oldEndVnode移动到oldStartVnode的前边,

++newStartIdx;

--oldEndIdx;

oldEndVnode= oldChildren[oldStartIdx] ;

newStartVnode = newChildren[newStartIdx];

**针对一些dom的操作进行了优化:**在尾部部节点移动头部;

例子4:节点:ABCD => DABC

开始时:

oldChildren:['A','B','C','D']
oldStartIdx:0
oldStartVnode: A虚拟dom
oldEndIdx:3
oldEndVnode:D虚拟dom
newChildren:['D','A','B','C']
newStartIdx:0
newStartVnode:B虚拟dom
newEndIdx:3
newEndVnode:A虚拟dom

过程:

(图4.4.5.5)

['A','B','C','D']  -> ['D','A','B','C']
1:老[0] -> 新[0] 不等 
2: 老[3] -> 新[3] 不等
3:老[0] -> 新[3] 不等
4: 老[3] -> 新[0] 相等, 移动老[3].elm到老[0].elm前
++newStartIdx;--oldEndIdx;移动索引指针来比较
以下都按照情况一来比较了
5:老[2] -> 新[3] 相等
6:老[1] -> 新[2] 相等
7:老[0] -> 新[1] 相等

比较过后,

oldChildren:['A','B','C','D']
oldStartIdx:3
oldStartVnode: D虚拟dom
oldEndIdx:2
oldEndVnode:C虚拟dom
newChildren:['B','C','D','A']
newStartIdx:4
newStartVnode: undefined
newEndIdx:3
newEndVnode:A虚拟dom

情况5、利用key对比

oldKeyToIdx:oldChildren中key及相对应的索引的map

 oldChildren = [{key:'A'},{key:'B'},{key:'C'},{key:'D'},{key:'E'}];

oldKeyToIdx = {'A':0,'B':1,'C':2,'D':3,'E':4}

此时用 是老的key在oldChildren的索引map,来映射新节点的key在oldChildren中的索引map,通过方法创建,有助于之后通过 key 去拿下标 。

实现原理流程:

1、 oldKeyToIdx没有我们需要新创建

2、 保存newStartVnode.keyoldKeyToIdx 中的索引

3、 这个索引存在,新开始节点在老节点中有这个key,在判断sel也跟这个oldChildren[oldIdxByKeyMap]相等说明是相似的vnode,patch,将这个老节点赋值为undefined,移动这个oldChildren[oldIdxByKeyMap].elm到oldStartVnode之前

4、 这个索引不存在,那么说明 newStartVnode 是全新的 vnode,直接 创建对应的 dom 并插入 oldStartVnode.elm之前

++newStartIdx;

newStartVnode = newChildren[newStartIdx];

案例说明:

可能的原因有

1、此时的节点(需要比较的新节点)时新创建的,

2、当前节点(需要比较的新节点)在原来的位置是处于中间的(oldStartIdx 和 oldEndIdx之间)

例子5:ABCD -> EBADF

oldChildren:['A','B','C','D']
oldStartIdx:0
oldStartVnode: A虚拟dom
oldEndIdx:3
oldEndVnode:D虚拟dom
newChildren:['E','B','A','D','F']
newStartIdx:0
newStartVnode:E虚拟dom
newEndIdx:4
newEndVnode:D虚拟dom

比较过程

1、

(图4.4.5.6-1)

解释:

、、、前面四种首尾双指针比较都不等时,、、、
创建了一个map:oldKeyToIdx= {'A':0,'B':1,'C':2,'D':3}
此时的newStartVnode.key是E 在oldKeyToIdx不存在,
说明E是需要创建的新节点,
则执行创建真是DOM的方法创建,然后这个DOM插入到oldEndVnode.elm之前;
newStartVnode = newChildren[++newStartIdx] ->即B

2、

(图4.4.5.6-2)

解释:

、、、前面四种首尾双指针比较都不等时,、、、
oldKeyToIdx= {'A':0,'B':1,'C':2,'D':3}
B在oldKeyToIdx存在索引为1,
在判断sel是否相同,
相同说明这个newStartVnode在oldChildren存在,
patch(oldChildren[1], newStartVnode);
oldChildren[1] = undefined;//
则移动oldChildren[1]到oldStartVnode.elm之前;
newStartVnode = newChildren[++newStartIdx] ->即A

3、

(图4.4.5.6-3)

解释:

第一种情况的头头相等,按照情况一逻辑走
newStartVnode = newChildren[++newStartIdx] ->D
oldStartVnode = oldChildren[++EndIdx] = undefined;->B为undefined

4、

(图4.4.5.6-4)

解释:

oldStartVnode是 undefined
会执行++oldStartIdx;
oldStartVnode -> C

5、

(图4.4.5.6-5)

解释:

5、头头不等、尾尾不等、尾头相等
执行第三种情况;
patch(oldEndVnode, newStartVnode);
oldEndVnode.elm移动到oldStartVnode.elm;
oldEndVnode = oldChildren[--oldEndIdx] -> 即C
newStartVnode = newChildren[++newStartIdx] ->F

6、

(图4.4.5.6-6)

解释:

五种比较都不相等
newStartVnode = newChildren[++newStartIdx] ->undefined
newStartIdx > newEndIdx跳出循环

最后,

(图4.4.5.6-7)

此时oldStartIdx = oldEndIdx -> 2 --- C
说明需要删除oldChildren中的这些节点元素C 

对于列表节点提供唯一的 key 属性可以帮助代码正确的节点进行比较,从而大幅减少 DOM 操作次数,提高了性能。 对于不同层级的,没有key,是没关系的。比如我们vue和react中通过for循环创建一些列表的时候常常提示我们要传key也是这个原因。

4.5、react中的diff策略规则(重点)

根据两个虚拟对象创建出补丁,描述改变的内容,将这个补丁用来更新DOM

如果你不知道React: reactjs.org/docs/gettin…

4.5.1 diff策略

1.web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。

2.拥有相同类型的两个组件将会生成相似的树形结构,拥有不同类型的两个组件将会生成不同树形结构。

3.对于同一层级的一组子节点,他们可以通过唯一key进行区分。

基于以上策略,react分别对tree diff、component diff 以及 element diff 进行算法优化。

ps: 我们需要注意在react中我们调用setState函数来

4.5.2 tree diff

基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较如4.3.1;先序深度循环遍历(如4.3.2);这种改进方案大幅度的降低了算法复杂度。 当进行跨层级的移动操作,React并不是简单的进行移动,而是进行了删除和创建的操作,会影响到React性能。

(图4.5.2.1)

当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A。

4.5.3 component diff

这里指的是函数组件和类组件(如案例2.2.2和2.2.3),比较流程:

1、比较组件是否为同一类型;不是 则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。

2、是同一类型的组件: 按照原策略继续比较 virtual DOM tree 。

ps:

​ 函数组件:先运行(此时的虚拟dom的type(props)); 得到返回的结果;在按照原策略比较;

​ 类组件:需要先构建实例(new type(props).render())在调render()函数;得到返回的结果;在按照原策略比较;

在component diff阶段的主要优化策略就是使用shouldComponentUpdate() 方法。可查看4.6具体说明。

4.5.4 element diff

当节点处于同一层级时, React diff 提供了三种节点操作,我们可以给不同类型定义区分规则。

可以定义为:INSERT(插入)、MOVE(移动)和 REMOVE(删除)。

1、INSERT:表示新的虚拟dom的类型不在老集合(我们会生成一个针对老的虚拟dom的key-index的集合)里,说明这个节点时新的,需要对新节点进行插入操作。
2、MOVE:表示在老的集合中存在,我们这个时候要比较上一次保存的比较索引跟这个老的节点的本身索引比,且element是可更新的类型,这时候就需要做移动操作,可以复用以前的DOM节点
3、REMOVE:旧组件类型,在新集合里也有,但对应的element不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作

根据例子来说明,如下:

例子:

<ul>
    <li key='A'>A<li/>
    < li key= 'B' > B < li / >
    < li key= 'C' > C < li / >
    < li key='D' > D < li / >
</ul>

改为:

<ul>
    <li key='A'>A<li/>
    < li key= 'C' > C < li / >
    < li key= 'B' > B < li / >
    < li key='E' > E < li / >
    < li key='F' > F < li / >
</ul>

(图4.5.4.1)

准备:

lastIndex:记录遍历比较最后一次的索引
oldChUMap:老儿子的对应key:节点的集合
newCh: 新儿子
newCHUMap:新儿子对应的key:节点集合
diffQueue; //差异队列
updateDepth = 0; //更新的级别
每一个节点本身挂载了一个索引值_mountIndex


循环新儿子开始比较:

第一次比较:i=0;

(图4.5.4.2)

第二次比较:i=1;

(图4.5.4.3)

第三次比较:i=2;

(图4.5.4.4)

第四次:i=3;

(图4.5.4.5)

第五次:i=4;

跟第四次相同;lasIndex = 4

新儿子已经循环完了,在循环老儿子,有没有在新儿子集合中没有的newCHUMap,则打包类型删除MOVE,插入到队列;

(图4.5.4.6)

最后进行补丁包的更新;

4.6 dom-diff什么时候触发

我们知道再次触发需要在此调用render函数,那render函数什么时候执行呢?下边来看看react的声明周期

4.6.1、旧版生命周期

(图4.6.1)

4.6.2、新版的声明周期

(图4.6.2)

4.6.3总结:

ReactDOM.render()函数在次调用即更新阶段中:不管是新版还是旧版的声明周期,我们都需要注意:在react中是否继续调用是render函数,需要先通过生命周期的钩子函数 shouldComponentUpdate() 来判断该组件,如果返回true,需要进行深度比较;如果返回false就不用继续,只判断当前的两个虚拟dom是不是同类型,这明显影响影响了react的性能, 正如 React 官方博客所言:不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的;默认返回的是true。

ps:vue中将数据维护成了可观察的数据,数据的每一项都通过getter来收集依赖,然后将依赖转化成watcher保存在闭包中,数据修改后,触发数据的setter方法,然后调用所有的watcher修改旧的虚拟dom,从而生成新的虚拟dom,然后就是运用diff算法 ,得出新旧dom不同,根据不同更新真实dom。

4.7总结

DOM-diff比较两个虚拟DOM的区别,也就是在比较两个对象的区别。

  • 采用先序深度优先遍历的算法
  • 根据两个虚拟对象创建出补丁,描述改变的内容,将这个补丁用来更新DOM

5、模拟初步的diff算法实现

5.1 不同sel类型实现

第一步:判断参数是否是虚拟dom,isVnode(vnode)方法实现

/** 
 * 校验是不是 vnode, 主要检查 __type。
 * @param  {Object}  vnode 要检查的对象
 * @return {Boolean}       是则 true,否则 false
*/
export function isVnode(vnode){
   return vnode && vnode._type === VNODE_TYPE
}

第二步:增加isSameVnode判断是同一个vnode

/**
 * 检查两个 vnode 是不是同一个: key 相同且 type 相同
 *
 * @param {Object} oldVnode
 * @param {Object} newVnode
 * @returns {Boolean}  是则 true,否则 false
 */
export function isSameVnode(oldVnode,newVnode){
    return oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key;
}

第三步:patch第一个参数元素不是虚拟dom,改为虚拟dom

/**
 *将一个真是的dom节点转化成vnode
 * <div id="a" class="b c"></div> 转化为 
 * {sel:'div#a.b.c',data:{},children:[],text:undefined,
 * elm:<div id="a" class="b c"></div>}
 * @param {*} oldVnode
 */
function createEmptyNode(elm) {
  let id = elm.id ? '#' + elm.id : '';
  let c = elm.className ? '.'+ elm.className.split(' ').join('.'):'';
  return VNode(htmlApi.tagName(elm).toLowerCase() + id + c, {}, [], undefined, undefined, elm);
}

第四步:新旧节点同一个vnode,直接替换

patch.js中patch函数更新 有关代码:

/**
 * 用于挂载或者更新 DOM
 *
 * @param {*} container 
 * @param {*} vnode
 */
function patch(container, vnode) {
    let  elm, parent;
    // let insertedVnodeQueue = [];
    console.log(isVnode(vnode));
    // 如果不是vnode,那么此时那此时以旧的 DOM 为模板构造一个空的 VNode。
    if (!isVnode(container)) {
      container = createEmptyNode(container);
    }
 // 如果 oldVnode 和 vnode 是同一个 vnode(相同的 key 和相同的选择器),
//  那么更新 oldVnode。
    if (isSameVnode(container, vnode)) {
      patchVnode(container, vnode)
    }else {
    // 新旧vnode不同,那么直接替换掉 oldVnode 对应的 DOM
      elm = container.elm;
      parent = htmlApi.parentNode(elm);
      createElement(vnode);
      if(parent !== null){
        // 如果老节点对应的dom父节点有并且有同级节点,
        // 那就在其同级节点之后插入 vnode 的对应 DOM。
        htmlApi.insertBefore(parent,vnode.elm,htmlApi.nextSibling(elm));
        // 在把 vnode 的对应 DOM 插入到 oldVnode 的父节点内后,移除 oldVnode 的对应 DOM,完成替换。
        removeVnodes(parent, [container], 0, 0);      
      }
    }   
};

patch.js增加removeVnodes函数处理

/**
 *从parent dom删除vnode 数组对应的dom
 *
 * @param {Element} parentElm 父元素
 * @param {Array} vnodes  vnode数组
 * @param {Number} startIdx 要删除的对应的vnodes的开始索引
 * @param {Number} endIdx  要删除的对应的vnodes的结束索引
 */
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    let ch = vnodes[startIdx];
    if(ch){
      // if (ch.sel) {
      //   // 先不写事件、hook的处理

      // } else {
      //   htmlApi.removeChild(parentElm,ch.elm);
      // }
       htmlApi.removeChild(parentElm, ch.elm);
    }
  }
}

第五步:代码测试:

index.js代码更改:

import { h,patch } from './vdom';
  //  h函数返回虚拟节点
  let vnode = h('ul#list', {}, [
      h('li.item', {style:{'color':'red'}}, 'itemA'),
      h('li.item', {
        className:['c1','c2']
      }, 'itemB'),
      h('input', {
            props: {
              type: 'radio',
              name: 'test',
              value: '0',
              className:'inputClass'
        }  })
  ]);
  let container = document.getElementById('app');
  patch(container, vnode);

  setTimeout(() => {
    patch(vnode, h('p',{},'ul改变为p'));
  }, 3000);

(图5.1.1)

5.2 属性更新

src\vdom\updataAttrUtils.js

import {
    isArray
} from './utils'

/**
 *更新style属性
 *
 * @param {Object} vnode 新的虚拟dom节点对象
 * @param {Object} oldStyle 
 * @returns
 */
function undateStyle(vnode, oldStyle = {}) {
    let doElement = vnode.elm;
    let newStyle = vnode.data.style || {};

    // 删除style
    for(let oldAttr in oldStyle){
        if (!newStyle[oldAttr]) {
            doElement.style[oldAttr] = '';
        }
    }

    for(let newAttr in newStyle){
        doElement.style[newAttr] = newStyle[newAttr];
    }
}
function filterKeys(obj) {
    return Object.keys(obj).filter(k => {
        return k !== 'style' && k !== 'id' && k !== 'class'
    })
}
/**
 *更新props属性
 * 支持 vnode 使用 props 来操作其它属性。
 * @param {Object} vnode 新的虚拟dom节点对象
 * @param {Object} oldProps 
 * @returns
 */
function undateProps(vnode, oldProps = {}) {
    let doElement = vnode.elm;
    let props = vnode.data.props || {};

    filterKeys(oldProps).forEach(key => {
        if (!props[key]) {
            delete doElement[key];
        }
     })

     filterKeys(props).forEach(key => {
         let old = oldProps[key];
         let cur = props[key];
         if (old !== cur && (key !== 'value' || doElement[key] !== cur)) {
            doElement[key] = cur;
         }
     })
}


/**
 *更新className属性 html 中的class
 * 支持 vnode 使用 props 来操作其它属性。
 * @param {Object} vnode 新的虚拟dom节点对象
 * @param {*} oldName 
 * @returns
 */
function updateClassName(vnode, oldName) {
    let doElement = vnode.elm;
    const newName = vnode.data.className;

    if (!oldName && !newName) return
    if (oldName === newName) return

    if (typeof newName === 'string' && newName) {
        doElement.className = newName.toString()
    } else if (isArray(newName)) {
        let oldList = [...doElement.classList];
        oldList.forEach(c => {
            if (!newName.indexOf(c)) {
                doElement.classList.remove(c);
            }
        })
        newName.forEach(v => {
            doElement.classList.add(v)
        })
    } else {
        // 所有不合法的值或者空值,都把 className 设为 ''
        doElement.className = ''
    }
}

function initCreateAttr(vnode) {
    updateClassName(vnode);
    undateProps(vnode);
    undateStyle(vnode);
}

function updateAttrs(oldVnode, vnode) {
    updateClassName(vnode, oldVnode.data.className);
    undateProps(vnode, oldVnode.data.props);
    undateStyle(vnode, oldVnode.data.style);
}

export const styleApis = {
    undateStyle,
    undateProps,
    updateClassName,
    initCreateAttr,
    updateAttrs
};
  export default styleApis;

patch.js 中增加:

function patchVnode(oldVnode, vnode) {
  let elm = vnode.elm = oldVnode.elm;  
  if(isUndef(vnode.data)){
    // 属性的比较更新
    attr.updateAttrs(oldVnode, vnode);
  }
}

(图5.2.1)

5.3 children比较

5.3.1 新节点是文本节点

function patchVnode(oldVnode, vnode) {
  // let elm = vnode.elm = oldVnode.elm,因为vnode没有被渲染,这时的vnode.elm是undefined,
  // 新把老的给它
  let elm = vnode.elm = oldVnode.elm;	
  if(isUndef(vnode.data)){
    // 属性的比较更新
    attr.updateAttrs(oldVnode, vnode);
  }

  // 新节点不是文本节点
  if(!isUndef(vnode.text)){
  }  //如果oldvnode的text和vnode的text不同,则更新为vnode的text
 else  if(oldVnode.text !== vnode.text) {
    htmlApi.setTextContent(elm, vnode.text);
  }
  
}

5.3.2 只有一方有children

1、如果新vnode有子节点,oldvnode没子节点

function patchVnode(oldVnode, vnode) {

  // let elm = vnode.elm = oldVnode.elm,因为vnode没有被渲染,这时的vnode.elm是undefined,
  // 新把老的给它
  let elm = vnode.elm = oldVnode.elm,
  oldCh = oldVnode.children,newCh = vnode.children;

  if(!isUndef(vnode.data)){
    // 属性的比较更新
    attr.updateAttrs(oldVnode, vnode);
  }

// 新节点不是文本节点
  if(!vnode.text){
    if(!oldCh && (!newCh) ){

    } else if (newCh) {
      //如果vnode有子节点,oldvnode没子节点
      //oldvnode是text节点,则将elm的text清除,因为children和text不同同时有值
      if (!oldVnode.text) htmlApi.setTextContent(elm, '');
      //并添加vnode的children 
      addVnodes(elm, null, newCh, 0, newCh.length - 1);

    }
  }  //如果oldvnode的text和vnode的text不同,则更新为vnode的text
 else  if(oldVnode.text !== vnode.text) {
    htmlApi.setTextContent(elm, vnode.text);
  }
  
}

实现addVnodes方法:

function addVnodes(parentElm,before,vnodes,startIdx,endIdx){
     for(;startIdx<=endIdx;++startIdx){
        const ch = vnodes[startIdx];
        if(ch != null){
          htmlApi.insertBefore(parentElm,createElm(ch),before);
        }
     }
}

2、如果新节点没有children,老节点有子节点

function patchVnode(oldVnode, vnode) {

  // let elm = vnode.elm = oldVnode.elm,因为vnode没有被渲染,这时的vnode.elm是undefined,
  // 新把老的给它
  let elm = vnode.elm = oldVnode.elm,
  oldCh = oldVnode.children,newCh = vnode.children;
  // 如果两个vnode完全相同,直接返回
  if (oldVnode === vnode) return;
  if(!isUndef(vnode.data)){
    // 属性的比较更新
    attr.updateAttrs(oldVnode, vnode);
  }

// 新节点不是文本节点
  if (isUndef(vnode.text)) {
    if (oldCh.length>0 && newCh.length>0) {
      // 新旧节点均存在 children,且不一样时,对 children 进行 diff
      updateChildren(elm, oldCh, newCh);
     
    } else if(newCh.length>0) {
      //如果vnode有子节点,oldvnode没子节点
      //oldvnode是text节点,则将elm的text清除,因为children和text不同同时有值
      if (!oldVnode.text) htmlApi.setTextContent(elm, '');
      //并添加vnode的children 
      addVnodes(elm, null, newCh, 0, newCh.length - 1);

    } else if (oldCh.length>0) {
      // 新节点不存在 children 旧节点存在 children 移除旧节点的 children
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    }
  }  //如果oldvnode的text和vnode的text不同,则更新为vnode的text
 else  if(oldVnode.text !== vnode.text) {
    htmlApi.setTextContent(elm, vnode.text);
  }
  
}

5.3.3、 头头对比

在尾部新增、删除元素,

真是应用中效果从:ABCD =>ABCDE ABCD => ABC 具体的实现流程-----》请参考4.4.5.3情况一的实现原理讲解

src\vdom\patch.js updateChildren函数修改

function updateChildren(parentDOMElement, oldChildren, newChildren) {
  // 两组数据 首尾双指针比较
  let oldStartIdx = 0,oldStartVnode = oldChildren[0]; 
  let oldEndIdx = oldChildren.length - 1,oldEndVnode = oldChildren[oldEndIdx];

  let newStartIdx = 0,newStartVnode = oldChildren[0]; 
  let newEndIdx = newChildren.length - 1,newEndVnode = newChildren[newEndIdx];

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 先排除 vnode为空 4 个 vnode 非空,
    // 左侧的 vnode 为空就右移下标,右侧的 vnode 为空就左移 下标
    if (oldStartVnode == null) {
      oldStartVnode = oldChildren[++oldStartIdx];
    } else if (oldEndVnode == null) {
      oldEndVnode = oldChildren[--oldEndIdx];
    } else if (newStartVnode == null) {
      newStartVnode = newChildren[++newStartIdx];
    } else if (newEndVnode == null) {
      newEndVnode = newChildren[--newEndIdx];
    }
    /** oldStartVnode/oldEndVnode/newStartVnode/newEndVnode 两两比较,
     * 1、 oldStartVnode - > newStartVnode
     * 2、 oldEndVnode - > newEndVnode
     * 3、 newStartVnode - > oldEndVnode
     * 4、 newEndVnode - > oldStartVnode
     * 对上述四种情况执行对应的patch
     */
    // 1、新的开始节点跟老的开始节点相比较 是不是一样的vnode
    // oldStartVnode - > newStartVnode 比如在尾部新增、删除节点
    // 
    else if (isSameVnode(oldStartVnode, newStartVnode)) {
       patch(oldStartVnode, newStartVnode);
       oldStartVnode = oldChildren[++oldStartIdx];
       newStartVnode = newChildren[++newStartIdx];
    } 
  }
// 说明循环比较完后,新节点还有数据,这时候需要将这些虚拟节点的创建真是dom
// 新增引用到老的虚拟dom的`elm`上,且新增位置是老节点的oldStartVnode即末尾;
  if (newStartIdx <= newEndIdx) {
    addVnodes(parentDOMElement, null, newChildren, newStartIdx, newEndIdx);
  }

  if (oldStartIdx <= oldEndIdx) {
     // newChildren 已经全部处理完成,而 oldChildren 还有旧的节点,需要将多余的节点移除
     removeVnodes(parentDOMElement, oldChildren, oldStartIdx, oldEndIdx);
  }
}

5.3.4、 尾尾对比

应用:在头部新增、删除元素

实现效果从:ABCD => EABCD ABCD => BCD 具体的实现流程-----》请参考4.4.5.3情况二的实现原理讲解

更改src\vdom\patch.js updateChildren函数

    // 2、oldEndVnode - > newEndVnode 比如在头部新增、删除节点
    else if (isSameVnode(oldEndVnode, newEndVnode)) {
        patch(oldEndVnode, newEndVnode);
        oldEndVnode = oldChildren[--oldEndIdx];
        newEndVnode = newChildren[--newEndIdx];
    }

// 说明循环比较完后,新节点还有数据,这时候需要将这些虚拟节点的创建真是dom
// 新增引用到老的虚拟dom的`elm`上,且新增位置是老节点的oldStartVnode即末尾;
  if (newStartIdx <= newEndIdx) {
    let before = newChildren[newEndIdx + 1] == null ? null : newChildren[newEndIdx + 1].elm;
    addVnodes(parentDOMElement, before, newChildren, newStartIdx, newEndIdx);
  }

5.3.5、 旧尾新头对比

真是应用:将头部元素移动到尾部

实现效果:ABCD => DBCA 具体的实现流程-----》请参考4.4.5.3情况三的实现原理讲解

更改src\vdom\patch.js updateChildren函数

   // 3、newEndVnode - > oldStartVnode 将头部节点移动到尾部
    else if (isSameVnode(oldStartVnode, newEndVnode)) {
      patch(oldStartVnode, newEndVnode);
      // 把旧的开始节点插入到末尾
      htmlApi.insertBefore(parentDOMElement, oldStartVnode.elm, htmlApi.nextSibling(oldEndVnode.elm));
      oldStartVnode = oldChildren[++oldStartIdx];
      newEndVnode = newChildren[--newEndIdx];
     
    }

5.3.6、 旧头新尾对比

应用:将尾部元素移动到头部

实现效果:ABCD => BCDA 具体的实现流程-----》请参考4.4.5.3情况四的实现原理讲解

更改src\vdom\patch.js updateChildren函数

  // 4、oldEndVnode  -> newStartVnode 将尾部移动到头部
    else if (isSameVnode(oldEndVnode, newStartVnode)) {
      patch(oldEndVnode,newStartVnode);
      // 将老的oldEndVnode移动到oldStartVnode的前边,
      htmlApi.insertBefore(parentDOMElement, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldChildren[--oldEndIdx];
      newStartVnode = newChildren[++newStartIdx];
    }

5.3.7 、用key对比

第一步:创建老节点children中key及对应的index的map

/**
 * 为 vnode 数组 begin~ end 下标范围内的 vnode 
 * 创建它的 key 和 下标 的映射。
 *
 * @param {Array} children
 * @param {Number} startIdx
 * @param {Number} endIdx
 * @returns {Object}  key在children中所映射的index索引对象
 * children = [{key:'A'},{key:'B'},{key:'C'},{key:'D'},{key:'E'}];
 * startIdx = 1; endIdx = 3;
 * 函数返回{'B':1,'C':2,'D':3}
 */
function createOldKeyToIdx(children, startIdx, endIdx) {
  const map = {};
  let key;
  for (let i = startIdx; i <= endIdx; ++i) {
    let ch = children[i];
    if(ch != null){
      key = ch.key;
      if(!isUndef(key)) map[key] = i;
    }
  }
  return map;
}

第二步:保存新开始节点在老节点中的索引

 oldIdxByKeyMap = oldKeyToIdx[newStartVnode.key];

第三步: 判断oldIdxByKeyMap是否存在

 /** 5、4种情况都不相等
     * // 1. 从 oldChildren 数组建立 key --> index 的 map。
     // 2. 只处理 newStartVnode (简化逻辑,有循环我们最终还是会处理到所有 vnode),
     //    以它的 key 从上面的 map 里拿到 index;
     // 3. 如果 index 存在,那么说明有对应的 old vnode,patch 就好了;
     // 4. 如果 index 不存在,那么说明 newStartVnode 是全新的 vnode,直接
     //    创建对应的 dom 并插入。
     */
    else{
      
      /** 如果 oldKeyToIdx 不存在,
       * 1、创建 old children 中 vnode 的 key 到 index 的
       * 映射, 方便我们之后通过 key 去拿下标
       *  */
       if (isUndef(oldKeyToIdx)) {
        oldKeyToIdx = createOldKeyToIdx(oldChildren,oldStartIdx,oldEndIdx);
       }
       // 2、尝试通过 newStartVnode 的 key 去拿下标
       oldIdxByKeyMap = oldKeyToIdx[newStartVnode.key];
      // 4、 下标索引不存在,说明newStartVnode 是全新的 vnode
       if (oldIdxByKeyMap == null) {
        // 那么为 newStartVnode 创建 dom 并插入到 oldStartVnode.elm 的前面。
        htmlApi.insertBefore(parentDOMElement,createElement(newStartVnode),oldStartVnode.elm);
        newStartVnode = newChildren[++newStartIdx];
       }
      //  3、下标存在 说明oldChildren中有相同key的vnode
       else{
        elmToMove = oldChildren[oldIdxByKeyMap];
        // key相同还要比较sel,sel不同,需要创建 新dom
        if (elmToMove.sel !== newStartVnode.sel) {
          htmlApi.insertBefore(parentDOMElement,createElement(newStartVnode),oldStartVnode.elm);
        }
        // sel相同,key也相同,说明是一样的vnode,需要打补丁patch
        else{
          patch(elmToMove,newStartVnode);
          oldChildren[oldIdxByKeyMap] = undefined;
          htmlApi.insertBefore(parentDOMElement,elmToMove.elm,oldStartVnode);
        }
        newStartVnode = newChildren[++newStartIdx];
       }
    }

具体完成代码可查看: github.com/learn-fe-co…

6、模拟vdom在人react中的初步渲染实现

前言:

根据2.2中react初步渲染案例及效果分析,我们要实现vdom和渲染页面真是dom,具体步骤如下:

1、创建项目

2、创建react.js:

​ 导出createElment函数:返回vdom对象

​ 导出Component类:用继承此类,可以传递参数props

3、创建react-dom.js

​ 导出render函数,用于更新虚拟dom到要挂载的elment元素上

​ 注意1:文本节点、函数、类组件、元素组件不能的处理方式


正式代码部分:

6.1 创建项目、环境搭建

第一步:我们先用脚手架快速创建一个react项目:

npm i create-react-app -g
create-react-app lee-vdom-react

第二步:删除多余文件、代码如图:

(图6.1.1)实现了2.2.1的例子

6.2 初步实现react.js的创建虚拟节点

第一步:创建react.js

src\react.js

import createElement from './element';

export default {
    createElement
}

第二步:创建element.js

src\element.js

// 虚拟DOM元素的类,构建实例对象,用来描述DOM
class Element{
    constructor(type, props) {
        this.type = type;
        this.props = props;
        this.key = props.key ? props.key : undefined;//用于后边的list diff做准备
    }
    
}


/**
 *
 * 创建虚拟DOM
 * @param {String} type 标签名
 * @param {Object} [config={}]  属性
 * @param {*} children  表示指定元素子节点数组,长度为1是文本节点,长度为0表示是不存在文本节点
 * @returns  
 */
function createElement(type,config = {},...children){
    const props = {};
    for(let propsName in config){
        props[propsName] = config[propsName];
    }
    //表示指定元素子节点数组,长度为1是文本节点,长度为0表示是不存在文本节点
    let len = children.length;
    if (len>0) {
        props.children = children.length === 1 ? children[0] :children;
    }
    
    return new Element(type, props);
}

export {Element,createElement};

测试:

index.js

import React from './react'; //引入对应的方法来创建虚拟DOM
import ReactDOM from 'react-dom';

let virtualDom = React.createElement('h1', null,'hello,lee');

console.log('引用自己创建的reactjs生成的虚拟dom:',virtualDom);

(图6.2.1)

总结:

// 原生react中的createElement函数的返回值:虚拟dom返回的对象如下
{
  $$typeof:REACT_ELEMENT_TYPE,  //用于表示是一个React元素本文中忽略
  type:type,
  key:key,
  ref:ref,        //忽略
  props:props,
  _owner:owner,  //忽略
  _store:{},    //忽略
  _self:{},     //忽略
  _source:{}   //忽略
};
//_store、_self和_source属性都是用来在开发环境中方便测试提供的,用来比对两个ReactElement

createElement函数参数分析

  • type: 指定元素的标签类型,如'li', 'div', 'a'等
  • props: 表示指定元素身上的属性,如class, style, 自定义属性等
  • children: 表示指定元素子节点数组,长度为1是文本节点,长度为0表示是不存在文本节点

6.3 模拟react的vdom初步渲染实现

第一步:创建react-dom.js

src\react-dom.js

import {isPrimitive} from './utils';
import htmlApi from './domUtils';   
/**
 * render方法可以将虚拟DOM转化成真实DOM
 *
 * @param {*} element 如果是字符串
 * @param {Element} container
 */
function render(element,container){
  // 如果是字符串或者数字,创建文本节点插入到container中
  if (isPrimitive(element)) {
      return htmlApi.appendChild(htmlApi.createTextNode(element));
  }
  let type,props;
  type = element.type;  
  let domElement = htmlApi.createElement(container,type;);  

  htmlApi.appendChild(container,element);
}

export default { render}

第二步:引用之前项目lee-vdom中src\vdom\的utils.js和domUtils.js到当前的项目src目录下

第三步:处理参数element中的props到真是dom上

// 循环所有属性,然后设置属性
  for (let [key, val] of Object.entries(element.props)) {
      htmlApi.setAttr(domElement, key, val);
  }

/**
 *
 * 给dom设置属性
 * @param {Element} el 需要设置属性的dom元素
 * @param {*} key   需设置属性的key值
 * @param {*} val   需设置属性的value值
 */
function setAttr(el, key, val) {
    if (key === 'children') {
        val = isArray(val)? val : [val];
        val.forEach(c=>{
            render(c,el);
        })

    }else if(key === 'value'){
        let tagName = htmlApi.tagName(el) || '';
        tagName = tagName.toLowerCase();
        if (tagName === 'input' || tagName === 'textarea') {
            el.value = val;
        } else {
            // 如果节点不是 input 或者 textarea, 则使用 `setAttribute` 去设置属性
            htmlApi.setAttribute(el,key, val);
        }

    } 
    // 类名
    else if (key === 'className') {
        if (val) el.className = val;
    }else if(key === 'style'){
        //需要注意的是JSX并不是html,在JSX中属性不能包含关键字,
        // 像class需要写成className,for需要写成htmlFor,并且属性名需要采用驼峰命名法
        let cssText = Object.keys(val).map(attr => {
            return `${attr.replace(/([A-Z])/g,()=>{ return"-"+arguments[1].toLowerCase()})}:${val[attr]}`;
        }).join(';');
        el.style.cssText = cssText;
    }else if(key === 'on'){  //目前忽略

    }else{
      htmlApi.setAttribute(el, key, val);
    }

6.4 增加类组件和函数组件的逻辑

1、增加类组件的逻辑渲染

我们在例子:2.2.3中代码

class Wecome1 extends Reat.Component{
    render(){ return (....)}
}

我们在使用类组件的时候:

  • 需要继承Reat.Component;
  • 需要通过render()函数返回一个react元素;
  • 属性的接收是用this.props = {....},所以需要构造器赋值,使用类的时候继承即可;

第一步:react.js 修改

import createElement from './element';

class Component{
  //用于判断是否是类组件  
  static isReactComponent = true;  
  constructor(props) {
      this.props = props;
  }
  
}

export default {
    createElement,
    Component
}

第二步:修改react-dom.js

//   类组件
  if (type.isReactComponent) {
    // 如果是类组件,需要先创建实例,在render(),得到React元素
    element = new type(props).render();
    props = element.props;
    type = element.type;
  }

2、增加函数组件的渲染

修改react-dom.js

 //函数组件
  else if(isFun(type)){
    // 如果是函数组件,需要先执行,得到React元素
    element = type(props);
    props = element.props;
    type = element.type; 
  }

6.5 优化render方法

我们可以看到render方法中有对文本节点、组件的一些判断很多类似的方法,每次都要改render函数,根据设计模式的思想不符合。

我们创建一个类来单独处理不同的文本组件、类组件处理不同的逻辑。

src\unit.js

/** 
 *  凡是挂载到私有属性上的_开头
 *  */

import {
    isPrimitive,
    isArray,
    isFun,
    isRectElement,
    isStr    
} from './utils';
import htmlApi from './domUtils';
import EventFn from './event'; 

class Unit{
    constructor(elm) {
        // 将
        this._selfElm = elm;
        this._events = new EventFn();
    }
    getHtml(){

    }
    
}

// 文本节点
class TextUnit extends Unit{
    getHtml(){
        return htmlApi.createTextNode(this._selfElm);
    }
}

// 
class NativeUnit extends Unit{
    getHtml() {
         let {type,props} = this._selfElm;
          // 创建dom
          let domElement = htmlApi.createElement(type);
          props = props ||{};
          // 循环所有属性,然后设置属性
          for (let [key, val] of Object.entries(props)) {
              this.setProps(domElement, key, val);
          }
        return domElement;
    }
    /**
     *
     * 给dom设置属性
     * @param {Element} el 需要设置属性的dom元素
     * @param {*} key   需设置属性的key值
     * @param {*} val   需设置属性的value值
     */
    setProps(el, key, val) {
        if (key === 'children') {
            val = isArray(val) ? val : [val];
            val.forEach(c => {
                let cUnit = createUnit(c);
                let cHtml = cUnit.getHtml();
                htmlApi.appendChild(el,cHtml);
            });

        } else if (key === 'value') {
            let tagName = htmlApi.tagName(el) || '';
            tagName = tagName.toLowerCase();
            if (tagName === 'input' || tagName === 'textarea') {
                el.value = val;
            } else {
                // 如果节点不是 input 或者 textarea, 则使用 `setAttribute` 去设置属性
                htmlApi.setAttribute(el, key, val);
            }

        }
        // 类名
        else if (key === 'className') {
            if (val) el.className = val;
        } else if (key === 'style') {
            //需要注意的是JSX并不是html,在JSX中属性不能包含关键字,
            // 像class需要写成className,for需要写成htmlFor,并且属性名需要采用驼峰命名法
            let cssText = Object.keys(val).map(attr => {
                return `${attr.replace(/([A-Z])/g,()=>{ return"-"+arguments[1].toLowerCase()})}:${val[attr]}`;
            }).join(';');
            el.style.cssText = cssText;
        } else if (key === 'on') { //目前忽略

        } else {
            htmlApi.setAttribute(el, key, val);
        }
    }
}

class ComponentUnit extends Unit{
    getHtml(){
        let {type,props} = this._selfElm;
        let component = new type(props);
        // 如果有组件将要渲染的函数的话需要执行
        component.componentWillMount && component.componentWillMount();
        let vnode = component.render();
        let elUnit = createUnit(vnode);
        let mark =  elUnit.getHtml();
        this._events.on('mounted', () => {
            component.componentDidMount && component.componentDidMount();
        });
        return mark;
    }
}
// 不考虑hook
class FunctionUnit extends Unit{
        getHtml(){
            let {type,props} = this._selfElm;
            let fn = type(props);
            let vnode = fn.render();
            let elUnit = createUnit(vnode);
            let mark =  elUnit.getHtml();
            return mark;
        }
}
function createUnit(vnode){
    if(isPrimitive(vnode)){
        return new TextUnit(vnode);
    }
    if (isRectElement(vnode) && isStr(vnode.type)) {
        return new NativeUnit(vnode);
    }
    if (isRectElement(vnode) && vnode.type.isReactComponent) {
        return new ComponentUnit(vnode);
    }
    if (isRectElement(vnode) && isFun(vnode.type)) {
        return new FunctionUnit(vnode);
    }
}

export default createUnit;

src\react-dom.js

import htmlApi from './domUtils';   
import EventFn from './event';
import createUnit from './unit';
/**
 * render方法可以将虚拟DOM转化成真实DOM
 *
 * @param {*} element 如果是字符串
 * @param {Element} container
 */
function render(element,container){
 
 let unit = createUnit(element);
 let domElement = unit.getHtml();
 htmlApi.appendChild(container, domElement);
 unit._events.emit('mounted');
}

export default { render}

7、diff算法在react的实现

前言:

我们根据4.5的react diff策略分析,实现新老节点比较到页面更新的过程实现的步骤如下:

1、创建types.js存放节点变更类型

2、创建diff.js

diff函数:diff(oldTree, newTree)  返回一个patches 补丁包

​ deepTraversal函数: 先序深度优先遍历树:

3、patch.js

​ patch(node,patches)函数:针对改变

4、


正式代码部分:

7.1、文本更新

我们预计可以实现的案例:如下:src\index.js

import React from './react'; //引入对应的方法来创建虚拟DOM
import ReactDOM from './react-dom';

class Counter extends React.Component{
    constructor(props) {
        super(props);
        this.state = {number:0}
    }
    componentDidMount() {
        setTimeout(() => {
           this.setState({number:this.state.number+1})
        }, 3000);        
    }
        // 组件是否要深度比较 默认为true
    componentShouldUpdate(nextProps,newState) {
        return true;
    }
    render(){
        return this.state.number;
    }
}
let el = React.createElement(Counter);

ReactDOM.render(el,document.getElementById('root'));

ps: React 元素都是immutable不可变的。当元素被创建之后,你是无法改变其内容或属性的。

那么怎么办呢?我们可以通过setTimeout()这种定时器重新调用render()函数,在创建一个新的元素传入其中;或者通过setState更改状态。如上例,用的setState方法。

第一步:加入setState方法

将component分离到一个js中 src\component.js

class Component {
    //用于判断是否是类组件  
    static isReactComponent = true;
    constructor(props) {
        this.props = props;
    }
   //更新 调用每个单元自身的unit的update方法,state状态对象或者函数 现在不考虑也不考虑异步
    setState(state) {
        //第一个参数是新节点,第二个参数是新状态
        this._selfUnit.update(null, state);
    }

}

export default Component

第二步:在react.js中导入import Component from './component';

第三步:增加update方法

src\unit.js

需要保存_selfUnit\当前组件实例保存在this._componentInstance

react提供了组件生命周期函数,shouldComponentUpdate,组件在决定重新渲染(虚拟dom比对完毕生成最终的dom后)之前会调用该函数,该函数将是否重新渲染的权限交给了开发者,该函数默认直接返回true,表示默认直接出发dom更新:

class ComponentUnit extends Unit{
    getHtml(){
        let {type,props} = this._selfElm;
        let component = this._componentInstance = new type(props);
        // 保存当前unit到当前实例上
        component._selfUnit = this;
        
        // 如果有组件将要渲染的函数的话需要执行
        component.componentWillMount && component.componentWillMount();
        let vnode  = component.render();
        let elUnit = this._renderUnit = createUnit(vnode);
        let mark = this._selfDomHtml = elUnit.getHtml();
        this._events.on('mounted', () => {
            component.componentDidMount && component.componentDidMount();
        });
        return mark;
    }
     // 这里负责处理组件的更新操作  setState方法调用更新
    update(newEl, partState) {
        // 获取新元素
        this._selfElm = newEl || this._selfElm;
        // 获取新状态 不管组件更新不更新 状态一定会修改
        let newState = this._componentInstance.state = Object.assign(this._componentInstance.state, partState);
        // 新的属性对象
        let newProps = this._selfElm.props;
        let shouldUpdate = this._componentInstance.componentShouldUpdate;
        if (shouldUpdate && !shouldUpdate(newProps, newState)) {
            return;
        }
        // 下边是需要深度比较
        let preRenderUnit = this._renderUnit;
        let preRenderEl = preRenderUnit._selfElm;
        let preDomEl = this._selfDomHtml;
        let newRenderEl = this._componentInstance.render();
        // 新旧两个元素类型一样 则可以进行深度比较,不一样,直接删除老元素,新建新元素
        if (shouldDeepCompare(preRenderEl, newRenderEl)) {
            // 调用相对应的unit中的update方法
            preRenderUnit.update(preDomEl,newRenderEl);
            this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate();
        } else {
        }

    }

}
// 文本节点
class TextUnit extends Unit{
    getHtml(){
        return htmlApi.createTextNode(this._selfElm);
    }
    update(node,newEl) {
        // 新老文本节点不相等,才需要替换
        if (this._selfElm !== newEl) {
            this._selfElm = newEl;
            htmlApi.setTextContent(node.parentNode, this._selfElm);
        }
    }
}

7.2 不同类型的更新直接替换

实现例子:src\index.js

import React from './react'; //引入对应的方法来创建虚拟DOM
import ReactDOM from './react-dom';


class Counter extends React.Component{
    constructor(props) {
        super(props);
        this.state = {number:0,isFlag:true}
    }
    componentWillMount() {
        console.log('componentWillMount 执行');
    }
        // 组件是否要更新
    componentShouldUpdate(nextProps,newState) {
        return true;
    }
    componentDidMount() {
        console.log('componentDidMount 执行');
        setTimeout(() => {
           this.setState({isFlag:false})
        }, 3000);        
    }
    componentDidUpdate() {
        console.log('componentDidUpdate Counter');
    }

    render(){
        return this.state.isFlag ? this.state.number : React.createElement('p',{id:'p'},'hello');
    }
}
let el = React.createElement(Counter,{name:'lee'});

ReactDOM.render(el,document.getElementById('root'));

更改src\unit.js中ComponentUnit的update函数

   // 这里负责处理组件的更新操作  setState方法调用更新
    update(newEl, partState) {
        // 获取新元素
        this._selfElm = newEl || this._selfElm;
        // 获取新状态 不管组件更新不更新 状态一定会修改
        let newState = this._componentInstance.state = Object.assign(this._componentInstance.state, partState);
        // 新的属性对象
        let newProps = this._selfElm.props;
        let shouldUpdate = this._componentInstance.componentShouldUpdate;
        if (shouldUpdate && !shouldUpdate(newProps, newState)) {
            return;
        }
        // 下边是需要深度比较
        let preRenderUnit = this._renderUnit;
        let preRenderEl = preRenderUnit._selfElm;
        let preDomEl = this._selfDomHtml;
        let parentNode = preDomEl.parentNode;
        let newRenderEl = this._componentInstance.render();
        // 新旧两个元素类型一样 则可以进行深度比较,不一样,直接删除老元素,新建新元素
        if (shouldDeepCompare(preRenderEl, newRenderEl)) {
            // 调用相对应的unit中的update方法
            preRenderUnit.update(preDomEl,newRenderEl);
            this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate();
        } else {
            // 类型相同 直接替换
            this._renderUnit = createUnit(newRenderEl);
            let newDom = this._renderUnit.getHtml();
            parentNode.replaceChild(newDom,preDomEl);
        }

    }

7.3 属性更新

1、src\unit.js 中 class NativeUnit extends Unit{} 增加方法

    // 记录属性的差异
    updateProps(oldNode,oldProps, props) {
        for (let key in oldProps) {
            if (!props.hasOwnProperty(key) && key != 'key') {
                if (key == 'style') {
                    oldNode.style[key] = '';
                }else{
                    delete oldNode[key];
                }
            }
            if (/^on[A-Z]/.test(key)) {
                // 解除绑定
            }
        }
        for (let propsName in props) {
            let val = props[propsName];
            if (propsName === 'key') {
                continue;
            }
            // 事件
            else if (propsName.startsWith('on')) {
                // 绑定事件
            } else if (propsName === 'children') {
                continue;
            } else if (propsName === 'className') {
                oldNode.className = val;
            } else if (propsName === 'style') {
                let cssText = Object.keys(val).map(attr => {
                    return `${attr.replace(/([A-Z])/g,()=>{ return"-"+arguments[1].toLowerCase()})}:${val[attr]}`;
                }).join(';');
                oldNode.style.cssText = cssText;
            } else {
                htmlApi.setAttribute(oldNode,propsName,val);
            }
        }

    }
    update(oldNode,newEl){
        let oldProps = this._selfElm.props;
        let props = newEl.props;
        // 比较节点的属性是否相同
        this.updateProps(oldNode,oldProps, props);
    }

7.4 children更新

把新的儿子们传递过来,与老的儿子们进行对比diff,然后找出差异patch,进行修改

修改src\unit.js

第一步:在全局定义、

let diffQueue; //差异队列
let updateDepth = 0; //更新的级别

第二步:比较差异

1、将之前的儿子们存起来,存在当前的_renderedChUs

2、获取老节点的key->index相应的集合

3、获取新的儿子们、和新儿子们的key->index的集合

4、循环新儿子,如果在老的key-》index集合中有,且当前的_mountIndex<lastIndex,且可复用插入队列,类型为MOVE;

新老不相等,说明没有复用,如果老的集合存在,为删除(REMOVE),老的集合不存在说明是新增INSERT,,这些都插入队列;

5、循环老儿子,不在新的儿子集合的,说明是删除,插入到队列

6、将得到的队列,即补丁包,进行更新到dom

完整的代码 src\unit.js

/** 
 *  凡是挂载到私有属性上的_开头
 *  */

import {
    isPrimitive,
    isArray,
    isFun,
    isRectElement,
    isStr    
} from './utils';
import htmlApi from './domUtils';
import EventFn from './event'; 
// import diff from './diff';
import types from './types';
// import patch from './patch';
// import diff form './diff';
let diffQueue = []; //差异队列
let updateDepth = 0; //更新的级别

class Unit{
    constructor(elm) {
        // 将
        this._selfElm = elm;
        this._events = new EventFn();
    }
    getHtml(){

    }
    
}

// 文本节点
class TextUnit extends Unit{
    getHtml(){
        this._selfDomHtml = htmlApi.createTextNode(this._selfElm);
        return this._selfDomHtml;
    }
    update(newEl) {
        // 新老文本节点不相等,才需要替换
        if (this._selfElm !== newEl) {
            this._selfElm = newEl;
            htmlApi.setTextContent(this._selfDomHtml.parentNode, this._selfElm);
        }
    }
}

// 
class NativeUnit extends Unit{
    getHtml() {
        let {type,props} = this._selfElm;
        // 创建dom
        let domElement = htmlApi.createElement(type);
        props = props || {};
    //   存放children节点
        this._renderedChUs = [];
        // 循环所有属性,然后设置属性
        for (let [key, val] of Object.entries(props)) {
            this.setProps(domElement, key, val,this);
        }
        this._selfDomHtml = domElement;
        return domElement;
    }
    /**
     *
     * 给dom设置属性
     * @param {Element} el 需要设置属性的dom元素
     * @param {*} key   需设置属性的key值
     * @param {*} val   需设置属性的value值
     */
    setProps(el, key, val,selfU) {
        if (key === 'children') {
            val = isArray(val) ? val : [val];
            val.forEach((c,i) => {
                if(c != undefined){
                    let cUnit = createUnit(c);
                    cUnit._mountIdx = i;
                    selfU._renderedChUs.push(cUnit);
                    let cHtml = cUnit.getHtml();
                    htmlApi.appendChild(el, cHtml);
                }

            });

        } else if (key === 'value') {
            let tagName = htmlApi.tagName(el) || '';
            tagName = tagName.toLowerCase();
            if (tagName === 'input' || tagName === 'textarea') {
                el.value = val;
            } else {
                // 如果节点不是 input 或者 textarea, 则使用 `setAttribute` 去设置属性
                htmlApi.setAttribute(el, key, val);
            }

        }
        // 类名
        else if (key === 'className') {
            if (val) el.className = val;
        } else if (key === 'style') {
            //需要注意的是JSX并不是html,在JSX中属性不能包含关键字,
            // 像class需要写成className,for需要写成htmlFor,并且属性名需要采用驼峰命名法
            let cssText = Object.keys(val).map(attr => {
                return `${attr.replace(/([A-Z])/g,()=>{ return"-"+arguments[1].toLowerCase()})}:${val[attr]}`;
            }).join(';');
            el.style.cssText = cssText;
        } else if (key === 'on') { //目前忽略

        } else {
            htmlApi.setAttribute(el, key, val);
        }
    }
    // 记录属性的差异
    updateProps(oldProps, props) {
        let oldNode = this._selfDomHtml;
        for (let key in oldProps) {
            if (!props.hasOwnProperty(key) && key != 'key') {
                if (key == 'style') {
                    oldNode.style[key] = '';
                }else{
                    delete oldNode[key];
                }
            }
            if (/^on[A-Z]/.test(key)) {
                // 解除绑定
            }
        }
        for (let propsName in props) {
            let val = props[propsName];
            if (propsName === 'key') {
                continue;
            }
            // 事件
            else if (propsName.startsWith('on')) {
                // 绑定事件
            } else if (propsName === 'children') {
                continue;
            } else if (propsName === 'className') {
                oldNode.className = val;
            } else if (propsName === 'style') {
                let cssText = Object.keys(val).map(attr => {
                    return `${attr.replace(/([A-Z])/g,()=>{ return"-"+arguments[1].toLowerCase()})}:${val[attr]}`;
                }).join(';');
                oldNode.style.cssText = cssText;
            } else {
                htmlApi.setAttribute(oldNode,propsName,val);
            }
        }

    }
    update(newEl){
        let oldProps = this._selfElm.props;
        let props = newEl.props;
        // 比较节点的属性是否相同
        this.updateProps(oldProps, props);
        // 比较children
        this.updateDOMChildren(props.children);

        
    }
    // 把新的儿子们传递过来,与老的儿子们进行对比,然后找出差异,进行修改
    updateDOMChildren(newChEls) {
        updateDepth++;
        this.diff(diffQueue, newChEls);
        updateDepth--;
        if (updateDepth === 0) {
            this.patch(diffQueue);
            diffQueue = [];
        }
    }
    // 计算差异
    diff(diffQueue, newChEls) {
        let oldChUMap = this.getOldChKeyMap(this._renderedChUs);
        let {newCh,newChUMap} = this.getNewCh(oldChUMap,newChEls);
        let lastIndex = 0; //上一个的确定位置的索引
        for (let i = 0; i < newCh.length; i++) {
            let c = newCh[i];
            let newKey = this.getKey(c,i);
            let oldChU = oldChUMap[newKey];
            if (oldChU === c) { //如果新老一致,说明是复用老节点
                if (oldChU._mountIdx < lastIndex) { //需要移动
                    diffQueue.push({
                        parentNode: oldChU._selfDomHtml.parentNode,
                        type: types.MOVE,
                        fromIndex: oldChU._mountIdx,
                        toIndex: i
                    });
                }
                lastIndex = Math.max(lastIndex, oldChU._mountIdx);

            } else {
                if (oldChU) {
                    diffQueue.push({
                        parentNode: oldChU._selfDomHtml.parentNode,
                        type: types.REMOVE,
                        fromIndex: oldChU._mountIdx
                    });
                     // 去掉当前的需要删除的unit
                     this._renderedChUs = this._renderedChUs.filter(item => item != oldChU);
                    // 去除绑定事件
                }

                let node = c.getHtml();
                diffQueue.push({
                    parentNode: this._selfDomHtml,
                    type: types.INSERT,
                    markUp: node,
                    toIndex: i
                });
            }
            // 
            c._mountIdx = i;
        }
   
        // 循环老儿子的key:节点的集合,在新儿子集合中没有找到的都打包到删除
        for (let oldKey in oldChUMap) {
            let oldCh = oldChUMap[oldKey];
            let parentNode = oldCh._selfDomHtml.parentNode;
            if (!newChUMap[oldKey]) {
                diffQueue.push({
                    parentNode: parentNode,
                    type: types.REMOVE,
                    fromIndex: oldCh._mountIdx
                });
                // 去掉当前的需要删除的unit
                this._renderedChUs = this._renderedChUs.filter(item => item != oldCh);
                // 去除绑定
            }
        }
        

    }
    // 打补丁
        patch(diffQueue) {
            let deleteCh = [];
            let delMap = {}; //保存可复用节点集合
           
            for (let i = 0; i < diffQueue.length; i++) {
                let curDiff = diffQueue[i];
                if (curDiff.type === types.MOVE || curDiff.type === types.REMOVE) {
                    let fromIndex = curDiff.fromIndex;
                    let oldCh = curDiff.parentNode.children[fromIndex];
                    delMap[fromIndex] = oldCh;
                    deleteCh.push(oldCh);
                }
            }
            deleteCh.forEach((item)=>{htmlApi.removeChild(item.parentNode, item)});

            for (let i = 0; i < diffQueue.length; i++) {
                let curDiff = diffQueue[i];
                switch (curDiff.type) {
                    case types.INSERT:
                        this.insertChildAt(curDiff.parentNode, curDiff.toIndex, curDiff.markUp);
                        break;
                    case types.MOVE:
                        this.insertChildAt(curDiff.parentNode, curDiff.toIndex, delMap[curDiff.fromIndex]);

                        break;
                    default:
                        break;
                }
            }

    }
    insertChildAt(parentNode, fromIndex, node) {
        let oldCh = parentNode.children[fromIndex];
        oldCh ? htmlApi.insertBefore(parentNode, node, oldCh) : htmlApi.appendChild(parentNode,node);
    }
    getKey(unit, i) {
        return (unit && unit._selfElm && unit._selfElm.key) || i.toString();
    }
    // 老的儿子节点的 key-》i节点 集合
    getOldChKeyMap(cUs = []) {
        let map = {};
        for (let i = 0; i < cUs.length; i++) {
            let c = cUs[i];
            let key = this.getKey(c,i);
            map[key] = c;
        }
        return map;
    }
    // 获取新的children,和新的儿子节点 key-》节点 结合
    getNewCh(oldChUMap, newChEls) {
        let newCh = [];
        let newChUMap = {};
        newChEls.forEach((c,i)=>{
            let key = (c && c.key) || i.toString();
            let oldUnit = oldChUMap[key];
            let oldEl = oldUnit && oldUnit._selfElm;
            if (shouldDeepCompare(oldEl, c)) {
                oldUnit.update(c);
                newCh.push(oldUnit);
                newChUMap[key] = oldUnit;
            } else {
                let newU = createUnit(c);
                newCh.push(newU);
                newChUMap[key] = newU;
                this._renderedChUs[i] = newCh;
            }
        });
        return {newCh,newChUMap};
    }
}

class ComponentUnit extends Unit{
    getHtml(){
        let {type,props} = this._selfElm;
        let component = this._componentInstance = new type(props);
        // 保存当前unit到当前实例上
        component._selfUnit = this;
        
        // 如果有组件将要渲染的函数的话需要执行
        component.componentWillMount && component.componentWillMount();
        let vnode  = component.render();
        let elUnit = this._renderUnit = createUnit(vnode);
        let mark = this._selfDomHtml = elUnit.getHtml();
        this._events.once('mounted', () => {
            component.componentDidMount && component.componentDidMount();
        });
        return mark;
    }
     // 这里负责处理组件的更新操作  setState方法调用更新
    update(newEl, partState) {
        // 获取新元素
        this._selfElm = newEl || this._selfElm;
        // 获取新状态 不管组件更新不更新 状态一定会修改
        let newState = this._componentInstance.state = Object.assign(this._componentInstance.state, partState);
        // 新的属性对象
        let newProps = this._selfElm.props;
        let shouldUpdate = this._componentInstance.componentShouldUpdate;
        if (shouldUpdate && !shouldUpdate(newProps, newState)) {
            return;
        }
        // 下边是需要深度比较
        let preRenderUnit = this._renderUnit;
        let preRenderEl = preRenderUnit._selfElm;
        let preDomEl = this._selfDomHtml;
        let parentNode = preDomEl.parentNode;
        let newRenderEl = this._componentInstance.render();
        // 新旧两个元素类型一样 则可以进行深度比较,不一样,直接删除老元素,新建新元素
        if (shouldDeepCompare(preRenderEl, newRenderEl)) {
            // 调用相对应的unit中的update方法
            preRenderUnit.update(newRenderEl);
            this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate();
        } else {
            // 类型相同 直接替换
            this._renderUnit = createUnit(newRenderEl);
            let newDom = this._renderUnit.getHtml();
            parentNode.replaceChild(newDom,preDomEl);
        }

    }

}
// 不考虑hook
class FunctionUnit extends Unit{
        getHtml(){
            let {type,props} = this._selfElm;
            let fn = type(props);
            let vnode = fn.render();
            let elUnit = createUnit(vnode);
            let mark =  elUnit.getHtml();
            this._selfDomHtml = mark;
            return mark;
        }
}
// 获取key,没有key获取当前可以在儿子节点内的索引
function getKey(unit,i){
    return (unit && unit._selfElm && unit._selfElm.key) || i.toString();
}
// 判断两个元素的类型是不是一样 需不需要进行深度比较
function shouldDeepCompare(oldEl, newEl) {
    if (oldEl != null && newEl != null) {
        if (isPrimitive(oldEl) && isPrimitive(newEl)) {
            return true;
        }
        if (isRectElement(oldEl) && isRectElement(newEl)) {
            return oldEl.type === newEl.type;
        }

    }
    return false;
}

function createUnit(vnode){
    if(isPrimitive(vnode)){
        return new TextUnit(vnode);
    }
    if (isRectElement(vnode) && isStr(vnode.type)) {
        return new NativeUnit(vnode);
    }
    if (isRectElement(vnode) && vnode.type.isReactComponent) {
        return new ComponentUnit(vnode);
    }
    if (isRectElement(vnode) && isFun(vnode.type)) {
        return new FunctionUnit(vnode);
    }
}

export default createUnit;

可查看完整的项目:github.com/learn-fe-co…

8、总结

1、虚拟dom是一个JavaScript对象

2、使用虚拟dom,运用dom-diff比较差异,复用节点是目的。为了减少dom的操作。

3、本文通过snabbdom.js和react中的虚拟dom的初步渲染,及dom-diff流程详细讲解了实现过程

4、需要注意react 最新的react fiber不太一样的diff实现,后续还会在有文章来具体分析

5、整个虚拟dom的实现流程:

  • 1、用JavaScript对象模拟DOM
  • 2、把此虚拟DOM转成真是DOM插入到页面中
  • 3、如果有事件发生修改了,需生成新的虚拟DOM
  • 4、比较两颗虚拟dom树的差异,得到差异对象 (也可称为补丁)
  • 5、把差异对象应用到真是的DOM树上

9、重点知识的讲解~~~~~~~~~

9.1、重绘和回流及浏览器的渲染机制

1、浏览器的运行机制

浏览器内核拿到html文件后,大致分为一下5个步骤

  • \1. 用HTML分析器,解析html元素,构建dom 树
  • \2. 用CSS分析器,解析CSS和元素上的样式,生成页面css规则树(Style Rules)
  • \3. 将上面的DOM树和样式表,关联起来,构建一颗Render树。这一过程又称为Attachment。每个DOM节点都有attach方法,接受样式信息,返回一个render对象(又名renderer)。这些render对象最终会被构建成一颗Render树。
  • \4. 布局(layout/ reflow),浏览器会为Render树上的每个节点确定在屏幕上的尺寸、位置
  • \5. 绘制Render树,绘制页面像素信息到屏幕上,这个过程叫paint, 页面显示出来

**所以,**当你用原生js 或jquery等库去操作DOM时,浏览器会从构建DOM树开始将整个流程执行一遍,所以频繁操作DOM会引起不需要的计算,导致页面卡顿,影响用户体验。那怎么办呢?所以这时有了Virtual DOM。Virtual DOM能很好的解决这个问题。它用javascript对象表示virtual node(VNode),根据VNode 计算出真实DOM需要做的最小变动,然后再操作真实DOM节点,提高渲染效率。

2、重绘和重排

参考资料:你真的了解浏览器页面渲染机制吗?

9.2 jsx

  1. 什么是jsx:

    jsx是js的扩展,基于JavaScript的语言,融合了XML,我们可以再js中书写XML。,将组件的结构、数据甚至样式都聚合在一起定义组件。

    ReactDOM.render( <h1>Hello</h1>, document.getElementById('root') );

    好处:

    • 1、更快的执行速度,
    • 2、类型安全
    • 3、开发效率
  2. 使用jsx元素

    浏览器无法直接解析jsx,需要通过插件来解析,(babel转化),例如react中:2.2.1

    • 最终会通过babeljs转译成createElement语法
    ReactDOM.render(<h1>hello</h1>,document.getElementById('app'));
    

    通过babel解析后的代码是:

    ReactDOM.render(React.createElement("h1", null, "hello"), document.getElementById('app'));
    
  3. jsx语法

    可以在js中书写XML

      1. xml中可以包含子元素,但是结构中只能有且仅有一个顶级元素。
    
    ReactDOM.render(<h1>hello</h1><h2>world</h2>,document.getElementById('app'));
    

    上边报错,应改为:

    ReactDOM.render(<div><h1>hello</h1><h2>world</h2><div>,document.getElementById('app'));
    
    1. 支持插值表达式:{}内部可以下的东西: 插值表达式:类似ES6模板字符串${表达式} 插值表达式语法: {表达式} (得到的是一个结果:值) 表达式中值如果是: 类型: 空、布尔值、未定义(不会报错,浏览器不会看到输出不输出任何职) 对象:插值表达式中不能直接输出对象,会报错,但是如果是一个数组对象是可以的,比如 { [1,2,3]} 浏览器看到的是:123 也就是说react对数组进行了转字符串操作,并且是用空字符串进行连接,arr.join('')
    ReactDOM.render(<h1>hello</h1><h2>{ 1+2 }</h2>,document.getElementById('app'));
    

    React没有模板语法,插值表达式中只支持表达式,不支持语句:for,if;

    但是我们可以:

    • if或者 for语句里使用JSX
    • 将它赋值给变量,当作参数传入,作为返回值都可以
     var users = [12,23,34];
       ReactDOM.render(
       <div>
        <ul>
         {
       /**
              根据数组中的内容生成一个包含有结构的新数组
       通过数组生成的结构,每一个元素必须包含一个key属性,同时key属性的值必须唯一
       */
          users.map( (user,index )=>{
           return <li key={index}>{user}</li>
       })
       }
        </ul>
       <div>,document.getElementById('app'));
    
    
    
  4. JSX 属性

    ​ JSX标签也是可以支持属性设置的。 ​ 基本使用和html/XML类型,在标签上添加属性名=属性值,值必须使用""包含 ​ 值是可以接受插值表达式的

    ​ 并且属性名需要采用驼峰命名法

    注意: 1、 class属性:使用className属性来代替 2、style:值必须使用对象

    例子:

    var idName = 'h2Id';
    ReactDOM.render(<div>
    <h1 id="title">hello</h1>
    <h2 id={idName }>world</h2>
    <h2 style={ {color:'yellow'}}>style</h2>
    <h2 className="classA">class样式</h2>
    <div>,
    document.getElementById('app'));
    

    9.3 、symbol

    我们在vnode中为了判断对象是否是虚拟节点加入了_type属性,其值我们用了symbol,那这个到底是什么呢?----》 Symbol ——ES6引入了第6种原始类型,表示独一无二的值。

    回忆:es5中的物种数据类型有: 字符串、数字、布尔值、null和undefined 。

    1、创建:

    可以用 Symbol()函数生成 Symbol值。

    Symbol函数接受一个可选参数,可以添加一段文本来描述即将创建的Symbol,这段描述不可用于属性访问,但是建议在每次创建Symbol时都添加这样一段描述,以便于阅读代码和调试Symbol程序

    let firstName = Symbol("first name");
    let person = {};
    person[firstName] = "lee";
    console.log("first name" in person); // false
    console.log(person[firstName]); // "lee"
    console.log(firstName); // "Symbol(first name)"
    

    ps: Symbol的描述被存储在内部[[Description]]属性中,只有当调用Symbol的toString()方法时才可以读取这个属性。在执行console.log()时隐式调用了firstName的toString()方法,所以它的描述会被打印到日志中,但不能直接在代码里访问[[Description]]

    2、 Symbol函数前不能使用new命令,否则会报错。因为生成的 Symbol 是一个原始类型的值,不是对象 .

    var sym = new Symbol(); // TypeError
    

    3、 Symbol是原始值,ES6扩展了typeof操作符,返回"symbol"。所以可以用typeof来检测变量是否为symbol类型

    4、 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。

    5、 Symbol 值作为对象属性名时,不能用点运算符,要使用[]

    var mySymbol = Symbol();
    var a = {};
    a.mySymbol = 'Hello!';
    
    a.mySymbol // undefined
    a[mySymbol] // “Hello!”
    

    6、Symbol 值作为属性名时,该属性还是公开属性,不是私有属性 ,可以在类的外部访问。但是不会出现在 for...in 、 for...of 的循环中,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 返回。如果要读取到一个对象的 Symbol 属性,可以通过 Object.getOwnPropertySymbols() 和 Reflect.ownKeys() 取到, 返回值是一个包含所有Symbol自有属性的数组 。

    let syObject = {};
    syObject[sy] = "kk";
    console.log(syObject);
     
    for (let i in syObject) {
      console.log(i);
    }    // 无输出
     
    Object.keys(syObject);                     // []
    Object.getOwnPropertySymbols(syObject);    // [Symbol(key1)]
    Reflect.ownKeys(syObject);                 // [Symbol(key1)]
    

    7、 可以接受一个字符串作为参数,为新创建的 Symbol 提供描述,用来显示在控制台或者作为字符串的时候使用,便于区分。

    9.3.1

    8、共享symbol值

    有时希望在不同的代码中共享同一个Symbol,例如,在应用中有两种不同的对象类型,但是希望它们使用同一个Symbol属性来表示一个独特的标识符。一般而言,在很大的代码库中或跨文件追踪Symbol非常困难而且容易出错,出于这些原因,ES6提供了一个可以随时访问的全局Symbol注册表 。

    Symbol.for() 可以生成同一个Symbol

    var a = Symbol('a');
    var b = Symbol('a');
    console.log(a===b); // false
    
    var a1 = Symbol.for('a');
    var b1 = Symbol.for('a');
    console.log(a1 === b1); //true
    
    let uid = Symbol.for("uid");
    let object = {};
    object[uid] = "12345";
    console.log(object[uid]); // "12345"
    console.log(uid); // "Symbol(uid)"
    

    Symbol.for()方法首先在全局Symbol注册表中搜索键为"uid"的Symbol是否存在。如果存在,直接返回已有的Symbol,否则,创建一个新的Symbol,并使用这个键在Symbol全局注册表中注册,随即返回新创建的Symbol

    let uid = Symbol.for("uid");
    let object = {
        [uid]: "12345"
    };
    console.log(object[uid]); // "12345"
    console.log(uid); // "Symbol(uid)"
    let uid2 = Symbol.for("uid");
    console.log(uid === uid2); // true
    console.log(object[uid2]); // "12345"
    console.log(uid2); // "Symbol(uid)
    

    在这个示例中,uid和uid2包含相同的Symbol并且可以互换使用。第一次调用Symbol.for()方法创建这个Symbol,第二次调用可以直接从Symbol的全局注册表中检索到这个Symbol

    9、Symbol值不能进行隐式转换,因此它与其他类型值进行运算,会报错。

    10、可以显示或隐式转成Boolean,却不能转成数值。

    var a = Symbol('a');
    Boolean(a) // true
    if(a){
      console.log(a);
    } // Symbol('a')
    

(图9.3.1)

参考: developer.mozilla.org/zh-CN/docs/…

https://www.runoob.com/w3cnote/es6-symbol.html 

9.4、export default 与export 区别

(图9.4.1)

有这种报错的原因是:

1、是真的没有导出;

2、代码:export default { patch}改为 export default patch 或者export {patch}

export default 与export 区别:

  1. export与export default均可用于导出常量、函数、文件、模块等,你可以在其它文件或模块中通过import+(常量 | 函数 | 文件 | 模块)名的方式,将其导入

  2. export、import可以有多个,export default仅有一个

  3. export导出对象需要用{ },export default不需要{ }

    export const str = 'hello world'
    
    export function f(a){
        return a+1
    }
    

    对应的导入方式:

    import { str, f } from 'demo1' //也可以分开写两次,导入的时候带花括号
    

    export default

    export default const str = 'hello world'
    

    对应的导入方式

    import str from 'demo1' //导入的时候没有花括号
    
  4. 使用export default命令,为模块指定默认输出,这样就不需要知道所要加载模块的变量名,我们在import时,可以任意取名

    //demo.js
    let str = 'hello world';
    export default str(str不能加大括号)
    //原本直接export str外部是无法识别的,加上default就可以了.但是一个文件内最多只能有一个export default。
    //其实此处相当于为str变量值"hello world"起了一个系统默认的变量名default,自然default只能有一个值,所以一个文件内不能有多个export default。
    
    

    对应的导入方式

    import any from "./demo.js"
    import any12 from "./demo.js" 
    console.log(any,any12)   // hello world,hello world