vue源碼之數據控制視圖

分析vue是如何實現數據改變動新視圖的.html

前記

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

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

目的

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

從以前的結論開始

先來複習一下以前的結論:webpack

  • vue構造的時候會在data(和一些別的字段)上創建Observer對象, getter和setter被作了攔截, getter觸發依賴收集, setter觸發notify.
  • 另外一個對象是Watcher, 註冊watch的時候會調用一次watch的對象, 這樣觸發了watch對象的getter, 把依賴收集到當前Watcher的deps裏, 當任何dep的setter被觸發就會notify當前Watcher來調用Watcher的update()方法.

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

找到了文件在src/core/instance/lifecycle.js中.github

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

mountComponent

渲染相關的Watcher是在mountComponent()這個方法中調用的, 那麼咱們搜一下這個方法是在哪裏調用的. 只有2處, 分別是src/platforms/web/runtime/index.jssrc/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件事:

  • 調用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的入口文件

如今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()的時候會報錯.

結論

  • vue的視圖渲染是一種特殊的Watcher, watch的內容是一個函數, 函數運行的過程調用了render函數, render又是由template或者el的dom編譯成的(template中含有一些被observe的數據). 因此template中被observe的數據有變化觸發Watcher的update()方法就會從新渲染視圖.
  • Vue的平臺無關的內容在core中, 最後打成dist的時候根據不一樣的發佈平臺(web, weex), 發佈模式(browser, es-module)來給核心Vue對象掛載更多的方法和屬性(代碼在platforms中). render()__patch__()是在platforms裏掛上的.

遺留

  • template編譯成render的實現
  • __patch__和VNode的分析

原文地址

相關文章
相關標籤/搜索