面試的時候,面試官常常會問 Vue 雙向綁定的原理是什麼?
我猜大部分人會跟我同樣,不假思索的回答利用 Object.defineProperty
實現的。html
其實這個回答很籠統,並且也沒回答完整?Vue 中 Object.defineProperty
只是對數據作了劫持,具體的如何渲染到頁面上,並無考慮到。接下來從初始化開始,看看 Vue
都作了什麼事情。前端
在讀源碼前,須要瞭解 Object.defineProperty
的使用,以及 Vue Dep
的用法。這裏就簡單帶過,各位大佬能夠直接跳過,進行源碼分析。vue
當使用 Object.defineProperty
對對象的屬性進行攔截時,調用該對象的屬性,則會調用 get
函數,屬性值則是 get
函數的返回值。當修改屬性值時,則會調用 set
函數。node
固然也能夠經過 Object.defineProperty
給對象添加屬性值,Vue 中就是經過這個方法將 data
、computed
等屬性添加到 vm 上。react
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // 用於依賴收集,Dep 中講到 if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { val = newVal // val 發生變化時,發出通知,Dep 中講到 dep.notify() } }) 複製代碼
這裏不講什麼設計模式了,直接看代碼。web
let uid = 0 export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { // 添加 Watcher this.subs.push(sub) } removeSub (sub: Watcher) { // 從列表中移除某個 Watcher remove(this.subs, sub) } depend () { // 當 target 存在時,也就是目標 Watcher 存在的時候, // 就能夠爲這個目標 Watcher 收集依賴 // Watcher 的 addDep 方法在下文中 if (Dep.target) { Dep.target.addDep(this) } } notify () { // 對 Watcher 進行排序 const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { subs.sort((a, b) => a.id - b.id) } // 當該依賴發生變化時, 調用添加到列表中的 Watcher 的 update 方法進行更新 for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } // target 爲某個 Watcher 實例,一次只能爲一個 Watcher 收集依賴 Dep.target = null // 經過堆棧存放 Watcher 實例, // 當某個 Watcher 的實例未收集完,又有新的 Watcher 實例須要收集依賴, // 那麼舊的 Watcher 就先存放到 targetStack, // 等待新的 Watcher 收集完後再爲舊的 Watcher 收集 // 配合下面的 pushTarget 和 popTarget 實現 const targetStack = [] export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target } export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] } 複製代碼
當某個 Watcher 須要依賴某個 dep 時,那麼調用 dep.addSub(Watcher)
便可,當 dep 發生變化時,調用 dep.notify()
就能夠觸發 Watcher 的 update 方法。接下來看看 Vue 中 Watcher 的實現。面試
class Watcher { // 不少屬性,這裏省略 ... // 構造函數 constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { ... } get () { // 當執行 Watcher 的 get 函數時,會將當前的 Watcher 做爲 Dep 的 target pushTarget(this) let value const vm = this.vm try { // 在執行 getter 時,當遇到響應式數據,會觸發上面講到的 Object.defineProperty 中的 get 函數 // Vue 就是在 Object.defineProperty 的 get 中調用 dep.depend() 進行依賴收集。 value = this.getter.call(vm, vm) } catch (e) { ... } finally { ... // 當前 Watcher 的依賴收集完後,調用 popTarget 更換 Watcher popTarget() this.cleanupDeps() } return value } // dep.depend() 收集依賴時,會通過 Watcher 的 addDep 方法 // addDep 作了判斷,避免重複收集,而後調用 dep.addSub 將該 Watcher 添加到 dep 的 subs 中 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(this) } } } } 複製代碼
經過 Object.defineProperty
中的 get
,Dep
的 depend
以及 Watcher
的 addDep
這三個函數的配合,完成了依賴的收集,就是將 Watcher
添加到 dep
的 subs
列表中。設計模式
當依賴發生變化時,就會調用 Object.defineProperty
中的 set
,在 set
中調用 dep
的 notify
,使得 subs
中的每一個 Watcher
都執行 update
函數。數組
Watcher
中的 update
最終會從新調用 get
函數,從新求值並從新收集依賴。瀏覽器
先看看 new Vue
都作了什麼?
// vue/src/core/instance/index.js function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { // 只能使用 new Vue 調用該方法,不然輸入警告 warn('Vue is a constructor and should be called with the `new` keyword') } // 開始初始化 this._init(options) } 複製代碼
_init
方法經過原型掛載在 Vue 上
// vue/src/core/instance/init.js export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag // 初始化前打點,用於記錄 Vue 實例初始化所消耗的時間 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // 合併參數到 $options if (options && options._isComponent) { initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } if (process.env.NODE_ENV !== 'production') { // 非生產環境以及支持 Proxy 的瀏覽器中,對 vm 的屬性進行劫持,並將代理後的 vm 賦值給 _renderProxy // 當調用 vm 不存在的屬性時,進行錯誤提示。 // 在不支持 Proxy 的瀏覽器中,_renderProxy = vm; 爲了簡單理解,就當作等同於 vm // 代碼在 src/core/instance/proxy.js initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm // 初始化聲明周期函數 initLifecycle(vm) // 初始化事件 initEvents(vm) // 初始化 render 函數 initRender(vm) // 觸發 beforeCreate 鉤子 callHook(vm, 'beforeCreate') // 初始化 inject initInjections(vm) // resolve injections before data/props // 初始化 data/props 等 // 經過 Object.defineProperty 對數據進行劫持 initState(vm) // 初始化 provide initProvide(vm) // resolve provide after data/props // 數據處理完後,觸發 created 鉤子 callHook(vm, 'created') // 從 new Vue 到 created 所消耗的時間 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } // 若是 options 有 el 參數則進行 mount if (vm.$options.el) { vm.$mount(vm.$options.el) } } } 複製代碼
接下來進入 $mount
,由於用的是完整版的 Vue,直接看 vue/src/platforms/web/entry-runtime-with-compiler.js
這個文件。
// vue/src/platforms/web/entry-runtime-with-compiler.js // 首先將 runtime 中的 $mount 方法賦值給 mount 進行保存 const mount = Vue.prototype.$mount // 重寫 $mount,對 template 編譯爲 render 函數後再調用 runtime 的 $mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) // 掛載元素不容許爲 body 或 html if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this } const options = this.$options if (!options.render) { let template = options.template // render 函數不存在時,將 template 轉化爲 render 函數 // 具體就不展開了 ... if (template) { ... } else if (el) { // template 不存在,則將 el 轉成 template // 從這裏能夠看出 Vue 支持 render、template、el 進行渲染 template = getOuterHTML(el) } if (template) { const { render, staticRenderFns } = compileToFunctions(template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns } } // 調用 runtime 中 $mount return mount.call(this, el, hydrating) } 複製代碼
查看 runtime 中的 $mount
// vue/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) } 複製代碼
mountComponent
定義在 vue/src/core/instance/lifecycle.js
中
// vue/src/core/instance/lifecycle.js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el if (!vm.$options.render) { // 未定義 render 函數時,將 render 賦值爲 createEmptyVNode 函數 vm.$options.render = createEmptyVNode if (process.env.NODE_ENV !== 'production') { /* istanbul ignore if */ if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || vm.$options.el || el) { // 用了 Vue 的 runtime 版本,而沒有 render 函數時,報錯處理 warn( 'You are using the runtime-only build of Vue where the template ' + 'compiler is not available. Either pre-compile the templates into ' + 'render functions, or use the compiler-included build.', vm ) } else { // template 和 render 都未定義時,報錯處理 warn( 'Failed to mount component: template or render function not defined.', vm ) } } } // 調用 beforeMount 鉤子 callHook(vm, 'beforeMount') // 定義 updateComponent 函數 let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { // 須要作監控性能時,在 updateComponent 內加入打點的操做 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 = () => { // updateComponent 主要調用 _update 進行瀏覽器渲染 // _render 返回 VNode // 先繼續往下看,等會再回來看這兩個函數 vm._update(vm._render(), hydrating) } } // new 一個渲染 Watcher new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // 掛載完成,觸發 mounted if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm } 複製代碼
先繼續往下看,看看 new Watcher
作了什麼,再回過頭看 updateComponent
中的 _update
和 _render
。
Watcher
的構造函數以下
// vue/src/core/observer/watcher.js constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers ... // expOrFn 爲上文的 updateComponent 函數,賦值給 getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop ... } } // lazy 爲 false,調用 get 方法 this.value = this.lazy ? undefined : this.get() } // 執行 getter 函數,getter 函數爲 updateComponent,並收集依賴 get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { ... } finally { if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value } 複製代碼
在 new Watcher
後會調用 updateComponent
函數,上文中 updateComponent
內執行了 vm._update
,_update
執行前會經過 _render
得到 vnode,接下里看看 _update
作了什麼。_update
定義在 vue/src/core/instance/lifecycle.js
中
// vue/src/core/instance/lifecycle.js Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevVnode = vm._vnode vm._vnode = vnode ... if (!prevVnode) { // 初始渲染 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // 更新 vnode vm.$el = vm.__patch__(prevVnode, vnode) } ... } 複製代碼
接下來到了 __patch__
函數進行頁面渲染。
// vue/src/platforms/web/runtime/index.js import { patch } from './patch' Vue.prototype.__patch__ = inBrowser ? patch : noop 複製代碼
// vue/src/platforms/web/runtime/patch.js import { createPatchFunction } from 'core/vdom/patch' export const patch: Function = createPatchFunction({ nodeOps, modules }) 複製代碼
createPatchFunction
提供了不少操做 virtual dom 的方法,最終會返回一個 path
函數。
export function createPatchFunction (backend) { ... // oldVnode 表明舊的節點,vnode 表明新的節點 return function patch (oldVnode, vnode, hydrating, removeOnly) { // vnode 爲 undefined, oldVnode 不爲 undefined 則須要執行 destroy if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // oldVnode 不存在,表示初始渲染,則根據 vnode 建立元素 isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // oldVnode 與 vnode 爲相同節點,調用 patchVnode 更新子節點 patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { if (isRealElement) { // 服務端渲染的處理 ... } // 其餘操做 ... } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) // 最終渲染到頁面上 return vnode.elm } } 複製代碼
當渲染 Watcher 的依賴的數據發生變化時,會觸發 Object.defineProperty
中的 set
函數。
從而調用 dep.notify()
通知該 Watcher 進行 update
操做。最終達到數據改變時,自動更新頁面。 Watcher
的 update
函數就再也不展開了,有興趣的小夥伴能夠自行查看。
最後再回過頭看看前面遺留的 _render
函數。
updateComponent = () => { vm._update(vm._render(), hydrating) } 複製代碼
以前說了 _render
函數會返回 vnode
,看看具體作了什麼吧。
// vue/src/core/instance/render.js Vue.prototype._render = function (): VNode { const vm: Component = this // 從 $options 取出 render 函數以及 _parentVnode // 這裏的 render 函數能夠是 template 或者 el 編譯的 const { render, _parentVnode } = vm.$options if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) } vm.$vnode = _parentVnode let vnode try { currentRenderingInstance = vm // 最終會執行 $options 中的 render 函數 // _renderProxy 能夠看作 vm // 將 vm.$createElement 函數傳遞給 render,也就是常常看到的 h 函數 // 最終生成 vnode vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { // 異常處理 ... } finally { currentRenderingInstance = null } // 若是返回的數組只包含一個節點,則取第一個值 if (Array.isArray(vnode) && vnode.length === 1) { vnode = vnode[0] } // vnode 若是不是 VNode 實例,報錯並返回空的 vnode if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // 設置父節點 vnode.parent = _parentVnode // 最終返回 vnode return vnode } 複製代碼
接下來就是看 vm.$createElement
也就是 render
函數中的 h
// vue/src/core/instance/render.js import { createElement } from '../vdom/create-element' export function initRender (vm: Component) { ... vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) ... } 複製代碼
// vue/src/core/vdom/create-element.js export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> { // data 是數組或簡單數據類型,表明 data 沒傳,將參數值賦值給正確的變量 if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } // 將正確的參數傳遞給 _createElement return _createElement(context, tag, data, children, normalizationType) } export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { if (isDef(data) && isDef((data: any).__ob__)) { // render 函數中的 data 不能爲響應式數據 process.env.NODE_ENV !== 'production' && warn( `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + 'Always create fresh vnode data objects in each render!', context ) // 返回空的 vnode 節點 return createEmptyVNode() } // 用 is 指定標籤 if (isDef(data) && isDef(data.is)) { tag = data.is } if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // key 值不是簡單數據類型時,警告提示 if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.key) && !isPrimitive(data.key) ) { ... } if (Array.isArray(children) && typeof children[0] === 'function' ) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } // 處理子節點 if (normalizationType === ALWAYS_NORMALIZE) { // VNode 數組 children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } // 生成 vnode let vnode, ns if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { ... vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { vnode = createComponent(Ctor, data, context, children, tag) } else { vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { vnode = createComponent(tag, data, context, children) } // 返回 vnode if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() } } 複製代碼
代碼看起來不少,其實主要流程能夠分爲如下 4 點:
一、 new Vue
初始化數據等
二、$mount
將 render、template 或 el 轉爲 render 函數
三、生成一個渲染 Watcher 收集依賴,並將執行 render 函數生成 vnode 傳遞給 patch 函數執行,渲染頁面。
四、當渲染 Watcher 依賴發生變化時,執行 Watcher 的 getter 函數,從新依賴收集。而且從新執行 render 函數生成 vnode 傳遞給 patch 函數進行頁面的更新。
以上內容均是我的理解,若是有講的不對的地方,還請各位大佬指點。
若是以爲內容還不錯的話,但願小夥伴能夠幫忙點贊轉發,給更多的小夥伴看到,感謝感謝!
若是你喜歡個人文章,還能夠關注個人公衆號【前端develop】