响应式
数据变化的时候触发DOM的变化,数据变化对DOM的处理有几个问题:
- 修改哪块的 DOM?
- 效率和性能是不是最优的?
- 是每次数据变化都去修改DOM吗
响应式对象
Object.defineProperty(obj, prop, descriptor)
方法)
响应式对象核心就是使用这个方法为对象属性添加
getter
与setter
Vue
会把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
,根据不同情况做不同的更新逻辑。
新旧节点不同的更新流程是创建新节点->更新父占位符节点->删除旧节点。