Vue2 原理解析

现代主流框架均使用一种数据=>视图的方式,隐藏了繁琐的dom操做,采用了声明式编程(Declarative Programming)替代了过去的类jquery的命令式编程(Imperative Programming)javascript

$("#xxx").text("xxx"); // 变为下者 view = render(state);

前者咱们详细地写了如何去操做dom节点的过程,咱们命令什么,它就操做什么;
后者则是咱们输入了数据状态,输出视图(咱们不关心中间的过程,它们均由框架帮助咱们实现);
前者当然直接,可是当应用变得复杂则代码将难以维护,然后者框架帮咱们实现了一系列的操做,无需管理过程,优点显然可见。html

为了实现这一点,就是实现如何输入数据,输出视图,咱们就会注意到上面的render函数,render函数的实现,主要在对dom性能的优化上,固然实现方式也多种多样,直接的innerHTML、使用documentFragment、还有virtual dom,在不一样场景下性能上有所不一样,可是框架追求的是在大部分场景中框架已经知足你的优化需求,这里咱们也不加以赘述,后文会提到。前端

固然还有数据变化侦测,从而re-render视图,数据变化侦测中,值得一提的是数据生产者(Producer)和数据消费者(Consumer)之间的联系,这里,咱们能够暂且将系统(视图)做为一个数据的消费者,咱们的代码设置数据的变化,做为数据的生产者
咱们这里能够分为系统不可感知数据变化系统可感知数据变化vue

Rx.js中是将二者通讯分红拉取(Pull)和推送(Push),比较很差理解,这里我本身就分了个类java

  • 系统不可感知数据变化

像React/Angular这类框架并不知道数据何时变了,可是它视图何时更新呢,好比React就是经过setState发信号告诉系统有可能数据变了,而后经过virtual dom diff去渲染视图,angular则是有一个脏值检查流程,遍历比对node

  • 系统可感知数据变化

Rx.js / vue这一类响应式的,经过观察者模式,使用Observable (可观察对象),Observer (观察者)(或者是watcher)去订阅(好比视图渲染这一类,其实也能够当成一个观察者去订阅数据了,后面会提到),系统是能够很准确知道哪里数据变了的,从而也就能实现视图更新渲染。react

上者系统不可感知数据变化,粒度粗,有时候还得手动优化(好比pureComponet和shouldComponentUpdate)去跳过一些数据不会更新的视图从而提高性能
下者系统可感知数据变化,粒度细,可是绑定大量观察者,有大量的依赖追踪的内存开销jquery

因此算法

这里也就终于提到本文的主角Vue2,它采用了折中粒度的方式,粒度到组件级别上,由watcher订阅数据,当数据变化咱们能够得知哪一个组件数据变了,而后采用virtual dom diff的方式去更新相应组件。编程

后文咱们也将展开它是如何实现这些过程的,咱们能够先从一个简单的应用开始。

从一个简单的应用看起

<div id="app">
  {{ message }}
</div>

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})
app.message = `xxx`; // 发现视图发生了变化

从这里咱们也能够提出几个问题,让后面原理的解析更有针对性。

  • 数据响应?如何得知数据变化?

    还有一个小细节,app.message如何拿到vue data中的message?

  • 数据变更如何和视图联系在一块儿?
  • virtual dom是什么?virtual dom diff又是什么?

固然同时咱们也会讲解一些收集依赖等相关的概念。

数据响应原理

Object.defineProperty

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变了」

观察者模式(Observer, Watcher, Dep)

Vue实现响应式有三个很重要的类,Observer类,Watcher类,Dep类
我这里先笼统介绍一下(详细可见源码英文注解)

  • Observer类主要用于给Vue的数据defineProperty增长getter/setter方法,而且在getter/setter中收集依赖或者通知更新
  • Watcher类来用于观察数据(或者表达式)变化而后执行回调函数(其中也有收集依赖的过程),主要用于$watch API和指令上
  • 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变了"))

  • 首先是new Vue遍历了数据,给数据defineProperty加上了getter/setter方法
  • 咱们new Watcher(vm, 'msg', () => console.log("msg变了")),首先标记了全局变量Dep.target = 该Watcher实例,而后执行msg的get操做,触发到了它的getter,而后dep成功获取到它的订阅者,放入它的订阅者数组,最后咱们将Dep.target = null
  • 最后设置vm.msg = 2,触发到了setter,闭包中的dep.notify,遍历订阅者数组,执行相应的回调操做。

其实讲到这里,核心的响应式原理就讲得差很少了。

可是其实Object.defineProperty并非万能的,

  • 数组的push/pop等操做
  • 不能监测数组length长度的变化
  • 数组的arr[xxx] = yyy没法感知
  • 一样的,对象属性的添加和删除没法感知

为了解决这些自己js限制的问题

  • Vue首先是对数组方法进行变异,用__proto__继承那些方法(若是不行则直接一个个defineProperty到数组上),具体的变异方法就是在后面加上dep.notify的操做
  • 至于属性的添加和删除,咱们能够想象到,增长属性,那咱们根本没有defineProperty,删除属性则连咱们以前的defineProperty都给删了,因此这里Vue增长了一个$set/$delete的API去实现这些操做,一样也是在最后加上了dep.notify的操做
  • 固然以上就不是单纯靠defineProperty中每个数据所对应的dep来实现了,在Observer类也有一个dep实例,同时会给数据挂载一个__ob__属性去获取它的Observer实例,像数组和对象的上面特殊操做,在watch收集依赖的时候都会把这个依赖收集到,而后最后使用的是这个dep去notify更新

    这部分就不详细介绍了,有兴趣的读者能够阅读源码

这里咱们能够稍微提一下一个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 }

咱们能够看到上面分红三部分

  • 将模板转化为抽象语法树
  • 优化抽象语法树
  • 根据抽象语法树生成代码

那里面具体作了什么呢?这里我简略讲一下

  • 第一部分其实就是各类正则了,对左右开闭标签的匹配以及属性的收集,经过栈的形式,不断出栈入栈去匹配以及更换父节点,最后生成一个对象,包含children,children又包含children的对象
  • 第二部分则是以第一部分为基础,根据节点类型找出一些静态的节点并标记
  • 第三部分就是生成render函数代码了

因此最后会产生这样的效果

模板

<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会执行一个new Watcher
  • new Watcher首先会有一个求值的操做,它的求值就是执行一个函数,这个函数会执行render,其中可能会有编译模板成render函数的操做,而后生成vnode(virtual dom),再将virtual dom应用到视图中
  • 其中将virtual dom应用到视图中(这里涉及到diff后文会讲),必定会对其中的表达式求值(好比{{message}},咱们确定会取到它的值再去渲染的),这里会触发到相应的getter操做完成依赖的收集
  • 当数据变化的时候,就会notify到这个组件级别的Watcher,而后它还会去求值,从而从新收集依赖,而且从新渲染视图

咱们再一次来看看Vue官网的这张图


一切瓜熟蒂落!

 

Virtual DOM

咱们上一节隐藏了不少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)描述树(vm._render),转换dom插入(第一次渲染)
  • 状态变化,生成新的js对象(Virtual DOM),比对新旧对象
  • 将变动应用到DOM上,并保存新的js对象(Virtual DOM),重复第二步操做

用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大概作下面几件事

  • 判断老节点存不存在
    • 不存在则为首次渲染,直接建立元素
    • 存在的话则sameVnode使用判断根节点是否相同
      • 相同则使用patchVnode给老节点打补丁
      • 不相同则使用新节点直接替换老节点

对于sameVnode判断,其实就是简单比较了几个属性判断

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
这部分其实就是leetcode.com/problems/ed… 最小编辑距离问题,这里也并无用复杂的动态规划算法(复杂度为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原理也就讲解结束了,还有不少细节没有深刻,读者能够阅读源码去深刻研究。
咱们能够再回顾下开头的问题(其实文中也是不断的在提出问题解决问题),做为看到这里的你,但愿你能有所收获~

    • 数据响应?如何得知数据变化?(提示:defineProperty)

      还有一个小细节,app.message如何拿到vue data中的message?

    • 数据变更如何和视图联系在一块儿?(提示:Watcher、Dep、Observer)
    • virtual dom是什么?virtual dom diff又是什么?(提示:特殊的js对象)
相关文章
相关标签/搜索