本文重点讲述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
欢迎继续观光个人新博客~