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

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

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

*文章比较长,你们耐心查看,居然写的超了专栏字数~~~~~o(╥﹏╥)o*css

在阅读文章以前,下面有几个问题,你先看看能不能答出来,这也是如今面试官常常会问到的问题,若是不知道也不要紧,咱们会在本文章讲到,你也能够带着这些问题来看看~html

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

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

  • 一、介绍vdom(Virtual DOM)
  • 二、简述vdom(Virtual DOM)的实现流程及核心api
  • 三、模拟vdom(Virtual DOM)的实现(不包括diff部分)
  • 四、diff算法实现流程原理 (重点、难点!!!!!!)
  • 五、模拟初步的diff算法实现
  • 六、应用:模拟vdom(Virtual DOM)在react中的实现
  • 七、应用:模拟vdom(Virtual DOM)中diff算法在react的实现
  • 八、总结
  • 九、重点知识的讲解及注释(也是一些面试官常常会问的问题

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

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

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

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

一、介绍Virtual DOM

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

1.1)dom是什么?

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

案例:jquery

真实dom:(代码1.1)linux

<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便可解决这些问题————————

二、简述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>'做为文本插入到页面。

总结:

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

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

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

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

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

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

ps:注意:

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

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

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

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

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

  • render()时注意

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

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

三、模拟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:代码理解注意:

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

二、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)

四、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.一、同级节点的比较,不能跨级

(网上找的图),以下图

(图4.3.1)

4.3.二、先序深度优化、广度优先:

一、深度优先

(图4.3.2)

二、广度优先

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

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

4.4.一、在比较以前咱们发现snabbdom中是用patch同一个函数来操做的,因此咱们须要判断。第一个参数传的是虚拟dom仍是 HTML 元素 。

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

4.4.三、经过方法来判断是不是同一个节点

方法:比较新节点(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.四、data 属性更新

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

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

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

4.4.五、children比较(最核心重点)

4.4.5.一、新节点的children是文本节点且oldvnode的text和vnode的text不一样,则更新为vnode的text

4.4.5.二、判断双方是只有一方有children,

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

ii、若是老节点的children没有,新的节点的children有,直接建立新的节点的children的dom引用到老的节点children上。

4.4.5.三、 将旧新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
复制代码

状况三、老尾与新头对比:

判断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
复制代码

状况四、老头与新尾对比

将老的结束节点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
复制代码

状况五、利用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 去拿下标 。

实现原理流程:

一、 oldKeyToIdx没有咱们须要新建立

二、 保存newStartVnode.keyoldKeyToIdx 中的索引

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

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

++newStartIdx;

newStartVnode = newChildren[newStartIdx];

案例说明:

可能的缘由有

一、此时的节点(须要比较的新节点)时新建立的,

二、当前节点(须要比较的新节点)在原来的位置是处于中间的(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
复制代码

比较过程

一、

(图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
复制代码

二、

(图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
复制代码

三、

(图4.4.5.6-3)

解释:

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

四、

(图4.4.5.6-4)

解释:

oldStartVnode是 undefined
会执行++oldStartIdx;
oldStartVnode -> C
复制代码

五、

(图4.4.5.6-5)

解释:

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

六、

(图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.五、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),比较流程:

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

二、是同一类型的组件: 按照原策略继续比较 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(删除)。

一、INSERT:表示新的虚拟dom的类型不在老集合(咱们会生成一个针对老的虚拟dom的key-index的集合)里,说明这个节点时新的,须要对新节点进行插入操做。
二、MOVE:表示在老的集合中存在,咱们这个时候要比较上一次保存的比较索引跟这个老的节点的自己索引比,且element是可更新的类型,这时候就须要作移动操做,能够复用之前的DOM节点
三、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.一、旧版生命周期

(图4.6.1)

4.6.二、新版的声明周期

(图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

五、模拟初步的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

一、若是新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);
        }
     }
}
复制代码

二、若是新节点没有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.三、 头头对比

在尾部新增、删除元素,

真是应用中效果从: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 两两比较, * 一、 oldStartVnode - > newStartVnode * 二、 oldEndVnode - > newEndVnode * 三、 newStartVnode - > oldEndVnode * 四、 newEndVnode - > oldStartVnode * 对上述四种状况执行对应的patch */
    // 一、新的开始节点跟老的开始节点相比较 是否是同样的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.四、 尾尾对比

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

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

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

// 二、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.五、 旧尾新头对比

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

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

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

// 三、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.六、 旧头新尾对比

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

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

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

// 四、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是否存在

/** 五、4种状况都不相等 * // 1. 从 oldChildren 数组创建 key --> index 的 map。 // 2. 只处理 newStartVnode (简化逻辑,有循环咱们最终仍是会处理到全部 vnode), // 以它的 key 从上面的 map 里拿到 index; // 3. 若是 index 存在,那么说明有对应的 old vnode,patch 就行了; // 4. 若是 index 不存在,那么说明 newStartVnode 是全新的 vnode,直接 // 建立对应的 dom 并插入。 */
    else{
      
      /** 若是 oldKeyToIdx 不存在, * 一、建立 old children 中 vnode 的 key 到 index 的 * 映射, 方便咱们以后经过 key 去拿下标 * */
       if (isUndef(oldKeyToIdx)) {
        oldKeyToIdx = createOldKeyToIdx(oldChildren,oldStartIdx,oldEndIdx);
       }
       // 二、尝试经过 newStartVnode 的 key 去拿下标
       oldIdxByKeyMap = oldKeyToIdx[newStartVnode.key];
      // 四、 下标索引不存在,说明newStartVnode 是全新的 vnode
       if (oldIdxByKeyMap == null) {
        // 那么为 newStartVnode 建立 dom 并插入到 oldStartVnode.elm 的前面。
        htmlApi.insertBefore(parentDOMElement,createElement(newStartVnode),oldStartVnode.elm);
        newStartVnode = newChildren[++newStartIdx];
       }
      // 三、下标存在 说明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…

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

前言:

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

一、建立项目

二、建立react.js:

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

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

三、建立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 增长类组件和函数组件的逻辑

一、增长类组件的逻辑渲染

咱们在例子: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;
  }
复制代码

二、增长函数组件的渲染

修改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}
复制代码

七、diff算法在react的实现

前言:

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

一、建立types.js存放节点变动类型

二、建立diff.js

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

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

三、patch.js

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

四、


正式代码部分:

7.一、文本更新

咱们预计能够实现的案例:以下: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 属性更新

一、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; //更新的级别
复制代码

第二步:比较差别

一、将以前的儿子们存起来,存在当前的_renderedChUs

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

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

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

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

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

六、将获得的队列,即补丁包,进行更新到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…

八、总结

一、虚拟dom是一个JavaScript对象

二、使用虚拟dom,运用dom-diff比较差别,复用节点是目的。为了减小dom的操做。

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

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

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

  • 一、用JavaScript对象模拟DOM
  • 二、把此虚拟DOM转成真是DOM插入到页面中
  • 三、若是有事件发生修改了,需生成新的虚拟DOM
  • 四、比较两颗虚拟dom树的差别,获得差别对象 (也可称为补丁)
  • 五、把差别对象应用到真是的DOM树上

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

9.一、重绘和回流及浏览器的渲染机制

一、浏览器的运行机制

浏览器内核拿到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节点,提升渲染效率。

二、重绘和重排

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

9.2 jsx

  1. 什么是jsx:

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

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

    好处:

    • 一、更快的执行速度,
    • 二、类型安全
    • 三、开发效率
  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类型,在标签上添加属性名=属性值,值必须使用""包含 ​ 值是能够接受插值表达式的

    ​ 而且属性名须要采用驼峰命名法

    注意: 一、 class属性:使用className属性来代替 二、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 。

    一、建立:

    能够用 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]]

    二、 Symbol函数前不能使用new命令,不然会报错。由于生成的 Symbol 是一个原始类型的值,不是对象 .

    var sym = new Symbol(); // TypeError
    复制代码

    三、 Symbol是原始值,ES6扩展了typeof操做符,返回"symbol"。因此能够用typeof来检测变量是否为symbol类型

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

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

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

    六、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)]
    复制代码

    七、 能够接受一个字符串做为参数,为新建立的 Symbol 提供描述,用来显示在控制台或者做为字符串的时候使用,便于区分。

    9.3.1

    八、共享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

    九、Symbol值不能进行隐式转换,所以它与其余类型值进行运算,会报错。

    十、能够显示或隐式转成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.四、export default 与export 区别

(图9.4.1)

有这种报错的缘由是:

一、是真的没有导出;

二、代码: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
    复制代码
相关文章
相关标签/搜索