該文章內容節選自團隊的開源項目 InterviewMap。項目目前內容包含了 JS、網絡、瀏覽器相關、性能優化、安全、框架、Git、數據結構、算法等內容,不管是基礎仍是進階,亦或是源碼解讀,你都能在本圖譜中獲得滿意的答案,但願這個面試圖譜可以幫助到你們更好的準備面試。html
在 Vue 的初始化中,會先對 props 和 data 進行初始化vue
Vue.prototype._init = function(options?: Object) {
// ...
// 初始化 props 和 data
initState(vm)
initProvide(vm)
callHook(vm, 'created')
if (vm.$options.el) {
// 掛載組件
vm.$mount(vm.$options.el)
}
}
複製代碼
接下來看下如何初始化 props 和 datareact
export function initState (vm: Component) {
// 初始化 props
if (opts.props) initProps(vm, opts.props)
if (opts.data) {
// 初始化 data
initData(vm)
}
}
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// 緩存 key
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// 非根組件的 props 不須要觀測
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
// 驗證 prop
const value = validateProp(key, propsOptions, propsData, vm)
// 經過 defineProperty 函數實現雙向綁定
defineReactive(props, key, value)
// 可讓 vm._props.x 經過 vm.x 訪問
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// 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)) {
} else if (!isReserved(key)) {
// 可讓 vm._data.x 經過 vm.x 訪問
proxy(vm, `_data`, key)
}
}
// 監聽 data
observe(data, true /* asRootData */)
}
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 若是 value 不是對象或者使 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
}
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 經過 defineProperty 爲對象添加 __ob__ 屬性,而且配置爲不可枚舉
// 這樣作的意義是對象遍歷時不會遍歷到 __ob__ 屬性
def(value, '__ob__', this)
// 判斷類型,不一樣的類型不一樣處理
if (Array.isArray(value)) {
// 判斷數組是否有原型
// 在該處重寫數組的一些方法,由於 Object.defineProperty 函數
// 對於數組的數據變化支持的很差,這部份內容會在下面講到
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
// 遍歷對象,經過 defineProperty 函數實現雙向綁定
walk (obj: Object) {
const keys = Object.keys(obj)
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])
}
}
}
複製代碼
不管是對象仍是數組,須要實現雙向綁定的話最終都會執行這個函數,該函數能夠監聽到 set
和 get
的事件。git
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
// 建立依賴實例,經過閉包的方式讓
// set get 函數使用
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 是對象的話遞歸監聽
let childOb = !shallow && observe(val)
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()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 若是新值是對象的話遞歸監聽
childOb = !shallow && observe(newVal)
// 派發更新
dep.notify()
}
})
}
複製代碼
在 Object.defineProperty
中自定義 get
和 set
函數,並在 get
中進行依賴收集,在 set
中派發更新。接下來咱們先看如何進行依賴收集。github
依賴收集是經過 Dep
來實現的,可是也與 Watcher 息息相關面試
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 () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 同一時間只有一個觀察者使用,賦值觀察者
Dep.target = null
複製代碼
對於 Watcher 來講,分爲兩種 Watcher,分別爲渲染 Watcher 和用戶寫的 Watcher。渲染 Watcher 是在初始化中實例化的。算法
export function mountComponent( vm: Component, el: ?Element, hydrating?: boolean ): Component {
// ...
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {} else {
// 組件渲染,該回調會在初始化和數據變化時調用
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 實例化渲染 Watcher
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
},
true /* isRenderWatcher */
)
return vm
}
複製代碼
接下來看一下 Watcher 的部分實現express
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
get () {
// 該函數用於緩存 Watcher
// 由於在組件含有嵌套組件的狀況下,須要恢復父組件的 Watcher
pushTarget(this)
let value
const vm = this.vm
try {
// 調用回調函數,也就是 updateComponent 函數
// 在這個函數中會對須要雙向綁定的對象求值,從而觸發依賴收集
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)
}
// 恢復 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)) {
// 調用 Dep 中的 addSub 函數
// 將當前 Watcher push 進數組
dep.addSub(this)
}
}
}
}
export function pushTarget (_target: ?Watcher) {
// 設置全局的 target
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
複製代碼
以上就是依賴收集的全過程。核心流程是先對配置中的 props 和 data 中的每個值調用 Obeject.defineProperty()
來攔截 set
和 get
函數,再在渲染 Watcher 中訪問到模板中須要雙向綁定的對象的值觸發依賴收集。數組
改變對象的數據時,會觸發派發更新,調用 Dep
的 notify
函數瀏覽器
notify () {
// 執行 Watcher 的 update
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
update () {
if (this.computed) {
// ...
} else if (this.sync) {
// ...
} else {
// 通常會進入這個條件
queueWatcher(this)
}
}
export function queueWatcher(watcher: Watcher) {
// 得到 id
const id = watcher.id
// 判斷 Watcher 是否 push 過
// 由於存在改變了多個數據,多個數據的 Watch 是同一個
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
// 將全部 Watcher 統一放入 nextTick 調用
// 由於每次派發更新都會引起渲染
nextTick(flushSchedulerQueue)
}
}
}
function flushSchedulerQueue() {
flushing = true
let watcher, id
// 根據 id 排序 watch,確保以下條件
// 1. 組件更新從父到子
// 2. 用戶寫的 Watch 先於渲染 Watch
// 3. 若是在父組件 watch run 的時候有組件銷燬了,這個 Watch 能夠被跳過
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()
// 判斷無限循環
// 好比在 watch 中又從新給對象賦值了,就會出現這個狀況
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' +
(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),
watcher.vm
)
break
}
}
}
// ...
}
複製代碼
以上就是派發更新的全過程。核心流程就是給對象賦值,觸發 set
中的派發更新函數。將全部 Watcher 都放入 nextTick
中進行更新,nextTick
回調中執行用戶 Watch 的回調函數而且渲染組件。
以上已經分析完了 Vue 的響應式原理,接下來講一點 Object.defineProperty
中的缺陷。
若是經過下標方式修改數組數據或者給對象新增屬性並不會觸發組件的從新渲染,由於 Object.defineProperty
不能攔截到這些操做,更精確的來講,對於數組而言,大部分操做都是攔截不到的,只是 Vue 內部經過重寫函數的方式解決了這個問題。
對於第一個問題,Vue 提供了一個 API 解決
export function set (target: Array<any> | Object, key: any, val: any): any {
// 判斷是否爲數組且下標是否有效
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 調用 splice 函數觸發派發更新
// 該函數已被重寫
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 判斷 key 是否已經存在
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
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
}
// 若是對象不是響應式對象,就賦值返回
if (!ob) {
target[key] = val
return val
}
// 進行雙向綁定
defineReactive(ob.value, key, val)
// 手動派發更新
ob.dep.notify()
return val
}
複製代碼
對於數組而言,Vue 內部重寫了如下函數實現派發更新
// 得到數組原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 重寫如下函數
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 緩存原生函數
const original = arrayProto[method]
// 重寫函數
def(arrayMethods, method, function mutator (...args) {
// 先調用原生函數得到結果
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
// 調用如下幾個函數時,監聽新數據
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 手動派發更新
ob.dep.notify()
return result
})
})
複製代碼
最近本人在尋找工做機會,若是有杭州的不錯崗位的話,歡迎聯繫我 zx597813039@gmail.com。
若是你有不清楚的地方或者認爲我有寫錯的地方,歡迎評論區交流。
相關文章