本文重點講述Vue2渲染的總體流程,包括數據響應的實現(雙向綁定)、模板編譯、virtual dom原理等,但願讀者看完有所收穫。html
此部份內容初步介紹前端主流框架部分特色,來提升你們對框架的認識,從而最後導出對vue2原理的總體介紹
參考尤雨溪的live 不吹不黑聊聊前端框架
有興趣的同窗能夠聽聽前端
現代主流框架均使用一種數據=>視圖的方式,隱藏了繁瑣的dom操做,採用了聲明式編程(Declarative Programming)替代了過去的類jquery的命令式編程(Imperative Programming)vue
$(
"#xxx"
).text(
"xxx"
);
// 變爲下者
view = render(state);
|
前者咱們詳細地寫了如何去操做dom節點的過程,咱們命令什麼,它就操做什麼;
後者則是咱們輸入了數據狀態,輸出視圖(咱們不關心中間的過程,它們均由框架幫助咱們實現);
前者當然直接,可是當應用變得複雜則代碼將難以維護,然後者框架幫咱們實現了一系列的操做,無需管理過程,優點顯然可見。node
爲了實現這一點,就是實現如何輸入數據,輸出視圖,咱們就會注意到上面的render函數,render函數的實現,主要在對dom性能的優化上,固然實現方式也多種多樣,直接的innerHTML、使用documentFragment、還有virtual dom,在不一樣場景下性能上有所不一樣,可是框架追求的是在大部分場景中框架已經知足你的優化需求,這裏咱們也不加以贅述,後文會提到。react
固然還有數據變化偵測,從而re-render視圖,數據變化偵測中,值得一提的是數據生產者(Producer)和數據消費者(Consumer)之間的聯繫,這裏,咱們能夠暫且將系統(視圖)做爲一個數據的消費者,咱們的代碼設置數據的變化,做爲數據的生產者
咱們這裏能夠分爲系統不可感知數據變化和系統可感知數據變化jquery
Rx.js中是將二者通訊分紅拉取(Pull)和推送(Push),比較很差理解,這裏我本身就分了個類git
像React/Angular這類框架並不知道數據何時變了,可是它視圖何時更新呢,好比React就是經過setState發信號告訴系統有可能數據變了,而後經過virtual dom diff去渲染視圖,angular則是有一個髒值檢查流程,遍歷比對github
<div id=
"app"
>
{{ message }}
</div>
var
app =
new
Vue({
el:
'#app'
,
data: {
message:
'Hello Vue!'
}
})
app.message = `xxx`;
// 發現視圖發生了變化
|
從這裏咱們也能夠提出幾個問題,讓後面原理的解析更有針對性。算法
還有一個小細節,app.message如何拿到vue data中的message?編程
固然同時咱們也會講解一些收集依賴等相關的概念。
Vue數據響應核心是使用了Object.defineProperty方法(IE9+)在對象中定義屬性或者修改屬性,其中存取描述符很關鍵的就是get和set,提供給屬性getter和setter方法
能夠看下面例子,咱們攔截到了數據獲取以及設置
var
obj = {};
Object.defineProperty(obj,
'msg'
, {
get () {
console.log(
'get'
)
},
set (newValue) {
console.log(
'set'
, newValue)
}
});
obj.msg
// get
obj.msg =
'hello world'
// set hello world
|
順便提到那個小細節的問題
app.message如何拿到vue data中的message?
其實也是跟Object.defineProperty有關
Vue在初始化數據的時候會遍歷data代理這些數據
function
initData (vm) {
let data = vm.$options.data
vm._data = data
const keys = Object.keys(data)
let i = keys.length
while
(i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
observe(data)
}
|
proxy作了哪些操做呢?
function
proxy (target, sourceKey, key) {
Object.defineProperty(target, key, {
enumerable:
true
,
configurable:
true
,
get () {
return
this
[sourceKey][key]
}
set () {
this
[sourceKey][key] = val
}
})
}
|
其實就是用Object.defineProperty多加了一層的訪問
所以咱們就能夠用app.message訪問到app.data.message
也算個Object.defineProperty小應用吧
講完這語法的核心層面得知了如何知道數據發生變化,可是響應,是還有迴應的,接下來來談下Vue是如何實現數據響應的?
其實就是解決下面的問題,如何實現$watch?
const vm =
new
Vue({
data:{
msg: 1,
}
})
vm.$watch(
"msg"
, () => console.log(
"msg變了"
));
vm.msg = 2;
//輸出「msg變了」
|
Vue實現響應式有三個很重要的類,Observer類,Watcher類,Dep類
我這裏先籠統介紹一下(詳細可見源碼英文註解)
觀察者模式,跟發佈/訂閱模式有點像
可是其實略有不一樣,發佈/訂閱模式是由統一的事件分發調度中心,on則往中心中數組加事件(訂閱),emit則從中心中數組取出事件(發佈),發佈和訂閱以及發佈後調度訂閱者的操做都是由中心統一完成
可是觀察者模式則沒有這樣的中心,觀察者訂閱了可觀察對象,當可觀察對象發佈事件,則就直接調度觀察者的行爲,因此這裏觀察者和可觀察對象其實就產生了一個依賴的關係,這個是發佈/訂閱模式上沒有體現的。
其實Dep就是dependence依賴的縮寫
如何實現觀察者模式呢?
咱們先看下面代碼,下面代碼實現了Watcher去訂閱Dep的過程,Dep因爲是能夠被多個Watcher所訂閱的,因此它擁有着訂閱者數組,訂閱了它,就把Watcher放入數組便可。
class Dep {
constructor () {
this
.subs = []
}
notify () {
const subs =
this
.subs.slice()
for
(let i = 0; i < subs.length; i++) {
subs[i].update()
}
}
addSub (sub) {
this
.subs.push(sub)
}
}
class Watcher {
constructor () {
}
update () {
}
}
let dep =
new
Dep()
dep.addSub(
new
Watcher())
// Watcher訂閱了依賴
|
咱們實現了訂閱,那通知發佈呢,也就是上面的notify在哪裏實現呢?
咱們到這裏就能夠聯繫到數據響應,咱們須要的是數據變化去通知更新,那顯然是會在defineProperty中的setter中去實現了,聰明的你應該想到了,咱們能夠把每個數據當成一個Dep實例,而後setter的時候去notify就好了,因此咱們能夠在defineProperty中new Dep(),經過閉包setter就能夠取到Dep實例了
就像下面這樣
function
defineReactive (obj, key, val) {
const dep =
new
Dep()
Object.defineProperty(obj, key, {
enumerable:
true
,
configurable:
true
,
get:
function
reactiveGetter () {
//...
},
set:
function
reactiveSetter (newVal) {
//...
dep.notify()
}
})
}
|
而後這裏就又產生了一個問題
你都把Dep實例放裏面了,我怎麼讓個人Watcher實例訂閱到這個Dep實例呢,Vue在這裏實現了精妙的一筆,從get裏面作手腳,在get中是能夠取到這個Dep實例的,因此能夠在執行watch操做的時候,執行獲取數值,觸發getter去收集依賴
function
defineReactive (obj, key, val) {
const dep =
new
Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
const getter = property && property.get
const setter = property && property.set
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable:
true
,
configurable:
true
,
get:
function
reactiveGetter () {
const value = getter ? getter.call(obj) : val
if
(Dep.target) {
dep.depend()
// 等價執行dep.addSub(Dep.target),在這裏收集
}
return
value
},
set:
function
reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if
(newVal === value) {
return
}
if
(setter) {
setter.call(obj, newVal)
}
else
{
val = newVal
}
dep.notify()
}
})
|
這裏咱們也要結合Watcher的實現來看
class Watcher () {
constructor (vm, expOrFn, cb, options) {
this
.cb = cb
this
.value =
this
.get()
}
get () {
pushTarget(
this
)
// 標記全局變量Dep.target
let value =
this
.getter.call(vm, vm)
// 觸發getter
if
(
this
.deep) {
traverse(value)
}
popTarget()
// 標記全局變量Dep.target
return
value
}
update () {
this
.run()
}
run () {
const value =
this
.get()
// new Value
// re-collect dep
if
(value !==
this
.value ||
isObject(value)) {
const oldValue =
this
.value
this
.value = value
this
.cb.call(
this
.vm, value, oldValue)
}
}
}
|
因此咱們在new Watcher的時候會執行一個求值的操做,而後由於標記了這個Watcher觸發的,因此收集了依賴,也就是觀察者訂閱了依賴(這個求值有可能不止觸發了一個getter,有可能觸發了不少個getter,那就收集了多個依賴),咱們能夠再注意一下上面的run操做,也就是dep.notify()後watcher會執行的操做,還會出現一個get操做,咱們能夠注意到這裏從新收集了一波依賴!(固然裏面有相關的去重操做)
咱們再回來回顧上面咱們要解決的小例子
const vm =
new
Vue({
data: {
msg: 1,
}
})
vm.$watch(
"msg"
, () => console.log(
"msg變了"
));
vm.msg = 2;
//輸出「變了」
|
$watcher其實就是一個new Watcher的封裝
即new Watcher(vm, ‘msg’, () => console.log(「msg變了」))
其實講到這裏,核心的響應式原理就講得差很少了。
可是其實Object.defineProperty並非萬能的,
爲了解決這些自己js限制的問題
這部分就不詳細介紹了,有興趣的讀者能夠閱讀源碼
這裏咱們能夠稍微提一下一個ES6的新特性Proxy,頗有多是下一代響應機制的主角,由於它能夠解決咱們上面的缺陷,可是因爲兼容問題還不能很好地使用,可讓咱們期待一下~
如今咱們再來看看Vue官網的這張圖
至少目前咱們對右半部分很清晰了,Data如何和Watcher聯繫已經很清楚,可是Render Function,Watcher怎麼Trigger Render Function這個還須要去解答,固然還有左下角的Virtual DOM Tree
我這裏摘出一段關鍵的Vue代碼
class Watcher () {
constructor (vm, expOrFn, cb, options) {
}
}
updateComponent = () => {
// hydrating有關ssr本文不涉及
vm._update(vm._render(), hydrating)
}
vm._watcher =
new
Watcher(vm, updateComponent, noop)
// noop是回調函數,它是空函數
|
這個其實就是Watcher和Render的核心關係
還記得咱們上面所說的,在執行new Watcher會有一個求值的操做,這裏的求值是一個函數表達式,也就是執行updateComponent,執行updateComponent後,會再執行vm._render(),傳參數給vm._update(vm._render(), hydrating),收集完依賴之後才結束,這裏有兩個關鍵的點,vm._render在作什麼?vm._update在作什麼?
vm._render
咱們看下Vue.prototype._render是何方神聖(如下爲刪減代碼)
Vue.prototype._render =
function
(): VNode {
const vm: Component =
this
const {
render,
staticRenderFns,
_parentVnode
} = vm.$options
// ...
let vnode
try
{
// vm._renderProxy咱們直接當成vm,其實就是爲了開發環境報warning用的
vnode = render.call(vm._renderProxy, vm.$createElement)
}
catch
(e) {
}
// set parent
vnode.parent = _parentVnode
return
vnode
}
|
因此它這裏咱們能夠看到裏面是執行了render函數,render函數來自options,而後返回了vnode
因此到這裏咱們能夠把咱們的目光移到這個render函數從哪裏來的
若是熟悉Vue2的朋友可能知道,Vue提供了一個選項是render就是做爲這個函數的,假如沒有提供這個選項呢
咱們不妨看看生命週期
咱們能夠看到Compile template into render function(沒有template會將el的outerHTML當成template),因此這裏就有一個模板編譯的過程
模板編譯
再摘一段核心代碼
const ast = parse(template.trim(), options)
// 構建抽象語法樹
optimize(ast, options)
// 優化
const code = generate(ast, options)
// 生成代碼
return
{
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
|
咱們能夠看到上面分紅三部分
那裏面具體作了什麼呢?這裏我簡略講一下
因此最後會產生這樣的效果
模板
<div id=
"container"
>
<p>Message is: {{ message }}</p>
</div>
|
生成render函數
(
function
() {
with
(
this
) {
return
_c(
'div'
, {
attrs: {
"id"
:
"container"
}
}, [_c(
'p'
, [_v(
"Message is: "
+ _s(message))])])
}
}
)
|
這裏咱們又能夠結合上面的代碼了
vnode = render.call(vm._renderProxy, vm.$createElement)
|
其中_c就是vm.$createElement
咱們將virtual dom具體實現移到下一節,以防影響咱們Vue2主線
vm.$createElement其實就是一個建立vnode的一個API
知道了vm._render()建立了vnode返回,接下來就是vm._update了
vm._update
vm._update部分也是跟virtual dom有關,下一節具體介紹,咱們能夠先透露下函數的功能,顧名思義,就是更新視圖,根據傳入的vnode更新到視圖中。
數據到視圖的總體流程
因此到這裏咱們就能夠得出一個數據到視圖的總體流程的結論了
咱們再一次來看看Vue官網的這張圖
咱們上一節隱藏了不少Virtual DOM的細節,是由於Virtual DOM大篇幅有可能讓咱們忘記咱們所要探究的問題,這裏咱們來揭開Virtual DOM的謎團,它其實並無那麼神祕。
爲何會有Virtual DOM?
作過前端性能優化的朋友應該都知道,DOM操做都是很慢的,咱們要減小對它的操做
爲啥慢呢?
咱們能夠嘗試打出一層DOM的key
咱們能夠看出它的屬性是龐大,更況且這只是一層
同時直接對DOM的操做,就必須很注意一些有可能觸發重排的操做。
那Virtual DOM是什麼角色呢?它其實就是咱們代碼到操做DOM的一層緩衝,既然操做DOM慢,那我操做js對象快吧,我就操做js對象,而後最後把這個對象再一塊兒轉換成真正的DOM就好了
因此就變成 代碼 => Virtual DOM( 一個特殊的js對象) => DOM
什麼是Virtual DOM
上文其實咱們就解答了什麼是虛擬DOM,它就是一個特殊的js對象
咱們能夠看看Vue中的Vnode是怎麼定義的?
export class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this
.tag = tag
this
.data = data
this
.children = children
this
.text = text
this
.elm = elm
this
.ns = undefined
this
.context = context
this
.functionalContext = undefined
this
.key = data && data.key
this
.componentOptions = componentOptions
this
.componentInstance = undefined
this
.parent = undefined
this
.raw =
false
this
.isStatic =
false
this
.isRootInsert =
true
this
.isComment =
false
this
.isCloned =
false
this
.isOnce =
false
this
.asyncFactory = asyncFactory
this
.asyncMeta = undefined
this
.isAsyncPlaceholder =
false
}
}
|
用以上這些屬性就能來表示一個DOM節點
Virtual DOM算法
這裏咱們講的就是涉及上面vm.update的操做
用js對象描述樹(生成Virtual DOM),Vue中就是先轉成AST生成code,而後經過$creatElement經過Vnode的那種形式生成Virtual DOM (vm._render的操做)
這裏咱們能夠具體看下vm._update(其實就是Virtual DOM算法的後兩步)
Vue.prototype._update =
function
(vnode: VNode, hydrating?: boolean) {
const vm: Component =
this
if
(vm._isMounted) {
callHook(vm,
'beforeUpdate'
)
}
const prevEl = vm.$el
const prevVnode = vm._vnode
// ...
if
(!prevVnode) {
// initial render
// 第一次渲染
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating,
false
/* removeOnly */
,
vm.$options._parentElm,
vm.$options._refElm
)
}
else
{
// updates
// 更新視圖
vm.$el = vm.__patch__(prevVnode, vnode)
}
// ...
}
|
能夠看到一個關鍵點vm.__patch__,其實它就是Virtual DOM Diff的核心,也是它最後把真實DOM插入的
Virtual DOM Diff
完整Virtual DOM Diff算法,根據有一篇論文(我忘記在哪裏了),是須要O(n^3)的,由於它涉及跨層級的複用,這種時間複雜度是不可接受的,同時考慮到DOM較少涉及跨層級的複用,因此就減小至當前層級的複用,這個算法的複雜度就降到O(n)了,Perfect~
引用一張React經典的圖來幫助你們理解吧,左右同一顏色圈起來的就是比較/複用的範圍
步入正題,咱們看看Vue的patch函數
function
patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
if
(isUndef(vnode)) {
if
(isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch =
false
const insertedVnodeQueue = []
if
(isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
// 老節點不存在,直接建立元素
isInitialPatch =
true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
}
else
{
const isRealElement = isDef(oldVnode.nodeType)
if
(!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
// 新節點和老節點相同,則給老節點打補丁
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
}
else
{
// ... 省略ssr代碼
// replacing existing element
// 新節點和老節點相同,直接替換老節點
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ?
null
: parentElm,
nodeOps.nextSibling(oldElm)
)
}
}
// ...省略代碼
return
vnode.elm
}
|
因此patch大概作下面幾件事
function
sameVnode (a, b) {
return
(
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
|
對於patchVnode
其實就是比較節點的子節點,分別對新老節點的擁有的子節點作判斷,假如二者都沒有或者一者有一者沒有,就比較容易,直接刪除或者增長便可,可是假如二者都有子節點,這裏就涉及到列表對比以及一些複用操做了,實現的方法是updateChildren
function
patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
if
(oldVnode === vnode) {
// 新老節點相同
return
}
// ... 省略代碼
if
(isUndef(vnode.text)) {
// 假如新節點沒有text
if
(isDef(oldCh) && isDef(ch)) {
// 假如老節點和新節點都有子節點
// 不相等則更新子節點
if
(oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
else
if
(isDef(ch)) {
// 新節點有子節點,老節點沒有
// 老節點加上
if
(isDef(oldVnode.text)) nodeOps.setTextContent(elm,
''
)
addVnodes(elm,
null
, ch, 0, ch.length - 1, insertedVnodeQueue)
}
else
if
(isDef(oldCh)) {
// 老節點有子節點,新節點沒有
// 老節點移除
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
else
if
(isDef(oldVnode.text)) {
// 老節點有文本,新節點沒有文本
nodeOps.setTextContent(elm,
''
)
}
}
else
if
(oldVnode.text !== vnode.text) {
// 假如新節點和老節點text不相等
nodeOps.setTextContent(elm, vnode.text)
}
if
(isDef(data)) {
if
(isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
|
咱們最後再來看看這個updateChildren
這部分其實就是https://leetcode.com/problems/edit-distance/ 最小編輯距離問題,這裏也並無用複雜的動態規劃算法(複雜度爲O(m * n))去實現最小的移動操做,而是選擇可犧牲必定的dom操做去優化部分場景,複雜度能夠下降到O(max(m, n),比較分別首尾節點,若是沒有匹配到,則使用第一個節點key(這裏就是咱們常在v-for用的)去找相同的key去patch比較,假如沒有key的話,則是直接遍歷找類似的節點,有則patch移動,沒有則建立新節點
這裏告訴咱們
列表假若有可能有複用的節點,可使用惟一的key去標識,提高patch效率,可是也不能亂設置key,假如根本不同,可是你設置同樣的話,會致使框架沒找到真正類似的節點去複用,反而下降效率,會增長一個建立dom的消耗這裏代碼較多,有興趣的讀者能夠深刻閱讀,這裏我就不畫圖了,讀者也能夠找網上的相應updateChildren的圖,有助於理解patch的過程
function
updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group<
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
while
(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if
(isUndef(oldStartVnode)) {
// 假如老節點的第一個子節點不存在
// 老節點頭指針就往下一個移動
oldStartVnode = oldCh[++oldStartIdx]
// Vnode has been moved left
}
else
if
(isUndef(oldEndVnode)) {
// 假如老節點的最後一個子節點不存在
// 老節點尾指針就往上一個移動
oldEndVnode = oldCh[--oldEndIdx]
}
else
if
(sameVnode(oldStartVnode, newStartVnode)) {
// 假如新節點的第一個和老節點的第一個相同
// patch該節點而且新老節點頭指針分別往下一個移動
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
else
if
(sameVnode(oldEndVnode, newEndVnode)) {
// 假如新節點的最後一個和老節點的最後一個相同
// patch該節點而且新老節點尾指針分別往上一個移動
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
else
if
(sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// 假如新節點的最後一個和老節點的第一個相同
// patch該節點而且新節點尾指針往上一個移動,老節點頭指針往下一個移動
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}
else
if
(sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// 假如新節點的第一個和老節點的最後一個相同
// patch該節點而且老節點尾指針往上一個移動,新節點頭指針往下一個移動
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
else
{
// 建立老節點key to index的映射
if
(isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
// 假如新節點第一個有key,找該key下老節點的index
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 假如新節點沒有key,直接遍歷找相同的index
if
(isUndef(idxInOld)) {
// New element
// 假如沒有找到index,則建立節點
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
else
{
// 假若有index,則找出這個須要move的老節點
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if
(process.env.NODE_ENV !==
'production'
&& !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. '
+
'Make sure each v-for item has a unique key.'
)
}
if
(sameVnode(vnodeToMove, newStartVnode)) {
// move老節點和新節點的第一個基本相同則開始patch
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 設置老節點空
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
}
else
{
// 不一樣則仍是建立新節點
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if
(oldStartIdx > oldEndIdx) {
// 假如老節點的頭指針超過了尾部的指針
// 說明缺乏了節點
refElm = isUndef(newCh[newEndIdx + 1]) ?
null
: newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
}
else
if
(newStartIdx > newEndIdx) {
// 假如新節點的頭指針超過了尾部的指針
// 說明多了節點
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
|
到這裏總體Vue2原理也就講解結束了,還有不少細節沒有深刻,讀者能夠閱讀源碼去深刻研究。
咱們能夠再回顧下開頭的問題(其實文中也是不斷的在提出問題解決問題),做爲看到這裏的你,但願你能有所收穫~
還有一個小細節,app.message如何拿到vue data中的message?
深度剖析:如何實現一個 Virtual DOM 算法
Vue源碼詳解:compile,link,依賴,批處理…一網打盡,全解析!
深刻響應式原理
謝謝閱讀~
歡迎follow我哈哈https://github.com/BUPT-HJM
歡迎繼續觀光個人新博客~