手把手带你理解Vue响应式原理
来源 | https://www.cnblogs.com/chanwahfung/p/13175515.html
前言
Vue
的核心,使用数据劫持实现数据驱动视图。在面试中是经常考查的知识点,也是面试加分项。分析主要成员,了解它们有助于理解流程
将流程拆分,理解其中的作用
结合以上的点,理解整体流程
主要成员
Observe
、Dep
、Watcher
这三个类是构成完整原理的主要成员。Observe
,响应式原理的入口,根据数据类型处理观测逻辑Dep
,依赖收集器,属性都会有一个Dep
,方便发生变化时能够找到对应的依赖触发更新Watcher
,用于执行更新渲染,组件会拥有一个渲染Watcher
,我们常说的收集依赖,就是收集Watcher
Observe:我会对数据进行观测
// 源码位置:/src/core/observer/index.js
class Observe {
constructor(data) {
this.dep = new Dep()
// 1
def(data, '__ob__', this)
if (Array.isArray(data)) {
// 2
protoAugment(data, arrayMethods)
// 3
this.observeArray(data)
} else {
// 4
this.walk(data)
}
}
walk(data) {
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key])
})
}
observeArray(data) {
data.forEach(item => {
observe(item)
})
}
}
为观测的属性添加
__ob__
属性,它的值等于this
,即当前Observe
的实例为数组添加重写的数组方法,比如:
push
、unshift
、splice
等方法,重写目的是在调用这些方法时,进行更新渲染观测数组内的数据,
observe
内部会调用new Observe
,形成递归观测观测对象数据,
defineReactive
为数据定义get
和set
,即数据劫持
Dep:我会为数据收集依赖
// 源码位置:/src/core/observer/dep.js
let id = 0
class Dep{
constructor() {
this.id = ++id // dep 唯一标识
this.subs = [] // 存储 Watcher
}
// 1
depend() {
Dep.target.addDep(this)
}
// 2
addSub(watcher) {
this.subs.push(watcher)
}
// 3
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
// 4
Dep.target = null
export function pushTarget(watcher) {
Dep.target = watcher
}
export function popTarget(){
Dep.target = null
}
export default Dep
数据收集依赖的主要方法,
Dep.target
是一个watcher
实例添加
watcher
到数组中,也就是添加依赖属性在变化时会调用
notify
方法,通知每一个依赖进行更新Dep.target
用来记录watcher
实例,是全局唯一的,主要作用是为了在收集依赖的过程中找到相应的watcher
pushTarget
和 popTarget
这两个方法显而易见是用来设置 Dep.target
的。Dep.target
也是一个关键点,这个概念可能初次查看源码会有些难以理解,在后面的流程中,会详细讲解它的作用,需要注意这部分的内容。Watcher:我会触发视图更新
// 源码位置:/src/core/observer/watcher.js
let id = 0
export class Watcher {
constructor(vm, exprOrFn, cb, options){
this.id = ++id // watcher 唯一标识
this.vm = vm
this.cb = cb
this.options = options
// 1
this.getter = exprOrFn
this.deps = []
this.depIds = new Set()
this.get()
}
run() {
this.get()
}
get() {
pushTarget(this)
this.getter()
popTarget(this)
}
// 2
addDep(dep) {
// 防止重复添加 dep
if (!this.depIds.has(dep.id)) {
this.depIds.add(dep.id)
this.deps.push(dep)
dep.addSub(this)
}
}
// 3
update() {
queueWatcher(this)
}
}
this.getter
存储的是更新视图的函数watcher
存储dep
,同时dep
也存储watcher
,进行双向记录触发更新,
queueWatcher
是为了进行异步更新,异步更新会调用run
方法进行更新页面
响应式原理流程
数据观测
observe
方法来调用 Observe
// 源码位置:/src/core/observer/index.js
export function observe(data) {
// 1
if (!isObject(data)) {
return
}
let ob;
// 2
if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observe) {
ob = data.__ob__
} else {
// 3
ob = new Observe(data)
}
return ob
}
observe
拿到的 data
就是我们在 data
函数内返回的对象。observe
函数只对object
类型数据进行观测观测过的数据都会被添加上
__ob__
属性,通过判断该属性是否存在,防止重复观测创建
Observe
实例,开始处理观测逻辑
对象观测
Observe
内部,由于初始化的数据是一个对象,所以会调用 walk
方法:walk(data) {
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key])
})
}
defineReactive
方法内部使用 Object.defineProperty
对数据进行劫持,是实现响应式原理最核心的地方。function defineReactive(obj, key, value) {
// 1
let childOb = observe(value)
// 2
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
// 3
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set(newVal) {
if (newVal === value) {
return
}
value = newVal
// 4
childOb = observe(newVal)
// 5
dep.notify()
return value
}
})
}
由于值可能是对象类型,这里需要调用
observe
进行递归观测这里的
dep
就是上面讲到的每一个属性都会有一个dep
,它是作为一个闭包的存在,负责收集依赖和通知更新在初始化时,
Dep.target
是组件的渲染watcher
,这里dep.depend
收集的依赖就是这个watcher
,childOb.dep.depend
主要是为数组收集依赖设置的新值可能是对象类型,需要对新值进行观测
值发生改变,
dep.notify
通知watcher
更新,这是我们改变数据后能够实时更新页面的触发点
Object.defineProperty
对属性定义后,属性的获取触发 get
回调,属性的设置触发 set
回调,实现响应式更新。Vue3.0
要使用 Proxy
代替 Object.defineProperty
了。Object.defineProperty
只能对单个属性进行定义,如果属性是对象类型,还需要递归去观测,会很消耗性能。而 Proxy
是代理整个对象,只要属性发生变化就会触发回调。数组观测
observeArray
方法:observeArray(data) {
data.forEach(item => {
observe(item)
})
}
observe
对数组内的对象类型进行观测,并没有对数组的每一项进行 Object.defineProperty
的定义,也就是说数组内的项是没有 dep
的。this.$set
来修改触发更新。那么问题来了,为什么 Vue
要这样设计?export default {
data() {
return {
list: [
{id: 1, name: 'Jack'},
{id: 2, name: 'Mike'}
]
}
},
cretaed() {
// 如果想要修改 name 的值,一般是这样使用
this.list[0].name = 'JOJO'
// 而不是以下这样
// this.list[0] = {id:1, name: 'JOJO'}
// 当然你可以这样更新
// this.$set(this.list, '0', {id:1, name: 'JOJO'})
}
}
数组方法重写
Vue
内部重写了数组的方法,调用这些方法时,数组会更新检测,触发视图更新。这些方法包括:push()
pop()
shift()
unshift()
splice()
sort()
reverse()
Observe
的类中,当观测的数据类型为数组时,会调用 protoAugment
方法。if (Array.isArray(data)) {
protoAugment(data, arrayMethods)
// 观察数组
this.observeArray(data)
} else {
// 观察对象
this.walk(data)
}
arrayMethods
,当调用改变数组的方法时,优先使用重写后的方法。function protoAugment(data, arrayMethods) {
data.__proto__ = arrayMethods
}
arrayMethods
是如何实现的:// 源码位置:/src/core/observer/array.js
// 1
let arrayProto = Array.prototype
// 2
export let arrayMethods = Object.create(arrayProto)
let methods = [
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
]
methods.forEach(method => {
arrayMethods[method] = function(...args) {
// 3
let res = arrayProto[method].apply(this, args)
let ob = this.__ob__
let inserted = ''
switch(method){
case 'push':
case 'unshift':
inserted = args
break;
case 'splice':
inserted = args.slice(2)
break;
}
// 4
inserted && ob.observeArray(inserted)
// 5
ob.dep.notify()
return res
}
})
将数组的原型保存起来,因为重写的数组方法里,还是需要调用原生数组方法的
arrayMethods
是一个对象,用于保存重写的方法,这里使用Object.create(arrayProto)
创建对象是为了使用者在调用非重写方法时,能够继承使用原生的方法调用原生方法,存储返回值,用于设置重写函数的返回值
inserted
存储新增的值,若inserted
存在,对新值进行观测ob.dep.notify
触发视图更新
依赖收集
伪代码流程
// data 数据
let data = {
name: 'joe'
}
// 渲染watcher
let watcher = {
run() {
dep.tagret = watcher
document.write(data.name)
}
}
// dep
let dep = [] // 存储依赖
dep.tagret = null // 记录 watcher
// 数据劫持
let oldValue = data.name
Object.defineProperty(data, 'name', {
get(){
// 收集依赖
dep.push(dep.tagret)
return oldValue
},
set(newVal){
oldValue = newVal
dep.forEach(watcher => {
watcher.run()
})
}
})
首先会对
name
属性定义get
和set
然后初始化会执行一次
watcher.run
渲染页面这时候获取
data.name
,触发get
函数收集依赖。
data.name
,触发 set
函数,调用 run
更新视图。真正流程
function defineReactive(obj, key, value) {
let childOb = observe(value)
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend() // 收集依赖
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set(newVal) {
if (newVal === value) {
return
}
value = newVal
childOb = observe(newVal)
dep.notify()
return value
}
})
}
defineReactive
函数对数据进行劫持。export class Watcher {
constructor(vm, exprOrFn, cb, options){
this.getter = exprOrFn
this.get()
}
get() {
pushTarget(this)
this.getter()
popTarget(this)
}
}
watcher
挂载到 Dep.target
,this.getter
开始渲染页面。渲染页面需要对数据取值,触发 get
回调,dep.depend
收集依赖。class Dep{
constructor() {
this.id = id++
this.subs = []
}
depend() {
Dep.target.addDep(this)
}
}
Dep.target
为 watcher
,调用 addDep
方法,并传入 dep
实例。export class Watcher {
constructor(vm, exprOrFn, cb, options){
this.deps = []
this.depIds = new Set()
}
addDep(dep) {
if (!this.depIds.has(dep.id)) {
this.depIds.add(dep.id)
this.deps.push(dep)
dep.addSub(this)
}
}
}
addDep
中添加完 dep
后,调用 dep.addSub
并传入当前 watcher
实例。class Dep{
constructor() {
this.id = id++
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
}
watcher
收集起来,至此依赖收集流程完毕。watcher
,即组件的渲染 watcher
。数组的依赖收集
methods.forEach(method => {
arrayMethods[method] = function(...args) {
let res = arrayProto[method].apply(this, args)
let ob = this.__ob__
let inserted = ''
switch(method){
case 'push':
case 'unshift':
inserted = args
break;
case 'splice':
inserted = args.slice(2)
break;
}
// 对新增的值观测
inserted && ob.observeArray(inserted)
// 更新视图
ob.dep.notify()
return res
}
})
ob.dep.notify
更新视图,__ob__
是我们在 Observe
为观测数据定义的标识,值为 Observe
实例。那么 ob.dep
的依赖是在哪里收集的?function defineReactive(obj, key, value) {
// 1
let childOb = observe(value)
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend()
// 2
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set(newVal) {
if (newVal === value) {
return
}
value = newVal
childOb = observe(newVal)
dep.notify()
return value
}
})
}
observe
函数返回值为Observe
实例childOb.dep.depend
执行,为Observe
实例的dep
添加依赖
ob.dep
内已经收集到依赖了。整体流程
初始化流程
// 源码位置:/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
// 源码位置:/src/core/instance/init.js
export function initMixin (Vue: Class
) { Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// mergeOptions 对 mixin 选项和传入的 options 选项进行合并
// 这里的 $options 可以理解为 new Vue 时传入的对象
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// 初始化数据
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
// 初始化渲染页面 挂载组件
vm.$mount(vm.$options.el)
}
}
}
initState
初始化数据,vm.$mount(vm.$options.el)
初始化渲染页面。initState
:// 源码位置:/src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// data 初始化
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
function initData (vm: Component) {
let data = vm.$options.data
// data 为函数时,执行 data 函数,取出返回值
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
// 这里就开始走观测数据的逻辑了
observe(data, true /* asRootData */)
}
observe
内部流程在上面已经讲过,这里再简单过一遍:new Observe
观测数据defineReactive
对数据进行劫持
initState
逻辑执行完毕,回到开头,接下来执行 vm.$mount(vm.$options.el)
渲染页面:// 源码位置:/src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// 源码位置:/src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
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)
}
}
// 执行 Watcher,这个就是上面所说的渲染wacther
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
// 源码位置:/src/core/observer/watcher.js
let uid = 0
export default class Watcher {
constructor(vm, exprOrFn, cb, options){
this.id = ++id
this.vm = vm
this.cb = cb
this.options = options
// exprOrFn 就是上面传入的 updateComponent
this.getter = exprOrFn
this.deps = []
this.depIds = new Set()
this.get()
}
get() {
// 1. pushTarget 将当前 watcher 记录到 Dep.target,Dep.target 是全局唯一的
pushTarget(this)
let value
const vm = this.vm
try {
// 2. 调用 this.getter 相当于会执行 vm._render 函数,对实例上的属性取值,
//由此触发 Object.defineProperty 的 get 方法,在 get 方法内进行依赖收集(dep.depend),这里依赖收集就需要用到 Dep.target
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
// 3. popTarget 将 Dep.target 置空
popTarget()
this.cleanupDeps()
}
return value
}
}
更新流程
// 源码位置:/src/core/observer/dep.js
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array
;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
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++) {
// 执行 watcher 的 update 方法
subs[i].update()
}
}
}
// 源码位置:/src/core/observer/watcher.js
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) { // 计算属性更新
this.dirty = true
} else if (this.sync) { // 同步更新
this.run()
} else {
// 一般的数据都会进行异步更新
queueWatcher(this)
}
}
// 源码位置:/src/core/observer/scheduler.js
// 用于存储 watcher
const queue: Array
= [] // 用于 watcher 去重
let has: { [key: number]: ?true } = {}
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
let watcher, id
// 对 watcher 排序
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
// run方法更新视图
watcher.run()
}
}
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
// watcher 加入数组
queue.push(watcher)
// 异步更新
nextTick(flushSchedulerQueue)
}
}
// 源码位置:/src/core/util/next-tick.js
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
// 遍历回调函数执行
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将回调函数加入数组
callbacks.push(() => {
if (cb) {
cb.call(ctx)
}
})
if (!pending) {
pending = true
// 遍历回调函数执行
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
p.then
。最终,会调用 watcher.run
更新页面。