1. 程式人生 > 前端設計 >vue響應式實現&vue及react的diff演算法

vue響應式實現&vue及react的diff演算法

ps 最近總結的東西比較多,下面只是摘出來的一部分第四階段部分。東西多了難免出現差錯望指正

下面程式碼均放在github上了

響應式原始碼地址 diff原始碼地址

第四階段 vue原理及其基本實現

不能手寫一個vue,你拿什麼和小白裝x?

在這裡插入圖片描述

4.1 從響應式入手

首先初始化狀態

class myVue {
    constructor(options) {
        // 初始化
        this._init(options)
    }

    // 初始化
    _init(options) {
        this.$options = options;

        // 初始化狀態
initstate(this) // 初始化渲染頁面 if (this.$options.el) { this.$mount() } } 複製程式碼

實現資料代理和資料監聽

基本思路:資料代理即把vm._data上的資料代理到vm上,資料監聽則需要在屬性getset中做些相應的邏輯處理

實現方法:vue2.x 的主要使用方法Object.defineProperty

下面程式碼中將對物件的監聽交予Observe例項處理

import Observe from './observe'


export
function initstate(vm) { let opts = vm.$options if (opts.data) { initData(vm) } if (opts.computed) {} if (opts.watch) {} } // 初始化data function initData(vm) { // 獲取data let data = vm.$options.data data = vm._data = typeof data === 'function' ? data.call(vm) : data || {} // 實現代理
for (let key in data) { proxy(vm,'_data',key) } // 實現資料監聽 observe(data) } // 資料監聽 export function observe(data) { // 判斷型別 if (typeof data !== 'object' || data === null) return return new Observe(data) } // 資料代理 function proxy(obj,tar,key) { Object.defineProperty(obj,key,{ get() { return obj[tar][key] },set(newValue) { obj[tar][key] = newValue } }) } 複製程式碼

Observe類程式碼

目前這裡只是實現了對個個物件屬性的監聽,為保證可以深度監聽,我們要不斷判斷進入defineReactive方法中的value的型別,若其仍為物件我們需要“遞迴”處理(相似於遞迴)

還要注意一點的是,我們在set一個新值時,新值的型別也可能為一個物件故仍需判斷

class Observe {

    constructor(data) {
        
        this.walk(data)
       

    }
    walk(data) {
        let keys = Object.keys(data)

        for (let i = 0; i < keys.length; i++) {
            let key = keys[i]
            let value = data[key]
            defineReactive(data,value)
        }
    }
}

function defineReactive(obj,value) {
    // 觀察value是不是一個物件
    if (typeof value === 'object') {
        childOb = observe(value)
    }
    Object.defineProperty(obj,{
        get() {
            console.log('獲取了資料')
            return value
        },set(newValue) {
            if (newValue == value) return
            if (typeof newValue === 'object') {
                observe(newValue)
            }
            console.log('設定了資料')
            value = newValue

        }
    })


}
複製程式碼

擴充套件陣列方法

上面已經完成了對物件屬性的監聽,但是Object.defineProperty是監聽不到陣列操作的。故下面需要擴充套件陣列的方法

需要擴充套件的陣列方法['push','pop','push','unshift','reverse','sort','splice'](在vue2.x中只重寫了這幾個陣列的方法)

這裡運用AOP的思想,在原型鏈中間進行攔截擴充套件

push,unshift操作可能像陣列中推進去了一個物件屬性,故我們仍需對這個新增屬性實現監聽

// 監聽陣列
const methods = ['push','pop','shift','unshift','reverse','sort','splice']

// AOP的思想擴充套件陣列
const oldMethods = Array.prototype
export let arrayMethods = Object.create(oldMethods)
methods.forEach((methods) => {
    arrayMethods[methods] = function(...args) {
        let res = oldMethods[methods].apply(this,args)
        console.log('監聽邏輯')

        // 獲取新增屬性
        let insert
        switch (methods) {
            case 'push':
            case 'unshift':
                insert = args
                break;
            case 'splice':
                insert = args.slice(2)
                break
        }
        // 新增屬性的監聽
        if (insert) {
            observeArray(insert)
        }
         return res
    }
})

// 新增屬性的監聽
export function observeArray(insert) {
    for (let i = 0; i < insert.length; i++) {
        observe(insert[i])
    }
}
複製程式碼

Observe類中相應判斷data型別

class Observe {

    constructor(data) {
        
        if (Array.isArray(data)) {
            // 擴充套件陣列方法
            data.__proto__ = arrayMethods

            // 監聽陣列元素
            observeArray(data)

        } else {
            this.walk(data)
        }

    }
    walk(data) {
        let keys = Object.keys(data)

        for (let i = 0; i < keys.length; i++) {
            let key = keys[i]
            let value = data[key]
            defineReactive(data,value)
        }
    }
}
}
複製程式碼

掛載和頁面渲染

主要思想:根據選項所傳的el拿到所要掛載的節點及其表示式{{demo}}等,然後進行資料與表示式的替換。頁面渲染所需的更新方法其核心演算法是diff,diff將在4.2中總結。這裡只實現資料和模板的替換

// 初始化
    _init(options) {
        this.$options = options;

        // 初始化狀態
        initstate(this)

        // 初始化渲染頁面
        if (this.$options.el) {
            this.$mount()
        }
    }

    $mount() {
        // 拿到節點
        let vm = this
        let el = this.$options.el
        el = this.$el = query(el)
        const updataComponent = () => {
            console.log('更新和渲染的實現')
            vm._update()
        }

        // 渲染節點
        new Watch(this,updataComponent)
    }
    _update() {
        let vm = this
        let el = vm.$el

        let node = document.createDocumentFragment()
        let firstChild
        while (firstChild = el.firstChild) {
            node.appendChild(firstChild)
        }

        // 文字替換
        compiler(node,vm)

        //最終掛載
        el.appendChild(node)

    }
}

function query(el) {
    if (typeof el !== 'string') return
    return document.querySelector(el)
}
複製程式碼

文字替換中核心程式碼

思想:取得當前elDOM下的文字節點,即各種表示式。然後進行文字的替換

const util = {
    getval: function(vm,expr) {
        // 可能是msg.foo.name.age,故需要一層一層的取
        let keys = expr.split('.')
        return keys.reduce((pre,next) => {
            pre = pre[next]
            return pre
        },vm)
    },compilerText: function(node,vm) {
        if (!node.expr) {
            node.expr = node.textContent
        }
        node.textContent = node.expr.replace(/\{\{((?:.|\r?\n)+?)\}\}/g,function(...args) {

            return util.getval(vm,args[1])

        })
    }
}

export function compiler(node,vm) {
    let childNodes = node.childNodes;

    [...childNodes].forEach(child => {
        if (child.nodeType === 1) {
            compiler(child,vm)
        }
        if (child.nodeType === 3) {
            util.compilerText(child,vm)
        }
    })
}
複製程式碼

vue中,幹活的一般均是watcher。上面的更新操作也是交予watcher去完成的。

上面現階段watcher類的程式碼

let id = 0
class Watch {
    constructor(vm,exprs,cb = () => {},opts) {
        this.vm = vm
        this.exprs = exprs
        this.cb = cb
        this.id = id++;
       

        if (typeof exprs === 'function') {
            this.getter = exprs
        }

        this.get()
    }
    get() {    
        this.getter()    
    }

}
複製程式碼

依賴收集和派發更新

上面我們實現了資料的監聽,接下來開始實現依賴的收集和派發更新。以保證監聽到資料的變化,頁面可以響應式的進行更新

首先編寫dep類,用於收集watcher

watcher棧存在的目的即保證只有一個全域性watcher

dep類的程式碼

let id = 0

class Dep {
    constructor() {
        this.subs = []
        this.id=id++
    }

    // 訂閱
    addSub(watcher) {
        this.subs.push(watcher)
    }

    // 釋出
    notify() {
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
    
    //實現與watcher關聯
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }

}

let stack = []

export function pushTarget(wacher) {
    stack.push(wacher)
    Dep.target = wacher
}

export function popTarget() {
    stack.pop()
    Dep.target = stack[stack.length]
}

export default Dep
複製程式碼

get時進行依賴收集,set是進行派發更新

function defineReactive(obj,value) {
    // 觀察value是不是一個物件
  
    if (typeof value === 'object') {
         observe(value)
    }
    let dep = new Dep()
    Object.defineProperty(obj,{
        get() {

            // 進行依賴收集
            if (Dep.target) {
                dep.depend()
            }
            console.log('獲取了資料')
            return value
        },set(newValue) {
            if (newValue == value) return
            if (typeof newValue === 'object') {
                observe(newValue)
            }
            console.log('設定了資料')
            value = newValue

            // 進行派發更新
            dep.notify()
        }
    })

複製程式碼

注意上面程式碼中,依賴收集我們使用的方法是dep.depend()但是Dep類中我們明明可以直接使用addSub。為什麼要這麼做?

我們要清楚dep和watcher的關係

每一屬性一一對應一個watcher,而一個watcher則會對應多個dep。

此時的watcher類中的程式碼

上面依賴收集是呼叫了Dep.target.addDep(this),Dep.target指的是此時的全域性watcher,即此時呼叫了全域性watcher的addDep.並將此時的dep例項帶了過去

注意dep的去重,這裡用了set結構去重更為方便

同時在addDep中也需要將這個watcher再加到dep中

import { pushTarget,popTarget } from './dep'

let id = 0
class Watch {
    constructor(vm,opts) {
        this.vm = vm
        this.exprs = exprs
        this.cb = cb
        this.id = id++;
        this.deps = []
        this.depsId = new Set()

        if (typeof exprs === 'function') {
            this.getter = exprs
        }

        this.get()
    }
    get() {
        pushTarget(this)
        this.getter()
        popTarget()
    }
  	update() {
         this.get()     
    }
    addDep(dep) {
        let id = dep.id
        if (!this.depsId.has(id)) {
            this.depsId.add(id)
            this.deps.push(dep)
            dep.addSub(this)
        }
    }

}
複製程式碼

派發更新就不用管了,直接在set時呼叫dep的notify通知watcher去更新即可

實現非同步更新

我們都知道vue中的元件更新是非同步執行的,即我們不斷的設定vm.age=1,vm.age=2,vm,age=3,到最後其實它只做了一次更新渲染的操作。

實現改寫watcher中的update

即利用巨集或微任務,將更新操作放到非同步佇列中執行,同時因為可能會存在多個watcher,故需要儲存到一個數組中去注意去重。注意去重

import { pushTarget,opts) {
        this.vm = vm
        this.exprs = exprs
        this.cb = cb
        this.id = id++;
        this.deps = []
        this.depsId = new Set()

        if (typeof exprs === 'function') {
            this.getter = exprs
        }

        this.get()
    }
    get() {
        pushTarget(this)
        this.getter()
        popTarget()
    }
    update() {
        // 實現非同步更新
        // this.get()
        queneWatcher(this)

    }
    run() {
        this.get()
    }
    addDep(dep) {
        let id = dep.id
        if (!this.depsId.has(id)) {
            this.depsId.add(id)
            this.deps.push(dep)
            dep.addSub(this)
        }
    }

}

//非同步更新
let has = {}
let queue = []

function queneWatcher(watcher) {
    let id = watcher.id
    if (has[id] == null) {
        has[id] = true
        queue.push(watcher)
    }
    nextTick(fluqueue)

}

function fluqueue() {
    queue.forEach(watcher => {
        watcher.run()
    })
    has = {}
    queue = []
}



function nextTick(fluqueue) {
    setTimeout(fluqueue,0)
}
export default Watch
複製程式碼

完成陣列的依賴收集和派發更新

上述對陣列的操作僅完成了對陣列方法和屬性的監聽,還沒完善監聽到之後的處理。

主要處理思想:為所有需要監聽的物件或陣列增加一個屬性,此屬性儲存的是其對應的dep。然後再有dep進行依賴收集和派發更新。進行依賴收集的位置不做改變

更新observe類程式碼如下:

class Observe {
    constructor(data){ // data就是我們定義的data vm._data例項
        // 將使用者的資料使用defineProperty定義
        // 建立陣列專用 的dep
        this.dep = new Dep()
        // 給我們的物件包括我們的陣列新增一個屬性__ob__ (這個屬性即當前的observe)
        Object.defineProperty(data,'__ob__',{
            get:() => this
        })
        if (Array.isArray(data)){
            data.__proto__ = arrayMethods
            observerArray(data)
        }else {
            this.walk(data)
        }
    }
    walk(data){
        let keys = Object.keys(data)
        for (let i = 0;i<keys.length;i++){
            let key  = keys[i]; // 所有的key
            let value = data[keys[i]] //所有的value
            defineReactive(data,value)
        }
    }
}
export function defineReactive(data,value) {
    // 觀察value是不是物件,是的話需要監聽它的屬性。
    let childOb = observe(value)
    let dep = new Dep()
    Object.defineProperty(data,{
        get(){
            if (Dep.target){
                dep.depend() //讓dep儲存watcher,也讓watcher儲存這個dep

                if (childOb){
                    childOb.dep.depend()
                    dependArray(value) //收集兒子的依賴
                }
            }
            return value
        },set(newValue){
            if (newValue === value) return
            value = newValue
            observe(value)

            // 當設定屬性的時候,實現更新
            dep.notify()

        }
    })
}


export default Observe

複製程式碼

在陣列相應方法中進行派發更新

即處理完陣列擴充套件的相應操作之後,進行派發更新this.__ob__.dep.notify()

methods.forEach(method=>{
    arrayMethods[method] = function (...arg) {
        // 不光要返回新的陣列方法,還要執行監聽
        let res = oldArrayPrototypeMethods[method].apply(this,arg)
        // 實現新增屬性的監聽
        console.log("我是{}物件中的push,我在這裡實現監聽");
        // 實現新增屬性的監聽
        let inserted
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = arg
                break
            case 'splice':
                inserted = arg.slice(2)
                break
            default:
                break
        }
        // 實現新增屬性的監聽
        if (inserted){
            observerArray(inserted)
        }
        this.__ob__.dep.notify()
        return res
    }
複製程式碼

4.2 來看DIFF

在這裡插入圖片描述

所謂虛擬Dom也就是用js物件來描述真實的DOM,使用虛擬DOM的原因即減少對真實DOM操作的開支,眾所周知對DOM的操作是十分消耗效能的。下面開始從建立虛擬DOM開始到手寫react和vue中的兩種diff演算法

虛擬DOM

希望得到的結果:

   tag: 'div',data: { id: 'app' },children: [{
            tag: 'p',data: {
                class: 'demo'
            }
        }]
複製程式碼

index.js中要建立的VNode

const prevVNode = createElement('div',null,[
    createElement('p',{ key: 'a',style: { color: 'blue' } },'節點a'),createElement('p',{ key: 'b','@click': () => { alert('呵呵') } },'節點b'),{ key: 'c' },'節點c'),{ key: 'd' },'節點d'),])
複製程式碼

createElement函式,這裡將函式的第三個引數作為文字或孩子節點,若引數型別是一個數組則表示傳過來的是孩子節點否則是文字節點。即孩子標記位為SINGLE的,代表其是一個子文字

export const vnodeType = {
     HTML: 'HTML',TEXT: 'TEXT'
 }
 export const childrenType = {
     EMPTY: 'EMPTY',SINGLE: 'SINGLE',//代表文字節點
     MANY: 'MANY'
 }

 export default function createElement(tag,data,children = null) {
     let flag,childrenFlag
     typeof tag === 'string' ? flag = vnodeType.HTML : flag = vnodeType.TEXT


     if (children == null) {
         childrenFlag = childrenType.EMPTY

     } else if (Array.isArray(children)) {

         if (children.length == 0) {
             childrenFlag = childrenType.EMPTY
         } else {
             childrenFlag = childrenType.MANY
         }
     }
     //  如果children不是一個數組,則代表它是一個文字節點
     else if (typeof children === 'string') {

         childrenFlag = childrenType.SINGLE
         children = createTextVnode(children + "")
     }

     return {
         flag,//表示vnode的型別
         tag,key: data && data.key,childrenFlag,children,el: null
     }
 }

 function createTextVnode(text) {
     return {
         flag: vnodeType.TEXT,tag: null,data: null,childrenFlag: childrenType.EMPTY,children: text,}
 }
複製程式碼

此時建立的結果:

\[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-9sSwH3Z7-1593230471144)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593222372229.png)\]

渲染虛擬DOM

虛擬DOM已經生成,下面開始渲染。渲染分為首次和再次。首次比較簡單直接將此虛擬DOM掛載到真實DOM即可,再次則需要新老節點的patch

render函式

import { mount } from './mount'
import { patch } from './patch'
export function render(vnode,container) {

    if (container.vnode) {
        // 再次渲染 需要diff
        patch(container.vnode,vnode,container)

    } else {
        // 首次渲染 直接mount
        mount(vnode,container)
    }
    // 儲存vnode以供下次diff
    container.vnode = vnode

}
複製程式碼

再來看mount函式

mount裡面也十分簡單,就是掛載一下元素,掛載一下屬性,掛載文字,掛載孩子

import { vnodeType,childrenType } from './createElement'
import { patchData } from './patch'
export function mount(vnode,container,refNode) {
    let { flag } = vnode
    if (flag == vnodeType.HTML) {
        mountElement(vnode,refNode)
    } else if (flag == vnodeType.TEXT) {
        mountText(vnode,container)
    }
}

// 掛載元素節點
function mountElement(vnode,refNode) {
    const dom = document.createElement(vnode.tag)
    vnode.el = dom
    const { data,childrenFlag } = vnode

    // 掛載屬性
    if (data) {
        for (let key in data) {
            patchData(dom,data[key])
        }
    }
    // 掛載子元素

    if (childrenFlag !== childrenType.EMPTY) {
        // 掛載文字子節點
        if (childrenFlag === childrenType.SINGLE) {
            mount(children,dom)
        }
        //掛載元素子節點
        else if (childrenFlag === childrenType.MANY) {
            for (let i = 0; i < children.length; i++) {
                mount(children[i],dom)
            }
        }
    }
    refNode ? container.insertBefore(dom,refNode) : container.appendChild(dom)
}

// 掛載文字節點
function mountText(vnode,container) {
    const dom = document.createTextNode(vnode.children)
    vnode.el = dom
    container.appendChild(dom)
}

複製程式碼

patchData函式:現在要做的只是把此節點的data中遍歷一下,一次掛到它的真實dom上

export function patchData(el,pre,next) {
    
    switch (key) {
        case 'style':
            for (let k in next) {
                el.style[k] = next[k]
            }
            break
        case 'class':
            el.className = next
            break
        default:
            if (key[0] === '@') {
                el.addEventListener(key.slice(1),next)
            } else {
                el.setAttribute(key,next)
            }


    }
}
複製程式碼

此時頁面便已經被渲染出來了

在這裡插入圖片描述

新老節點的對比

開始逐步進入重點咯

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-SbraH93V-1593230471154)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593223160153.png)\]

當頁面發生變動,render被再次呼叫,patch就要來了。

放心這裡還仍然沒有到核心部分(滑稽),故相對來言也就是按照邏輯往下走。

首先我們在VNode建立的時候寫了兩個標記位,一個是標記的Vnode的型別,一個是孩子的型別。接下來我們就可以根據他們進行判斷了。即標記位不一樣的沒有比較的價值直接替換

export function patch(pre,next,container) {
    const { flag: preFlag } = pre
    const { flag: nextFlag } = next


    // 新老虛擬節點型別不一致,直接替換
    if (preFlag !== nextFlag) {
        replaceVnode(pre,container)
    } else if (nextFlag === vnodeType.HTML) {
        patchElement(pre,container)
    } else if (nextFlag === vnodeType.TEXT) {
        patchText(pre,container)
    }
}

複製程式碼

patchText函式:直接將DOM上的文字換成新的即可

function patchText(pre,) {
    let el = next.el = pre.el


    if (el.children != pre.children) {
        el.nodeValue = next.children
    }
}
複製程式碼

patchElement函式,比較兩個元素。先看標籤tag是否相同。不相同也沒有比較的價值直接替換即可,tag一樣則開始比較data,再比較孩子

function patchElement(pre,container) {
    // 先檢視tag是否一樣,直接把老的虛擬節點換成新的
    if (pre.tag !== next.tag) {
        replaceVnode(pre,container)
    } else {
        // tag一樣開始diff屬性
        let el = next.el = pre.el
        let preData = pre.data
        let nextData = next.data

        // data裡面鍵一樣的拿去更新
        if (nextData) {
            for (let k in nextData) {
                let nextVal = nextData[k]
                patchData(el,k,nextVal)
            }
        }

        // 老的有新的沒有
        if (preData) {
            for (let k in preData) {
                let preVal = preData[k]
                if (preVal && !nextData.hasOwnProperty(k)) {
                    patchData(el,preVal,null)
                }
            }
        }

        //屬性diff完成後,patch子節點
        patchChildren(pre.childrenFlag,next.childrenFlag,pre.children,next.children,el)
    }
}
複製程式碼

替換節點replaceVnode:即新的搞進去老的移出

function replaceVnode(pre,container) {
    container.removeChild(pre.el)
    mount(next,container)
}
複製程式碼

patchData:即先將新屬性全部放上去,再把老的有新的沒有的全部幹掉。事件的話老的全部移出,新的監聽上去就ok了

export function patchData(el,next) {
    // 宗旨:新的全覆蓋,老的有新的沒有直接幹掉
    switch (key) {
        case 'style':
            for (let k in next) {
                el.style[k] = next[k]
            }
            // 老的有,新的沒有的屬性直接刪除
            for (let k in pre) {
                if (!next.hasOwnProperty(k)) {
                    el.style[k] = ''
                }
            }
            break
        case 'class':
            el.className = next
            break
        default:
            if (key[0] === '@') {
                //先把老的事件清除
                if (pre) {
                    el.removeEventListener(key.slice(1),pre)
                }
                el.addEventListener(key.slice(1),next)
            }


    }
}
複製程式碼

下面便來看patchChildren了,比較孩子還是先利用孩子的標記位。這裡需要考慮9種情況。不要嫌多,前八種的邏輯均是非常簡單的。先來看

即老的孩子flag為SINGLE,這時新的為空即只需把老的幹掉;新的也為SINGLE則直接patch;新的為MANY,則老的幹掉,新的迴圈掛上去

下面同理,只有最後一情況多對多複雜一些下面再討論

function patchChildren(preChildrenFlag,nextChildrenFlag,preChildren,nextChildren,container) {
    switch (preChildrenFlag) {
        case childrenType.SINGLE:
            switch (nextChildrenFlag) {

                case childrenType.SINGLE:
                    patch(preChildren,container)
                    break;

                case childrenType.EMPTY:
                    container.removeChild(preChildren)
                    break;

                case childrenType.MANY:
                    container.removeChild(preChildren)
                    nextChildren.forEach(children => mount(children,container))
                    break;
            }
            break;

        case childrenType.EMPTY:
            switch (nextChildrenFlag) {

                case childrenType.SINGLE:
                    mount(nextChildren,container)
                    break;

                case childrenType.EMPTY:
                    break;

                case childrenType.MANY:
                    nextChildren.forEach(children => mount(children,container))
                    break;
            }
            break;
        case childrenType.MANY:
            switch (nextChildrenFlag) {

                case childrenType.SINGLE:
                    preChildren.forEach(children => container.removeChild(children))
                    mount(nextChildren,container)
                    break;

                case childrenType.EMPTY:
                    preChildren.forEach(children => container.removeChild(children))
                    break;

                case childrenType.MANY:
                    // 重點
                    // reactDiffChild(preChildren,container)
                    vueDiffChild(preChildren,container)
                    break;
            }
            break;
    }
}
複製程式碼

終於來到diff的絕對核心了

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-A0EBKN1g-1593230471157)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593224344679.png)\]

​ 這裡的情況及oldVnode和newVnode均有多個孩子,這裡情況比較複雜點。比上面單純照著邏輯走的就稍微不是那麼好寫一點。當前也就是複雜點的邏輯而已,不比困難的力扣

​ 這裡又要分情況了,react和vue針對這裡的diff處理邏輯是不一樣的,因為這裡是diff演算法的核心,我下面會總結的文字稍微多那麼一點哈

react中的處理思想一對指標

先來看簡單的:react中的處理

react中只使用了一組指標。

看圖:首先new裡面拿出指標指的第一個c去old裡面找,c在old的位置為2,在新的為0,老的中大即不需要移動位置,new中指標後移

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-9L8VDaTt-1593230471159)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593225181097.png)\]

接下來即new中的a,去old裡面找,找到了位置在old裡面為0,在new裡面為1,old裡面的小則需要移動,移動到哪呢,看圖也知道是old裡面c的後面。怎麼表示呢?不要忘了上面我們儲存了一些一個index,它就是old中c的位置。即插到c的下一個兄弟之前

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-uFfDnLkz-1593230471162)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593225497801.png)\]

指標再後移,到b了,顯然b也是要移動位置的,即

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-kw2v0TGP-1593230471164)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593225616626.png)\]

下面new中該e了,e去老的裡面沒有找到。則此時該新掛一個節點了,新掛到哪呢?b的後面啊

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-1LbGkVx8-1593230471166)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593225841051.png)\]

此時new的指標已經走完,我們需要做的操作只需把此時老的裡面有新的沒有的節點幹掉即可,大功告成編寫程式碼

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-hNaZbMvB-1593230471167)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593225924447.png)\]

程式碼實現

//react中diff核心的處理方法,較為簡單但是缺點很大
function reactDiffChild(prevChildren,container) {
    let lastIndex = 0
    for (let i = 0; i < nextChildren.length; i++) {
        const nextVNode = nextChildren[i]
        let j = 0,find = false
        for (j; j < prevChildren.length; j++) {
            const prevVNode = prevChildren[j]
            if (nextVNode.key === prevVNode.key) {
                find = true

                // 去更新一下內部
                patch(prevVNode,nextVNode,container)

                if (j < lastIndex) {
                    // 老節點需要移動
                    const refNode = nextChildren[i - 1].el.nextSibling
                    container.insertBefore(prevVNode.el,refNode)
                    break
                } else {
                    // 更新 lastIndex
                    lastIndex = j
                }
            }
        }
        if (!find) {
            // 掛載新節點
            const refNode =
                // 判斷一下是不是首節點
                i - 1 < 0 ?
                prevChildren[0].el :
                nextChildren[i - 1].el.nextSibling

            mount(nextVNode,refNode)
        }
    }
    // 移除已經不存在的節點
    for (let i = 0; i < prevChildren.length; i++) {
        const prevVNode = prevChildren[i]
        const has = nextChildren.find(
            nextVNode => nextVNode.key === prevVNode.key
        )
        if (!has) {
            // 移除
            container.removeChild(prevVNode.el)
        }
    }
}
複製程式碼
vue中的處理思想兩隊指標

相比於react,vue便更為複雜一些了。當然複雜有複雜的好處,react的那種處理有很大的缺陷(比如新的頭和老的尾k一樣,那麼old中除了開始的尾其餘的均需要移動)。

vue這裡有兩隊指標,故需要考慮5種情況。

1 先來看最為簡單的

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-3LuebY9T-1593230471168)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593226307493.png)\]

old和new的頭匹配到了,則直接兩頭指標後移。迴圈以最短的為主,即最後

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-Jg9AB3Bw-1593230471170)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593226620314.png)\]

這時發現new中有剩餘,我們的操作只需把剩餘的掛到後面即可

2 與1類似,尾尾匹配,即從尾部往前找即可

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-olrwEG9e-1593230471172)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593226718797.png)\]

3 新尾老頭匹配

在這裡插入圖片描述

4 新頭老尾匹配

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-dbz9aFlD-1593230471173)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593226795862.png)\]

5 都不匹配

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-JgtVICZD-1593230471175)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593226881781.png)\]

直接拿我程式碼中的栗子來舉例吧

開始如圖:

在這裡插入圖片描述

新頭老尾匹配,則老尾移動到前面去,指標向中間移動

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-gk78KJXu-1593230471180)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593227364120.png)\]

此時再觀察發現頭頭匹配,指標後移

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-On3c21D3-1593230471181)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593227412731.png)\]

還是頭頭,再後移

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-u7lERdjO-1593230471183)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593227435740.png)\]

此時均不能匹配,則拿new裡的f去看old裡有沒有,沒有則掛到此時old的startIndex之前的位置,指標後移

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-C4euWMHK-1593230471184)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593229742448.png)\]

同理e也沒找到,則像上面一樣掛到startIndex之前

\[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-zqw6HNto-1593230471185)(C:\Users\T540P\AppData\Roaming\Typora\typora-user-images\1593229847900.png)\]

此時new中遍歷走完,old中只需幹掉後面的即可

程式碼實現

//vue中的diff核心處理方法,較為複雜,優於react
function vueDiffChild(prevChildren,container) {
    let oldStartIndex = 0
    let oldEndIndex = prevChildren.length - 1
    let oldStartVnode = prevChildren[0]
    let oldEndVnode = prevChildren[oldEndIndex]

    let newStartIndex = 0
    let newEndIndex = nextChildren.length - 1
    let newStartVnode = nextChildren[0]
    let newEndVnode = nextChildren[newEndIndex]

    // 五種情況:頭頭,尾尾,頭尾,尾頭,暴力迴圈
    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        //頭頭匹配
        if (oldStartVnode.key === newStartVnode.key) {
            // 更新內部
            patch(oldStartVnode,newStartVnode,container)

            // 指標後移
            oldStartVnode = prevChildren[++oldStartIndex]
            newStartVnode = nextChildren[++newStartIndex]
        }
        // 尾尾匹配
        else if (oldEndVnode.key === newEndVnode.key) {
            // 更新內部
            patch(oldEndVnode,newEndVnode,container)

            // 指標前移
            oldEndVnode = prevChildren[--oldEndIndex]
            newEndVnode = nextChildren[--newEndIndex]
        }
        // 新尾老頭匹配
        else if (newEndVnode.key === oldStartVnode.key) {

            patch(oldStartVnode,container)

            // 將老的頭整到最後
            container.appendChild(oldStartVnode.el)

            // 新指標前移,老指標後移

            oldStartVnode = prevChildren[++oldStartIndex]
            newEndVnode = nextChildren[--newEndIndex]
        }
        // 新頭老尾匹配
        else if (newStartVnode.key === oldEndVnode.key) {

            patch(oldEndVnode,container)

            // 將老的尾整到前面去
            container.insertBefore(oldEndVnode.el,oldStartVnode.el)

            //新指標後移,老指標前移
            newStartVnode = nextChildren[++newStartIndex]
            oldEndVnode = prevChildren[--oldEndIndex]


        }
        // 均不能匹配
        else {
            let index;
            let map = keyMapByIndex(prevChildren)
            if (map.hasOwnProperty(newStartVnode.key)) {
                index = map[newStartVnode.key]
                    // 找到了,則開始移動
                let toMoveNode = prevChildren[index]
                patch(toMoveNode,container)
                container.insertBefore(toMoveNode,oldStartIndex)
            } else {
                // 沒有找到即掛到新指標之前的位置
                mount(newStartVnode,oldStartVnode.el)
            }

            // 後移指標
            newStartVnode = nextChildren[++newStartIndex]
        }



    }


    // 建立一個對映表
    // {a:0,b:1}
    function keyMapByIndex(prevChildren) {
        let map = {}
        for (let i = 0; i < prevChildren; i++) {
            let current = prevChildren[i]
            if (current.key) {
                map[current.key] = i
            }
        }
        return map
    }

    // 將多餘的放進去
    if (newStartIndex <= newEndIndex) {
        for (let i = newStartIndex; i <= newEndIndex; i++) {

            let beforeElement = nextChildren[newEndIndex + 1] == null ? null : nextChildren[newEndIndex + 1].el
            if (beforeElement == null) {
                // 從前往後匹配剩餘

                container.appendChild(nextChildren[i])

            } else {
                // 從後往前匹配剩餘
                // 開始插
                container.insertBefore(nextChildren[i],beforeElement.el)

            }

        }
    }
    // 如果老的還有迴圈完,即剩下的是要刪除的
    if (oldStartIndex <= oldEndIndex) {
        for (let i = oldStartIndex; i <= oldEndIndex; i++) {
            container.removeChild(prevChildren[i].el)
        }
    }
}
複製程式碼