Чтение исходного кода Vue — файловая структура и механизм работы

исходный код Vue.js

Vue уже составляет треть текущего отечественного веб-терминала, а также является одним из моих основных технологических стеков. Я знаю его в повседневном использовании, и мне это любопытно. Кроме того, большое количество исходного кода Vue Недавно в сообществе появились классы чтения. В этой статье я воспользовался этой возможностью, чтобы почерпнуть пищу из всех статей и обсуждений. В то же время я обобщил некоторые идеи при чтении исходного кода и подготовил несколько статей в качестве результатов своей работы. собственное мышление.мой уровень ограничен.добро пожаловать, чтобы оставить сообщение для обсуждения~

Целевая версия Vue:2.5.17-beta.0

Комментарии к исходному коду Vue:GitHub.com/Шерлок Эд9…

Отказ от ответственности: Синтаксис исходного кода в статье использует Flow, и исходный код сокращен по мере необходимости (чтобы не путать @_@), если вы хотите увидеть полную версию, пожалуйста, введите вышегитхаб-адрес, эта статья представляет собой серию статей, адрес статьи внизу ~

Заинтересованные студенты могут добавить группу WeChat в конце статьи для совместного обсуждения~

0. Предварительные знания

  • Flow
  • Синтаксис ES6
  • Общие шаблоны проектирования
  • Идеи функционального программирования, такие как каррирование

Вот несколько предварительных статей:Инструмент статической проверки типов JS Flow,Начало работы с ECMAScript 6 - Руан Ифэн,Каррирование в JS,Шаблон JS-наблюдателя,JS использует функции высшего порядка для реализации кэширования функций (режим мемо)

1. Структура файла

файловая структура в vueCONTRIBUTING.mdЕсть введение, которое напрямую переведено здесь:

├── scripts ------------------------------- 包含与构建相关的脚本和配置文件
│   ├── alias.js -------------------------- 源码中使用到的模块导入别名
│   ├── config.js ------------------------- 项目的构建配置
├── build --------------------------------- 构建相关的文件,一般情况下我们不需要动
├── dist ---------------------------------- 构建后文件的输出目录
├── examples ------------------------------ 存放一些使用Vue开发的应用案例
├── flow ---------------------------------- JS静态类型检查工具 [Flow](https://flowtype.org/) 的类型声明
├── package.json
├── test ---------------------------------- 测试文件
├── src ----------------------------------- 源码目录
│   ├── compiler -------------------------- 编译器代码,用来将 template 编译为 render 函数
│   │   ├── parser ------------------------ 存放将模板字符串转换成元素抽象语法树的代码
│   │   ├── codegen ----------------------- 存放从抽象语法树(AST)生成render函数的代码
│   │   ├── optimizer.js ------------------ 分析静态树,优化vdom渲染
│   ├── core ------------------------------ 存放通用的,平台无关的运行时代码
│   │   ├── observer ---------------------- 响应式实现,包含数据观测的核心代码
│   │   ├── vdom -------------------------- 虚拟DOM的 creation 和 patching 的代码
│   │   ├── instance ---------------------- Vue构造函数与原型相关代码
│   │   ├── global-api -------------------- 给Vue构造函数挂载全局方法(静态方法)或属性的代码
│   │   ├── components -------------------- 包含抽象出来的通用组件,目前只有keep-alive
│   ├── server ---------------------------- 服务端渲染(server-side rendering)的相关代码
│   ├── platforms ------------------------- 不同平台特有的相关代码
│   │   ├── weex -------------------------- weex平台支持
│   │   ├── web --------------------------- web平台支持
│   │   │   ├── entry-runtime.js ---------------- 运行时构建的入口
│   │   │   ├── entry-runtime-with-compiler.js -- 独立构建版本的入口
│   │   │   ├── entry-compiler.js --------------- vue-template-compiler 包的入口文件
│   │   │   ├── entry-server-renderer.js -------- vue-server-renderer 包的入口文件
│   ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│   ├── shared ---------------------------- 整个代码库通用的代码

Несколько важных каталогов:

  • компилятор:Компиляция, используемая для преобразования шаблона в функцию рендеринга
  • основной:Основной код Vue, включая адаптивную реализацию, виртуальную DOM, монтирование методов экземпляра Vue, глобальные методы, абстрактные общие компоненты и т. д.
  • Платформа:Входные файлы разных платформ в основном предназначены для веб-платформы и платформы weex.Разные платформы имеют свой собственный специальный процесс построения.Конечно, наше внимание сосредоточено на веб-платформе.
  • сервер:Код, связанный с рендерингом на стороне сервера (SSR), SSR в основном отображает компоненты непосредственно в HTML и предоставляет их клиенту напрямую с сервера.
  • ПФС:В основном логика разбора файла .vue
  • общий:Некоторые общие служебные методы, некоторые настроены для повышения читабельности кода.

Среди них под платформойsrc/platforms/web/entry-runtime.jsФайл используется в качестве точки входа для построения среды выполнения.Метод ESM выводит dist/vue.runtime.esm.js, метод CJS выводит dist/vue.runtime.common.js, а метод UMD выводит dist/vue. runtime.js, за исключением шаблона шаблона для компилятора для функции рендерингаsrc/platforms/web/entry-runtime-with-compiler.jsФайл используется в качестве точки входа для построения среды выполнения: метод ESM выводит dist/vue.esm.js, метод CJS выводит dist/vue.common.js, а метод UMD выводит dist/vue.js, включая компилятор.

2. Входной файл

Любой интерфейсный проект можно скачать сpackage.jsonфайл, давайте посмотрим на егоscript.devчто мы бежимnpm run devКогда его командная строка:

"scripts": {
    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
}

здесьrollupэто сборщик модулей JS, похожий на webpack, на самом делеVue - v1.0.10Предыдущая версия использовала webpack , а затем изменила его на rollup , если вы хотите знать, почему он был изменен на rollup , вы можете проверить это.Вы собственный ответ Юсики, в общем, чтобы уменьшить размер пакета и увеличить скорость инициализации.

Вы можете увидеть здесь накопительный пакет для запускаscripts/config.jsфайл и заданный параметрTARGET:web-full-dev, Давайте посмотримscripts/config.jsчто там

// scripts/config.js

const builds = {
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),  // 入口文件
    dest: resolve('dist/vue.js'),                          // 输出文件
    format: 'umd',                                         // 参看下面的编译方式说明
    env: 'development',                                    // 环境
    alias: { he: './entity-decoder' },                     // 别名
    banner                                        // 每个包前面的注释-版本/作者/日期.etc
  },
}

Описание метода компиляции формата:

  • Эс:Модули ES, вывод с использованием синтаксиса шаблона ES6
  • cjs:Модуль CommonJs, вывод файла в соответствии со спецификацией модуля CommonJs.
  • и:Модуль AMD, вывод файла в соответствии со спецификацией модуля AMD
  • умд:Поддержка вывода файла спецификации внешней ссылки, этот файл может напрямую использовать тег скрипта

здесьweb-full-devЭто соответствует команде, которую мы только что передали в командной строке, затем накопительный пакет начнет упаковываться в соответствии с файлом записи ниже, и есть много других команд и других методов и форматов вывода.Вы можете проверить исходный код самостоятельно.

Поэтому основное внимание в этой статье уделяется включению компилятора компилятора.src/platforms/web/entry-runtime-with-compiler.jsфайл, в среде производства и разработки используем vue-loader для компиляции шаблона, так что пакет с компилятором не нужен, но для лучшего понимания принципа и процесса рекомендуется посмотреть файл входа с компилятор.

Давайте сначала посмотрим на этот файл, где импортируется Vue, чтобы увидеть, откуда он берется.

// src/platforms/web/entry-runtime-with-compiler.js

import Vue from './runtime/index'

Продолжай читать

// src/platforms/web/runtime/index.js

import Vue from 'core/index'

keep moving

// src/core/index.js

import Vue from './instance/index'

keep moving*2

// src/core/instance/index.js

/* 这里就是vue的构造函数了,不用ES6的Class语法是因为mixin模块划分的方便 */
function Vue(options) {
  this._init(options)         // 初始化方法,位于 initMixin 中
}

// 下面的mixin往Vue.prototype上各种挂载
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

когда мыnew Vue( )Когда , этот конструктор фактически вызывается, и вы можете начать отсюда.

3. Рабочий механизм

Здесь я использую xmind, чтобы нарисовать грубую схему механизма работы, в основном, последующий анализ находится в некоторых частях этой картины.

Все примеры Vue в этой статье используютсяvmПредставлять

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

3.1 Initialization_init()

Когда мы в main.jsnew Vue( ), Vue вызовет конструктор_init( )метод, этот метод находится в core/instance/index.jsinitMixin( )определяется в методе

// src/core/instance/index.js

/* 这里就是Vue的构造函数 */
function Vue(options) {
  this._init(options)              // 初始化方法,位于 initMixin 中
}

// 下面的mixin往Vue.prototype上各种挂载,这是在加载的时候已经挂载好的
initMixin(Vue)                     // 给Vue.prototype添加:_init函数,...
stateMixin(Vue)                    // 给Vue.prototype添加:$data属性, $props属性, $set函数, $delete函数, $watch函数,...
eventsMixin(Vue)                   // 给Vue.prototype添加:$on函数, $once函数, $off函数, $emit函数, $watch方法,...
lifecycleMixin(Vue)                // 给Vue.prototype添加: _update方法, $forceUpdate函数, $destroy函数,...
renderMixin(Vue)                   // 给Vue.prototype添加: $nextTick函数, _render函数,...

export default Vue

мы можем видетьinit( )Какую инициализацию выполняет этот метод:

// src/core/instance/index.js

Vue.prototype._init = function(options?: Object) {
  const vm: Component = this

  initLifecycle(vm)                     // 初始化生命周期 src/core/instance/lifecycle.js
  initEvents(vm)                        // 初始化事件 src/core/instance/events.js
  initRender(vm)                        // 初始化render src/core/instance/render.js
  callHook(vm, 'beforeCreate')          // 调用beforeCreate钩子
  initInjections(vm)                    // 初始化注入值 before data/props src/core/instance/inject.js
  initState(vm)                         // 挂载 data/props/methods/watcher/computed
  initProvide(vm)                       // 初始化Provide after data/props
  callHook(vm, 'created')               // 调用created钩子

  if (vm.$options.el) {                    // $options可以认为是我们传给 `new Vue(options)` 的options
    vm.$mount(vm.$options.el)              // $mount方法
  }
}

здесь_init()метод к текущемуvmЭкземпляр выполняет ряд настроек инициализации, более важным является метод инициализации StateinitState(vm)когдаdata/propsотзывчивость, это легендарный пасObject.defineProperty()Метод устанавливает объект, который должен быть отзывчивымgetter/setter, на этой основе выполняется сбор зависимостей для достижения целей изменения данных, приводящих к изменениям представления.

Окончательная проверкаvm.$optionsЕсть ли на немelатрибут, если есть, используйтеvm.$mountметод монтированияvm, чтобы сформировать связь между уровнем данных и уровнем представления. Это также, если не предусмотреноelОпция должна быть сделана вручнуюvm.$mount('#app')причина.

Мы виделиcreatedКрючок монтируется$mountзвонил раньше, так что мы вcreatedDOM нельзя манипулировать, пока не сработает хук, потому что он еще не отрендерен в DOM.

3.2 Крепление $mount()

Метод крепленияvm.$mount( )Он определен в нескольких местах и ​​связан с различными методами упаковки и платформами.src/platform/web/entry-runtime-with-compiler.js,src/platform/web/runtime/index.js,src/platform/weex/runtime/index.js, наше внимание сосредоточено на первом файле, но вentry-runtime-with-compiler.jsСначала файл будет помещенruntime/index.jsсередина$mountМетод сохраняется и запускается с вызовом в конце:

// src/platform/web/entry-runtime-with-compiler.js

const mount = Vue.prototype.$mount    // 把原来的$mount保存下来,位于 src/platform/web/runtime/index.js
Vue.prototype.$mount = function(
  el?: string | Element,    // 挂载的元素
  hydrating?: boolean       // 服务端渲染相关参数
): Component {
  el = el && query(el)
  
  const options = this.$options
  if (!options.render) {                // 如果没有定义render方法
    let template = options.template
    
    // 把获取到的template通过编译的手段转化为render函数
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {...}, this)
      options.render = render
    }
  }
  return mount.call(this, el, hydrating)      // 执行原来的$mount
}

В версии Vue 2.0 для рендеринга всех компонентов Vue в конечном итоге требуется метод рендеринга.Независимо от того, разрабатываем ли мы компонент в виде одного файла .vue или пишем атрибут el или template, он в конечном итоге будет преобразован в метод рендеринга. здесьcompileToFunctionsЭто метод компиляции шаблона в рендер, который будет представлен позже.

// src/platform/weex/runtime/index.js

Vue.prototype.$mount = function (
  el?: string | Element,    // 挂载的元素
  hydrating?: boolean       // 服务端渲染相关参数
): Component {
  el = el && inBrowser ? query(el) : undefined        // query就是document.querySelector方法
  return mountComponent(this, el, hydrating)          // 位于core/instance/lifecycle.js
}

здесьelЕсли это не элемент DOM в начале, он будет заменен методом запроса с элементом DOM, а затем передаваться вmountComponentметод, мы продолжаем видетьmountComponentОпределение:

// 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
  }
  callHook(vm, 'beforeMount')            // 调用beforeMount钩子

  // 渲染watcher,当数据更改,updateComponent作为Watcher对象的getter函数,用来依赖收集,并渲染视图
  let updateComponent
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  // 渲染watcher, Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数
  // ,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')            // 调用beforeUpdate钩子
      }
    }
  }, true /* isRenderWatcher */)

  // 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例
  if (vm.$vnode == null) {
    vm._isMounted = true               // 表示这个实例已经挂载
    callHook(vm, 'mounted')            // 调用mounted钩子
  }
  return vm
}

существуетmountComponentметод создает экземпляр рендераWatcher, и прошел вupdateComponent,Сюда:() => { vm._update(vm._render(), hydrating) }первое использование_renderгенерация методаVNode, затем позвоните_updateспособ обновления DOM. Вы можете ознакомиться с введением раздела обновления просмотра

Здесь вызывается несколько хуков, и можно наблюдать за их синхронизацией.

3.3 Компиляция compile()

Если нам нужно преобразовать сцену рендеринга, такую ​​как написанный нами шаблон, она будет преобразована компилятором в функцию рендеринга, которая состоит из нескольких шагов:

Запись находится только в src/platform/web/entry-runtime-with-compiler.js.compileToFunctionsметод:

// src/platforms/web/compiler/index.js

const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }

продолжайте видеть здесьcreateCompilerметод:

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

Здесь можно увидеть три важных процесса.parse,optimize,generate, а затем сгенерируйте код метода рендеринга.

  • parse: он будет использовать обычные методы для анализа инструкций, классов, стилей и других данных в шаблоне шаблона для формирования абстрактного синтаксического дерева AST.
  • optimize: оптимизация AST, создание шаблона дерева AST, обнаружение статических поддеревьев, не требующих изменений DOM, и снижение нагрузки на исправление.
  • generate: генерировать код метода рендеринга из AST.

3.4 Реактивное наблюдение()

Vue, как инфраструктура MVVM, мы знаем, что мост между его уровнем модели и уровнем представления ViewModel является ключом к управлению данными, отзывчивость Vue достигается за счетObject.definePropertyДля этого установите отзывчивый объектgetter/setterКогда визуализируется функция рендеринга, запускается объект ответа на чтение.getterпровестиКоллекция зависимостей, и эта настройка срабатывает при изменении адаптивного объекта.setter,setterметод будетnotifyвсе, что он собрал доwatcherчтобы сообщить им, что их значение было обновлено, что вызываетwatcherизupdateидтиpatchОбновите вид.

Отвечающая запись находится в src/core/instance/init.js.initStateсередина:

// src/core/instance/state.js

export function initState(vm: Component) {
  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)
  }
}

Он очень регулярно определяет несколько методов для инициализацииprops,methods,data,computed,wathcer, вот только посмотриinitDataметод, взгляните на леопарда

// src/core/instance/state.js

function initData(vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
                    ? getData(data, vm)
                    : data || {}
  
  observe(data, true /* asRootData */) // 给data做响应式处理
}

Сначала оцените, являются ли данные функцией, если да, возьмите возвращаемое значение, если нет, затем возьмите сами себя, а затемobserveпара методовdataДля обработки посмотрите этот метод

// src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  ob = new Observer(value)
  return ob
}

Этот метод в основном используетdataЧтобы создать экземпляр объекта Observer, Observer является классом, конструктор Observer используетdefineReactiveметод для реактивного стиля ключей объекта, он рекурсивно добавляет к свойствам объектаgetter/setter, для сбора зависимостей и уведомления об обновлении этот метод, вероятно, такой

// src/core/observer/index.js

function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            /* 进行依赖收集 */
            return val;
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            notify();                // 触发更新
        }
    });
}

3.5 Просмотр патча обновления( )

когда используешьdefineReactiveПосле того, как метод сделает объект отзывчивым, при рендеринге функции рендеринга он будет считыватьgetterтем самым вызываяgetterпровестиwatcherКоллекция зависимостей, которая запускается при изменении значения отзывчивого объекта.setterуведомлятьnotifyЗависимости, собранные ранее, уведомляют себя о том, что они были изменены, пожалуйста, повторно визуализируйте представление по мере необходимости. уведомленwatcherпередачаupdateметод для обновления представления, которое передается вnew Watcher( )изupdateComponentВ методе этот метод называетсяupdateпутьpatchОбновите вид.

// src/core/instance/lifecycle.js

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

// 渲染watcher, Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数
// ,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数
new Watcher(vm, updateComponent, noop, {...}, true /* isRenderWatcher */)

это_renderспособ создания виртуального узла,_updateметод будет передаваться в новый VNode вместе со старым VNodepatch

// src/core/instance/lifecycle.js

Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) { // 调用此方法去更新视图
  const vm: Component = this
  const prevVnode = vm._vnode
  vm._vnode = vnode

  if (!prevVnode) {
    // 初始化
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    //更新
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}

_updateпередача__patch__метод, который в основном сравнивает старые и новые виртуальные узлыpatchVnode, после того, как алгоритм diff напрямую рисует их различия, и, наконец, соответствующий DOM этих различий обновляется.

На этом этапе в основном представлен основной процесс. У нас есть общее понимание того, как работает Vue, начиная с создания экземпляра конструктора. Мы обсудим содержание каждой части позже. обсудить~


Эта статья представляет собой серию статей, и более поздние части будут обновлены позже, чтобы добиться прогресса вместе ~

  1. Чтение исходного кода Vue — файловая структура и механизм работы
  2. Чтение исходного кода Vue — принцип сбора зависимостей
  3. Чтение исходного кода Vue — пакетное асинхронное обновление и принцип nextTick

Большинство сообщений в Интернете имеют разную глубину и даже некоторые несоответствия. Следующие статьи являются кратким изложением процесса обучения. Если вы найдете какие-либо ошибки, пожалуйста, оставьте сообщение, чтобы указать ~

Ссылаться на:

  1. Изучение исходного кода Vue2.1.7
  2. Демистификация технологии Vue.js
  3. Анализ внутренней работы Vue.js
  4. Документация Vue.js
  5. [Большие галантереи] Взявшись за руки, я познакомлю вас с исходным кодом vue.
  6. MDN - Object.defineProperty()

PS: Всех приглашаю обратить внимание на мой публичный аккаунт [Front End Afternoon Tea], давайте работать вместе~

Кроме того, вы можете присоединиться к группе WeChat «Front-end Afternoon Tea Exchange Group», нажмите и удерживайте, чтобы определить QR-код ниже, чтобы добавить меня в друзья, обратите вниманиеДобавить группу, я заберу тебя в группу~