03| [Vue2 ソースコードを読む] Vue の初期化プロセスを簡単に理解する

ソース コード リポジトリを読んでください: github.com/AlanLee97/r…

マインドマッピング

Vue アプリケーションの初期化時に基本的な呼び出しリンクを記録します。

デモの例

todomvc を例に挙げます

ソースファイルの場所: github.com/AlanLee97/r…

アプリ.js

/* eslint-disable no-undef */
// Full spec-compliant TodoMVC with localStorage persistence
// and hash-based routing in ~150 lines.

// localStorage persistence
var STORAGE_KEY = 'todos-vuejs-2.0'
var todoStorage = {
  fetch: function () {
    var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
    todos.forEach(function (todo, index) {
      todo.id = index
    })
    todoStorage.uid = todos.length
    return todos
  },
  save: function (todos) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
  }
}

// visibility filters
var filters = {
  all: function (todos) {
    return todos
  },
  active: function (todos) {
    return todos.filter(function (todo) {
      return !todo.completed
    })
  },
  completed: function (todos) {
    return todos.filter(function (todo) {
      return todo.completed
    })
  }
}

// eslint-disable-next-line no-debugger
debugger
// app Vue instance
var app = new Vue({
  // app initial state
  props: {
    hello: {
      type: String,
      default: 'hello todomvc'
    }
  },
  data: {
    count: 0,
    todos: todoStorage.fetch(),
    newTodo: '',
    editedTodo: null,
    visibility: 'all',
    currentTodoItem: {}
  },

  // watch todos change for localStorage persistence
  watch: {
    todos: {
      handler: function (todos) {
        todoStorage.save(todos)
      },
      deep: true
    }
  },

  // computed properties
  // https://vuejs.org/guide/computed.html
  computed: {
    filteredTodos: function () {
      return filters[this.visibility](this.todos)
    },
    remaining: function () {
      return filters.active(this.todos).length
    },
    allDone: {
      get: function () {
        return this.remaining === 0
      },
      set: function (value) {
        this.todos.forEach(function (todo) {
          todo.completed = value
        })
      }
    }
  },

  filters: {
    pluralize: function (n) {
      return n === 1 ? 'item' : 'items'
    }
  },

  // methods that implement data logic.
  // note there's no DOM manipulation here at all.
  methods: {
    setCurrent(item) {
      // this.count += 1
      this.currentTodoItem = item
      // setTimeout(() => {
      //   console.log('alan->count', this.count)
      // })
    },
    addTodo: function () {
      var value = this.newTodo && this.newTodo.trim()
      if (!value) {
        return
      }
      this.todos.push({
        id: todoStorage.uid++,
        title: value,
        completed: false
      })
      this.newTodo = ''
    },

    removeTodo: function (todo) {
      this.todos.splice(this.todos.indexOf(todo), 1)
    },

    editTodo: function (todo) {
      this.beforeEditCache = todo.title
      this.editedTodo = todo
    },

    doneEdit: function (todo) {
      if (!this.editedTodo) {
        return
      }
      this.editedTodo = null
      todo.title = todo.title.trim()
      if (!todo.title) {
        this.removeTodo(todo)
      }
    },

    cancelEdit: function (todo) {
      this.editedTodo = null
      todo.title = this.beforeEditCache
    },

    removeCompleted: function () {
      this.todos = filters.active(this.todos)
    }
  },

  // a custom directive to wait for the DOM to be updated
  // before focusing on the input field.
  // https://vuejs.org/guide/custom-directive.html
  directives: {
    'todo-focus': function (el, binding) {
      if (binding.value) {
        el.focus()
      }
    }
  }
})

// handle routing
function onHashChange () {
  var visibility = window.location.hash.replace(/#/?/, '')
  if (filters[visibility]) {
    app.visibility = visibility
  } else {
    window.location.hash = ''
    app.visibility = 'all'
  }
}

window.addEventListener('hashchange', onHashChange)
onHashChange()

// mount
// 后置挂载元素
// mountComponent -> updateComponent(更新时 beforeUpdate) / new Watcher(初始化时), 观察vm,vm变化->执行updateComponent
// mounted
app.$mount('.todoapp')

console.log('alan-> app', app)
window.appVue = app

インデックス.html

<!doctype html>
<html data-framework="vue">
  <head>
    <meta charset="utf-8">
    <title>Vue.js • TodoMVC</title>
    <link rel="stylesheet" href="https://unpkg.com/[email protected]/index.css">
    <script src="https://unpkg.com/[email protected]/build/director.js"></script>
    <style>[v-cloak] { display: none; }</style>
  </head>
  <body>
    <section class="todoapp">
      <header class="header">
        <h1>todos</h1>
        <input class="new-todo"
          autofocus autocomplete="off"
          placeholder="What needs to be done?"
          v-model="newTodo"
          @keyup.enter="addTodo">
      </header>
      <section class="main" v-show="todos.length" v-cloak>
        <input class="toggle-all" type="checkbox" v-model="allDone">
        <ul class="todo-list">
          <li v-for="todo in filteredTodos"
            class="todo"
            :key="todo.id"
            :class="{ completed: todo.completed, editing: todo == editedTodo }" 
            @click="setCurrent(todo)">
            <div class="view">
              <input class="toggle" type="checkbox" v-model="todo.completed">
              <label @dblclick="editTodo(todo)">{{ todo.title }}</label>
              <button class="destroy" @click="removeTodo(todo)"></button>
            </div>
            <input class="edit" type="text"
              v-model="todo.title"
              v-todo-focus="todo == editedTodo"
              @blur="doneEdit(todo)"
              @keyup.enter="doneEdit(todo)"
              @keyup.esc="cancelEdit(todo)">
          </li>
        </ul>
      </section>
      <footer class="footer" v-show="todos.length" v-cloak>
        <span class="todo-count">
          <strong>{{ remaining }}</strong> {{ remaining | pluralize }} left
        </span>
        <ul class="filters">
          <li><a href="#/all" :class="{ selected: visibility == 'all' }">All</a></li>
          <li><a href="#/active" :class="{ selected: visibility == 'active' }">Active</a></li>
          <li><a href="#/completed" :class="{ selected: visibility == 'completed' }">Completed</a></li>
        </ul>
        <button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
          Clear completed
        </button>
      </footer>
    </section>
    <footer class="info">
      <p>Double-click to edit a todo</p>
      <p>Written by <a href="http://evanyou.me">Evan You</a></p>
      <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
    </footer>

    <script>
    // for testing
    if (navigator.userAgent.indexOf('PhantomJS') > -1) localStorage.clear()
    </script>
    <!-- Delete ".min" for console warnings in development -->
    <script src="../../dist/vue.js"></script>
    <script src="app.js"></script>
  </body>
</html>

Index.html はエントリです

分析する

Vueの初期化

このデモ例では、スクリプトを通じて vue.js をインポートします。

<script src="../../dist/vue.js"></script>

このとき、このスクリプトのコードが最初に実行され、vue.js のコードが導入されます。対応するソース コード ファイルは です。src/core/index.jsこれがエントリ ファイルです。ここで Vue の初期化が開始されます

import Vue from './instance/index' // 导入Vue,会先执行这个脚本中的代码
import { initGlobalAPI } from './global-api/index'
// ...

initGlobalAPI(Vue)

// ...
export default Vue

Vue はここから再度インポートされます。このファイルをインポートするとき、主にいくつかのインスタンス メソッド/プロパティ ( ) とプライベート メソッド/プロパティ ( )src/instance/index.jsをVue.prototype にマウントし、コードを同期するために、このファイル内の同期コード ブロックが最初に実行されます。ブロックのロジックは次のとおりです。 $xxx _xxx

  • Vue 関数を定義する
function Vue (options) { // 定义Vue函数,开发者new Vue()时,才会执行_init()
  // 开发者new Vue()时,才会执行
  // 里面做的操作:初始化生命周期、事件收集对象、渲染需要的一些属性、beforeCreate/created、状态(props,data,computed,watch)、provide/inject、执行挂载$mount
  this._init(options) 
}

インスタンス API の初期化 (vm.$xxx/vm._xxx)

  • いくつかの初期化を行う
// 外部导入这个文件时,会先执行一下代码
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
  • initMixin (Vue)、初期化、初期化関数を _init に割り当てる
Vue.prototype._init = function(options) { ... }
  • stateMixin (Vue)、状態を設定し、Vue.prototype のインスタンス API メソッド/プロパティをハングします。

    • $data
    • $props
    • $set
    • $delete
    • $watch
  • eventsMixin (Vue)、イベントを初期化します。ここではパブリッシュおよびサブスクライブモードを実装し、Vue.prototype で 4 つのメソッドをハングします。

    • $on
    • $once
    • $off
    • $emit
  • lifecycleMixin (Vue)、ライフサイクルの一部を初期化し、Vue.prototype の 3 つのメソッドをハングします。

    • _update

      • _update実行時に前のVNodeノードが存在するかどうかを判断し、存在しない場合は初期化処理、__patch__()実際のDOMにレンダリングされた要素をメソッドで取得し$elに代入する処理、そうでない場合はVNodeノードを更新する処理ノード。
    • $forceUpdate

    • $destroy

  • renderMixin(Vue),初始化渲染,安装渲染助手installRenderHelpers,挂载两个方法

    • $nextTick
    • _render

初始化全局API(Vue.xxx)

执行完src/instance/index.js里的同步代码,再回来执行剩下的代码,也就是执行initGlobalAPI(Vue),再初始化全局API,这里会给Vue构造器函数上挂上一些公有API(Vue.xxx),挂载的有:

  • util

  • set

  • del

  • nextTick

  • observable

  • options

  • 还让options.component继承了内置组件builtInComponents

    • KeepAlive
    • Transition
  • 还做了一些初始化

    • initUse,初始化插件
    • initMixin,初始化mixins
    • initExtend,初始化继承
    • initAssetRegisters

最后导出Vue函数

以上,引入Vue.js文件时完成了Vue自身的初始化,接下来就是根据开发者提供的options,new Vue()开始初始化组件。

【备注】

挂载实例方法与挂载在构造器上的全局方法的区别

  1. 挂载实例方法:Vue.prototype.xxx = xxx
  2. 挂载在构造器全局方法:Vue.xxx = xxx

区别在于

  • 挂载在prototype上的方法/属性,通过new关键字new出来的对象可以直接方法prototype里的方法,如const vm = new Vue(); vm.xxx
  • 挂载在构造器上的方法/属性,只能通过构造器来访问,或者通过实例.construtor访问,如Vue.xxx,vm.constructor.xxx

new Vue(options)

调用链路示意图

我们写的组件的代码都是写在options里的,当new Vue的时候,会调用Vue构造函数里的_init方法,

_init方法根据options初始化组件:

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
	// ...
  // merge options
  if (options && options._isComponent) {
    // ...
  } else {
    // 合并选项,并挂载到$options
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
	// ...
  initLifecycle(vm) // 初始化生命周期:$parent,$children,$refs,_watcher,_isMounted,_isDestroyed,_isBeingDestroyed等一些属性
  initEvents(vm) // 初始化事件收集对象_events,初始化父组件的监听器
  initRender(vm) // 初始化$slots,$scopedSlots,$createElement,响应式$attrs,$listeners
  callHook(vm, 'beforeCreate') // 执行beforeCreate
  initInjections(vm) // resolve injections before data/props // 初始化inject
  initState(vm) // 初始化props,data,computed,watch
  initProvide(vm) // resolve provide after data/props // 初始化 provide,原理:把options.provide挂载到vm._provide
  callHook(vm, 'created') // 执行created

  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}
  1. mergeOptions,合并选项,并挂载到$options
  2. initLifecycle(vm),初始化生命周期:$parent ,$children ,$refs ,_watcher ,_isMounted ,_isDestroyed ,_isBeingDestroyed 等一些属性
  3. initEvents(vm),初始化事件收集对象_events,初始化父组件的监听器
  4. initRender(vm),初始化$slots ,$scopedSlots ,$createElement ,响应式$attrs,$listeners
  5. callHook(vm, 'beforeCreate'),执行beforeCreate
  6. initInjections(vm),初始化inject
  7. initState(vm) ,初始化propsdatacomputedwatch
  8. initProvide(vm),初始化 provide,原理:把options.provide挂载到vm._provide
  9. callHook(vm, 'created'),执行created
  10. vm. m o u n t ( v m . マウント(vm. options.el),挂载元素,如果有$options.el

这里比较重要的是initState方法,具体看下initState里面做了什么:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) // 调用defineReactive将props定义成响应式
  if (opts.methods) initMethods(vm, opts.methods) // 将methods的函数平铺到vm
  if (opts.data) {
    initData(vm) // 将data定义成响应式
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed) // 初始化computed
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch) // 初始化watch,createWatcher->vm.$watch
  }
}

通过上面的代码,可以知道,主要是初始化props,data,computed,watch,我们主要看下初始化data

将data转成响应式

调用链路示意图

initData(vm)的逻辑

  • 调用observe方法
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
  }
	// ...
  let i = keys.length
  while (i--) {
    // ...
    // 略过这里的代码,主要是做一些属性名有没有被props,methods占用的检测
  }
  // observe data,主要是要关注observe方法
  observe(data, true /* asRootData */)
}
  • observe方法new Observer
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 这里是重要的地方
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
  • Observer初始化时遍历data对象,调用defineReactive方法
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value // data
    this.dep = new Dep() // 初始化一个依赖
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // ...
    } else {
      // 在这里遍历data
      this.walk(value)
    }
  }

	// 遍历所有属性并将它们转换为getter/setter。此方法只在值类型为Object时调用。
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 通过defineReactive方法
      defineReactive(obj, keys[i])
    }
  }

  // ...
}
  • defineReactive方法调用Object.defineProperty,修改对象的属性描述符,get/set,加入依赖的处理,get时,收集依赖;set时,依赖触发更新
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  // ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // 获取属性值得时候,收集依赖
        dep.depend()
        // ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // ...
      val = newVal
      // 在更新值得时候,触发依赖通知更新
      dep.notify()
    }
  })
}

通过上面的代码链路调用,已经将data的属性完成响应式。

小总结

简单总结new Vue的操作,就是根据开发者的options初始化组件,初始化props、data、methods、computed、watch;将打它转为响应式,以实现数据变化时,能够更新视图。

其中我们比较关注的是data如何初始化,主要通过遍历data的属性,使用Object.defineProperty,修改对象的属性描述符get/set,以实现响应式效果。

当然还有props、methods、watch等的初始化,这个我们后期再分析。

挂载元素

调用链路

  1. 执行app.$mount('.todoapp'),挂载元素,进入到src\platforms\web\entry-runtime-with-compiler.js入口文件,里面的主要逻辑:
  • 先缓存一份原 m o u n t 方法为 m o u n t ,主要是要包装一下原来的 マウント方法はマウントで、主にオリジナルをラップします。 mount方法,加入编译函数compileToFunctions,将template转成render方法,并把render方法挂载会options上
  • 执行mount方法(原Vue.prototype.$mount)
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  // ...

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      // 调用compileToFunctions方法,将模板转换成render函数,挂载到options
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 
  return mount.call(this, el, hydrating)
}
  1. 再调用mount方法,其实就是Vue.prototype.$mount,在src\platforms\web\runtime\index.js定义的函数,实际调用mountComponent函数
// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
  1. mountComponent方法
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  	// ...
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
    	// ... 省略做一些标记mark代码
      const vnode = vm._render()
      vm._update(vnode, hydrating)
      // ...
    }
  } else {
    // 这里是核心逻辑,把vm._render()执行结果VNode作为参数传递给_update执行
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // 我们设它为vm._watcher在观察器的构造函数中,因为观察器的初始补丁可能调用$forceUpdate(例如在子组件的挂载钩子中),它依赖于vm._watchr已经定义
  // 这里是最核心的逻辑:把updateComponent更新函数放到Watcher的回调中进行监听,如果vm的数据有更新,则执行updateComponent函数,更新视图。
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, 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
}

mountComponent主要做了两个操作

  • 一是定义一个更新组件的函数updateComponent,里面把_render()渲染函数执行结果VNode作为参数放到_update()更新函数里执行更新操作
  • 二是把更新函数放到Watcher的回调中进行监听,如果vm的数据有更新,则执行updateComponent函数,更新视图。

小总结:

$mount挂载元素,核心逻辑就是:

  1. 把template转成渲染函数_render,再调用挂载组件函数mountComponent,封装一个更新组件的函数updateComponent,调用更新函数vm._update,用_render函数的结果VNode作为参数
  2. new Watcher观察vm的变化,把updateComponent函数作为回调函数,当vm的数据有更新时,则执行updateComponent函数,_update更新视图(更新视图涉及到diff,path更新,这里不做分析)。

总结

简易流程总结

  1. 引入Vue.js时,实例方法/属性和公共方法/属性
  • 挂载实例方法/属性,给Vue的prototype挂载一些全局实例方法, x x x ,如 xxx、「」など set`等
  • 挂载公共方法/属性,给Vue构造器上挂载一些公用方法/属性,Vue.xxx,如Vue.set()
  1. 开发者在new Vue(options)的时候,执行vm._init(options)初始化方法
  • 初始化状态
    • propsdata定义成响应式,读取属性时收集依赖,属性更新时通知更新
    • methodscomputed(computed需使用Watcher观测)平铺到vm上
    • 初始化watch
  • 执行生命周期函数
  1. 挂载元素,执行vm.$mount(),并监听通过new Watcher监听自身数据的变化,以_update作为回调函数,当数据变动时执行更新函数进行更新视图。

官方解释:

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

后记

前面分析的过程中出现了ObserverDepWatcher;那么,它们三者之间的关系是什么呢

如图所示:

エントリがインスタンス化されると、依存関係を収集して更新を通知するためObserverにクラスがインスタンス化される通過しインスタンスを収集し、コールバック関数のキューを格納します を実行すると、コールバック関数のキューが実行され、ビューの更新が行われます関数はこのコールバック キュー関数に格納されます。DepDepsubsWatcherWatcherrun()

Watcher単独で使用することもできます。たとえば、computed単独で実装した場合、コンポーネント自体にも、自身のデータ変更を監視するためのWatcher新しいコンポーネントが追加されます。コンポーネント内のオプションも個別に追加されますWatcherwatchWatcher

したがって、WatcherVue では重要な役割を果たしており、基本的な機能のほとんどが Vue によって実現されています。

おすすめ

転載: juejin.im/post/7253453908039172157