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

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

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

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

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

Rx.js中是将两者通信分成拉取(Pull)和推送(Push),比较不好理解,这里我自己就分了个类

  • 系统不可感知数据变化

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

  • 系统可感知数据变化

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

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

所以

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

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

从一个简单的应用看起

  1. <div id="app">
  2. {{ message }}
  3. </div>
  4. var app = new Vue({
  5. el: '#app',
  6. data: {
  7. message: 'Hello Vue!'
  8. }
  9. })
  10. app.message = `xxx`; // 发现视图发生了变化

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

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

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

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

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

数据响应原理

Object.defineProperty

Vue数据响应核心是使用了Object.defineProperty方法(IE9+)在对象中定义属性或者修改属性,其中存取描述符很关键的就是get和set,提供给属性getter和setter方法

可以看下面例子,我们拦截到了数据获取以及设置

  1. var obj = {};
  2. Object.defineProperty(obj, 'msg', {
  3. get () {
  4. console.log('get')
  5. },
  6. set (newValue) {
  7. console.log('set', newValue)
  8. }
  9. });
  10. obj.msg // get
  11. obj.msg = 'hello world' // set hello world

顺便提到那个小细节的问题

app.message如何拿到vue data中的message?

其实也是跟Object.defineProperty有关
Vue在初始化数据的时候会遍历data代理这些数据

  1. function initData (vm) {
  2. let data = vm.$options.data
  3. vm._data = data
  4. const keys = Object.keys(data)
  5. let i = keys.length
  6. while (i--) {
  7. const key = keys[i]
  8. proxy(vm, `_data`, key)
  9. }
  10. observe(data)
  11. }

proxy做了哪些操作呢?

  1. function proxy (target, sourceKey, key) {
  2. Object.defineProperty(target, key, {
  3. enumerable: true,
  4. configurable: true,
  5. get () {
  6. return this[sourceKey][key]
  7. }
  8. set () {
  9. this[sourceKey][key] = val
  10. }
  11. })
  12. }

其实就是用Object.defineProperty多加了一层的访问
因此我们就可以用app.message访问到app.data.message
也算个Object.defineProperty小应用吧

讲完这语法的核心层面得知了如何知道数据发生变化,但是响应,是还有回应的,接下来来谈下Vue是如何实现数据响应的?
其实就是解决下面的问题,如何实现$watch?

  1. const vm = new Vue({
  2. data:{
  3. msg: 1,
  4. }
  5. })
  6. vm.$watch("msg", () => console.log("msg变了"));
  7. 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放入数组即可。

  1. class Dep {
  2. constructor () {
  3. this.subs = []
  4. }
  5. notify () {
  6. const subs = this.subs.slice()
  7. for (let i = 0; i < subs.length; i++) {
  8. subs[i].update()
  9. }
  10. }
  11. addSub (sub) {
  12. this.subs.push(sub)
  13. }
  14. }
  15. class Watcher {
  16. constructor () {
  17. }
  18. update () {
  19. }
  20. }
  21. let dep = new Dep()
  22. dep.addSub(new Watcher()) // Watcher订阅了依赖

我们实现了订阅,那通知发布呢,也就是上面的notify在哪里实现呢?

我们到这里就可以联系到数据响应,我们需要的是数据变化去通知更新,那显然是会在defineProperty中的setter中去实现了,聪明的你应该想到了,我们可以把每一个数据当成一个Dep实例,然后setter的时候去notify就行了,所以我们可以在defineProperty中new Dep(),通过闭包setter就可以取到Dep实例了

就像下面这样

  1. function defineReactive (obj, key, val) {
  2. const dep = new Dep()
  3. Object.defineProperty(obj, key, {
  4. enumerable: true,
  5. configurable: true,
  6. get: function reactiveGetter () {
  7. //...
  8. },
  9. set: function reactiveSetter (newVal) {
  10. //...
  11. dep.notify()
  12. }
  13. })
  14. }

然后这里就又产生了一个问题
你都把Dep实例放里面了,我怎么让我的Watcher实例订阅到这个Dep实例呢,Vue在这里实现了精妙的一笔,从get里面做手脚,在get中是可以取到这个Dep实例的,所以可以在执行watch操作的时候,执行获取数值,触发getter去收集依赖

  1. function defineReactive (obj, key, val) {
  2. const dep = new Dep()
  3. const property = Object.getOwnPropertyDescriptor(obj, key)
  4. const getter = property && property.get
  5. const setter = property && property.set
  6. let childOb = observe(val)
  7. Object.defineProperty(obj, key, {
  8. enumerable: true,
  9. configurable: true,
  10. get: function reactiveGetter () {
  11. const value = getter ? getter.call(obj) : val
  12. if (Dep.target) {
  13. dep.depend() // 等价执行dep.addSub(Dep.target),在这里收集
  14. }
  15. return value
  16. },
  17. set: function reactiveSetter (newVal) {
  18. const value = getter ? getter.call(obj) : val
  19. if (newVal === value) {
  20. return
  21. }
  22. if (setter) {
  23. setter.call(obj, newVal)
  24. } else {
  25. val = newVal
  26. }
  27. dep.notify()
  28. }
  29. })

这里我们也要结合Watcher的实现来看

  1. class Watcher () {
  2. constructor (vm, expOrFn, cb, options) {
  3. this.cb = cb
  4. this.value = this.get()
  5. }
  6. get () {
  7. pushTarget(this) // 标记全局变量Dep.target
  8. let value = this.getter.call(vm, vm) // 触发getter
  9. if (this.deep) {
  10. traverse(value)
  11. }
  12. popTarget() // 标记全局变量Dep.target
  13. return value
  14. }
  15. update () {
  16. this.run()
  17. }
  18. run () {
  19. const value = this.get() // new Value
  20. // re-collect dep
  21. if (value !== this.value ||
  22. isObject(value)) {
  23. const oldValue = this.value
  24. this.value = value
  25. this.cb.call(this.vm, value, oldValue)
  26. }
  27. }
  28. }

所以我们在new Watcher的时候会执行一个求值的操作,然后因为标记了这个Watcher触发的,所以收集了依赖,也就是观察者订阅了依赖(这个求值有可能不止触发了一个getter,有可能触发了很多个getter,那就收集了多个依赖),我们可以再注意一下上面的run操作,也就是dep.notify()后watcher会执行的操作,还会出现一个get操作,我们可以注意到这里重新收集了一波依赖!(当然里面有相关的去重操作)

我们再回来回顾上面我们要解决的小例子

  1. const vm = new Vue({
  2. data: {
  3. msg: 1,
  4. }
  5. })
  6. vm.$watch("msg", () => console.log("msg变了"));
  7. 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代码

  1. class Watcher () {
  2. constructor (vm, expOrFn, cb, options) {
  3. }
  4. }
  5. updateComponent = () => {
  6. // hydrating有关ssr本文不涉及
  7. vm._update(vm._render(), hydrating)
  8. }
  9. vm._watcher = new Watcher(vm, updateComponent, noop)
  10. // noop是回调函数,它是空函数

这个其实就是Watcher和Render的核心关系

还记得我们上面所说的,在执行new Watcher会有一个求值的操作,这里的求值是一个函数表达式,也就是执行updateComponent,执行updateComponent后,会再执行vm._render(),传参数给vm._update(vm._render(), hydrating),收集完依赖以后才结束,这里有两个关键的点,vm._render在做什么?vm._update在做什么?

vm._render

我们看下Vue.prototype._render是何方神圣(以下为删减代码)

  1. Vue.prototype._render = function (): VNode {
  2. const vm: Component = this
  3. const {
  4. render,
  5. staticRenderFns,
  6. _parentVnode
  7. } = vm.$options
  8. // ...
  9. let vnode
  10. try {
  11. // vm._renderProxy我们直接当成vm,其实就是为了开发环境报warning用的
  12. vnode = render.call(vm._renderProxy, vm.$createElement)
  13. } catch (e) {
  14. }
  15. // set parent
  16. vnode.parent = _parentVnode
  17. return vnode
  18. }

所以它这里我们可以看到里面是执行了render函数,render函数来自options,然后返回了vnode

所以到这里我们可以把我们的目光移到这个render函数从哪里来的

如果熟悉Vue2的朋友可能知道,Vue提供了一个选项是render就是作为这个函数的,假如没有提供这个选项呢
我们不妨看看生命周期


我们可以看到Compile template into render function(没有template会将el的outerHTML当成template),所以这里就有一个模板编译的过程

模板编译

再摘一段核心代码

  1. const ast = parse(template.trim(), options) // 构建抽象语法树
  2. optimize(ast, options) // 优化
  3. const code = generate(ast, options) // 生成代码
  4. return {
  5. ast,
  6. render: code.render,
  7. staticRenderFns: code.staticRenderFns
  8. }

我们可以看到上面分成三部分

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

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

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

所以最后会产生这样的效果

模板

  1. <div id="container">
  2. <p>Message is: {{ message }}</p>
  3. </div>

生成render函数

  1. (function() {
  2. with (this) {
  3. return _c('div', {
  4. attrs: {
  5. "id": "container"
  6. }
  7. }, [_c('p', [_v("Message is: " + _s(message))])])
  8. }
  9. }
  10. )

这里我们又可以结合上面的代码了

  1. 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是怎么定义的?

  1. export class VNode {
  2. constructor (
  3. tag?: string,
  4. data?: VNodeData,
  5. children?: ?Array<VNode>,
  6. text?: string,
  7. elm?: Node,
  8. context?: Component,
  9. componentOptions?: VNodeComponentOptions,
  10. asyncFactory?: Function
  11. ) {
  12. this.tag = tag
  13. this.data = data
  14. this.children = children
  15. this.text = text
  16. this.elm = elm
  17. this.ns = undefined
  18. this.context = context
  19. this.functionalContext = undefined
  20. this.key = data && data.key
  21. this.componentOptions = componentOptions
  22. this.componentInstance = undefined
  23. this.parent = undefined
  24. this.raw = false
  25. this.isStatic = false
  26. this.isRootInsert = true
  27. this.isComment = false
  28. this.isCloned = false
  29. this.isOnce = false
  30. this.asyncFactory = asyncFactory
  31. this.asyncMeta = undefined
  32. this.isAsyncPlaceholder = false
  33. }
  34. }

用以上这些属性就能来表示一个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算法的后两步)

  1. Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  2. const vm: Component = this
  3. if (vm._isMounted) {
  4. callHook(vm, 'beforeUpdate')
  5. }
  6. const prevEl = vm.$el
  7. const prevVnode = vm._vnode
  8. // ...
  9. if (!prevVnode) {
  10. // initial render
  11. // 第一次渲染
  12. vm.$el = vm.__patch__(
  13. vm.$el, vnode, hydrating, false /* removeOnly */,
  14. vm.$options._parentElm,
  15. vm.$options._refElm
  16. )
  17. } else {
  18. // updates
  19. // 更新视图
  20. vm.$el = vm.__patch__(prevVnode, vnode)
  21. }
  22. // ...
  23. }

可以看到一个关键点vm.__patch__,其实它就是Virtual DOM Diff的核心,也是它最后把真实DOM插入的

Virtual DOM Diff

完整Virtual DOM Diff算法,根据有一篇论文(我忘记在哪里了),是需要O(n^3)的,因为它涉及跨层级的复用,这种时间复杂度是不可接受的,同时考虑到DOM较少涉及跨层级的复用,所以就减少至当前层级的复用,这个算法的复杂度就降到O(n)了,Perfect~

引用一张React经典的图来帮助大家理解吧,左右同一颜色圈起来的就是比较/复用的范围

步入正题,我们看看Vue的patch函数

  1. function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
  2. if (isUndef(vnode)) {
  3. if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
  4. return
  5. }
  6. let isInitialPatch = false
  7. const insertedVnodeQueue = []
  8. if (isUndef(oldVnode)) {
  9. // empty mount (likely as component), create new root element
  10. // 老节点不存在,直接创建元素
  11. isInitialPatch = true
  12. createElm(vnode, insertedVnodeQueue, parentElm, refElm)
  13. } else {
  14. const isRealElement = isDef(oldVnode.nodeType)
  15. if (!isRealElement && sameVnode(oldVnode, vnode)) {
  16. // patch existing root node
  17. // 新节点和老节点相同,则给老节点打补丁
  18. patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
  19. } else {
  20. // ... 省略ssr代码
  21. // replacing existing element
  22. // 新节点和老节点相同,直接替换老节点
  23. const oldElm = oldVnode.elm
  24. const parentElm = nodeOps.parentNode(oldElm)
  25. createElm(
  26. vnode,
  27. insertedVnodeQueue,
  28. // extremely rare edge case: do not insert if old element is in a
  29. // leaving transition. Only happens when combining transition +
  30. // keep-alive + HOCs. (#4590)
  31. oldElm._leaveCb ? null : parentElm,
  32. nodeOps.nextSibling(oldElm)
  33. )
  34. }
  35. }
  36. // ...省略代码
  37. return vnode.elm
  38. }

所以patch大概做下面几件事

  • 判断老节点存不存在

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

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

  1. function sameVnode (a, b) {
  2. return (
  3. a.key === b.key && (
  4. (
  5. a.tag === b.tag &&
  6. a.isComment === b.isComment &&
  7. isDef(a.data) === isDef(b.data) &&
  8. sameInputType(a, b)
  9. ) || (
  10. isTrue(a.isAsyncPlaceholder) &&
  11. a.asyncFactory === b.asyncFactory &&
  12. isUndef(b.asyncFactory.error)
  13. )
  14. )
  15. )
  16. }

对于patchVnode
其实就是比较节点的子节点,分别对新老节点的拥有的子节点做判断,假如两者都没有或者一者有一者没有,就比较容易,直接删除或者增加即可,但是假如两者都有子节点,这里就涉及到列表对比以及一些复用操作了,实现的方法是updateChildren

  1. function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  2. if (oldVnode === vnode) {
  3. // 新老节点相同
  4. return
  5. }
  6. // ... 省略代码
  7. if (isUndef(vnode.text)) {
  8. // 假如新节点没有text
  9. if (isDef(oldCh) && isDef(ch)) {
  10. // 假如老节点和新节点都有子节点
  11. // 不相等则更新子节点
  12. if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
  13. } else if (isDef(ch)) {
  14. // 新节点有子节点,老节点没有
  15. // 老节点加上
  16. if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
  17. addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
  18. } else if (isDef(oldCh)) {
  19. // 老节点有子节点,新节点没有
  20. // 老节点移除
  21. removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  22. } else if (isDef(oldVnode.text)) {
  23. // 老节点有文本,新节点没有文本
  24. nodeOps.setTextContent(elm, '')
  25. }
  26. } else if (oldVnode.text !== vnode.text) {
  27. // 假如新节点和老节点text不相等
  28. nodeOps.setTextContent(elm, vnode.text)
  29. }
  30. if (isDef(data)) {
  31. if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  32. }
  33. }

我们最后再来看看这个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的过程

  1. function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  2. let oldStartIdx = 0
  3. let newStartIdx = 0
  4. let oldEndIdx = oldCh.length - 1
  5. let oldStartVnode = oldCh[0]
  6. let oldEndVnode = oldCh[oldEndIdx]
  7. let newEndIdx = newCh.length - 1
  8. let newStartVnode = newCh[0]
  9. let newEndVnode = newCh[newEndIdx]
  10. let oldKeyToIdx, idxInOld, vnodeToMove, refElm
  11. // removeOnly is a special flag used only by <transition-group>
  12. // to ensure removed elements stay in correct relative positions
  13. // during leaving transitions
  14. const canMove = !removeOnly
  15. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  16. if (isUndef(oldStartVnode)) {
  17. // 假如老节点的第一个子节点不存在
  18. // 老节点头指针就往下一个移动
  19. oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
  20. } else if (isUndef(oldEndVnode)) {
  21. // 假如老节点的最后一个子节点不存在
  22. // 老节点尾指针就往上一个移动
  23. oldEndVnode = oldCh[--oldEndIdx]
  24. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  25. // 假如新节点的第一个和老节点的第一个相同
  26. // patch该节点并且新老节点头指针分别往下一个移动
  27. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
  28. oldStartVnode = oldCh[++oldStartIdx]
  29. newStartVnode = newCh[++newStartIdx]
  30. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  31. // 假如新节点的最后一个和老节点的最后一个相同
  32. // patch该节点并且新老节点尾指针分别往上一个移动
  33. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
  34. oldEndVnode = oldCh[--oldEndIdx]
  35. newEndVnode = newCh[--newEndIdx]
  36. } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
  37. // 假如新节点的最后一个和老节点的第一个相同
  38. // patch该节点并且新节点尾指针往上一个移动,老节点头指针往下一个移动
  39. patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
  40. canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  41. oldStartVnode = oldCh[++oldStartIdx]
  42. newEndVnode = newCh[--newEndIdx]
  43. } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
  44. // 假如新节点的第一个和老节点的最后一个相同
  45. // patch该节点并且老节点尾指针往上一个移动,新节点头指针往下一个移动
  46. patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
  47. canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  48. oldEndVnode = oldCh[--oldEndIdx]
  49. newStartVnode = newCh[++newStartIdx]
  50. } else {
  51. // 创建老节点key to index的映射
  52. if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  53. idxInOld = isDef(newStartVnode.key)
  54. ? oldKeyToIdx[newStartVnode.key] // 假如新节点第一个有key,找该key下老节点的index
  55. : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 假如新节点没有key,直接遍历找相同的index
  56. if (isUndef(idxInOld)) { // New element
  57. // 假如没有找到index,则创建节点
  58. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
  59. } else {
  60. // 假如有index,则找出这个需要move的老节点
  61. vnodeToMove = oldCh[idxInOld]
  62. /* istanbul ignore if */
  63. if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
  64. warn(
  65. 'It seems there are duplicate keys that is causing an update error. ' +
  66. 'Make sure each v-for item has a unique key.'
  67. )
  68. }
  69. if (sameVnode(vnodeToMove, newStartVnode)) {
  70. // move老节点和新节点的第一个基本相同则开始patch
  71. patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
  72. // 设置老节点空
  73. oldCh[idxInOld] = undefined
  74. canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
  75. } else {
  76. // 不同则还是创建新节点
  77. // same key but different element. treat as new element
  78. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
  79. }
  80. }
  81. newStartVnode = newCh[++newStartIdx]
  82. }
  83. }
  84. if (oldStartIdx > oldEndIdx) {
  85. // 假如老节点的头指针超过了尾部的指针
  86. // 说明缺少了节点
  87. refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  88. addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  89. } else if (newStartIdx > newEndIdx) {
  90. // 假如新节点的头指针超过了尾部的指针
  91. // 说明多了节点
  92. removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  93. }
  94. }

总结

到这里整体Vue2原理也就讲解结束了,还有很多细节没有深入,读者可以阅读源码去深入研究。
我们可以再回顾下开头的问题(其实文中也是不断的在提出问题解决问题),作为看到这里的你,希望你能有所收获~

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

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

    • 数据变动如何和视图联系在一起?(提示:Watcher、Dep、Observer)
    • virtual dom是什么?virtual dom diff又是什么?(提示:特殊的js对象)

Vue2 原理解析的更多相关文章

  1. [原][Docker]特性与原理解析

    Docker特性与原理解析 文章假设你已经熟悉了Docker的基本命令和基本知识 首先看看Docker提供了哪些特性: 交互式Shell:Docker可以分配一个虚拟终端并关联到任何容器的标准输入上, ...

  2. 【算法】(查找你附近的人) GeoHash核心原理解析及代码实现

    本文地址 原文地址 分享提纲: 0. 引子 1. 感性认识GeoHash 2. GeoHash算法的步骤 3. GeoHash Base32编码长度与精度 4. GeoHash算法 5. 使用注意点( ...

  3. Web APi之过滤器执行过程原理解析【二】(十一)

    前言 上一节我们详细讲解了过滤器的创建过程以及粗略的介绍了五种过滤器,用此五种过滤器对实现对执行Action方法各个时期的拦截非常重要.这一节我们简单将讲述在Action方法上.控制器上.全局上以及授 ...

  4. Web APi之过滤器创建过程原理解析【一】(十)

    前言 Web API的简单流程就是从请求到执行到Action并最终作出响应,但是在这个过程有一把[筛子],那就是过滤器Filter,在从请求到Action这整个流程中使用Filter来进行相应的处理从 ...

  5. GeoHash原理解析

    GeoHash 核心原理解析       引子 一提到索引,大家脑子里马上浮现出B树索引,因为大量的数据库(如MySQL.oracle.PostgreSQL等)都在使用B树.B树索引本质上是对索引字段 ...

  6. alibaba-dexposed 原理解析

    alibaba-dexposed 原理解析 使用参考地址: http://blog.csdn.net/qxs965266509/article/details/49821413 原理参考地址: htt ...

  7. 支付宝Andfix 原理解析

    支付宝Andfix 原理解析 使用参考地址: http://blog.csdn.net/qxs965266509/article/details/49802429 原理参考地址: http://blo ...

  8. JavaScript 模板引擎实现原理解析

    1.入门实例 首先我们来看一个简单模板: <script type="template" id="template"> <h2> < ...

  9. Request 接收参数乱码原理解析三:实例分析

    通过前面两篇<Request 接收参数乱码原理解析一:服务器端解码原理>和<Request 接收参数乱码原理解析二:浏览器端编码原理>,了解了服务器和浏览器编码解码的原理,接下 ...

随机推荐

  1. java 日期工具类DateUtils

      日期工具类DateUtils CreateTime--2017年5月27日08:48:00Author:Marydon DateUtils.java-对日期类的进一步封装 import java. ...

  2. 关于java线程的daemon的认识

    在 JAVA中的CountDownLatch.CyclicBarrier.Semaphore的简单测试 这文章里说到了线程的daemon问题,特写一篇来分析一下. 上代码: package com.y ...

  3. 如何开发一个基于 Docker 的 Python 应用

    前言 Python 家族成员繁多,解决五花八门的业务需求.这里将通过 Python 明星项目 IPython Notebook,使其容器化,让大家掌握基础的 Docker 使用方法. IPython ...

  4. Spring4+Hibernate4事务小记

    学习Spring+Hibernate,非常强大的框架,为了追新,就直接从最高版本开始学习了,这要冒很大的风险,因为网上可查到的资料大多是针对旧版本的,比如Spring3,Hibernate3. 根据我 ...

  5. shell中$#等含义

    $# 是传给脚本的参数个数 $0 是脚本本身的名字 $1 是传递给该shell脚本的第一个参数 $2 是传递给该shell脚本的第二个参数 $@ 是传给脚本的所有参数的列表 $* 是以一个单字符串显示 ...

  6. 树莓派UFW防火墙简单设置

    ufw是一个主机端的iptables类防火墙配置工具,比较容易上手.如果你有一台暴露在外网的树莓派,则可通过这个简单的配置提升安全性. 安装方法 sudo apt-get install ufw 当然 ...

  7. ubuntu(14.04) 下安装yaf拓展

    #wget http://pecl.PHP.net/get/yaf-2.2.9.tgz #tar zxvf yaf-.tgz #cd yaf- [root@bogon yaf-]# whereis p ...

  8. 【JQuery】jQuery中的常用方法小结

    1.层级选择器     后代选择器     "父元素  后代元素" 比如:$("div p") 选取div元素下所有的p元素 子元素选择器   "父元 ...

  9. 数字的可视化:python画图之散点图sactter函数详解

    最近开始学习python编程,遇到scatter函数,感觉里面的参数不知道什么意思于是查资料,最后总结如下: 1.scatter函数原型 2.其中散点的形状参数marker如下: 3.其中颜色参数c如 ...

  10. java_selenium 开发环境搭建

    java selenium 开发环境搭建 很多同学问我java selenium的开发环境怎么搭建,在这里简要说明一下. 安装jdk 这个自己一定要会 下载IDE 对于初学者来说java IDE无疑是 ...