vue數(shù)據(jù)控制視圖源碼解析
分析vue是如何實(shí)現(xiàn)數(shù)據(jù)改變更新視圖的.
前記
三個(gè)月前看了vue源碼來(lái)分析如何做到響應(yīng)式數(shù)據(jù)的, 文章名字叫vue源碼之響應(yīng)式數(shù)據(jù), 最后分析到, 數(shù)據(jù)變化后會(huì)調(diào)用Watcher的update()方法. 那么時(shí)隔三月讓我們繼續(xù)看看update()做了什么. (這三個(gè)月用react-native做了個(gè)項(xiàng)目, 也無(wú)心總結(jié)了, 因?yàn)楹孟裉?jiǎn)單了).
本文敘事方式為樹(shù)藤摸瓜, 順著看源碼的邏輯走一遍, 查看的vue的版本為2.5.2. 我fork了一份源碼用來(lái)記錄注釋.
目的
明確調(diào)查方向才能直至目標(biāo), 先說(shuō)一下目標(biāo)行為: 數(shù)據(jù)變化以后執(zhí)行了什么方法來(lái)更新視圖的. 那么準(zhǔn)備開(kāi)始以這個(gè)方向?yàn)槟繕?biāo)從vue源碼的入口開(kāi)始找答案.
從之前的結(jié)論開(kāi)始
先來(lái)復(fù)習(xí)一下之前的結(jié)論:
vue構(gòu)造的時(shí)候會(huì)在data(和一些別的字段)上建立Observer對(duì)象, getter和setter被做了攔截, getter觸發(fā)依賴收集, setter觸發(fā)notify.
另一個(gè)對(duì)象是Watcher, 注冊(cè)watch的時(shí)候會(huì)調(diào)用一次watch的對(duì)象, 這樣觸發(fā)了watch對(duì)象的getter, 把依賴收集到當(dāng)前Watcher的deps里, 當(dāng)任何dep的setter被觸發(fā)就會(huì)notify當(dāng)前Watcher來(lái)調(diào)用Watcher的update()方法.
那么這里就從注冊(cè)渲染相關(guān)的Watcher開(kāi)始.
找到了文件在src/core/instance/lifecycle.js中.
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
mountComponent
渲染相關(guān)的Watcher是在mountComponent()這個(gè)方法中調(diào)用的, 那么我們搜一下這個(gè)方法是在哪里調(diào)用的. 只有2處, 分別是src/platforms/web/runtime/index.js和src/platforms/weex/runtime/index.js, 以web為例:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
原來(lái)如此, 是$mount()方法調(diào)用了mountComponent(), (或者在vue構(gòu)造時(shí)指定el字段也會(huì)自動(dòng)調(diào)用$mount()方法), 因?yàn)閣eb和weex(什么是weex?之前別的文章介紹過(guò))渲染的標(biāo)的物不同, 所以在發(fā)布的時(shí)候應(yīng)該引入了不同的文件最后發(fā)不成不同的dist(這個(gè)問(wèn)題留給之后來(lái)研究vue的整個(gè)流程).
下面是mountComponent方法:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el // 放一份el到自己的屬性里
if (!vm.$options.render) { // render應(yīng)該經(jīng)過(guò)處理了, 因?yàn)槲覀兘?jīng)常都是用template或者vue文件
// 判斷是否存在render函數(shù), 如果沒(méi)有就把render函數(shù)寫成空VNode來(lái)避免紅錯(cuò), 并報(bào)出黃錯(cuò)
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
// 注冊(cè)一個(gè)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
}
這段代碼其實(shí)只做了3件事:
- 調(diào)用beforeMount鉤子
- 建立Watcher
- 調(diào)用mounted鉤子
(哈哈哈)那么其實(shí)核心就是建立Watcher了.
看一下Watcher的參數(shù): vm是this, updateComponent是一個(gè)函數(shù), noop是空, null是空, true代表是RenderWatcher.
在Watcher里看了isRenderWatcher:
if (isRenderWatcher) {
vm._watcher = this
}
是的, 只是復(fù)制了一份用來(lái)在watcher第一次patch的時(shí)候判斷一些東西(從注釋里看的, 我現(xiàn)在還不知道是干嘛的).
那么只有一個(gè)問(wèn)題沒(méi)解決就是updateComponent是個(gè)什么東西.
updateComponent
在Watcher的構(gòu)造函數(shù)的第二個(gè)參數(shù)傳了function, 那么這個(gè)函數(shù)就成了watcher的getter. 聰明的你應(yīng)該已經(jīng)猜到, 在這個(gè)updateComponent里一定調(diào)用了視圖中所有的數(shù)據(jù)的getter, 才能在watcher中建立依賴從而讓視圖響應(yīng)數(shù)據(jù)的變化.
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的由來(lái)
// 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其實(shí)不需要看了, 都是做異常處理, _vnode是在vm._update的時(shí)候保存的, 也就是上次的狀態(tài)或是null(init的時(shí)候給的)
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
}
}
這個(gè)方法做了:
- 根據(jù)當(dāng)前vm的render方法來(lái)生成VNode. (render方法可能是根據(jù)template或vue文件編譯而來(lái), 所以推論直接寫render方法效率最高)
- 如果render方法有問(wèn)題, 那么首先調(diào)用renderError方法, 再不行就讀取上次的vnode或是null.
- 如果有父節(jié)點(diǎn)就放到自己的.parent屬性里.
- 最后返回VNode
所以核心是這句:
vnode = render.call(vm._renderProxy, vm.$createElement)
其中的render(), vm._renderProxy, vm.$createElement都不知道是什么.
先看vm._renderProxy: 是initMixin()的時(shí)候設(shè)置的, 在生產(chǎn)環(huán)境返回vm, 開(kāi)發(fā)環(huán)境返回代理, 那么我們認(rèn)為他是一個(gè)可以debug的vm(就是vm), 細(xì)節(jié)之后再看.
vm.$createElement的代碼在vdom文件夾下, 看了下是一個(gè)方法, 返回值一個(gè)VNode.
render有點(diǎn)復(fù)雜, 能不能以后研究, 總之就是把template或者vue單文件和mount目標(biāo)parse成render函數(shù).
小總結(jié): vm._render()的返回值是VNode, 根據(jù)當(dāng)前vm的render函數(shù)
接下來(lái)看vm._update()
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
// 記錄update之前的狀態(tài)
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創(chuàng)建新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.
}
我們關(guān)心的部分其實(shí)就是__patch()的部分, __patch()做了對(duì)dom的操作, 在_update()里判斷了是否是初次調(diào)用, 如果是的話創(chuàng)建新dom, 不是的話傳入新舊node進(jìn)行比較再操作.
結(jié)論
vue的視圖渲染是一種特殊的Watcher, watch的內(nèi)容是一個(gè)函數(shù), 函數(shù)運(yùn)行的過(guò)程調(diào)用了render函數(shù), render又是由template或者el的dom編譯成的(template中含有一些被observe的數(shù)據(jù)). 所以template中被observe的數(shù)據(jù)有變化觸發(fā)Watcher的update()方法就會(huì)重新渲染視圖.
遺留
render函數(shù)是在哪里被編譯的
vue源碼發(fā)布時(shí)引入不同平臺(tái)最后打成dist的流程是什么
__patch__和VNode的分析
- Vue2.0用戶權(quán)限控制解決方案的示例
- vue-router路由懶加載和權(quán)限控制詳解
- Vue通過(guò)URL傳參如何控制全局console.log的開(kāi)關(guān)詳解
- Vue-Access-Control 前端用戶權(quán)限控制解決方案
- Vue2.0用戶權(quán)限控制解決方案
- 詳解基于vue-router的動(dòng)態(tài)權(quán)限控制實(shí)現(xiàn)方案
- vue-router 權(quán)限控制的示例代碼
- 基于Vue實(shí)現(xiàn)后臺(tái)系統(tǒng)權(quán)限控制的示例代碼
- vue2.0結(jié)合Element實(shí)現(xiàn)select動(dòng)態(tài)控制input禁用實(shí)例
- 詳解VUE的狀態(tài)控制與延時(shí)加載刷新
相關(guān)文章
Vue el-table復(fù)選框全部勾選及勾選回顯功能實(shí)現(xiàn)
這篇文章主要介紹了Vue el-table復(fù)選框全部勾選及勾選回顯功能實(shí)現(xiàn),本文通過(guò)示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-05-05
關(guān)于antd-vue?a-menu菜單綁定路由的相關(guān)問(wèn)題
這篇文章主要介紹了關(guān)于antd-vue?a-menu菜單綁定路由的相關(guān)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10
vue3選項(xiàng)式api如何監(jiān)控?cái)?shù)組變化
這篇文章主要介紹了vue3選項(xiàng)式api如何監(jiān)控?cái)?shù)組變化問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06
vue從后端獲取到文件的?url?地址及前端根據(jù)?url?地址下載文件的實(shí)現(xiàn)思路
這篇文章主要介紹了vue?中從后端獲取到文件的?url?地址及前端根據(jù)?url?地址下載文件,項(xiàng)目用的是?vben?admin?框架,用的是?vue3?+?TS,后端返回的是文件的?url?地址,對(duì)vue后端獲取?url?地址的相關(guān)知識(shí)感興趣的朋友一起看看吧2024-02-02
解決vue.js this.$router.push無(wú)效的問(wèn)題
今天小編就為大家分享一篇解決vue.js this.$router.push無(wú)效的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09
Vue 簡(jiǎn)單實(shí)現(xiàn)前端權(quán)限控制的示例
這篇文章主要介紹了Vue 簡(jiǎn)單實(shí)現(xiàn)前端權(quán)限控制的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
Vue利用Mixin輕松實(shí)現(xiàn)代碼復(fù)用
Mixin,中文翻譯為"混入",在Vue中是一種非常有用的功能,可以解決許多開(kāi)發(fā)中的常見(jiàn)問(wèn)題,下面就讓我們一起深入了解一下Mixin在Vue中解決了哪些問(wèn)題吧2023-06-06

