收集的目的就是为了当我们修改数据的时候,可以对相关的依赖派发更新,那么这一节我们来详细分析这个过程。

setter 部分的逻辑:

/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
} // cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
} let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// ...
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

假设我们有如下模板:

<div id="demo">
{{name}}
</div>

我们知道这段模板将会被编译成渲染函数,接着创建一个渲染函数的观察者,从而对渲染函数求值,在求值的过程中会触发数据对象 name 属性的 get 拦截器函数,进而将该观察者收集到 name 属性通过闭包引用的“筐”中,即收集到 Dep 实例对象中。这个 Dep 实例对象是属于 name 属性自身所拥有的,这样当我们尝试修改数据对象 name 属性的值时就会触发 name 属性的 set 拦截器函数,这样就有机会调用 Dep 实例对象的 notify 方法,从而触发了响应,如下代码截取自 defineReactive 函数中的 set 拦截器函数:

set: function reactiveSetter (newVal) {
// 省略...
dep.notify()
}

如上高亮代码所示,可以看到当属性值变化时确实通过 set 拦截器函数调用了 Dep 实例对象的 notify 方法,这个方法就是用来通知变化的,我们找到 Dep 类的 notify 方法,如下:

export default class Dep {
// 省略... constructor () {
this.id = uid++
this.subs = []
} // 省略... notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

大家观察 notify 函数可以发现其中包含如下这段 if 条件语句块:

if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}

对于这段代码的作用,我们会在本章的 同步执行观察者 一节中对其详细讲解,现在大家可以完全忽略,这并不影响我们对代码的理解。如果我们去掉如上这段代码,那么 notify 函数将变为:

notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}

notify 方法只做了一件事,就是遍历当前 Dep 实例对象的 subs 属性中所保存的所有观察者对象,并逐个调用观察者对象的 update 方法,这就是触发响应的实现机制,那么大家应该也猜到了,重新求值的操作应该是在 update 方法中进行的,那我们就找到观察者对象的 update 方法,看看它做了什么事情,如下:

update () {
/* istanbul ignore else */
if (this.computed) {
// 省略...
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}

在 update 方法中代码被拆分成了三部分,即 if...else if...else 语句块。首先 if 语句块的代码会在判断条件 this.computed 为真的情况下执行,我们说过 this.computed 属性是用来判断该观察者是不是计算属性的观察者,这部分代码我们将会在计算属性部分详细讲解。也就是说渲染函数的观察者肯定是不会执行 if 语句块中的代码的,此时会继续判断 else...if 语句的条件 this.sync 是否为真,我们知道 this.sync 属性的值就是创建观察者实例对象时传递的第三个选项参数中的 sync 属性的值,这个值的真假代表了当变化发生时是否同步更新变化。对于渲染函数的观察者来讲,它并不是同步更新变化的,而是将变化放到一个异步更新队列中,也就是 else 语句块中代码所做的事情,即 queueWatcher 会将当前观察者对象放到一个异步更新队列,这个队列会在调用栈被清空之后按照一定的顺序执行。关于更多异步更新队列的内容我们会在后面单独讲解,这里大家只需要知道一件事情,那就是无论是同步更新变化还是将更新变化的操作放到异步更新队列,真正的更新变化操作都是通过调用观察者实例对象的 run 方法完成的。所以此时我们应该把目光转向 run 方法,如下:

run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}

run 方法的代码很简短,它判断了当前观察者实例的 this.active 属性是否为真,其中 this.active 属性用来标识一个观察者是否处于激活状态,或者可用状态。如果观察者处于激活状态那么 this.active 的值为真,此时会调用观察者实例对象的 getAndInvoke 方法,并以 this.cb 作为参数,我们知道 this.cb 属性是一个函数,我们称之为回调函数,当变化发生时会触发,但是对于渲染函数的观察者来讲,this.cb 属性的值为 noop,即什么都不做

现在我们终于找到了更新变化的根源,那就是 getAndInvoke 方法,如下:

getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}

在 getAndInvoke 方法中,第一句代码就调用了 this.get 方法,这意味着重新求值,这也证明了我们在上一小节中的假设。对于渲染函数的观察者来讲,重新求值其实等价于重新执行渲染函数,最终结果就是重新生成了虚拟DOM并更新真实DOM,这样就完成了重新渲染的过程。在重新调用 this.get 方法之后是一个 if 语句块,实际上对于渲染函数的观察者来讲并不会执行这个 if 语句块,因为 this.get 方法的返回值其实就等价于 updateComponent 函数的返回值,这个值将永远都是 undefined。实际上 if 语句块内的代码是为非渲染函数类型的观察者准备的,它用来对比新旧两次求值的结果,当值不相等的时候会调用通过参数传递进来的回调。我们先看一下判断条件,如下:

const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// 省略...
}

首先对比新值 value 和旧值 this.value 是否相等,只有在不相等的情况下才需要执行回调,但是两个值相等就一定不执行回调吗?未必,这个时候就需要检测第二个条件是否成立,即 isObject(value),判断新值的类型是否是对象,如果是对象的话即使值不变也需要执行回调,注意这里的“不变”指的是引用不变,如下代码所示:

const data = {
obj: {
a: 1
}
}
const obj1 = data.obj
data.obj.a = 2
const obj2 = data.obj console.log(obj1 === obj2) // true

上面的代码中由于 obj1 与 obj2 具有相同的引用,所以他们总是相等的,但其实数据已经变化了,这就是判断 isObject(value) 为真则执行回调的原因。

接下来我们就看一下 if 语句块内的代码:

const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}

代码如果执行到了 if 语句块内,则说明应该执行观察者的回调函数了。首先定义了 oldValue 常量,它的值是旧值,紧接着使用新值更新了 this.value 的值。我们可以看到如上代码中是如何执行回调的:

cb.call(this.vm, value, oldValue)

将回调函数的作用域修改为当前 Vue 组件对象,然后传递了两个参数,分别是新值和旧值。

另外大家可能注意到了这句代码:this.dirty = false,将观察者实例对象的 this.dirty 属性设置为 false,实际上 this.dirty 属性也是为计算属性准备的,由于计算属性是惰性求值,所以在实例化计算属性的时候 this.dirty 的值会被设置为 true,代表着还没有求值,后面当真正对计算属性求值时,也就是执行如上代码时才会将 this.dirty 设置为 false,代表着已经求过值了。

除此之外,我们注意如下代码:

if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}

在调用回调函数的时候,如果观察者对象的 this.user 为真意味着这个观察者是开发者定义的,所谓开发者定义的是指那些通过 watch 选项或 $watch 函数定义的观察者,这些观察者的特点是回调函数是由开发者编写的,所以这些回调函数在执行的过程中其行为是不可预知的,很可能出现错误,这时候将其放到一个 try...catch 语句块中,这样当错误发生时我们就能够给开发者一个友好的提示。并且我们注意到在提示信息中包含了 this.expression 属性,我们前面说过该属性是被观察目标(expOrFn)的字符串表示,这样开发者就能清楚的知道是哪里发生了错误。

异步更新队列

异步更新的意义----性能优化

当所有的突变完成之后,再一次性的执行队列中所有观察者的更新方法,同时清空队列,这样就达到了优化的目的

看一看其具体实现,我们知道当修改一个属性的值时,会通过执行该属性所收集的所有观察者对象的 update 方法进行更新,那么我们就找到观察者对象的 update 方法,如下:

update () {
/* istanbul ignore else */
if (this.computed) {
// 省略...
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

如果没有指定这个观察者是同步更新(this.sync 为真),那么这个观察者的更新机制就是异步的,这时当调用观察者对象的 update 方法时,在 update 方法内部会调用 queueWatcher 函数,并将当前观察者对象作为参数传递,queueWatcher 函数的作用就是我们前面讲到过的,它将观察者放到一个队列中等待所有突变完成之后统一执行更新。

queueWatcher 函数来自 src/core/observer/scheduler.js 文件,如下是 queueWatcher 函数的全部代码:

export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}

queueWatcher 函数接收观察者对象作为参数,首先定义了 id 常量,它的值是观察者对象的唯一 id,然后执行 if 判断语句,如下是简化的代码:

export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// let has: { [key: number]: ?true } = {}
if (has[id] == null) {
has[id] = true
// 省略...
}
}

当 queueWatcher 函数被调用之后,会尝试将该观察者放入队列中,并将该观察者的 id 值登记到 has 对象上作为 has 对象的属性同时将该属性值设置为 true。该 if 语句以及变量 has 的作用就是用来避免将相同的观察者重复入队的。在该 if 语句块内执行了真正的入队操作,如下代码高亮的部分所示:

export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
// let flushing = false 初始值是 false
if (!flushing) {
// const queue: Array<Watcher> = []
queue.push(watcher)
} else {
// 省略...
}
// 省略...
}
}

flushing 变量是一个标志,我们知道放入队列 queue 中的所有观察者将会在突变完成之后统一执行更新,当更新开始时会将 flushing 变量的值设置为 true,代表着此时正在执行更新,所以根据判断条件 if (!flushing) 可知只有当队列没有执行更新时才会简单地将观察者追加到队列的尾部有的同学可能会问:“难道在队列执行更新的过程中还会有观察者入队的操作吗?”,实际上是会的,典型的例子就是计算属性,比如队列执行更新时经常会执行渲染函数观察者的更新,渲染函数中很可能有计算属性的存在,由于计算属性在实现方式上与普通响应式属性有所不同,所以当触发计算属性的 get 拦截器函数时会有观察者入队的行为,这个时候我们需要特殊处理,也就是 else 分支的代码,如下:

export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 省略...
}
}

当变量 flushing 为真时,说明队列正在执行更新,这时如果有观察者入队则会执行 else 分支中的代码,这段代码的作用是为了保证观察者的执行顺序,现在大家只需要知道观察者会被放入 queue 队列中即可

接着我们再来看如下代码:

export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
// 省略...
// queue the flush
> if (!waiting) {
> waiting = true
> if (process.env.NODE_ENV !== 'production' && !config.async) {
> flushSchedulerQueue()
> return
> }
> nextTick(flushSchedulerQueue)
> }
}
}

大家观察如上代码中有这样一段 if 条件语句:

if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}

在接下来的讲解中我们将会忽略这段代码,并在 同步执行观察者 一节中补充讲解,

我们回到那段高亮的代码,这段代码是一个 if 语句块,其中变量 waiting 同样是一个标志,它也定义在 scheduler.js 文件头部,初始值为 false

let waiting = false

我们看 if 语句块内的代码就知道了,在 if 语句块内先将 waiting 的值设置为 true,这意味着无论调用多少次 queueWatcher 函数,该 if 语句块的代码只会执行一次。接着调用 nextTick 并以 flushSchedulerQueue 函数作为参数,其中 flushSchedulerQueue 函数的作用之一就是用来将队列中的观察者统一执行更新的。对于 nextTick 相信大家已经很熟悉了,其实最好理解的方式就是把 nextTick 看做 setTimeout(fn, 0),如下:

export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
// 省略...
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
setTimeout(flushSchedulerQueue, 0)
}
}
}

我们对 Vue 数据修改派发更新的过程也有了认识,实际上就是当数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcher 的 run,最后执行它们的回调函数。nextTick 是 Vue 一个比较核心的实现了。

四、vue派发更新的更多相关文章

  1. 读Vue源码 (依赖收集与派发更新)

    vue的依赖收集是定义在defineReactive方法中,通过Object.defineProperty来设置getter,红字部分主要做依赖收集,先判断了Dep.target如果有的情况会执行红字 ...

  2. Vue异步更新机制以及$nextTick原理

    相信很多人会好奇Vue内部的更新机制,或者平时工作中遇到的一些奇怪的问题需要使用$nextTick来解决,今天我们就来聊一聊Vue中的异步更新机制以及$nextTick原理 Vue的异步更新 可能你还 ...

  3. Vue数组更新及过滤排序

    前面的话 Vue为了增加列表渲染的功能,增加了一组观察数组的方法,而且可以显示一个数组的过滤或排序的副本.本文将详细介绍Vue数组更新及过滤排序 变异方法 Vue 包含一组观察数组的变异方法,它们将会 ...

  4. vue-cli搭建vue项目更新

    vue-cli搭建vue项目更新 更新之前一篇博客错误的地方,在使用vue init webpack xxx 之后并不需要使用npm install 下载依赖包,而是直接根据提示 打开文件夹 再npm ...

  5. vue 数组更新 this.$set(this.dataList, data.index, data.data)

    vue 数组更新 this.$set(this.dataList, data.index, data.data) https://www.cnblogs.com/huangenai/p/9836811 ...

  6. FreeSql (十四)批量更新数据

    FreeSql支持丰富的更新数据方法,支持单条或批量更新,在特定的数据库执行还可以返回更新后的记录值. var connstr = "Data Source=127.0.0.1;Port=3 ...

  7. vue数组更新界面无变化

    1. vue数组更新界面无变化 1.1. 说明 对数组进行更新或者添加,一定要注意方式,我的情况是数组套数组,双重循环,在造数据的时候,不断从尾部添加数据,所以写成了如下形式,每次下拉都会去加载一批相 ...

  8. 对于vue项目更新迭代导致上传至服务器后出现Loading chunk {n} failed和Unexpected token <的解决方式

    相信大家对于vue项目的维护与更新中会遇见很多问题,其中有两种情况最为常见. 一种是Loading chunk {n} failed,这种情况出现的原因是vue页面更新上传至服务器后,由于vue默认打 ...

  9. 从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 十四 ║ VUE 计划书 & 我的前后端开发简史

    ---新内容开始--- 番外 大家周一好呀,又是元气满满的一个周一呀!感谢大家在周一这个着急改Bug的黄金时期,抽出时间来看我的博文哈哈哈,时间真快,已经到第十四篇博文了,也很顺顺(跌跌)利利 (撞撞 ...

随机推荐

  1. node.js启动调试方式

    node.js启动调试方式(nodeJs不能像js一样在控制台调试) 以express项目为例,启动路径是localhost:3000 一.通过node命令启动 node server/bin/www ...

  2. 3.Mysql集群------Mycat分库分表

    前言: 分库分表,在本节里是水平切分,就是多个数据库里包含的表是一模一样的. 只是把字段散列的分到不同的库中. 实践: 1.修改schema.xml 这里是在同一台服务器上建立了4个数据库db1,db ...

  3. java基础必备单词讲解 day five

    Rectangle width high height area employee tool param version author math guess resources 之前单词复习 path ...

  4. Redis高可用

    redis高可用只要在于三个方面 主从复制 哨兵机制 集群机制 主从复制 主从复制作用: 1.数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式.2.故障恢复:当主节点出现问题时,可 ...

  5. python基础数据类型之列表,元组操作

    一.列表的索引和切片1.列表的索引列表和字符串一样样拥有索引 lst = ["a","b","c"] print(lst[0]) # 获取第 ...

  6. Linux文件服务器实战(系统用户)

    ftp匿名用户设置完成之后任何人都可以访问服务器端文件,目录,甚至可以修改删除文件和目录,,那如何存放私密文件并保证文件或者目录专属于拥有者呢,就需要使用vsftp系统用户来实现了. 1.在linux ...

  7. springboot+Druid+mybatis整合

    一.添加Druid.MySQL连接池.mybatis依赖 <!--整合Druid--> <dependency> <groupId>com.alibaba</ ...

  8. SEOer必读:50个网站推广方法

    1.论坛推广 这里所说的论坛推广绝对不是在论坛里一个一个版贴广告,也不是将网站地址加在签名里然后疯狂刷屏,那样既耗费精力而且效果也不见得好,论坛管理员只要点几下鼠标就能将你的帖子全部删除,顺便封掉你的 ...

  9. 学习python第十四天,模块

    Python 模块(Module),是一个 Python 文件,以 .py 结尾,包含了 Python 对象定义和Python语句. 模块让你能够有逻辑地组织你的 Python 代码段. 把相关的代码 ...

  10. 笔记-scrapy-setting

    笔记-scrapy-setting 1.     简介 Scrapy设置允许您自定义所有Scrapy组件的行为,包括核心,扩展,管道和蜘蛛本身. 可以使用不同的机制来填充设置,每种机制都有不同的优先级 ...