手把手带你理解Vue响应式原理

来源 | https://www.cnblogs.com/chanwahfung/p/13175515.html
前言
Vue 的核心,使用数据劫持实现数据驱动视图。在面试中是经常考查的知识点,也是面试加分项。分析主要成员,了解它们有助于理解流程
将流程拆分,理解其中的作用
结合以上的点,理解整体流程
主要成员
Observe、Dep、Watcher 这三个类是构成完整原理的主要成员。Observe,响应式原理的入口,根据数据类型处理观测逻辑Dep,依赖收集器,属性都会有一个Dep,方便发生变化时能够找到对应的依赖触发更新Watcher,用于执行更新渲染,组件会拥有一个渲染Watcher,我们常说的收集依赖,就是收集Watcher
Observe:我会对数据进行观测
// 源码位置:/src/core/observer/index.jsclass Observe {constructor(data) {this.dep = new Dep()// 1def(data, '__ob__', this)if (Array.isArray(data)) {// 2protoAugment(data, arrayMethods)// 3this.observeArray(data)} else {// 4this.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.jslet id = 0class Dep{constructor() {this.id = ++id // dep 唯一标识this.subs = [] // 存储 Watcher}// 1depend() {Dep.target.addDep(this)}// 2addSub(watcher) {this.subs.push(watcher)}// 3notify() {this.subs.forEach(watcher => watcher.update())}}// 4Dep.target = nullexport 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.jslet id = 0export class Watcher {constructor(vm, exprOrFn, cb, options){this.id = ++id // watcher 唯一标识this.vm = vmthis.cb = cbthis.options = options// 1this.getter = exprOrFnthis.deps = []this.depIds = new Set()this.get()}run() {this.get()}get() {pushTarget(this)this.getter()popTarget(this)}// 2addDep(dep) {// 防止重复添加 depif (!this.depIds.has(dep.id)) {this.depIds.add(dep.id)this.deps.push(dep)dep.addSub(this)}}// 3update() {queueWatcher(this)}}
this.getter存储的是更新视图的函数watcher存储dep,同时dep也存储watcher,进行双向记录触发更新,
queueWatcher是为了进行异步更新,异步更新会调用run方法进行更新页面
响应式原理流程
数据观测
observe 方法来调用 Observe// 源码位置:/src/core/observer/index.jsexport function observe(data) {// 1if (!isObject(data)) {return}let ob;// 2if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observe) {ob = data.__ob__} else {// 3ob = 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) {// 1let childOb = observe(value)// 2const dep = new Dep()Object.defineProperty(obj, key, {get() {if (Dep.target) {// 3dep.depend()if (childOb) {childOb.dep.depend()}}return value},set(newVal) {if (newVal === value) {return}value = newVal// 4childOb = observe(newVal)// 5dep.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// 1let arrayProto = Array.prototype// 2export let arrayMethods = Object.create(arrayProto)let methods = ['push','pop','shift','unshift','reverse','sort','splice']methods.forEach(method => {arrayMethods[method] = function(...args) {// 3let res = arrayProto[method].apply(this, args)let ob = this.__ob__let inserted = ''switch(method){case 'push':case 'unshift':inserted = argsbreak;case 'splice':inserted = args.slice(2)break;}// 4inserted && ob.observeArray(inserted)// 5ob.dep.notify()return res}})
将数组的原型保存起来,因为重写的数组方法里,还是需要调用原生数组方法的
arrayMethods是一个对象,用于保存重写的方法,这里使用Object.create(arrayProto)创建对象是为了使用者在调用非重写方法时,能够继承使用原生的方法调用原生方法,存储返回值,用于设置重写函数的返回值
inserted存储新增的值,若inserted存在,对新值进行观测ob.dep.notify触发视图更新
依赖收集
伪代码流程
// data 数据let data = {name: 'joe'}// 渲染watcherlet watcher = {run() {dep.tagret = watcherdocument.write(data.name)}}// deplet dep = [] // 存储依赖dep.tagret = null // 记录 watcher// 数据劫持let oldValue = data.nameObject.defineProperty(data, 'name', {get(){// 收集依赖dep.push(dep.tagret)return oldValue},set(newVal){oldValue = newValdep.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 = newValchildOb = observe(newVal)dep.notify()return value}})}
defineReactive 函数对数据进行劫持。export class Watcher {constructor(vm, exprOrFn, cb, options){this.getter = exprOrFnthis.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 = argsbreak;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) {// 1let childOb = observe(value)const dep = new Dep()Object.defineProperty(obj, key, {get() {if (Dep.target) {dep.depend()// 2if (childOb) {childOb.dep.depend()}}return value},set(newVal) {if (newVal === value) {return}value = newValchildOb = observe(newVal)dep.notify()return value}})}
observe函数返回值为Observe实例childOb.dep.depend执行,为Observe实例的dep添加依赖
ob.dep 内已经收集到依赖了。整体流程
初始化流程
// 源码位置:/src/core/instance/index.jsimport { 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.jsexport function initMixin (Vue: Class) { Vue.prototype._init = function (options?: Object) {const vm: Component = this// a uidvm._uid = uid++// merge optionsif (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 selfvm._self = vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/props// 初始化数据initState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')if (vm.$options.el) {// 初始化渲染页面 挂载组件vm.$mount(vm.$options.el)}}}
initState 初始化数据,vm.$mount(vm.$options.el) 初始化渲染页面。initState:// 源码位置:/src/core/instance/state.jsexport function initState (vm: Component) {vm._watchers = []const opts = vm.$optionsif (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 instanceconst keys = Object.keys(data)const props = vm.$options.propsconst methods = vm.$options.methodslet i = keys.lengthwhile (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.jsVue.prototype.$mount = function (el?: string | Element,hydrating?: boolean): Component {el = el && inBrowser ? query(el) : undefinedreturn mountComponent(this, el, hydrating)}
// 源码位置:/src/core/instance/lifecycle.jsexport function mountComponent (vm: Component,el: ?Element,hydrating?: boolean): Component {vm.$el = elcallHook(vm, 'beforeMount')let updateComponent/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {updateComponent = () => {const name = vm._nameconst id = vm._uidconst 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,这个就是上面所说的渲染wacthernew 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 hookif (vm.$vnode == null) {vm._isMounted = truecallHook(vm, 'mounted')}return vm}
// 源码位置:/src/core/observer/watcher.jslet uid = 0export default class Watcher {constructor(vm, exprOrFn, cb, options){this.id = ++idthis.vm = vmthis.cb = cbthis.options = options// exprOrFn 就是上面传入的 updateComponentthis.getter = exprOrFnthis.deps = []this.depIds = new Set()this.get()}get() {// 1. pushTarget 将当前 watcher 记录到 Dep.target,Dep.target 是全局唯一的pushTarget(this)let valueconst vm = this.vmtry {// 2. 调用 this.getter 相当于会执行 vm._render 函数,对实例上的属性取值,//由此触发 Object.defineProperty 的 get 方法,在 get 方法内进行依赖收集(dep.depend),这里依赖收集就需要用到 Dep.targetvalue = 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 watchingif (this.deep) {traverse(value)}// 3. popTarget 将 Dep.target 置空popTarget()this.cleanupDeps()}return value}}
更新流程
// 源码位置:/src/core/observer/dep.jslet 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 firstconst 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// ordersubs.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// 用于存储 watcherconst 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 watchersfor (index = 0; index < queue.length; index++) {watcher = queue[index]id = watcher.idhas[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.idif (has[id] == null) {has[id] = true// watcher 加入数组queue.push(watcher)// 异步更新nextTick(flushSchedulerQueue)}}
// 源码位置:/src/core/util/next-tick.jsconst callbacks = []let pending = falsefunction flushCallbacks () {pending = falseconst copies = callbacks.slice(0)callbacks.length = 0// 遍历回调函数执行for (let i = 0; i < copies.length; i++) {copies[i]()}}let timerFuncif (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-lineif (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}}
p.then。最终,会调用 watcher.run 更新页面。写在最后

