Анализ исходного кода Vue (версия vue-2.4.4)

внешний интерфейс переводчик JavaScript Vue.js

Прежде чем приступить к работе, необходимо иметь прочные базовые навыки чтения исходного кода, а также терпение, чтобы вникнуть в общую ситуацию, не вычитать деталей!

Сначала загляните в каталог

├── build --------------------------------- 构建相关的文件
├── dist ---------------------------------- 构建后文件的输出目录
├── examples ------------------------------ 存放使用Vue开发的的例子
├── flow ---------------------------------- 类型声明,使用开源项目 [Flow](https://flowtype.org/)
├── package.json -------------------------- 项目依赖
├── test ---------------------------------- 包含所有测试文件
├── src ----------------------------------- 这个是我们最应该关注的目录,包含了源码
│   ├──platforms --------------------------- 包含平台相关的代码
│   │   ├──web -----------------------------  包含了不同构建的包的入口文件
│   │   |   ├──entry-runtime.js ---------------- 运行时构建的入口,输出 dist/vue.common.js 文件,不包含模板(template)到render函数的编译器,所以不支持 `template` 选项,我们使用vue默认导出的就是这个运行时的版本。大家使用的时候要注意
│   │   |   ├── entry-runtime-with-compiler.js -- 独立构建版本的入口,输出 dist/vue.js,它包含模板(template)到render函数的编译器
│   ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数
│   │   ├── parser ------------------------ 存放将模板字符串转换成元素抽象语法树的代码
│   │   ├── codegen ----------------------- 存放从抽象语法树(AST)生成render函数的代码
│   │   ├── optimizer.js ------------------ 分析静态树,优化vdom渲染
│   ├── core ------------------------------ 存放通用的,平台无关的代码
│   │   ├── observer ---------------------- 反应系统,包含数据观测的核心代码
│   │   ├── vdom -------------------------- 包含虚拟DOM创建(creation)和打补丁(patching)的代码
│   │   ├── instance ---------------------- 包含Vue构造函数设计相关的代码
│   │   ├── global-api -------------------- 包含给Vue构造函数挂载全局方法(静态方法)或属性的代码
│   │   ├── components -------------------- 包含抽象出来的通用组件
│   ├── server ---------------------------- 包含服务端渲染(server-side rendering)的相关代码
│   ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│   ├── shared ---------------------------- 包含整个代码库通用的代码

2. Что такое конструктор Vue

использоватьnewоператор для звонкаVue,VueЯвляется конструктором, разберитесь со структурой каталогов, давайте посмотрим на файл входа

Открытымpackage.json

Когда мы запускаем npm run dev, чтобы посмотреть, что мы сделали, rollup также является инструментом упаковки, похожим на webpack, согласно

TARGET=web-full-dev

Перейдите в build/config.js, чтобы найти

Откройте найденный файл записиweb/entry-runtime-with-compiler.js 


По указанному выше пути поиска мы нашли конструктор Vue


Определите конструктор, введите зависимости, вызовите функцию инициализации и, наконец, экспортируйте Vue.

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

Откройте эти пять файлов и найдите соответствующие методы. Вы обнаружите, что функция этих методов состоит в том, чтобы монтировать методы или свойства в прототип прототипа Vue.

1. Сначала войдите в initMixin (Vue) и смонтируйте его на прототип

Vue.prototype._init = function (options) {} 

2. Войдите в stateMixin(Vue) и смонтируйте его на прототипе

Vue.prototype.$data 
Vue.prototype.$props 
Vue.prototype.$set = set 
Vue.prototype.$delete = del 
Vue.prototype.$watch = function(){} 

3. Введите eventsMixin (Vue) и смонтируйте его на прототип

Vue.prototype.$on 
Vue.prototype.$once 
Vue.prototype.$off 
Vue.prototype.$emit

4. Войдите в lifecycleMixin (Vue) и смонтируйте его на прототипе

Vue.prototype._update 
Vue.prototype.$forceUpdate 
Vue.prototype.$destroy  

5. Наконец, введите renderMixin (Vue) и смонтируйте его на прототипе.

Vue.prototype.$nextTick 
Vue.prototype._render 
Vue.prototype._o = markOnce 
Vue.prototype._n = toNumber 
Vue.prototype._s = toString 
Vue.prototype._l = renderList 
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual 
Vue.prototype._i = looseIndexOf 
Vue.prototype._m = renderStatic 
Vue.prototype._f = resolveFilter 
Vue.prototype._k = checkKeyCodes 
Vue.prototype._b = bindObjectProps 
Vue.prototype._v = createTextVNode 
Vue.prototype._e = createEmptyVNode 
Vue.prototype._u = resolveScopedSlots 
Vue.prototype._g = bindObjectListeners

Следующий шаг к src/core/index.js в соответствии с указанным выше путем поиска

Внедрение зависимостей, монтирование статических методов и свойств в Vue.

  1. import { initGlobalAPI } from './global-api/index'
    import { isServerRendering } from 'core/util/env'
    
    initGlobalAPI(Vue)
    
    Object.defineProperty(Vue.prototype, '$isServer', {
      get: isServerRendering
    })
    
    Object.defineProperty(Vue.prototype, '$ssrContext', {
      get () {
        /* istanbul ignore next */
        return this.$vnode && this.$vnode.ssrContext
      }
    })
    
    Vue.version = '__VERSION__'
    
    export default Vue

Войдите в initGlobalAPI (Vue), смонтируйте статические свойства и методы в Vue.

Vue.config Vue.util = util
Vue.set = set 
Vue.delete = del 
Vue.nextTick = util.nextTick 
Vue.options = { 
 components: { KeepAlive }, 
 directives: {}, 
 filters: {}, 
 _base: Vue 
} 
Vue.use 
Vue.mixin 
Vue.cid = 0 
Vue.extend 
Vue.component = function(){} 
Vue.directive = function(){} 
Vue.filter = function(){} 

затем смонтировать

Vue.prototype.$isServer 
Vue.version = '__VERSION__'



В соответствии с указанным выше путем поиска перейдите в runtime/index.js и установите инструменты для конкретной платформы.

Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// 安装平台特定的 指令 和 组件
Vue.options = {
    components: {
        KeepAlive,
        Transition,
        TransitionGroup
    },
    directives: {
        model,
        show
    },
    filters: {},
    _base: Vue
}
Vue.prototype.__patch__
Vue.prototype.$mount

В соответствии с последним шагом указанного выше пути поиска перейдите в web/entry-runtime-with-compiler.js.

  1. кеш изweb-runtime.jsдокумент$mountфункция, const mount = Vue.prototype.$mount
  2. Смонтировать на Vuecompileзатем перезапишитеVue.prototype.$mount
  3. Vue.compile = compileToFunctions — это шаблонtemplateСкомпилируйте в функцию рендеринга.

На этом этапе весь конструктор Vue восстанавливается.


3. Объясните весь процесс на примере

index.html

 <!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue.js grid component example</title>
    </head>
  <body>
    <div id="app">
        <ol>
          <li v-for="todo in todos">
            {{ todo.text }}
          </li>
        </ol>
        <Child></Child>
    </div>
  </body>
</html>

grid.js

let Child = {
	data: function() {
	  return {child: '你好哈'}
	},
	template: '<div>{{child}}</div>'
}
new Vue({
	el: '#app',
	data: {
		todos: [
                    {text: '学习 JavaScript'}, 
                    {text: '学习 Vue'}, 
                    {text: '整个牛项目'}]
	},
	components: {'Child': Child}
})

Чтобы узнать, что делает Vue, сначала посмотрите на функцию-конструктор Vue (параметры) { this._init(параметры) }

  • new Vue({//передаем указанный выше контент}) сначала введите Vue.prototype._init, первый метод конструктора для монтирования

Vue.prototype._init = function (options) {
    const vm= this
    vm._uid = uid++
    let startTag, endTag
    vm._isVue = true

    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    vm._renderProxy = vm
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) 
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

_init()В начале метода вthisВ объекте определены два свойства:_uidи_isVue, а затем определить, существует ли определениеoptions._isComponentпойдет сюдаelseBranch, то есть этот код:

vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )mergeOptions使用策略模式合并传入的options和Vue.options

mergeOptions использует режим стратегии для объединения входящих параметров и объединенной структуры кода Vue.options, Вы можете видеть, что глобальные наследуются путем слияния компонентов стратегии, директив и фильтров. Вот почему глобально зарегистрированный может использоваться везде, потому что каждый экземпляр наследует глобальный, Таким образом, вы можете найти


Тогда позвониinitLifecycle,initEvents,initRender、initState, И вinitStateХуки жизненного цикла вызываются туда и обратно соответственно.beforeCreateиcreated,Увидев это, я понимаю, почему DOM нельзя манипулировать при создании. Потому что на данный момент реальный элемент DOM еще не отрендерен в документе.createdЭто только означает, что инициализация состояния данных завершена.Сосредоточьтесь на initState()

initState (vm) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

Глядя на систему ответа данных Vue через initData, поскольку передаются только данные, выполните initData(vm)

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
      proxy(vm, `_data`, key)
  }
  observe(data, true /* asRootData */)
}

proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

Выньте ключ в данных, выполните цикл через прокси-сервер, вы можете напрямую получить доступ к значению в данных через атрибут this.и проксировать данные в объекте экземпляра, чтобы мы могли получить доступ к data.todos через этот .todos Далее официальная система ответа данных наблюдаем(данные, истина /* asRootData */), данные обрабатываются методом get и устанавливаются через Object.defineProperty, чтобы данные отвечали

class Observer {
  constructor(value) {
    this.value = value 
    this.dep = new Dep() 
    this.vmCount = 0 
    def(value, '__ob__', this) 
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  walk(obj: Object) {
    const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
} 

существуетObserverкласс, мы используемwalkМетод вызывается циклически по атрибутам данных datadefineReactiveметод,defineReactiveМетод очень прост, просто преобразуйте атрибуты данных данных в атрибуты доступа и рекурсивно наблюдайте за данными, в противном случае можно наблюдать только прямые податрибуты данных данных. Таким образом, наш первый шаг завершен.Когда мы изменяем или получаем значение атрибута данных, мы передаемgetиsetчтобы получать уведомления.

function defineReactive (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  const dep = new Dep()
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      val = newVal
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

  1. вlet childOb = !shallow && наблюдать(val), сделать рекурсивный вызов, установить все данные данных, включая подмножества, и ответить
  2. Среди них в классе Observe, если свойство является массивом, оно будет преобразовано

    if (Array.isArray(value)) {
          const augment = hasProto
            ? protoAugment
            : copyAugment
          augment(value, arrayMethods, arrayKeys)
          this.observeArray(value)
        } else {
          this.walk(value)
        }export const arrayMethods = Object.create(arrayProto)
    ;[
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    .forEach(function (method) {
      const original = arrayProto[method]
      def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        const ob = this.__ob__
        let inserted
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args
            break
          case 'splice':
            inserted = args.slice(2)
            break
        }
        if (inserted) ob.observeArray(inserted)
        // notify change
        ob.dep.notify()
        return result
      })
    })
    

Переопределить вышеуказанные методы в массиве, когда эти операции выполняются над массивом,ob.dep.notify(), уведомить о соответствующих изменениях


После выполнения initData(vm) соответствующая система завершена.В это время выполняется callHook(vm, 'created') для создания триггера, продолжается возврат к _init() и выполнение к vm.$mount(vm. $options.el)

Vue.prototype._init = function (options) {
    const vm= this
    vm._uid = uid++
    let startTag, endTag
    vm._isVue = true

    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    vm._renderProxy = vm
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) 
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

При вводе $mount сначала будет получен смонтированный узел el, а затем сначала будет определено, есть ли какие-либо входящиеrenderметод, я не ищу, чтобы пройти в шаблоне,

В этом случае нет, я возьму его
getOuterHTML(el) в качестве текущего шаблона

Vue.prototype.$mount = function (
  el,
  hydrating
){
  el = el && query(el)
  const options = this.$options
  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 {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

С шаблоном через compileToFunctions шаблон компилируется в синтаксическое дерево ast, после статической оптимизации, Наконец, он обрабатывается в функцию рендеринга.Функция рендеринга в этом примере использует with(this), Продвиньте эту сферу, {{ задача.текст }} Таким образом, мы можем использовать атрибут прямо в шаблоне, без этого!Конечно, также можно добавить этот Устранено много путаницы, почему это нельзя добавить в шаблон (рендеринг в реакции должен использовать это), v-for преобразуется в _l, согласно предыдущему Vue.prototype._l = renderList

function() {
    with (this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        }, [_c('ol', _l((this.todos), function(todo) {
            return _c('li', [_v("\n            " + _s(todo.text) + "\n          ")])
        })), _v(" "), _c('child')], 1)
    }
}

  • После создания функции рендеринга введите mountComponent,
  • первый звонок
    функция перед монтированием,
  • Затем выполните vm._watcher = new Watcher(vm, updateComponent, noop)
  • Наконец, callHook(vm, 'mounted'), выполнить смонтированный, поэтому он был смонтирован на dom до выполнения монтирования

Итак, суть в том, что vm._watcher = new Watcher(vm, updateComponent, noop)

function mountComponent (
  vm,
  el,
  hydrating
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')

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

  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

Посмотреть код наблюдателя

class Watcher {
  constructor (
    vm,
    expOrFn,
    cb,
    options
  ) {
    this.vm = vm
    vm._watchers.push(this)

    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    return value
  }
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

Выполнить конструктор из-за this.lazy=false, this.value = this.lazy ? неопределенный : это.получить();

Выполните метод get, где pushTarget(this), добавьте статический атрибут this в Dep.target (текущий экземпляр new Watcher() )

 function pushTarget (_target) { 

 if (Dep.target) targetStack.push(Dep.target) 

 Dep.target = _target

 }

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    return value
  }

接着执行 this.getter.call(vm, vm)
this.getter就是
updateComponent = () => {
vm._update(vm._render(), hydrating)
}      
  1. Сначала вызовите vm._render()

  Vue.prototype._render = function (){
    const vm = this
    const {
      render,
      staticRenderFns,
      _parentVnode
    } = vm.$options

    let vnode = render.call(vm._renderProxy, vm.$createElement)
    return vnode
  }

Функция рендеринга, скомпилированная до начала выполнения.При выполнении функции рендеринга путем получения атрибута todos и т. д.

function() {
    with (this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        }, [_c('ol', _l((this.todos), function(todo) {
            return _c('li', [_v("\n            " + _s(todo.text) + "\n          ")])
        })), _v(" "), _c('child')], 1)
    }
}

Метод get, на этот раз Dep.target, уже имеет статическое свойство, экземпляр Watcher.

Таким образом, соответствующий экземпляр dep будет собирать соответствующий экземпляр Watcher.

Вернуться к vnode после выполнения,



updateComponent = () => {
vm._update(vm._render(), hydrating)
} 
其中vm._render()执行render函数返回vnode作为参数
接下来执行vm._update
这是首次渲染,所以执行
vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false,
        vm.$options._parentElm,
        vm.$options._refElm
      )

Vue.prototype._update = function (vnode, hydrating) {
    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
    if (!prevVnode) {
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      vm.$options._parentElm = vm.$options._refElm = null
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }

vm.__patch__( vm.$el, vnode, увлажняющий, ложный, vm.$options._parentElm, vm.$options._refElm ) В соответствии с деревом во vnode создайте соответствующий элемент, вставьте его в родительский узел и создайте все дочерние узлы, рекурсивно перебирая vnode. Вставить в родительский узел Если дочерний элемент является компонентом, таким как Child в этом примере, будет создан и выполнен экземпляр соответствующего VueComponent. Тот же процесс, что и для нового Vue()

если еще неprevVnodeОписание — это первый рендеринг, а реальный DOM создается напрямую. Если у вас уже естьprevVnodeВ описании не первый рендер, значит пользуюсьpatchАлгоритм выполняет необходимые манипуляции с DOM. Это логика обновления DOM Vue. Просто мы не реализовали виртуальный DOM внутри.

При изменении значения свойства срабатывает метод set соответствующего свойства.Поскольку get срабатывает, когда рендер выполняется до этого и собирается соответствующий Watcher, set срабатывает при изменении значения, а ранее собранный Экземпляр Watcher уведомляется о выполнении, и метод рендеринга пересчитывается.

Наконец украсть картинку:



Давно пишу, но реально больше не могу писать.Если у меня в будущем будет хороший язык, давайте разбираться!