首頁  >  文章  >  web前端  >  關於vue資料控制視圖原始碼的分析

關於vue資料控制視圖原始碼的分析

不言
不言原創
2018-06-29 17:42:291343瀏覽

這篇文章主要介紹了關於vue資料控制視圖原始碼的分析,有著一定的參考價值,現在分享給大家,有需要的朋友可以參考一下

分析vue是如何實現資料變更更新視圖的。

前記

三個月前看了vue源碼來分析如何做到響應式數據的, 文章名字叫vue源碼之響應式數據, 最後分析到, 數據變化後會調用Watcher的update()方法. 那麼時隔三月讓我們繼續看看update()做了什麼. (這三個月用react-native做了個項目, 也無心總結了, 因為好像太簡單了).

本文敘事方式為樹藤摸瓜, 順著看源碼的邏輯走一遍, 查看的vue的版本為2.5.2. 我fork了一份源碼用來記錄註解.

目的

明確調查方向才能直至目標, 先說一下目標行為: 資料變更以後執行了什麼方法來更新視圖的. 那麼準備開始以這個方向為目標從vue源碼的入口開始找答案.

從之前的結論開始

先來複習一下之前的結論:

vue建構的時候會在data(和一些別的欄位)上建立Observer物件, getter和setter被做了攔截, getter觸發依賴收集, setter觸發notify.

另一個物件是Watcher, 註冊watch的時候會調用一次watch的物件, 這樣觸發了watch物件的getter, 把依賴收集到​​當前Watcher的deps裡, 當任何dep的setter被觸發就會notify當前Watcher來調用Watcher的update ()方法.

那麼這裡就從註冊渲染相關的Watcher開始.

找到了檔案在src/core/instance/lifecycle.js中.

new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)

mountComponent

渲染相關的Watcher是在mountComponent()這個方法中調用的, 那麼我們搜一下這個方法是在哪裡調用的. 只有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)
}

原來如此, 是$mount()方法呼叫了mountComponent( ), (或在vue構造時指定el字段也會自動調用$mount()方法), 因為web和weex(什麼是weex?之前別的文章介紹過)渲染的標的物不同, 所以在發布的時候應該引入了不同的檔案最後發不成不同的dist(這個問題留給之後來研究vue的整個流程).

下面是mountComponent方法:

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件事:

  • 呼叫beforeMount鉤子

  • #建立Watcher

  • ##呼叫mounted鉤子

(哈哈哈)那麼其實核心就是建立Watcher了.

看一下Watcher的參數: vm是this, updateComponent是個函數, noop是空, null是空, true代表是RenderWatcher.

在Watcher裡看了isRenderWatcher:

if (isRenderWatcher) {
  vm._watcher = this
 }

是的, 只是複製了一份用來在watcher第一次patch的時候判斷一些東西(從註釋裡看的, 我現在還不知道是幹嘛的).

那麼只有一個問題沒解決就是updateComponent是個什麼東西.

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
 }
}

這個方法做了:

  • 根據目前vm的render方法來產生VNode. (render方法可能是根據template或vue檔編譯而來, 所以推論直接寫render方法效率最高)

  • 如果render方法有問題, 那麼首先調用renderError方法, 再不行就讀取上次的vnode或null.

  • 如果有父節點就放到自己的.parent屬性裡.

  • 最後回傳VNode

所以核心是這句話:

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進行比較再操作.

結論

vue的視圖渲染是一種特殊的Watcher, watch的內容是一個函數, 函數運行的過程調用了render函數, render又是由template或者el的dom編譯成的(template中含有一些被observe的資料). 所以template中被observe的資料有變化觸發Watcher的update()方法就會重新渲染檢視.

##遺留

render函數是在哪裡被編譯的

vue原始碼發佈時引入不同平台最後打成dist的流程是什麼
__patch__和VNode的分析

以上就是本文的全部內容,希望對大家的學習有幫助,更多相關內容請關注PHP中文網!

相關推薦:

關於vue解決跨域路由衝突問題的想法

關於Vue 動態設定路由參數的介紹

關於vue 虛擬dom的patch原始碼分析

以上是關於vue資料控制視圖原始碼的分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn