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