分析vue是如何實現數據改變動新視圖的.html
三個月前看了vue源碼來分析如何作到響應式數據的, 文章名字叫vue源碼之響應式數據, 最後分析到, 數據變化後會調用Watcher
的update()
方法. 那麼時隔三月讓咱們繼續看看update()
作了什麼. (這三個月用react-native作了個項目, 也無意總結了, 由於好像太簡單了).vue
本文敘事方式爲樹藤摸瓜, 順着看源碼的邏輯走一遍, 查看的vue的版本爲2.5.2. 我fork了一份源碼用來記錄註釋.node
明確調查方向才能直至目標, 先說一下目標行爲: 數據變化之後執行了什麼方法來更新視圖的. 那麼準備開始以這個方向爲目標從vue源碼的入口開始找答案.react
先來複習一下以前的結論:webpack
update()
方法.那麼這裏就從註冊渲染相關的Watcher開始.git
找到了文件在src/core/instance/lifecycle.js
中.github
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
渲染相關的Watcher是在mountComponent()
這個方法中調用的, 那麼咱們搜一下這個方法是在哪裏調用的. 只有2處, 分別是src/platforms/web/runtime/index.js
和src/platforms/weex/runtime/index.js
, 以web爲例:web
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
原來如此, 是$mount()
方法調用了mountComponent()
, (或者在vue構造時指定el
字段也會自動調用$mount()
方法), 由於web和weex(什麼是weex?以前別的文章介紹過)渲染的標的物不一樣, 因此在發佈的時候應該引入了不一樣的文件最後發不成不一樣的dist(這個問題留給以後來研究vue的整個流程).json
下面是mountComponent方法:react-native
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // 放一份el到本身的屬性裏 if (!vm.$options.render) { // render應該通過處理了, 由於咱們常常都是用template或者vue文件 // 判斷是否存在render函數, 若是沒有就把render函數寫成空VNode來避免紅錯, 並報出黃錯 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) { 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 { warn( 'Failed to mount component: template or render function not defined.', vm ) } } } callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { // 不看這裏的代碼了, 直接看else裏的, 行爲是同樣的 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 = () => { vm._update(vm._render(), hydrating) } } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined // 註冊一個Watcher new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
這段代碼其實只作了3件事:
(哈哈哈)那麼其實核心就是創建Watcher了.
看一下Watcher的參數: vm是this, updateComponent是一個函數, noop是空, null是空, true表明是RenderWatcher.
在Watcher裏看了isRenderWatcher
:
if (isRenderWatcher) { vm._watcher = this }
是的, 只是複製了一份用來在watcher第一次patch的時候判斷一些東西(從註釋裏看的, 我如今還不知道是幹嗎的).
那麼只有一個問題沒解決就是updateComponent
是個什麼東西.
在Watcher的構造函數的第二個參數傳了function, 那麼這個函數就成了watcher的getter. 聰明的你應該已經猜到, 在這個updateComponent
裏必定調用了視圖中全部的數據的getter, 才能在watcher中創建依賴從而讓視圖響應數據的變化.
updateComponent = () => { vm._update(vm._render(), hydrating) }
那麼就去找vm._update()
和vm._render()
.
在src/core/instance/render.js
找到了._render()
方法.
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options // todo: render和_parentVnode的由來 // reset _rendered flag on slots for duplicate slot check if (process.env.NODE_ENV !== 'production') { for (const key in vm.$slots) { // $flow-disable-line vm.$slots[key]._rendered = false } } if (_parentVnode) { vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject } // set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode // render self let vnode try { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { // catch其實不須要看了, 都是作異常處理, _vnode是在vm._update的時候保存的, 也就是上次的狀態或是null(init的時候給的) handleError(e, vm, `render`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { if (vm.$options.renderError) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) } catch (e) { handleError(e, vm, `renderError`) vnode = vm._vnode } } else { vnode = vm._vnode } } else { vnode = vm._vnode } } // return empty vnode in case the render function errored out 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() } // set parent vnode.parent = _parentVnode return vnode } }
這個方法作了:
.parent
屬性裏.因此核心是這句:
vnode = render.call(vm._renderProxy, vm.$createElement)
其中的render()
, vm._renderProxy
, vm.$createElement
都不知道是什麼.
先看vm._renderProxy
: 是initMixin()
的時候設置的, 在生產環境返回vm, 開發環境返回代理, 那麼咱們認爲他是一個能夠debug的vm(就是vm), 細節以後再看.
vm.$createElement
的代碼在vdom文件夾下, 看了下是一個方法, 返回值一個VNode.
render有點複雜, 能不能之後研究, 總之就是把template或者vue單文件和mount目標parse成render函數.
小總結: vm._render()的返回值是VNode, 根據當前vm的render函數
接下來看vm._update()
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this if (vm._isMounted) { callHook(vm, 'beforeUpdate') } // 記錄update以前的狀態 const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = vm vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // 初次加載, 只有_update方法更新vm._vnode, 初始化是null // initial render vm.$el = vm.__patch__( // patch建立新dom vm.$el, vnode, hydrating, false /* removeOnly */, vm.$options._parentElm, vm.$options._refElm ) // no need for the ref nodes after initial patch // this prevents keeping a detached DOM tree in memory (#5851) vm.$options._parentElm = vm.$options._refElm = null } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) // patch更新dom } activeInstance = prevActiveInstance // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. }
咱們關心的部分其實就是__patch()
的部分, __patch()
作了對dom的操做, 在_update()
裏判斷了是不是初次調用, 若是是的話建立新dom, 不是的話傳入新舊node進行比較再操做.
如今render()
方法和__patch__()
方法都不在core
文件夾中被定義, 那麼如今來一塊兒看看咱們最終引用的vue
對象的總體.
以webpack的vue項目爲例, 用的是vue.esm.js
, package.json的main字段不是他, 因而看build命令:
node scripts/build.js
是用rollup把配置中的全部字段都對應地編譯, 配置以下:
const builds = { // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify 'web-runtime-cjs': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.common.js'), format: 'cjs', banner }, // Runtime+compiler CommonJS build (CommonJS) 'web-full-cjs': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.common.js'), format: 'cjs', alias: { he: './entity-decoder' }, banner }, // Runtime only (ES Modules). Used by bundlers that support ES Modules, // e.g. Rollup & Webpack 2 'web-runtime-esm': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.esm.js'), format: 'es', banner }, // Runtime+compiler CommonJS build (ES Modules) 'web-full-esm': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.esm.js'), format: 'es', alias: { he: './entity-decoder' }, banner }, ... // 如下省略, 還有不少... }
咱們找的文件vue.esm.js
的入口文件找到啦, 是web/entry-runtime-with-compiler.js
.
而在web/entry-runtime-with-compiler.js
中, 又從./runtime/index
引入了Vue, 最後才從core/index
中引入Vue.
因此Vue的平臺無關的內容放在core
中, 最後打成dist的時候根據不一樣的發佈平臺(web, weex), 發佈模式(browser, es-module)來給核心Vue對象掛載更多的方法和屬性, 那麼咱們如今來看看web/es-module這條路添加了些什麼~
從runtime/index
開始:
// runtime/index.js 部分代碼 // install platform specific utils Vue.config.mustUseProp = mustUseProp Vue.config.isReservedTag = isReservedTag Vue.config.isReservedAttr = isReservedAttr Vue.config.getTagNamespace = getTagNamespace Vue.config.isUnknownElement = isUnknownElement // install platform runtime directives & components extend(Vue.options.directives, platformDirectives) extend(Vue.options.components, platformComponents) // install platform patch function Vue.prototype.__patch__ = inBrowser ? patch : noop // public mount method Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
掛載了一些常量和平臺專屬directive和component. 咱們關心的__patch__()
方法是在這裏被掛上的, $mount()
方法也是這個時候掛上的, 正是調用了mountComponent()
.
而後看web/entry-runtime-with-compiler.js
:
// web/entry-runtime-with-compiler.js 部分代碼 const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) /* istanbul ignore if */ 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 // resolve template/el and convert to render function if (!options.render) { // 若是沒有render方法就嘗試把別的字段編譯成render方法 let template = options.template if (template) { // 嘗試template字段, 沒有的話就獲取el字段並編譯成template if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } // 把template編譯成render函數 const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, // 檢測瀏覽器的行爲, 是否會把一些東西url-encode shouldDecodeNewlinesForHref, delimiters: options.delimiters, // 默認是雙花括號 '{{' '}}', 用來編譯模板的 comments: options.comments // 默認是false, 若是true就不丟棄註釋 }, this) options.render = render options.staticRenderFns = staticRenderFns /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`vue ${this._name} compile`, 'compile', 'compile end') } } // 若是全部if都沒走到, 那麼就沒有render方法, 異常將在$mount的時候拋出. 這裏沒有作處理 } return mount.call(this, el, hydrating) }
註釋都貼在上面的代碼裏了, 在這個文件裏在$mount()
方法裏插入render()
方法的註冊, 總結爲:
render()
函數, 就用render()
函數.template
屬性編譯成render()
函數.template
屬性, 就用找el
屬性所指的dom, 並把他編譯成template
.template
(原來的template
或是el
編譯成的)編譯出render()
函數.render()
, template
, el
都沒有). 那麼什麼都不作, 這個Vue實例就沒有render()
函數, 但沒有報錯, 由於在mountComponent()
的時候會報錯.core
中, 最後打成dist的時候根據不一樣的發佈平臺(web, weex), 發佈模式(browser, es-module)來給核心Vue對象掛載更多的方法和屬性(代碼在platforms中). render()
和__patch__()
是在platforms裏掛上的.__patch__
和VNode的分析