开始结合Vue源码来整理响应式的原理(此篇代码有部分简写),上篇可看浅谈vue的响应式(上)
VUE响应式原理
了解了Object.defineProperty
和观察者模式
之后就可以开始解释Vue的响应式原理了,VUE响应式原理图如下:
Vue实现响应式分为3步:
1. init 阶段
对数据进行初始化
首先面对的就是vue的初始化,在执行new Vue(options) 的时候,会调用这个initMixin
方法。
export function initMixin (Vue: Class) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++
// options参数的处理
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
// mergeOptions 对 mixin 选项和 new Vue 传入的 options 选项进行合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._self = vm
// vm的生命周期相关变量初始化
initLifecycle(vm)
// vm的事件监听初始化
initEvents(vm)
// vm的编译render初始化
initRender(vm)
// vm的beforeCreate生命钩子的回调
callHook(vm, 'beforeCreate')
// vm在data/props初始化之前要进行绑定
initInjections(vm)
// 初始化数据
initState(vm)
// vm在data/props之后要进行提供
initProvide(vm)
// vm的created生命钩子的回调
callHook(vm, 'created')
if (vm.$options.el) {
// 初始化渲染页面 挂载组件
vm.$mount(vm.$options.el)
}
}
}
通过initState
进行数据初始化,这里的initProps
、initMethods
、initData
、initComputed
、initWatch
,都是针对Vue不同的数据进行初始化。
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)
if (opts.data) {
// 初始化data
initData(vm)
} else {
// 当data为空时observe 函数观测一个空对象:{}
observe(vm._data = {}, true /* asRootData */)
}
// 初始化计算属性
if (opts.computed) initComputed(vm, opts.computed)
// 初始化watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
接下来只关注observe
,上面通过observe(vm._data = {}, true /* asRootData */)
调用函数
// 用observe来进行数据观测
export function observe(data) {
// ../utils/index.js的函数用于判断data是否是对象
if (!isObject(data)) {
return
}
let ob;
// 如果这个数据被观测过,就会添加__ob__属性,并且它的构造函数是Observe
if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observe) {
// 这里表示观察过了
ob = data.__ob__
} else {
// 如果没有被观察过就创建新的Observe实例,传入data
ob = new Observe(data)
}
return ob
}
在observe
函数中只对object
类型的数据进行观测,并且还会判断这个观测对象是否之前被观测过,如果没有就会创建新的observe
实例
class Observe {
constructor(data) {
// 调用./dep的dep类,生成dep实例,用于收集依赖
this.dep = new Dep()
// 为观察的对象添加 __ob__ 标识
// def方法来自../utils/index.js,def(data, key, value),内部使用Object.defineProperty
def(data, '__ob__', this)
// 判断传入的是否是一个数组
if (Array.isArray(data)) {
// 重写数组方法
protoAugment(data, arrayMethods)
// 调用数组观测函数
this.observeArray(data)
} else {
// 观察对象
this.walk(data)
}
}
// 普通数据观测方法
walk(data) {
// 遍历对每个元素添加观测
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key])
})
}
// 数组数据观测方法
observeArray(data) {
// 遍历对每个元素添加观测
data.forEach(item => {
// 这里如果是个对象数组,那就是对每个对象元素添加观测,而不是对数组对象添加观测
// 可以data[0].name = 'taec'更改
// 但是不可以data[0] = {name:'taec'}
observe(item)
})
}
}
在observe
中会使用walk
和observeArray
分别对两种数据类型进行观测,通过defineReactive
方法来设置数据劫持,给每个data中的数据添加getter/setter属性,在get内使用dep.depend()
注册函数,在setter内使用dep.notify()
当数据变化时通知被观察者
// defineReactive主要为数据添加get和set,即数据劫持
function defineReactive(obj, key, value) {
// 考虑到有可能是对象形式,要使用递归来添加劫持
let childOb = observe(value)
// 新建Dep
const dep = new Dep()
//使用Object.defineProperty方法
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend()
// 子元素收集依赖
// 主要应用于数组变动时会调用 ob.dep.notify()
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set(newVal) {
if (newVal === value) {
return
}
value = newVal
// 新值有可能是对象或者数组,childOb收集依赖
childOb = observe(newVal)
// 更新视图
dep.notify()
return value
}
})
}
defineReactive
中new出来的dep
主要就是用于收集依赖的,代码如下
// 设置dep类
class Dep{
constructor() {
// id是唯一的id,
this.id = ++id
// 用于存放watch
this.subs = []
}
depend() {
// watcher 添加当前 dep
// 同时让 dep 添加 watcher
Dep.target.addDep(this)
}
addSub(watcher) {
this.subs.push(watcher)
}
// watcher 更新
notify() {
this.subs.forEach(watcher => watcher.update())
}
removeSub(watcher) {
for (let i = 0, len = this.subs.length; i < len; i++) {
if (this.subs[i] === watcher) {
this.subs.splice(i, 1)
return
}
}
}
}
// target是一个watch实例
Dep.target = null
// 存储 watcher 的栈
let stack = []
// 添加watch
export function pushTarget(watcher) {
stack.push(watcher)
Dep.target = watcher
}
// 删除最后一个watch
export function popTarget(){
stack.pop()
Dep.target = stack[stack.length - 1]
}
当defineReactive
执行完毕之后,所有的数据都被劫持过了,此时init阶段完成。
init阶段:initState
中调用initData
,并且在initData
中使用observe
对数据进行观测,observe
会区分对象和数组,分别使用walk
和observeArray
,通过defineReactive
来完成数据劫持以及使用dep
完成依赖收集。
2. mount 阶段
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 调用beforeMount钩子函数
callHook(vm, 'beforeMount')
// updateComponent作为Watcher对象的getter函数,用来依赖收集
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
// 数据改变时 会调用此方法
updateComponent = () => {
// vm._render() 返回 vnode,这里面会就对 data 数据进行取值
// vm._update 将 vnode 转为真实dom,渲染到页面上
vm._update(vm._render(), hydrating)
}
}
// 创建一个new ,Watcher的getter为updateComponent函数
// 用于触发所有渲染所需要用到的数据的getter,进行依赖收集,该Watcher实例会存在所有渲染所需数据的闭包Dep中
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
// 挂载完成,调用mounted钩子
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
在mountComponent
函数中会创建一个Watcher类,Watcher对应一个vue component,用于连接Vue组件与Dep,当new Watcher的时候,传入VM,updateComponent作为watcher的getter,
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
当updateComponent
执行时,会调用当前组件的render()
开始产生虚拟Dom,那么就要用到当前组件的data,就会用到init阶段给data添加的getter属性的方法,同时调用dep.depend()
注册观察对象。
3. 更新阶段
当数据开始更新,首先会触发observe中的setter。会开始执行dep.notify()
,来执行更新。
set(newVal) {
if (newVal === value) {
return
}
value = newVal
// 新值有可能是对象或者数组,childOb收集依赖
childOb = observe(newVal)
// 更新视图
dep.notify()
return value
}
而dep.notify()
会开始循环调用每个watcher的watcher.update()
notify() {
this.subs.forEach(watcher => watcher.update())
}
// watcher.update()
update () {
if (this.lazy) {
// 计算属性更新
this.dirty = true
} else if (this.sync) {
// 同步更新
this.run()
} else {
// 一般的数据都会进行异步更新
queueWatcher(this)
}
}
- 同步更新,dep通知某个watcher实例需要更新的时候,这个watcher实例直接调用callback方法进行更新。
- 异步更新,针对所有的DOM更新,watcher会调用名为queueWatcher的方法将自己放进更新队列,更新队列是一个由watcher组成的数组,Vue会在更新循环中遍历这个数组并依次调用其中每个watcher的callback。
这样我们看官网的图就能理解了:
1是init和mount阶段,2是更新阶段,个人理解如有错误欢迎大佬指正
参考文章:
- 最简化 VUE的响应式原理
- 手摸手带你理解Vue响应式原理
- 【探究Vue原理】watcher的异步更新