手把手帶你理解Vue響應式原理

來源 |?https://www.cnblogs.com/chanwahfung/p/13175515.html
前言
Vue?的核心,使用數據劫持實現數據驅動視圖。在面試中是經??疾榈闹R點,也是面試加分項。分析主要成員,了解它們有助于理解流程
將流程拆分,理解其中的作用
結合以上的點,理解整體流程
主要成員
Observe、Dep、Watcher?這三個類是構成完整原理的主要成員。Observe,響應式原理的入口,根據數據類型處理觀測邏輯Dep,依賴收集器,屬性都會有一個Dep,方便發(fā)生變化時能夠找到對應的依賴觸發(fā)更新Watcher,用于執(zhí)行更新渲染,組件會擁有一個渲染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:我會觸發(fā)視圖更新
// 源碼位置:/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,進行雙向記錄觸發(fā)更新,
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__?屬性,通過判斷該屬性是否存在,防止重復觀測創(chuàng)建?
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?主要是為數組收集依賴設置的新值可能是對象類型,需要對新值進行觀測
值發(fā)生改變,
dep.notify?通知?watcher?更新,這是我們改變數據后能夠實時更新頁面的觸發(fā)點
Object.defineProperty?對屬性定義后,屬性的獲取觸發(fā)?get?回調,屬性的設置觸發(fā)?set?回調,實現響應式更新。Vue3.0?要使用?Proxy?代替?Object.defineProperty?了。Object.defineProperty?只能對單個屬性進行定義,如果屬性是對象類型,還需要遞歸去觀測,會很消耗性能。而?Proxy?是代理整個對象,只要屬性發(fā)生變化就會觸發(fā)回調。數組觀測
observeArray?方法:observeArray(data) {data.forEach(item => {observe(item)})}
observe?對數組內的對象類型進行觀測,并沒有對數組的每一項進行?Object.defineProperty?的定義,也就是說數組內的項是沒有?dep?的。this.$set?來修改觸發(fā)更新。那么問題來了,為什么?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?內部重寫了數組的方法,調用這些方法時,數組會更新檢測,觸發(fā)視圖更新。這些方法包括:push()
pop()
shift()
unshift()
splice()
sort()
reverse()
Observe?的類中,當觀測的數據類型為數組時,會調用?protoAugment?方法。if (Array.isArray(data)) {protoAugment(data, arrayMethods)// 觀察數組this.observeArray(data)} else {// 觀察對象this.walk(data)}
arrayMethods?,當調用改變數組的方法時,優(yōu)先使用重寫后的方法。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)?創(chuàng)建對象是為了使用者在調用非重寫方法時,能夠繼承使用原生的方法調用原生方法,存儲返回值,用于設置重寫函數的返回值
inserted?存儲新增的值,若?inserted?存在,對新值進行觀測ob.dep.notify?觸發(fā)視圖更新
依賴收集
偽代碼流程
// 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然后初始化會執(zhí)行一次?
watcher.run?渲染頁面這時候獲取?
data.name,觸發(fā)?get?函數收集依賴。
data.name,觸發(fā)?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?開始渲染頁面。渲染頁面需要對數據取值,觸發(fā)?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?執(zhí)行,為?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 為函數時,執(zhí)行 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?邏輯執(zhí)行完畢,回到開頭,接下來執(zhí)行?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)}}// 執(zhí)行 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 就是上面?zhèn)魅氲?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 相當于會執(zhí)行 vm._render 函數,對實例上的屬性取值,//由此觸發(fā) 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++) {// 執(zhí)行 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// 遍歷回調函數執(zhí)行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// 遍歷回調函數執(zhí)行timerFunc()}// $flow-disable-lineif (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}}
p.then。最終,會調用?watcher.run?更新頁面。寫在最后

