詳解:虛擬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語法,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最好建立一個忽略文件來把忽略一些沒必要要的文件.ignore

第二步:安裝依賴包

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
    複製代碼
相關文章
相關標籤/搜索