vue響應式實現&vue及react的diff演算法
ps 最近總結的東西比較多,下面只是摘出來的一部分第四階段部分。東西多了難免出現差錯望指正
下面程式碼均放在github上了
第四階段 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
上,資料監聽則需要在屬性get
和set
中做些相應的邏輯處理
實現方法: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,}
}
複製程式碼
此時建立的結果:
渲染虛擬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)
}
}
}
複製程式碼
此時頁面便已經被渲染出來了
新老節點的對比
開始逐步進入重點咯
當頁面發生變動,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的絕對核心了
這裡的情況及oldVnode和newVnode均有多個孩子,這裡情況比較複雜點。比上面單純照著邏輯走的就稍微不是那麼好寫一點。當前也就是複雜點的邏輯而已,不比困難的力扣
這裡又要分情況了,react和vue針對這裡的diff處理邏輯是不一樣的,因為這裡是diff演算法的核心,我下面會總結的文字稍微多那麼一點哈
react中的處理思想一對指標
先來看簡單的:react中的處理
react中只使用了一組指標。
看圖:首先new裡面拿出指標指的第一個c去old裡面找,c在old的位置為2,在新的為0,老的中大即不需要移動位置,new中指標後移
接下來即new中的a,去old裡面找,找到了位置在old裡面為0,在new裡面為1,old裡面的小則需要移動,移動到哪呢,看圖也知道是old裡面c的後面。怎麼表示呢?不要忘了上面我們儲存了一些一個index,它就是old中c的位置。即插到c的下一個兄弟之前
指標再後移,到b了,顯然b也是要移動位置的,即
下面new中該e了,e去老的裡面沒有找到。則此時該新掛一個節點了,新掛到哪呢?b的後面啊
此時new的指標已經走完,我們需要做的操作只需把此時老的裡面有新的沒有的節點幹掉即可,大功告成編寫程式碼
程式碼實現
//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 先來看最為簡單的
old和new的頭匹配到了,則直接兩頭指標後移。迴圈以最短的為主,即最後
這時發現new中有剩餘,我們的操作只需把剩餘的掛到後面即可
2 與1類似,尾尾匹配,即從尾部往前找即可
3 新尾老頭匹配
4 新頭老尾匹配
5 都不匹配
直接拿我程式碼中的栗子來舉例吧
開始如圖:
新頭老尾匹配,則老尾移動到前面去,指標向中間移動
此時再觀察發現頭頭匹配,指標後移
還是頭頭,再後移
此時均不能匹配,則拿new裡的f去看old裡有沒有,沒有則掛到此時old的startIndex之前的位置,指標後移
同理e也沒找到,則像上面一樣掛到startIndex之前
此時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)
}
}
}
複製程式碼