响应式
数据变化的时候触发DOM的变化,数据变化对DOM的处理有几个问题:
- 修改哪块的 DOM?
- 效率和性能是不是最优的?
- 是每次数据变化都去修改DOM吗
响应式对象
Object.defineProperty(obj, prop, descriptor)方法)
响应式对象核心就是使用这个方法为对象属性添加
getter与setterVue会把props、data等变成响应式对象,在创建过程中,发现子属性也是对象,就递归把对象变成响应式
模板代码:
main.js
1 | import Vue from 'vue' |
从前两章说过_init中的的initState(vm)方法开始,这个方法对数据做了一些初始化操作
1 | function initState (vm) { |
主要看initData(vm);方法
1 | function initData (vm) { |
这个方法做了几件事
1)判断了data是否类型是function,是的话就调用getData(data, vm),否则直接返回data或者一个空对象
2)判断是否有重名的method、prop与data,之后通过proxy把每一个值 vm._data.xxx 都代理到 vm.xxx 上
3)最后调用observe(data, true /* asRootData */);,监测数据的变化
进入observe方法:
1 | function observe (value, asRootData) { |
这个方法做的事情:
1)判断value是不是对象,或者是VNode的实例就返回
2)如果有__ob__属性并且是Observer的实例,说明给对象类型数据添加过Observer,就直接返回
3)实例一个Observer
进入实例化Observer的地方new Observer(value)
1 | var Observer = function Observer (value) { |
这个方法做的事情:
1)实例化了Dep,那么Dep是什么,就是个观察者模式)嘛
1 | var Dep = function Dep () { |
2)执行 def 函数把自身实例添加到数据对象 value 的 __ob__ 属性上
1 | function def (obj, key, val, enumerable) { |
3)接下来会对 value 做判断,对于数组会调用 observeArray 方法,否则对纯对象调用 walk 方法,进入walk方法
1 | Observer.prototype.walk = function walk (obj) { |
进入defineReactive$$1方法
1 | function defineReactive$$1 ( |
这个方法做的事情:
1)var childOb = !shallow && observe(val);这里的意思就是如果val是对象,会递归调用observe
2)然后使用Object.defineProperty方法为对象属性添加getter与setter


所以响应式对象,就是通过observe方法,对data对象每一项添加属性描述符getter和setter得来的(这里只看了data的)
这是data最后的样子

依赖收集
依赖收集就是订阅数据变化的
watcher的收集,收集的目的是为了当响应式数据发生变化,触发setter的时候,通知订阅者去做逻辑处理响应式对象
getter相关的逻辑就是做依赖收集
依赖收集在mount阶段的 mountComponent 方法里
1 | function mountComponent ( |
从new Watcher实例化的地方进入
1 | var Watcher = function Watcher ( |
走进this.get()
1 | Watcher.prototype.get = function get () { |
先执行pushTarget(this);,其实就是把watcher实例存起来

value = this.getter.call(vm, vm)其实就是执行vm._update(vm._render(), hydrating),在vm._render() 过程中这个阶段会触发了所有数据的 getter。
走进_render方法
1 | Vue.prototype._render = function () { |
走进render.call(vm._renderProxy, vm.$createElement);(render方法是在说模板编译的时候产生的,后面会说),看一下这里的模板生成的render方法
1 | (function anonymous( |
顺便说一下with语句,语法:
1 | with (expression) { |
expression:将给定的表达式添加到在评估语句时使用的作用域链上。表达式周围的括号是必需的。
statement:任何语句。要执行多个语句,请使用一个块语句 ({ … })对这些语句进行分组。
举个例子:
1 | function test(obj){ |
也就是在调用render方法的时候触发所有数据了getter,完成了依赖收集,那么怎么触发getter的呢
(之前初始化数据的时候的initData方法里面有这个一步,所以在访问message的时候,会调用这里的proxyGetter方法)

就触发了this[sourceKey][key],意思也就是访问vm._data.message

紧接着又触发了这个message的get方法

这个get方法做了什么:
1)先判断是否有getter,这个是在上面闭包,从这个属性的描述符里拿的

2)判断是否有Dep.target,就是watcher的实例,有就调用dep.depend();(dep是之前实例化的,每个属性都有一个)
1 | Dep.prototype.depend = function depend () { |
进入addDep方法,方法里的this指向watcher实例
1 | Watcher.prototype.addDep = function addDep (dep) { |
dep.addSub(this);是把当前watcher实例订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。
之后取personInfo属性的时候,又触发了getter,并且这个属性是个对象所以进入到了childOb.dep.depend(),跟dep.depend();一个意思的

取name属性时候又重复了一次,这里就不写了,于是完成了全部的依赖收集之后,回到get方法里,执行popTarget()

1 | function popTarget () { |
这里把targetStack和Dep.target恢复成了上一次的状态
然后执行this.cleanupDeps()
1 | Watcher.prototype.cleanupDeps = function cleanupDeps () { |
Vue 是数据驱动的,所以每次数据变化都会重新render,那么 vm._render() 方法又会再次执行,并再次触发数据的 getters,所以 Wathcer 在构造函数中会初始化 2 个 Dep 实例数组,newDeps表示新添加的 Dep 实例数组,而 deps 表示上一次添加的 Dep 实例数组。
在执行 cleanupDeps 函数的时候,会首先遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅,然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换,并把 newDepIds 和 newDeps 清空。
nextTick
源码目录src/core/util/next-tick.js
1 |
|
说一下这里做了什么吧
1)首先声明了timerFunc变量
2)为timerFunc赋值,它会判断是否支持原生的方法,然后赋值为(优先级从上到下),只要满足其中一个就不再做赋值操作
- 等于
1 | timerFunc = function () { |
- 或者
1 | timerFunc = function () { |
- 或者
1 | timerFunc = function () { |
- 或者
1 | timerFunc = function () { |
派发更新(setter)
当数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update过程,在 nextTick 后执行所有 watcher 的 run,最后执行它们的回调函数
1 | function defineReactive$$1 ( |
触发setter
1)childOb = !shallow && observe(newVal);,进入observe如果typeof类型判断是object,又会变成响应式对象
2)调用dep.notify();
1 | Dep.prototype.notify = function notify () { |
遍历subs,调用update方法
1 | Watcher.prototype.update = function update () { |
最后走进queueWatcher,Vue 在做派发更新的时候不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里(这里会过滤有重复id的watcher),然后在 nextTick 后执行 flushSchedulerQueue。
1 | var queue = []; |
nextTick方法做的事情就是往callbacks数组push进去一个匿名函数,之后调用timerFunc方法
1 | function nextTick (cb, ctx) { |
进入timeFunc
1 | timerFunc = function () { |

这里做的事情是把flushCallbacks任务放到微任务队列,而这个方法做的事情就是遍历callbacks然后执行之前push进去的每一个匿名函数
1 | function flushCallbacks () { |
当setter操作完成之后开始执行上面的flushCallbacks方法

走进copies[i]()调用

cb.call(ctx)调用到flushSchedulerQueue方法
1 | function flushSchedulerQueue () { |
这个方法把queue数组排序,之后遍历(queue数组push 进watcher的地方在这里)

遍历每一项的时候,如果有watcher.before方法就执行,然后执行watcher.run方法。
先看before方法,调用befoerUpdate钩子函数
1 | before: function before () { |
看run方法,其实就是再执行
1 | vm._update(vm._render(), hydrating); |
之后返回,执行到resetSchedulerState方法,这个方法做一些初始化操作,把使用过的变量还原。
1 | function resetSchedulerState () { |
补充
一、数组操作
Vue 不能检测到以下变动的数组
- 直接使用索引设置某项
- 通过
length改变数组长度
修改一下示例:
main.js
1 | import Vue from 'vue' |
在initData里生成响应式对象的时候,有对数组对象做处理。
1 | var Observer = function Observer (value) { |
走进protoAugment方法
1 | function protoAugment (target, src) { |
这个方法把目标元素的原型指向了src,也就是指向了arrayMethods,看一下arrayMethods是什么
1 | var arrayProto = Array.prototype; |
这个方法做了什么:
1)首先,arrayProto指向Array.prototype
2)然后,arrayMethods.__proto__= Array.prototype
3)接着,遍历methodsToPatch

进去def

就是为arrayMethods添加下面几个属性
1 | 'push', |
访问这几个属性的时候,调用的方法是mutator
1 | function mutator () { |
返回到外面Observer方法调用里,进去this.observeArray方法

1 | Observer.prototype.observeArray = function observeArray (items) { |
又进去了observe方法,然而我们的数据每一项都是基础类型,就直接return了
1 | function observe (value, asRootData) { |
DOM触发点击事件,进入mutator方法,这个方法对于push与unshift的都是inserted = args;
1 | function mutator () { |
先进入ob.observeArray(inserted)发现是个基本类型数据,直接return了
然后调用ob.dep.notify();
1 | Dep.prototype.notify = function notify () { |
断点往下走最后调用到
1 | vm._update(vm._render(), hydrating); |
到这里,后面就不说了
调整一下例子:
main.js
1 | import Vue from 'vue' |
直接使用索引设置某项,或者通过length改变数组长度无法触发派发更新
解决方案:
第一个:Vue.set(example1.items, indexOfItem, newValue)直接修改某项的值
第二个:vm.items.splice(newLength)
1 | ... |
那看下Vue.set方法吧
1 | function set (target, key, val) { |
其实是调用splice方法

二、计算属性与监听属性
computed 计算属性
先写例子
main.js
1 | import Vue from 'vue' |
从initState方法开始
1 | function initState (vm) { |
进入initComputed方法
1 | function initComputed (vm, computed) { |
1)创建空对象watchers和vm._computedWatchers
2)遍历计算属性computed,拿到计算属性的每一个 userDef,然后判断 userDef 的类型是否是function,否则获取它的 getter 函数赋值给getter变量
3)创建watcher实例并且存在watchers[key]中,key是计算属性名,watcher实例的getter就是上面userDef的getter
4)判断key是否在vm上,存在的话说明已有重复的声明键名,否则走进defineComputed方法
1 | function defineComputed ( |
这个方法其实就是为计算属性,添加getter与setter,这里的getter方法是这样的
1 | function computedGetter () { |
然后在render的时候触发计算属性的getter,拿到计算属性的watcher,因为watcher.dirty的值为true,所以执行watcher.evaluate

断点往下看,发现最终执行了watcher.getter(是上面实例化watcher的时候存的)

调用完成后返回,接着执行watcher.depend();,断点进去,发现是渲染watcher对计算属性watcher的订阅

watcher 监听属性
示例
main.js
1 | import Vue from 'vue' |
也从initState开始
1 | function initState (vm) { |
进入到initWatch方法
1 | function initWatch (vm, watch) { |
遍历watch,进入createWatcher方法(如果watch的某一项是数组,又会遍历这一项调用createWatcher)
1 | function createWatcher ( |
对handler做类型判断,想要拿到回调函数,然后进入vm.$watch方法
1 | Vue.prototype.$watch = function ( |
1)如果cb是对象就继续调用createWatcher,因为watch可以传入一个对象
2)实例化一个watcher,是一个user wathcer,这个实例cb属性就是handler,然后getter属性是这个

1 | function parsePath (path) { |
这个方法返回的值就赋值给watcher.value,同时这里获取obj[segments[i]]触发了getter,也是在这里完成了依赖收集


3)如果immediate为true,就立刻调用回调。就是下面这样的输入参数,就会立刻调用handler
1 | ... |
触发一下点击事件看一下

firstName的值变化,先进setter

然后进入dep.notify()派发更新

这里的subs[i]就是user watcher(在上面实例化user watcher,调用parsePath返回的匿名函数的地方完成的依赖收集)

走进queueWatcher方法

走到nextTick这个方法上面有说,这里就忽略
一直往下走到watcher.run();

这里拿到firstName新值和旧值,调用回调函数,就是这个方法

然后触发totalName的setter

关于这个setter一开始有点搞不懂为什么执行完val = newVal以后,totalName的值就变成了新值?后面发现得从getter的角度来看,当执行完这句之后,val变为新值,所以getter取到的就是这个值了

之后又走dep.notify()函数调用,最后调用的是
1 | vm._update(vm._render(), hydrating); |
重复的步骤就不多说了
对于watcher options,也就不多说了。
options:
- deep
- user
- computed
- Sync
计算属性和监听属性的应用场景:
计算属性本质上是 computed watcher,而侦听属性本质上是 user watcher。就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而监听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。
组件更新
上面的例子都只讲到dep.notify后,最后调用vm._update(vm._render(), hydrating);,没有讲组件更新的逻辑
改一下例子
main.js
1 | import Vue from 'vue' |
触发点击事件之后,前面流程略过,直接进入到vm._update方法,因为存在preVnode所以走了else的逻辑

然后走进sameVnode,判断它们是否是相同的vnode,来执行不同的逻辑

sameVnode方法,首先直接判断key是否相同,如果相同再判断其他的一些属性是否相同
1 | function sameVnode (a, b) { |
因为这里是相同的,走进patchVnode方法
1 | function patchVnode ( |
1)执行update钩子函数
1 | if (isDef(data) && isPatchable(vnode)) { |
2)执行updateChildren

1 | function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { |
因为我们这边的两个节点是相同的,所以又进入patchVode方法

因为节点的text不是undefined,并且新vnode.text不等于旧的vnode.text,执行nodeOps.setTextContent方法

这个方法只是把节点的textContent直接替换成新的文本
1 | function setTextContent (node, text) { |
修改一下例子
main.js
1 | import Vue from 'vue' |
触发点击事件,前面就不说了,从进入到patchVnode方法调用开始
1 | function patchVnode ( |
存了oldVnode.children,也存了vnode.children

然后进入updateChildren

1 |
|
这里发生的事情大概是这个样子的:
1)

2)

3)

4)最后一个比较特别,createElm创建了一个text:3,然后insert进去

总结
新旧节点相同的更新流程是去获取它们的 children,根据不同情况做不同的更新逻辑。
新旧节点不同的更新流程是创建新节点->更新父占位符节点->删除旧节点。