文章目录
- 1. 说明
- 2. 初始化
- initState()
- initProps()
- initData()
- observe()
- Observer
- defineReactive()
- 3. 数据代理
- 4. 模板解析
- 4.1. 模板解析的基本流程
- 4.2. 模板解析(1): 大括号表达式解析
- 4.3. 模板解析(2): 事件指令解析
- 4.4. 模板解析(3): 一般指令解析
- 5. 数据绑定
- 5.1. 数据绑定
- 5.2. 数据劫持
- 5.3. 四个重要对象
- 5.3.1 Observer
- 5.3.2 Dep(Depend)
- 5.3.3 Compiler
- 5.3.4 Watcher
- 5.3.5 总结: dep 与 watcher 的关系: 多对多
- 6. MVVM 原理图分析
- 7. 双向数据绑定
- 依赖收集过程
- 移除订阅
- 派发更新
- notify()
- update()
- queueWatcher()
- flushSchedulerQueue()
- updated()
- defineProperty 缺陷及处理
- Vue.set()
1. 说明

-
分析 vue 作为一个
MVVM框架的基本实现原理- 数据代理
- 模板解析
- 数据绑定
-
准备知识
[].slice.call(lis): 将伪数组转换为真数组node.nodeType: 得到节点类型Object.defineProperty(obj, propName, {}): 给对象添加/修改属性(指定描述符)configurable: true/false是否可以重新 defineenumerable: true/false是否可以枚举(for…in / keys())value: 指定初始值writable: true/falsevalue 是否可以修改get: 回调函数, 用来得到当前属性值set: 回调函数, 用来监视当前属性值的变化
Object.keys(obj): 得到对象自身可枚举的属性名的数组DocumentFragment: 文档碎片(高效批量更新多个节点)obj.hasOwnProperty(prop): 判断 prop 是否是 obj 自身的属性
2. 初始化
在 new Vue 初始化的时候,会对我们组件的数据 props 和 data 进行初始化
源码地址:src/core/instance/init.js - 15行
export function initMixin (Vue: Class<Component>) {
// 在原型上添加 _init 方法
Vue.prototype._init = function (options?: Object) {
...
vm._self = vm
initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
initEvents(vm) // 初始化事件:$on, $off, $emit, $once
initRender(vm) // 初始化渲染: render, mixin
callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
initInjections(vm) // 初始化 inject
initState(vm) // 初始化组件数据:props, data, methods, watch, computed
initProvide(vm) // 初始化 provide
callHook(vm, 'created') // 调用生命周期钩子函数
...
}
}
initState()
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 初始化 props
if (opts.props) initProps(vm, opts.props)
// 初始化 methods
if (opts.methods) initMethods(vm, opts.methods)
// 初始化 data
if (opts.data) {
initData(vm)
} else {
// 没有 data 的话就默认赋值为空对象,并监听
observe(vm._data = {}, true /* asRootData */)
}
// 初始化 computed
if (opts.computed) initComputed(vm, opts.computed)
// 初始化 watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initProps()、initData()、observe()
initProps()
源码地址:src/core/instance/state.js - 65行
这里主要做的是:
- 遍历父组件传进来的
props列表 - 校验每个属性的命名、类型、default 属性等,都没有问题就调用
defineReactive设置成响应式 - 然后用
proxy()把属性代理到当前实例上,如把vm._props.xx变成vm.xx,就可以访问
function initProps (vm: Component, propsOptions: Object) {
// 父组件传入子组件的 props
const propsData = vm.$options.propsData || {}
// 经过转换后最终的 props
const props = vm._props = {}
// 存放 props 的 key,就算 props 值空了,key 也会在里面
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// 转换非根实例的 props
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
// 校验 props 类型、default 属性等
const value = validateProp(key, propsOptions, propsData, vm)
// 在非生产环境中
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(`hyphenatedKey 是保留属性,不能用作组件 prop`)
}
// 把 props 设置成响应式的
defineReactive(props, key, value, () => {
// 如果用户修改 props 发出警告
if (!isRoot && !isUpdatingChildComponent) {
warn(`避免直接改变 prop`)
}
})
} else {
// 把 props 设置为响应式
defineReactive(props, key, value)
}
// 把不在默认 vm 上的属性,代理到实例上
// 可以让 vm._props.xx 通过 vm.xx 访问
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
initData()
源码地址:src/core/instance/state.js - 113行
这里主要做的是:
- 初始化一个
data,并拿到keys集合 - 遍历
keys集合,来判断有没有和props里的属性名或者methods里的方法名重名的 - 没有问题就通过
proxy()把data里的每一个属性都代理到当前实例上,就可以通过this.xx访问了 - 最后再调用
observe监听整个data
function initData (vm: Component) {
// 获取当前实例的 data
let data = vm.$options.data
// 判断 data 的类型
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(`数据函数应该返回一个对象`)
}
// 获取当前实例的 data 属性名集合
const keys = Object.keys(data)
// 获取当前实例的 props
const props = vm.$options.props
// 获取当前实例的 methods 对象
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
// 非生产环境下判断 methods 里的方法是否存在于 props 中
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(`Method 方法不能重复声明`)
}
}
// 非生产环境下判断 data 里的属性是否存在于 props 中
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(`属性不能重复声明`)
} else if (!isReserved(key)) {
// 都不重名的情况下,代理到 vm 上
// 可以让 vm._data.xx 通过 vm.xx 访问
proxy(vm, `_data`, key)
}
}
// 监听 data
observe(data, true /* asRootData */)
}
observe()
源码地址:src/core/observer/index.js - 110行
这个方法主要就是用来给数据加上监听器的
这里主要做的是:
- 如果是
vnode的对象类型或者不是引用类型,就直接跳出 - 否则就给没有添加
Observer的数据添加一个Observer,也就是监听者
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果不是'object'类型 或者是 vnode 的对象类型就直接返回
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 使用缓存的对象
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 创建监听者
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Observer
源码地址:src/core/observer/index.js - 37行
这是一个类,作用是把一个正常的数据成可观测的数据
这里主要做的是:
- 给当前
value打上已经是响应式属性的标记,避免重复操作 - 然后判断数据类型
- 如果是对象,就遍历对象,调用
defineReactive()创建响应式对象 - 如果是数组,就遍历数组,调用
observe()对每一个元素进行监听
- 如果是对象,就遍历对象,调用
export class Observer {
value: any;
dep: Dep;
vmCount: number; // 根对象上的 vm 数量
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 给 value 添加 __ob__ 属性,值为value 的 Observe 实例
// 表示已经变成响应式了,目的是对象遍历时就直接跳过,避免重复操作
def(value, '__ob__', this)
// 类型判断
if (Array.isArray(value)) {
// 判断数组是否有__proty__
if (hasProto) {
// 如果有就重写数组的方法
protoAugment(value, arrayMethods)
} else {
// 没有就通过 def,也就是Object.defineProperty 去定义属性值
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
// 如果是对象类型
walk (obj: Object) {
const keys = Object.keys(obj)
// 遍历对象所有属性,转为响应式对象,也是动态添加 getter 和 setter,实现双向绑定
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 监听数组
observeArray (items: Array<any>) {
// 遍历数组,对每一个元素进行监听
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
defineReactive()
源码地址:src/core/observer/index.js - 135行
这个方法的作用是定义响应式对象
这里主要做的是:
- 先初始化一个
dep实例 - 如果是对象就调用
observe,递归监听,以保证不管结构嵌套多深,都能变成响应式对象 - 然后调用
Object.defineProperty()劫持对象属性的getter和getter - 如果获取时,触发
getter会调用dep.depend()把观察者push到依赖的数组subs里去,也就是依赖收集 - 如果更新时,触发
setter会做以下操作- 新值没有变化或者没有
setter属性的直接跳出 - 如果新值是对象就调用
observe()递归监听 - 然后调用
dep.notify()派发更新
- 新值没有变化或者没有
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建 dep 实例
const dep = new Dep()
// 拿到对象的属性描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 获取自定义的 getter 和 setter
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 如果 val 是对象的话就递归监听
// 递归调用 observe 就可以保证不管对象结构嵌套有多深,都能变成响应式对象
let childOb = !shallow && observe(val)
// 截持对象属性的 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 拦截 getter,当取值时会触发该函数
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 进行依赖收集
// 初始化渲染 watcher 时访问到需要双向绑定的对象,从而触发 get 函数
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
// 拦截 setter,当值改变时会触发该函数
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// 判断是否发生变化
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// 没有 setter 的访问器属性
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 如果新值是对象的话递归监听
childOb = !shallow && observe(newVal)
// 派发更新
dep.notify()
}
})
}
上面说了通过 dep.depend 来做依赖收集,可以说 Dep 就是整个 getter 依赖收集的核心了
3. 数据代理
- 数据代理: 通过一个对象代理对另一个对象(在前一个对象内部)中属性的操作(读/写)
- vue 数据代理: 通过
vm对象来代理data对象中所有属性的操作 - 好处: 更方便的操作 data 中的数据
- 基本实现流程 a. 通过
Object.defineProperty()给vm添加与data对象的属性对应的属性描述符 b. 所有添加的属性都包含getter/setterc.getter/setter内部去操作data中对应的属性数据
4. 模板解析
4.1. 模板解析的基本流程
- 将
el的所有子节点取出, 添加到一个新建的文档fragment对象中 - 对
fragment中的所有层次子节点递归进行编译解析处理- 对大括号表达式文本节点进行解析
- 对元素节点的指令属性进行解析
- 事件指令解析
- 一般指令解析
- 将解析后的
fragment添加到 el 中显示
4.2. 模板解析(1): 大括号表达式解析
- 根据正则对象得到匹配出的表达式字符串: 子匹配
/RegExp.\$1 name - 从
data中取出表达式对应的属性值 - 将属性值设置为文本节点的
textContent
4.3. 模板解析(2): 事件指令解析
- 从指令名中取出事件名
- 根据指令的值(表达式)从
methods中得到对应的事件处理函数对象 - 给当前元素节点绑定指定事件名和回调函数的 dom 事件监听
- 指令解析完后, 移除此指令属性
4.4. 模板解析(3): 一般指令解析
- 得到指令名和指令值(表达式)
text/html/classmsg/myClass - 从
data中根据表达式得到对应的值 - 根据指令名确定需要操作元素节点的什么属性
v-text—textContent属性v-html—innerHTML属性v-class–className属性
- 将得到的表达式的值设置到对应的属性上
- 移除元素的指令属性
5. 数据绑定
5.1. 数据绑定
一旦更新了 data 中的某个属性数据, 所有界面上直接使用或间接使用了此属性的节点都会 更新
5.2. 数据劫持
- 数据劫持是
vue中用来实现数据绑定的一种技术 - 基本思想: 通过
defineProperty()来监视 data 中所有属性(任意层次)数据的变化, 一旦变化就去更新界面
5.3. 四个重要对象
5.3.1 Observer
a. 用来对 data 所有属性数据进行劫持的构造函数
b. 给 data 中所有属性重新定义属性描述(get/set)
c. 为 data 中的每个属性创建对应的 dep 对象
5.3.2 Dep(Depend)
a. data 中的每个属性(所有层次)都对应一个 dep 对象
b. 创建的时机:
- 在初始化 data 中各个属性时创建对应的
dep对象 - 在
data中的某个属性值被设置为新的对象时
c. 对象的结构
{
id, // 每个 dep 都有一个唯一的 id
subs //包含 n 个对应 watcher 的数组(subscribes 的简写)
}
d. subs 属性说明
- 当
watcher被创建时, 内部将当前watcher对象添加到对应的dep对象的subs中 - 当此
data属性的值发生改变时,subs中所有的watcher都会收到更新的通知,从而最终更新对应的界面
Dep源码地址:src/core/observer/dep.js
这是一个类,它实际上就是对 Watcher 的一种管理
这里首先初始化一个 subs 数组,用来存放依赖,也就是观察者,谁依赖这个数据,谁就在这个数组里,然后定义几个方法来对依赖添加、删除、通知更新等
另外它有一个静态属性 target,这是一个全局的 Watcher,也表示同一时间只能存在一个全局的 Watcher
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 添加观察者
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除观察者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
// 调用 Watcher 的 addDep 函数
Dep.target.addDep(this)
}
}
// 派发更新(下一章节介绍)
notify () {
...
}
}
// 同一时间只有一个观察者使用,赋值观察者
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
5.3.3 Compiler
a. 用来解析模板页面的对象的构造函数(一个实例)
b. 利用 compile 对象解析模板页面
c. 每解析一个表达式(非事件指令)都会创建一个对应的 watcher 对象, 并建立 watcher 与 dep 的关系
d. complie 与 watcher 关系: 一对多的关系
5.3.4 Watcher
a. 模板中每个非事件指令或表达式都对应一个 watcher 对象
b. 监视当前表达式数据的变化
c. 创建的时机: 在初始化编译模板时
d. 对象的组成
{
vm, //vm 对象
exp, //对应指令的表达式
cb, //当表达式所对应的数据发生改变的回调函数
value, //表达式当前的值
depIds //表达式中各级属性所对应的 dep 对象的集合对象 //属性名为 dep 的 id, 属性值为 dep
}
源码地址:src/core/observer/watcher.js
Watcher 也是一个类,也叫观察者(订阅者),这里干的活还挺复杂的,而且还串连了渲染和编译
let uid = 0
export default class Watcher {
...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// Watcher 实例持有的 Dep 实例的数组
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.value = this.lazy
? undefined
: this.get()
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
}
get ()
// 该函数用于缓存 Watcher
// 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher
pushTarget(this)
let value
const vm = this.vm
try {
// 调用回调函数,也就是upcateComponent,对需要双向绑定的对象求值,从而触发依赖收集
value = this.getter.call(vm, vm)
} catch (e) {
...
} finally {
// 深度监听
if (this.deep) {
traverse(value)
}
// 恢复Watcher
popTarget()
// 清理不需要了的依赖
this.cleanupDeps()
}
return value
}
// 依赖收集时调用
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 把当前 Watcher push 进数组
dep.addSub(this)
}
}
}
// 清理不需要的依赖(下面有)
cleanupDeps () {
...
}
// 派发更新时调用(下面有)
update () {
...
}
// 执行 watcher 的回调
run () {
...
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
5.3.5 总结: dep 与 watcher 的关系: 多对多
a. data 中的一个属性对应一个 dep, 一个 dep 中可能包含多个 watcher(模板中有几个表达式使用到了同一个属性)
b. 模板中一个非事件表达式对应一个 watcher, 一个 watcher 中可能包含多个 dep(表达式是多层: a.b)
c. 数据绑定使用到 2 个核心技术
defineProperty()消息订阅与发布
6. MVVM 原理图分析

7. 双向数据绑定
- 双向数据绑定是建立在单向数据绑定(model==>View)的基础之上的
- 双向数据绑定的实现流程:
a. 在解析v-model指令时, 给当前元素添加input监听
b. 当 input 的value发生改变时, 将最新的值赋值给当前表达式所对应的data属性
依赖收集过程
在首次渲染挂载的时候,还会有这样一段逻辑
mountComponent 源码地址:src/core/instance/lifecycle.js - 141行
export function mountComponent (...): Component {
// 调用生命周期钩子函数
callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
// 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Diff )到真实DOM,这里是首次渲染
vm._update(vm._render(), hydrating)
}
// 为当前组件实例设置观察者,监控 updateComponent 函数得到的数据,下面有介绍
new Watcher(vm, updateComponent, noop, {
// 当触发更新的时候,会在更新之前调用
before () {
// 判断 DOM 是否是挂载状态,就是说首次渲染和卸载的时候不会执行
if (vm._isMounted && !vm._isDestroyed) {
// 调用生命周期钩子函数
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// 没有老的 vnode,说明是首次渲染
if (vm.$vnode == null) {
vm._isMounted = true
// 调用生命周期钩子函数
callHook(vm, 'mounted')
}
return vm
}
依赖收集:
- 挂载之前会实例化一个渲染
watcher,进入watcher构造函数里就会执行this.get()方法 - 然后就会执行
pushTarget(this),就是把Dep.target赋值为当前渲染watcher并压入栈(为了恢复用) - 然后执行
this.getter.call(vm, vm),也就是上面的updateComponent()函数,里面就执行了vm._update(vm._render(), hydrating) - 接着执行
vm._render()就会生成渲染vnode,这个过程中会访问vm上的数据,就触发了数据对象的getter - 每一个对象值的
getter都有一个dep,在触发getter的时候就会调用dep.depend()方法,也就会执行Dep.target.addDep(this) - 然后这里会做一些判断,以确保同一数据不会被多次添加,接着把符合条件的数据
push到subs里,到这就已经完成了依赖的收集,不过到这里还没执行完,如果是对象还会递归对象触发所有子项的getter,还要恢复Dep.target状态
移除订阅
移除订阅就是调用 cleanupDeps() 方法。比如在模板中有 v-if 我们收集了符合条件的模板 a 里的依赖。当条件改变时,模板 b 显示出来,模板 a 隐藏。这时就需要移除 a 的依赖
这里主要做的是:
- 先遍历上一次添加的实例数组
deps,移除dep.subs数组中的Watcher的订阅 - 然后把
newDepIds和depIds交换,newDeps和deps交换 - 再把
newDepIds和newDeps清空
// 清理不需要的依赖
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
派发更新
notify()
触发 setter 的时候会调用 dep.notify() 通知所有订阅者进行派发更新
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 如果不是异步,需要排序以确保正确触发
subs.sort((a, b) => a.id - b.id)
}
// 遍历所有 watcher 实例数组
for (let i = 0, l = subs.length; i < l; i++) {
// 触发更新
subs[i].update()
}
}
update()
触发更新时调用
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 组件数据更新会走这里
queueWatcher(this)
}
}
queueWatcher()
源码地址:src/core/observer/scheduler.js - 164行
这是一个队列,也是 Vue 在做派发更新时的一个优化点。就是说在每次数据改变的时候不会都触发 watcher 回调,而是把这些 watcher 都添加到一个队列里,然后在 nextTick 后才执行
这里和下一小节 flushSchedulerQueue() 的逻辑有交叉的地方,所以要联合起来理解
主要做的是:
- 先用
has对象查找id,保证同一个watcher只会push一次 else如果在执行watcher期间又有新的watcher插入进来就会到这里,然后从后往前找,找到第一个待插入的id比当前队列中的id大的位置,插入到队列中,这样队列的长度就发生了变化- 最后通过
waiting保证nextTick只会调用一次
export function queueWatcher (watcher: Watcher) {
// 获得 watcher 的 id
const id = watcher.id
// 判断当前 id 的 watcher 有没有被 push 过
if (has[id] == null) {
has[id] = true
if (!flushing) {
// 最开始会进入这里
queue.push(watcher)
} else {
// 在执行下面 flushSchedulerQueue 的时候,如果有新派发的更新会进入这里,插入新的 watcher,下面有介绍
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 最开始会进入这里
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
// 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用
nextTick(flushSchedulerQueue)
}
}
}
flushSchedulerQueue()
源码地址:src/core/observer/scheduler.js - 71行
这里主要做的是:
- 先排序队列,排序条件有三点,看注释
- 然后遍历队列,执行对应
watcher.run()。需要注意的是,遍历的时候每次都会对队列长度进行求值,因为在run之后,很可能又会有新的watcher添加进来,这时就会再次执行到上面的queueWatcher
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// 根据 id 排序,有如下条件
// 1.组件更新需要按从父到子的顺序,因为创建过程中也是先父后子
// 2.组件内我们自己写的 watcher 优先于渲染 watcher
// 3.如果某组件在父组件的 watcher 运行期间销毁了,就跳过这个 watcher
queue.sort((a, b) => a.id - b.id)
// 不要缓存队列长度,因为遍历过程中可能队列的长度发生变化
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// 执行 beforeUpdate 生命周期钩子函数
watcher.before()
}
id = watcher.id
has[id] = null
// 执行组件内我们自己写的 watch 的回调函数并渲染组件
watcher.run()
// 检查并停止循环更新,比如在 watcher 的过程中又重新给对象赋值了,就会进入无限循环
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(`无限循环了`)
break
}
}
}
// 重置状态之前,先保留一份队列备份
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// 调用组件激活的钩子 activated
callActivatedHooks(activatedQueue)
// 调用组件更新的钩子 updated
callUpdatedHooks(updatedQueue)
}
updated()
终于可以更新了,updated 大家都熟悉了,就是生命周期钩子函数
上面调用 callUpdatedHooks() 的时候就会进入这里, 执行 updated 了
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
至此 Vue2 的响应式原理流程的源码基本就分析完毕了,接下来就介绍一下上面流程中的不足之处
defineProperty 缺陷及处理
使用 Object.defineProperty 实现响应式对象,还是有一些问题的
- 比如给对象中添加新属性时,是无法触发
setter的 - 比如不能检测到数组元素的变化
Vue.set()
给对象添加新的响应式属性时,可以使用一个全局的 API,就是 Vue.set() 方法
源码地址:src/core/observer/index.js - 201行
set 方法接收三个参数:
- target:数组或普通对象
- key:表示数组下标或对象的 key 名
- val:表示要替换的新值
这里主要做的是:
- 先判断如果是数组,并且下标合法,就直接使用重写过的
splice替换 - 如果是对象,并且 key 存在于 target 里,就替换值
- 如果没有
__ob__,说明不是一个响应式对象,直接赋值返回 - 最后再把新属性变成响应式,并派发更新
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 如果是数组 而且 是合法的下标
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 直接使用 splice 就替换,注意这里的 splice 不是原生的,所以才可以监测到,具体看下面
target.splice(key, 1, val)
return val
}
// 到这说明是对象
// 如果 key 存在于 target 里,就直接赋值,也是可以监测到的
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 获取 target.__ob__
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 在 Observer 里介绍过,如果没有这个属性,就说明不是一个响应式对象
if (!ob) {
target[key] = val
return val
}
// 然后把新添加的属性变成响应式
defineReactive(ob.value, key, val)
// 手动派发更新
ob.dep.notify()
return val
}



















