1. 程式人生 > >原 Vue原始碼解析二(render&mount)

原 Vue原始碼解析二(render&mount)

前言:前面我們根據vue官網的生命週期圖走了一遍vue的原始碼,感興趣的小夥伴可以去看我之前的文章Vue原始碼解析(一),今天我們重點研究一下render跟mount方法,這也是vue中兩個比較重要的方法了,廢話不多說了,我們開擼~~

我們先回顧下上節內容,我們開啟vue的原始碼,然後找到/vue/src/core/instance/init.js:

 Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

可以說這段程式碼就是貫穿了vue的整個生命週期,我們可以看到最後一行程式碼:

if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }

當我們的vue例項中有el屬性的時候就會直接走vm的mount方法,那麼如果我們不提供el屬性的時候,我們該怎麼走呢?

我們建立一個簡單的vue頁面:

page-a.vue:

<template>
  <div id="page-a-container">
    <button @click="onClick">登入</button>
  </div>
</template>
<script>
  import Render3 from '../components/Render3'
  export default {
    name: 'pageA',
    mounted() {
    },
    methods: {
      onClick() {
        this.$store.dispatch('login')
      }
    }
  }
</script>
<style scoped>
  #page-a-container {
    color: white;
    font-size: 24px;
    height: 100%;
  }
</style>

在這裡插入圖片描述

然後我們在page-a.vue中引用一個叫Render3.js的元件:

export default {
  name: 'Render3',
  render() {
    return <div>我是render3</div>
  }
}

可以看到,我們直接用js檔案定義了一個物件,物件裡面有name屬性跟render方法,render方法我們待會再分析,那麼我們怎麼才能page-a.vue中用起來呢?

方法一:

 mounted() {
 	//繼承vue的屬性
      let Render3Class=this.$options._base.extend(Render3)
      //建立一個vue例項
      let render3VM=new Render3Class({
      //建立一個空元素
        el: document.createElement('div')
      })
      //把render3VM的el新增到當前vue的el中
      this.$el.appendChild(render3VM.$el)
    },

然後我們執行程式碼:
在這裡插入圖片描述

我們還可以直接把元件註冊為區域性元件或者全域性元件:

<template>
  <div id="page-a-container">
    <button @click="onClick">登入</button>
    <render3></render3>
  </div>
</template>
<script>
  import Render3 from '../components/Render3'
  export default {
    name: 'pageA',
    mounted() {
//      let Render3Class=this.$options._base.extend(Render3)
//      let render3VM=new Render3Class({
//        el: document.createElement('div')
//      })
//      this.$el.appendChild(render3VM.$el)
    },
    methods: {
      onClick() {
        this.$store.dispatch('login')
      }
    },
    components:{
      Render3
    }
  }
</script>
<style scoped>
  #page-a-container {
    font-size: 24px;
    height: 100%;
  }
</style>

第二種我就不介紹了哈,小夥伴都會用,我們看一下第一種方式:

 import Render3 from '../components/Render3'
  export default {
    name: 'pageA',
    mounted() {
      let Render3Class=this.$options._base.extend(Render3)
      let render3VM=new Render3Class({
        el: document.createElement('div')
      })
      this.$el.appendChild(render3VM.$el)
    },
    methods: {
      onClick() {
        this.$store.dispatch('login')
      }
    }

我們前面知道,當提供了el屬性給vm的時候,直接會走mount方法,我們把el屬性註釋掉試試:

 mounted() {
      let Render3Class=this.$options._base.extend(Render3)
      let render3VM=new Render3Class({
//        el: document.createElement('div')
      })
      this.$el.appendChild(render3VM.$el)
    },

在這裡插入圖片描述

可以看到,render3沒有被渲染,那我們怎麼辦呢? 我們可以自己呼叫一下:

mounted() {
      let Render3Class=this.$options._base.extend(Render3)
      let render3VM=new Render3Class({
//        el: document.createElement('div')
      })
      render3VM.$mount(document.createElement('div'))
      this.$el.appendChild(render3VM.$el)
    },

我就不截圖了哈,效果跟之前一樣,我們來分析一下mount方法,mount方法字面上意思就是“渲染”的意思,當我們直接執行:

render3VM.$mount(document.createElement('div'))

方法的時候,我們首先走了/vue/src/platforms/web/entry-runtime-with-compiler.js:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  //在vm中傳遞的el屬性
  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
  // 如果在options中沒有傳遞render方法
  if (!options.render) {
    let template = options.template
    //如果有template屬性,就把template編譯完畢後傳遞給render方法
    if (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屬性但是有el屬性的時候,把el的outerHtml當成template傳遞給render物件
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, 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')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

然後會走vue/src/platforms/web/runtime/index.js:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

最後會走vue/src/core/instance/lifecycle.js:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    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) {
    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
  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

可以看到,最後執行了回調了mounted事件,我們元件中的mounted方法被執行,在第一行中我們可以看到:

vm.$el = el

所以也是為什麼我們在beforecreated跟created方法中拿不到this. e l , t h i s . el的原因,因為this. el還沒被賦值.

mount方法的整個流程我們簡單的走了一遍了,我們再反過來一起研究一下render方法,render基本用法我就不解釋了,官網比我說的好,小夥伴可以自己看官網哈,
https://cn.vuejs.org/v2/guide/render-function.html

我們重點研究一下render方法的原始碼

小夥伴有沒有發現,在vue檔案中如果提供了render方法,render方法不執行?

這其實跟vue-loader有關,當vue-loader在載入.vue檔案的時候,當發現.vue檔案中有template標籤的時候,就會把template解析完畢後然後建立render方法,替換當前vm的render方法,所以我們在.vue檔案中提供的render方法其實是被覆蓋掉了,所以如果在.vue檔案中使用render方法的時候,我們需要去掉template標籤.

像這樣的:

<script>
  export default {
    name: 'Render2',
    render(h) {
      console.log(this.name)
      console.log(this.$options)
      return h('div', [
        < p > 111111 < /p>,
      this.$scopedSlots.default({title: 'yasin'}),
        this.$slots.default
    ])
    },
    inject: {
      name: {
        from: 'title'
      }
    }
  }
</script>
<style scoped>

</style>

vue-loader的原始碼我這就不解析了,不然寫不完了,哈哈~~
好啦,又說了一段廢話,我們還是快進入我們的主題哈

我們繼續看這個檔案中的mount方法vue/src/platforms/web/entry-runtime-with-compiler.js:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  //在vm中傳遞的el屬性
  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
  // 如果在options中沒有傳遞render方法
  if (!options.render) {
    let template = options.template
    //如果有template屬性,就把template編譯完畢後傳遞給render方法
    if (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屬性但是有el屬性的時候,把el的outerHtml當成template傳遞給render物件
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, 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')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

我們剛也說了,當.vue檔案的時候vue-loader會判斷template標籤,然後建立render方法,所以我們在這的時候預設認為是有render方法的,那如果.vue中沒有template標籤也沒有提供render方法的話該怎麼走呢?

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    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
        )
      }
    }
  }

在mountComponent方法中我們看到,如果我們沒有提供render方法的時候,vue就會自動給我們建立一個空的vnode物件:

vm.$options.render = createEmptyVNode
export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

我們還是繼續回到這個檔案哈
/vue/src/platforms/web/entry-runtime-with-compiler.js:

if (!options.render) {
    let template = options.template
    //如果有template屬性,就把template編譯完畢後傳遞給render方法
    if (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屬性但是有el屬性的時候,把el的outerHtml當成template傳遞給render物件
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, 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')
      }
    }

當判斷有template的時候,我們就走了下面程式碼:

 if (template) {
     ...
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

     ....
    }

我們可以看到當有template的時候,我們走了compileToFunctions方法,然後生成了render方法,我們看一下compileToFunctions方法,vue/src/platforms/web/compiler/index.js:

import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

然後compileToFunctions方法我們可以看到在這定義的
vue/src/compiler/index.js:

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

然後我們可以看到generate方法,vue/src/compiler/codegen/index.js:

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

可以看到,返回的結果中render為with(this){return _c(“div”)}或者是由genElement方法返回的code字串:

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      const data = el.plain ? undefined : genData(el, state)

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

我們可以看到有genStatic、genOnce、genFor等等…

// hoist static sub-trees out
function genStatic (el: ASTElement, state: CodegenState): string {
  el.staticProcessed = true
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  return `_m(${
    state.staticRenderFns.length - 1
  }${
    el.staticInFor ? ',true' : ''
  })`
}

所以最後返回的就是“_m()、_o()”這樣的~~

好啦,看完這個我們可能會疑惑,為什麼render方法是字串呢? 不應該是返回的方法嗎? 我們繼續往後看哈~

vue/src/compiler/index.js:

/* @flow */

import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'

// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

然後我們看createCompilerCreator方法,vue/src/compiler/create-compiler.js:

import { extend } from 'shared/util'
import { detectErrors } from './error-detector'
import { createCompileToFunctionFn } from './to-function'

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
     ....
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

createCompileToFunctionFn方法又是啥呢?vue/src/compiler/to-function.js:

export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    ...
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })
      ....
  }
}
function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}

好吧,終於是看到廬山真面目了,我們返回的"_c()、_o()"字串最後通過new Function(code)變成了Function物件,那麼_c()、_o()方法又是啥呢?

vue/src/core/instance/render.js:

export function initRender (vm: Component) {
...
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  ...
  export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)
  .....
  }
  

vue/src/core/instance/render-helpers/index.js:

/* @flow */

import { toNumber, toString, looseEqual, looseIndexOf } from 'shared/util'
import { createTextVNode, createEmptyVNode } from 'core/vdom/vnode'
import { renderList } from './render-list'
import { renderSlot } from './render-slot'
import { resolveFilter } from './resolve-filter'
import { checkKeyCodes } from './check-keycodes'
import { bindObjectProps } from './bind-object-props'
import { renderStatic, markOnce } from './render-static'
import { bindObjectListeners } from './bind-object-listeners'
import { resolveScopedSlots } from './resolve-slots'

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
}

可以看到,不只有_c、_o,還有方法~,再往下就是根據標籤跟屬性建立對應的vnode物件了,所以整個render物件看完我們可以知道,render物件最後就是返回了一個vnode物件.

我們回到我們的mount方法,vue/src/core/instance/lifecycle.js:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    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) {
    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
  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
}

可以看到,最重要的就是建立了一個Watcher物件:

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

然後傳入了updateComponent方法:

updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }

Watcher先介紹一下,Watcher物件可以監聽響應式資料的變化,然後Watcher物件中會判斷需不需要重新整理元件,觸發updateComponent方法,然後觸發vm的_update方法,最後更新dom~~

所以我們看一下_update幹了什麼?
vue/src/core/instance/lifecycle.js

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    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) {
      // initial render
      vm.$el = vm.__patch__(
        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)
    }
    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.
  }

可以看到,update方法中接受到了vnode物件後,最後通過下面程式碼轉成了dom物件:

 vm.$el = vm.__patch__(prevVnode, vnode)

那麼__patch__方法又是啥呢?vue/src/platforms/web/runtime/index.js:

Vue.prototype.__patch__ = inBrowser ? patch : noop

import * as nodeOps from 'web/runtime/node-ops'

export const patch: Function = createPatchFunction({ nodeOps, modules })

我們看一下nodeOps是啥?vue/src/platforms/web/runtime/node-ops.js:

/* @flow */

import { namespaceMap } from 'web/util/index'

export function createElement (tagName: string, vnode: VNode): Element {
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}

export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}

export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}

export function createComment (text: string): Comment {
  return document.createComment(text)
}

export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}

export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

export function parentNode (node: Node): ?Node {
  return node.parentNode
}

export function nextSibling (node: Node): ?Node {
  return node.nextSibling
}

export function tagName (node: Element): string {
  return node.tagName
}

export function setTextContent (node: Node, text: string) {
  node.textContent = text
}

export function setStyleScope (node: Element, scopeId: string) {
  node.setAttribute(scopeId, '')
}

翻過了一座又一座山,歷經了千辛萬苦,我們終於是看到了廬山真面目,小夥伴可別說你不熟悉這些程式碼哈~~

好啦~ 我們簡單的回顧一下前面的內容哈,首先我們定義了一些響應式資料並監聽變化—>執行mount方法–>生成render方法—>建立vnode物件–>通過update方法把vnode物件轉換成dom元素…

好啦!! 整個vue的原始碼研究先到這了,之後會正對vue-loader以及watcher物件做一次解析.

先到這裡啦,歡迎志同道合的小夥伴入群,一起交流一起學習~~ 加油騷年!!
qq群連結:
在這裡插入圖片描述