Анализ исходного кода Vue — рендеринг, патч, обновление, vnode

внешний интерфейс исходный код переводчик Vue.js
Анализ исходного кода Vue — рендеринг, патч, обновление, vnode

личный блог

Если есть какие-либо ошибки, я надеюсь, что вы оставите сообщение и дадите указатели, и вы будете очень счастливы.

Немного сумбурно, разные методы переплетаются, сложно разобраться в порядке, пожалуйста Хайхан

фронт потока

существуетVueВ исходном коде Youda принялFlowВ качестве проверки статического типаFlowдаfacebookПроизводится инструментом статической проверки типов.

зачем использоватьFlow? Как мы все знаем,JavaScriptявляется слабо типизированным языком.

Так называемый слабый тип означает, что при определении переменных тип не требуется, а тип будет определяться автоматически в процессе работы программы.Если язык может неявно преобразовывать все свои типы, то и свои переменные, выражения и т.д. участвовать в операции.Даже если тип неверен, правильный тип может быть получен неявным преобразованием, которое для пользователя, по-видимому, может выполнять все операции над всеми типами, поэтомуJavascriptназывается слабой типизацией.

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

Итак, исходя из этого требования, имеемTypescriptа такжеFlowпроизводится, ноTypeScriptСтоимость обучения относительно велика, и вообще говоря, вы не будете учить язык для какого-то удобства, поэтомуFacebookОткрытый исходный код четыре года назадFlow.

Vueзачем использоватьFlowвместоTypescriptЧто насчет фреймворков разработки?Ответ Юйси Чжиху таков:.

Как им пользоваться, вы можете узнать, просмотрев китайскую документацию здесь. В этой статье в основном рассказывается о навыках работы с исходным кодом Vue и не будет объясняться слишком много Flow.

Архитектура проекта

Vue.js — типичный фреймворк MVVM, основная идея которого — управление данными и компонентизация. DOM — это естественное отображение данных.В Vue вам нужно только изменить данные, чтобы достичь цели обновления DOM. Компонентизация заключается в том, чтобы рассматривать каждый независимый функциональный блок или интерактивный модуль на странице как компонент, а страницу рассматривать как контейнер, чтобы реализовать搭积木式метод развития. Скачиваем исходники на локалку и давайте посмотрим на структуру каталогов

Структура каталогов

Разделение исходного каталога VUE четкое. Весь каталог примерно разделен на

  • benchmarks: Тестовая демонстрация при обработке больших объемов данных
  • dist: Пакеты версий, необходимые для каждой среды.
  • examples: Некоторые практические демонстрации, реализованные с помощью Vue.
  • flow: Конфигурация определения типа данных
  • packages: подключаемый модуль, который необходимо установить отдельно для работы в определенной среде.
  • src: Ядро всего исходного кода.
  • script: файл конфигурации скрипта npm
  • test: прецедент
  • types: Новая версия конфигурации машинописного текста.

Основной код находится вsrcКаталог, который включает создание экземпляров, обработку данных, компиляцию шаблонов, центр событий, глобальную конфигурацию и т. д., находится в этом каталоге.

от входа

В компиляторе найдите корневой каталогpackage.jsonфайл, который можно увидеть вscriptесть одинdev, этот файл генерируетrollupконфигурация упаковщика,

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

rollupозначает, что он используетrollupупаковщик,-wвыражатьwatchотслеживать изменения файлов,cвыражатьconfigИспользуйте файл конфигурации для упаковки, если файл не указан позже, он будет указан по умолчаниюrollup.config.js, за которым следует указанныйscripts/config.jsнастроить сборку,--environmentУказывает на установку переменных среды, за которыми следуют параметрыTARGET:web-full-devУказывает имя и значение переменной среды, и мы переходим кscripts/config.js, Вы можете видеть, что параметры переменных среды были объединены и активированы.genConfig()функция

ЭтоgenConfig()Что вы наделали

Других скрытых предметов я пока не вижу, во-первыхconst opts = builds[name]существуетbuildsПоиск переменных в конфигурации. Файл ввода и конфигурация вывода определены, и если среда выполнения определена, она сохраняется в этом поле.
Затем в этом файле найдитеweb-full-devСоответствующая конфигурация выглядит следующим образом: она в основном объявляет записьentryи определение модуляformat, выходdest, имя средыenv, сборка сводкиalias, информация о кадреbanner, вход естьweb/entry-runtime-with-compiler.js, но не в текущем каталогеwebПапка, как найти? Выше мы видим, что естьresolve()Функция прокси пути

использоватьsplitСократите входящие имена файлов, чтобы они соответствовали входящимaliasконфигурация, окончательное позиционированиеsrc/platforms/web/entry-runtime-with-compiler.js, найдите, что Vue хранится здесь$mountметод и заново объявляет$mountметод, используя сохраненныйmountМетод снова монтируется внизу и возвращает результат. Зачем нужно заново декларировать?После ознакомления с информацией знаю оригиналruntime-onlyВерсии не объявляются после объявления$mountТаким образом, эту часть обработки можно повторно использовать на основе сохранения исходной функции, которую стоит изучить.

Нелегко изменить исходную логику, но исходную функцию можно сохранить и повторно объявить.

Общий процесс

Сначала посмотрите на общий процесс

  • Первый рендер, выполнениеcompileToFunctions()Разберите шаблон шаблона в renderFn (функция рендеринга), пропустите эту часть, если renderFn уже существует
  • Передать renderFn черезvm._render()Скомпилировал в Vnode, пока читал в нем переменные,Watcherпройти черезObject.defindProperty()изgetМетод собирает зависимости от dep и начинает слушать
  • воплощать в жизньupdataComponent(), сначала в домpatch()метод будетvnodeРендеринг в настоящий DOM
  • Смонтируйте DOM на узле и дождитесь изменения данных
  • dataПри изменении атрибута сначала проверьте, есть ли ссылка на значение данных в собранных зависимостях.Object.defindProperty()изsetМетод изменяет значение и выполняется_updataпровестиpatch()а такжеupdataComponent()Выполнить обновление компонента

грубо разделить на

Полная конструкция esm: включая компилятор шаблонов, HTML-строку процесса рендеринга → функцию рендеринга → VNode → настоящий DOM-узел

сборка среды выполнения только во время выполнения: не включает компилятор шаблонов, функцию рендеринга процесса рендеринга → VNode → реальный узел DOM

Версия только для времени выполнения не имеет шага template=>render и компилятора шаблонов.

Объясните различную лексику

  1. templateШаблон: шаблон Vue основан на чистом HTML, основанном на синтаксисе шаблона Vue, и структура все еще может быть написана в предыдущем стиле HTML.
  2. Абстрактное синтаксическое дерево AST:Abstract Syntax TreeАббревиатура, в основном делать три шага
    1. разбор: Vue использует HTMLParserРазобрать HTML-шаблон в AST
    2. оптимизатор: выполнить некоторую оптимизацию AST, чтобы пометить статические узлы, извлечь самое большое статическое дерево, когда_updateПри обновлении интерфейса будет происходить процесс исправления, а алгоритм diff будет напрямую пропускать статические узлы, тем самым сокращая процесс исправления и оптимизируя производительность исправления.
    3. generateCode: Генерировать из ASTrenderфункция
  3. Функция рендеринга renderFn: функция рендеринга используется для генерацииVirtual DOM(vdom)из. Vue рекомендует использовать шаблоны для создания интерфейса нашего приложения.В базовой реализации Vue будет компилировать шаблоны вrenderFn函数, конечно, мы также можем написать функцию рендеринга напрямую, без написания шаблона, чтобы лучше контролировать
  4. Виртуальный DOM (vdom, также известный как VNode): дерево виртуального DOM, Vue'sVirtual DOM Patchingалгоритм основан наSnabbdom库реализации, и внес множество корректировок и улучшений в эти основы. Может быть выполнено только через RenderFnvm._render()генерировать,patchцелиVnode, и каждыйVnodeглобально уникальный
  5. патч: я упоминал об этом в вдоме выше, но я все же должен сказать, что патч является самым основным методом во всем виртаул-доме, основная функция заключается в исправлении旧vnodeс новым vnodediffпроцесс и, наконец, сгенерируйте новый узел DOM черезupdataComponent()Метод повторно визуализируется, и Vue провел большую оптимизацию производительности для этого.
  6. Наблюдатель: у каждого компонента Vue есть соответствующий Watcher ,этоWatcherбудет в компонентеrenderСобирайте данные, от которых зависит компонент, когда компонент зависит, и запускайте компонент при обновлении зависимости.vm._updataпередачаpatch()Сделайте diff и повторно визуализируйте DOM.

Не говори глупостей, пошли

устанавливать

Этот метод нового монтирования $mount.

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  .....

key?:value (key: value|void);

el?:string|Elementэто синтаксис потока, указывающий, что входящая строка el может бытьstringа такжеElementИ тип пустоты --undefinedТипы,hydrating?: booleanОпять же, должен иметь тип boolean иundefined.

key:?value (key: value|void|null);

Значит этоkeyдолжно бытьvalueилиundefinedтак же какnullТипы.

function():value (:value) :ComponentУказывает, что возвращаемое значение функции должно бытьComponentТипы.

function(key:value1|value2)(ключ:значение1|значение2) выражатьkeyдолжно бытьvalue1илиvalue2Типы.

Скомпилировать RenderFn

el = el && query(el)к входящемуelУзел элемента подтвержден, если контейнер входящего узла не найден, он предупредит и вернетcreateElement('div')новый разд.

//判断传入的标签如果是body或者是页面根节点
//就警告禁止挂载在页面根节点上,因为挂载会替换该节点。最后返回该节点
  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;    
  if (!options.render) {    //如果接受的值已经有写好的RenderFn,则不用进行任何操作,如果render不存在,就进入此逻辑将模板编译成renderFn
    let template = options.template
    if (template) {
        ...   //有template就使用idToTemplate()解析,最终返回该节点的innerHTML
      } if (typeof template === 'string') {
        if (template.charAt(0) === '#') {//如果模板取到的第一个字符是#
          template = idToTemplate(template)
          if (process.env.NODE_ENV !== 'production' && !template) {//开发环境并且解析模板失败的报错:警告模板为空或者未找到
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      }else if (template.nodeType) {
        //如果有节点类型,判定是普通节点,也返回innerHTML
        template = template.innerHTML  
      } else {  
        //没有template就警告该模板无效
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
        //如果是节点的话,获取html模板片段,getOuterHTML()对传入的el元素做了兼容处理,最终目的是拿到节点的outerHTML
        //getOuterHTML()可以传入DOM节点,CSS选择器,HTML片段
      template = getOuterHTML(el)
    }
    if (template) {
     //编译HTML生成renderFn,赋给options,vm.$options.render此时发生变化
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        //开始标记
        mark('compile')
      }
      /*  compileToFunctions()主要是将getOuterHTML获取的模板编译成RenderFn函数,该函数的具体请往后翻看
       *  具体步骤之后再说,编译大致主要分成三步 
       *  1.parse:将 html 模板解析成抽象语法树(AST)。
       *  2.optimizer:对 AST 做优化处理。
       *  3.generateCode:根据 AST 生成 render 函数。
       */ 
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render;  //最后将解析的renderFn 赋值给当前实例
      options.staticRenderFns = staticRenderFns //编译的配置
      
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        //结束标记
        mark('compile end')
        //根据mark()编译过程计算耗时差,用于到控制台performance查看阶段渲染性能
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
    //最后返回之前储存的mount()方法进行挂载,如果此前renderFn存在就直接进行此步骤
    return mount.call(this, el, hydrating)
}

Самое главное здесьcompileToFunctions()Скомпилируйте шаблон в RenderFn, просмотрите этот метод через переход по каталогу.

Этот код абзацаtemplateСовместимость с различными методами письма и, наконец, получитьrenderFn, и выполнять вспомогательные функции, такие как скрытые точки производительности в процессе. наконецreturn mount.call(...)это в

  import Vue from './runtime/index'

Процесс компиляции более сложен, о чем будет рассказано позже. На данный момент обнаруживается, что метод прототипа Vue здесь не установлен, нам нужно перейти на верхний уровень.src/platforms/runtime/index.js,

// 配置了一些全局的方法
Vue.config.mustUseProp = mustUseProp 
Vue.config.isReservedTag = isReservedTag 
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// 安装平台的指令和组件
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

// 如果在浏览器里,证明不是服务端渲染,添加__patch__方法
Vue.prototype.__patch__ = inBrowser ? patch : noop

// 挂载$mount方法。
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
    //必须在浏览器环境才返回该节点,runtime-only版本会直接运行到这
  el = el && inBrowser ? query(el) : undefined
  
  return mountComponent(this, el, hydrating)
}

hydratingЭтот параметр можно понимать глобально как рендеринг на стороне сервера, и по умолчанию он равен false. прошлойmountComponent(this, el, hydrating)Фактически, этоupdateа такжеwatcherпроцесс. ПодробнееmountComponentЧто вы наделали. оказатьсяsrc/core/instance/lifecycle.js, этот файл отвечает за добавление функций класса жизненного цикла для экземпляра.

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean 
): Component {
  vm.$el = el  //首先将vm.$el将传入的el做缓存,$el现在为真实的node
  if (!vm.$options.render) {
    //因为最后只认renderFn,如果没有的话,就创建一个空节点Vnode
    vm.$options.render = createEmptyVNode
    
    if (process.env.NODE_ENV !== 'production') {//开发环境下
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        /*
         (如果定义了template但是template首位不是'#')或者(没有传入element),就会警告当前使用的是runtime-only版本,
         默认不带编译功能,如果需要编译的话,则需要更换构建版本,下面类似
         */
      } else {
        warn(//挂载组件失败:template或者renderFn未定义
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  // 在挂载之前为当前实例初始化beforMount生命周期
  callHook(vm, 'beforeMount');
  
  // 声明了一个 updateComponent 方法,这个是将要被 Watcher实例调用的更新组件的方法。
  // 根据性能的对比配置不同的更新方法,
  // performance+mark可以用于分析Vue组件在不同阶段中花费的时间,进而知道哪里可以优化。
  let updateComponent 
  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();//生成一个Vnode
      mark(endTag);//标记结束节点
      
      
      //做performance命名'vue ${name} render',这样就可以在proformance中查看应用程序的运行状况、渲染性能,最后删除标记和度量
      measure(`vue ${name} render`, startTag, endTag);
     
      mark(startTag);
      vm._update(vnode, hydrating);
     
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag);
    }
  } else {
    updateComponent = () => {
    // 定义一个渲染watcher函数
    // vm._render()里会调用render函数,并返回一个VNode,在生成VNode的过程中,会动态计算getter,同时推入到dep里面进行数据监听,每次数据更新后都出触发当前实例的_updata进行组件更新
    // _update()方法会将新vnode和旧vnode进行diff比较,最后完成dom的更新工作,该方法请往下移步
      vm._update(vm._render(), hydrating)
    }
  }
  /* 新建一个_watcher对象,将监听目标推入dep,vm实例上挂载的_watcher主要是为了更新DOM调用当前vm的_watcher 的 update 方法。用来强制更新。为什么叫强制更新呢?
   * vue里面有判断,如果newValue == oldValue, 那么就不触发watcher更新视图了
   * vm:当前实例
   * updateComponent:用来将vnode更新到之前的dom上
   * noop:无效函数,可以理解为空函数
   * {before(){...}}:配置,如果该实例已经挂载了,就配置beforeUpdate生命周期钩子函数
   * true:主要是用来判断是哪个watcher的。因为computed计算属性和如果你要在options里面配置watch了同样也是使用了 new Watcher ,加上这个用以区别这三者
   */
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true )
  hydrating = false  //关闭服务端渲染,服务端渲染只有created()和beforeCreate()
  
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

Конкретной функцией этой функции является монтирование узла и выполнение оперативной обработки данных. Что касается того, почему есть заявление о суждении, которое нужно объявить в соответствии с условиемupdateComponentметод, на самом деле по производительности видно, что один из методов используется для проверкиrenderа такжеupdateпредставление. удобно вChrome=>performanceПросмотр производительности рендеринга в

process.env.NODE_ENV !== 'production' && config.performance && mark

Сначала определите текущую среду и поддерживает ли конфигурацияperformance, а затем позвонитеmarkа такжеmeasureметод, гдеmarkИнкапсулирует метод, на который может ссылаться конкретный API.MDN performance, Сделайте отметку на текущем элементе, а затем вернитесь к определенному моменту времени, основная функция - производительность скрытой точки

if (process.env.NODE_ENV !== 'production') {
    //判断当前浏览器runtime是否支持performace
  const perf = inBrowser && window.performance
  if (
    perf &&
    perf.mark &&
    perf.measure &&
    perf.clearMarks &&
    perf.clearMeasures
  ) {
    mark = tag => perf.mark(tag);//标记该节点
    measure = (name, startTag, endTag) => {
      perf.measure(name, startTag, endTag)
      //作性能埋点后,删除所有的标记和度量
      perf.clearMarks(startTag)
      perf.clearMarks(endTag)
      perf.clearMeasures(name)
    }
  }
}

Как раз сейчасvm._update()наверхуlifecyle.jsуже определено

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    //首先接收vnode
    const vm: Component = this
    const prevEl = vm.$el;//真实的dom节点
    const prevVnode = vm._vnode;//之前旧的vnode
    const prevActiveInstance = activeInstance;// null
    activeInstance = vm;//获取当前的实例
    vm._vnode = vnode;//当前新的vnode 
    if (!prevVnode) {
      // 如果需要diff的旧vnode不存在,就无法进行__patch__
      // 因此需要用新的vnode创建一个真实的dom节点
      vm.$el = vm.__patch__(
                        vm.$el, //真实的dom节点
                        vnode,  //传入的vnode
                        hydrating, //是否服务端渲染
                        false /* removeOnly是一个只用于 <transition-group> 的特殊标签,确保移除元素过程中保持一个正确的相对位置。 */)
    } else {
      // 如果需要diff的prevVnode存在,那么首先对prevVnode和vnode进行diff
      // 并将需要的更新的dom操作已patch的形式打到prevVnode上,并完成真实dom的更新工作
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    
    activeInstance = prevActiveInstance;//
    // 如果存在真实的dom节点
    if (prevEl) {
      //就将之前的__vue__清空,再挂载新的
      prevEl.__vue__ = null
    }
    // 将更新后的vm挂载到的vm__vue__上缓存
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // 如果当前实例的$vnode与父组件的_vnode相同,也要更新其$el
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }

Как пропатчить?

__patch__это всеvirtaul-domСреди них самый основной метод, основная функция которого заключается вprevVnode(旧vnode)а также新vnodeпровестиdiffпроцесс, черезpatchСравните и, наконец, создайте новый узел реального дома, чтобы обновить вид измененной части. существует/packages/factory.js, определяетpatch(), кода слишком много, извлекаются только важные части, в настоящее время процесс ясен, vue2.0+ является эталономsnabbdomУстановленный алгоритм исправления виртуального дома

return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    //用到的参数,oldVnode:旧的vnode、vnode:新的vnode、hydrating:服务端渲染、removeOnly:避免误操作
    //当新的vnode不存在,并且旧的vnode存在时,直接返回旧的vnode,不做patch
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
      return
    }
    var insertedVnodeQueue = [];

    //如果旧的vnode不存在
    if (isUndef(oldVnode)) {
      //就创建一个新的节点
      createElm(vnode, insertedVnodeQueue, parentElm, refElm);
    } else {
      //获取旧vnode的节点类型
      var isRealElement = isDef(oldVnode.nodeType);
      // 如果不是真实的dom节点并且属性相同
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // 对oldVnode和vnode进行diff,并对oldVnode打patch
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
      } 
      }
    }
    //最后返回新vnode的节点内容
    return vnode.elm
  }

Это базовый патч, предназначенный для/src/core/vdom/patch.jsизpatchVnode(), и черезsameVnode()можно заранее сравнить旧vnodeа также新vnodeОсновные свойства двух, этот метод определяет, будет ли следующийoldVnodeа такжеvnodeпровестиdiff

function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
  )
}

Это рассматривается только в том случае, если базовые свойства одинаковы.2个vnodeПроисходит только локальное обновление, а потом это2个vnodeразница, если2个vnodeЕсть несоответствие в основных свойствах , тогда оно будет пропущено напрямуюdiffпроцесса, а затем на основеvnodeсоздать новый真实dom, удаляя при этом старые узлы. При рендеринге в первый разoldVnodeне существует, поэтому действуйте напрямуюdomcreateElm(vnode, insertedVnodeQueue, parentElm, refElm);Создает новый узел, вместо этого существуетoldVnode,когдаoldVnodeа такжеvnodeсуществуют иsameVnode(oldVnode, vnode)Основные атрибуты двух узлов одинаковы, поэтому вводится процесс сравнения двух узлов.

существует/src/core/vdom/patch.jsОпределите функцию patchVnode в

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    /* 
    * 比较新旧vnode节点,根据不同的状态对dom做合理的更新操作(添加,移动,删除)整个过程还会依次调用prepatch,update,postpatch等钩子函数,在编译阶段生成的一些静态子树
    * 在这个过程中由于不会改变而直接跳过比对,动态子树在比较过程中比较核心的部分就是当新旧vnode同时存在children,通过updateChildren方法对子节点做更新,
    * @param oldVnode 旧vnode
    * @param vnode    新vnode
    * @param insertedVnodeQueue  空数组,用于生命周期 inserted 阶段,记录下所有新插入的节点以备调用
    * @param removeOnly 是一个只用于 <transition-group> 的特殊标签,确保移除元素过程中保持一个正确的相对位置。
    */
    if (oldVnode === vnode) {
      return
    }
    
    const elm = vnode.elm = oldVnode.elm
    // 异步占位
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    
    //如果新vnode和旧vnode都是静态节点,key相同,或者新vnode是一次性渲染或者克隆节点,那么直接替换该组件实例并返回
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    // 可以往下翻去看vnode的例子,data是节点属性,包含class style attr和指令等
    let i
    const data = vnode.data
    // 如果组件实例存在属性并且存在prepatch钩子函数就更新attrs/style/class/events/directives/refs等属性
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    //如果新的vnode带有节点属性,isPatchable返回是否含有组件实例的tag标签,两者满足
    if (isDef(data) && isPatchable(vnode)) {
      // cbs保存了hooks钩子函数: 'create', 'activate', 'update', 'remove', 'destroy'
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 取出cbs保存的update钩子函数,依次调用,更新attrs/style/class/events/directives/refs等属性
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    //如果vnode没有文本节点
    if (isUndef(vnode.text)) {
      //如果旧vnode和新vnode的子节点都存在
      if (isDef(oldCh) && isDef(ch)) {
        // 如果子节点不同,updateChildren就对子节点进行diff
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        //如果只存在新vnode
      } else if (isDef(ch)) {
        // 先将旧节点的文本清空
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // 然后将vnode的children放进去
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        // 如果只存在旧vnode
      } else if (isDef(oldCh)) {
        // 就删除elm下的oldchildren
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        // 如果只有旧vnode的文本内容
      } else if (isDef(oldVnode.text)) {
        // 直接清空内容
        nodeOps.setTextContent(elm, '')
      }
      // 如果是两者文本内容不同
    } else if (oldVnode.text !== vnode.text) {
      // 直接更新vnode的文本内容
      nodeOps.setTextContent(elm, vnode.text)
    }
    // 更新完毕后,执行 data.hook.postpatch 钩子,表明 patch 完毕
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

Используйте ловушки для обновления свойств узла, сравнивая новые свойства старого узла VNODE, дочерние элементы, типы узлов и содержимое. Логика Некоторые комментарии были добавлены в исходный код для понимания и логики.

  1. Если два vnode равны, нет необходимости исправлять.
  2. Если это асинхронный заполнитель, выполнитеhydrateметод или определениеisAsyncPlaceholderверно, то выходит.
  3. Если оба vnode являются статическими, их не нужно обновлять, поэтому предыдущийcomponentInstanceЭкземпляр передается текущему vnode. патч выхода
  4. воплощать в жизньprepatchкрюк.
  5. Перебрать и вызвать обратный вызов обновления по очереди, выполнитьupdateкрюк. Обновите атрибуты, такие как attrs/style/class/events/directives/refs.
  6. Если оба vnode имеют дочерние элементы, а vnode не имеет текстового содержимого, и два vnode не равны, выполнитеupdateChildrenметод. Это суть виртуального DOM.
  7. Если у нового vnode есть дочерние элементы, а у старого нет, очистите текст и добавьте узел vnode.
  8. Если у старого vnode есть дети, а у нового нет, очистите текст и удалите vnode.
  9. Если ни один vnode не имеет дочерних элементов, старый vnode имеет текст, а новый vnode не имеет текста, очистите текстовое содержимое DOM.
  10. Если текст старого vnode и нового vnode отличаются, обновите текстовое содержимое элемента DOM.
  11. передачаpostpatchХук говорит, что патч сделан.

updateChildren

он немного круглый

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    /*
     * @ parentElm 父元素
     * @ oldCh  旧子节点
     * @ newCh  新子节点
     * @ insertedVnodeQueue 记录下所有新插入的节点以备调用
     * @ removeOnly 是仅由<transition-group>使用的特殊标志,在离开过渡期间,确保删除的元素保持正确的相对位置
     */
    let oldStartIdx = 0  //oldStartIdx => 旧头索引
    let newStartIdx = 0   //newStartIdx => 新头索引
    let oldEndIdx = oldCh.length - 1 //oldEndIdx => 旧尾索引
    let oldStartVnode = oldCh[0] // 旧首索引节点,第一个
    let oldEndVnode = oldCh[oldEndIdx] // 旧尾索引节点,最后一个
    let newEndIdx = newCh.length - 1 //newEndIdx => 新尾索引
    let newStartVnode = newCh[0] // 新首索引节点,第一个
    let newEndVnode = newCh[newEndIdx] // 新首索引节点,最后一个

    // 可以理解为
    // 1. 旧子节点数组的 startIndex, endIndex, startNode, endNode
    // 2. 新子节点数组的 startIndex, endIndex, startNode, endNode

    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    //可以进行移动
    const canMove = !removeOnly  

    if (process.env.NODE_ENV !== 'production') {
      //首先会检测新子节点有没有重复的key
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]

        //如果旧首索引节点和新首索引节点相同
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        //对旧头索引节点和新头索引节点进行diff更新, 从而达到复用节点效果
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        //旧头索引向后
        oldStartVnode = oldCh[++oldStartIdx]
        //新头索引向后
        newStartVnode = newCh[++newStartIdx]
                //如果旧尾索引节点和新尾索引节点相似,可以复用
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        //旧尾索引节点和新尾索引节点进行更新
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        //旧尾索引向前
        oldEndVnode = oldCh[--oldEndIdx]
        //新尾索引向前
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        /*  有一种情况,如果
          * 旧【5,1,2,3,4】
          * 新【1,2,3,4,5】,那岂不是要全删除替换一遍 5->1,1->2...?
          * 即便有key,也会出现[5,1,2,3,4]=>[1,5,2,3,4]=>[1,2,5,3,4]...这样太耗费性能了
          * 其实我们只需要将5插入到最后一次操作即可
        */
        // 对旧首索引和新尾索引进行patch
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 旧vnode开始插入到真实DOM中,旧首向右移,新尾向左移
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 同上中可能,旧尾索引和新首也存在相似可能
        // 对旧首索引和新尾索引进行patch
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 旧vnode开始插入到真实DOM中,新首向左移,旧尾向右移
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        //如果上面的判断都不通过,我们就需要key-index表来达到最大程度复用了
         //如果不存在旧节点的key-index表,则创建
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
         //找到新节点在旧节点组中对应节点的位置
        idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
          //如果新节点在旧节点中不存在,就创建一个新元素,我们将它插入到旧首索引节点前(createElm第4个参数)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 如果旧节点有这个新节点
          vnodeToMove = oldCh[idxInOld]
            // 将新节点和新首索引进行比对,如果类型相同就进行patch
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            // 然后将旧节点组中对应节点设置为undefined,代表已经遍历过了,不在遍历,否则可能存在重复插入的问题
            oldCh[idxInOld] = undefined
            // 如果不存在group群体偏移,就将其插入到旧首节点前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // 类型不同就创建节点,并将其插入到旧首索引前(createElm第4个参数)
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        //将新首往后移一位
        newStartVnode = newCh[++newStartIdx]
      }
    }
    //当旧首索引大于旧尾索引时,代表旧节点组已经遍历完,将剩余的新Vnode添加到最后一个新节点的位置后
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } //如果新节点组先遍历完,那么代表旧节点组中剩余节点都不需要,所以直接删除
      else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

Vnode

существует/src/core/vdom/vnode.jsЕсть определенные свойства Vnode

export default class VNode {
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag //标签属性
    this.data = data  //渲染成真实DOM后,节点上到class attr style 事件等...
    this.children = children //子节点,也上vnode
    this.text = text  // 文本
    this.elm = elm  //对应着真实的dom节点
    this.ns = undefined //当前节点的namespace(命名空间)
    this.context = context //编译的作用域
    this.fnContext = undefined // 函数化组件上下文
    this.fnOptions = undefined // 函数化组件配置项
    this.fnScopeId = undefined // 函数化组件ScopeId
    this.key = data && data.key  //只有绑定数据下存在,在diff的过程中可以提高性能
    this.componentOptions = componentOptions // 通过vue组件生成的vnode对象,若是普通dom生成的vnode,则此值为空
    this.componentInstance = undefined  //当前组件实例
    this.parent = undefined // vnode、组件的占位节点
    this.raw = false    //是否为原生HTML或只是普通文本
    this.isStatic = false  //静态节点标识 || keep-alive
    this.isRootInsert = true    // 是否作为根节点插入
    this.isComment = false  // 是否为注释节点
    this.isCloned = false  //是否为克隆节点
    this.isOnce = false    //是否为v-once节点
    this.asyncFactory = asyncFactory // 异步工厂方法
    this.asyncMeta = undefined //异步Meta
    this.isAsyncPlaceholder = false //是否为异步占位

  }

  //容器实例向后兼容的别名
  get child (): Component | void {
    return this.componentInstance
  }
}

Другие атрибуты не важны. Наиболее важными атрибутами являются тег, данные, дочерние элементы, ключ и текст. VNode может относиться к следующим категориям

  • TextVNode Текстовый узел.
  • ElementVNode Обычный узел элемента.
  • ComponentVNode Узел компонента.
  • EmptyVNode Узел комментария без содержимого.
  • Узел клона CloneVNode, который может быть любым из вышеперечисленных типов узлов, с той лишь разницей, что атрибут isCloned имеет значение true. Сначала мы определяем vnode
 {
    tag: 'div'
    data: {
        id: 'app',
        class: 'test'
    },
    children: [
        {
            tag: 'span',
            data:{
                
            },
            text: 'this is test'
        }
    ]
}

Каждый слой объекта является узлом. вне

    {
        tag:'标签1',
        attrs:{
            属性key1:属性value1,
            属性key2:属性value2,
            ...
        },
        children:[
            {
                tag:'子标签1',
                attrs:{
                    子属性key1:子属性value1,
                    子属性key2:子属性value2,
                    ...
                },
                children:[
                    {
                        ....
                    }
                ]
            },
            {
                tag:'子标签2',
                attrs:{
                    子属性key1:子属性value1,
                    子属性key2:子属性value2,
                    ...
                },
                children:[
                    {
                        ....
                    }
                ]
            }
        ]
    }

Вложенный рекурсивный способ получения окончательного рендеринга

<div id="app" class="test">
    <span>this is test</span>
</div>

Все дерево VNode, созданное деревом компонентов Vue, уникально. Это означает, что написанные от руки функции рендеринга не могут быть разделены на компоненты.

render: function (createElement) {
  var myVnode = createElement('p', 'hi')
  return createElement('div', [
    myVnode, myVnode
  ])
}

Официальный способ - использовать фабричную функцию для

render: function (createElement) {
  return createElement('div',
    Array.apply(null, { length: 20 }).map(function () {
      return createElement('p', 'hi')
    })
  )
}

почему ты хочешь сделать это? Понимание автора заключается в том, что createElement создает дочерний объект Vnode.В это время Vnode уже уникален.Если вы будете использовать его повторно в компоненте, он не будет уникальным.

Смотри, есть способ. объясните кстати Может быть, вы думаете, что это слишком хлопотно, чтобы писать так прямоArray(20).map()Как легко. но

new Array(20).map(function(v,i){
	console.log(v,i);//不会输出任何东西,
})

map последовательно проходит только элементы индекса со значениями (включая undefined) и, наконец, последовательно формирует массив, потому чтоnew Array(20)Если значение в массиве не инициализировано, результат вывода на печать[ empty * 20]. Таким образом, после карты ничего не будет напечатано, потому что она не инициализирована, пуста и игнорируется.

Однако второй способ,Array.apply(null, {length: 20}), выход[undefined,undefined,undefined....*20], является значением, которое было инициализировано и содержит 20undefinedмассив . Плюсmap(), то есть каждый разArray.apply(null,[undefined,undefined,.....], немного более знакомоArray(undefined,undefined,...*20), создайте 20 vnodes, зациклив return to createElement

Почему так сложно писать? Array.from ES6 может это сделать, но автору следует подумать о совместимости или о том, что можно сделать с ES5. Вздох, отличные базовые навыки...

compileToFunctions (шаблон компилируется в рендер)

первый в/src/platforms/web/compiler/index.jsопределенныйcompileToFunctions()метод,

// 设置编译的选项,不设置则使用默认配置,配置项比较多
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
// 通过模板导入配置生成AST和Render
const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

Первый взгляд на импортированную конфигурацию

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  isPreTag,
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}

вы можете увидеть определениеcompileа такжеcompileToFunctions, первое — это синтаксическое дерево AST, второе — скомпилированный файл renderFn.

import { parse } from './parser/index' // 将 HTML template解析为AST
import { optimize } from './optimizer'  // 对AST优化标记处理,提取最大的静态树
import { generate } from './codegen/index' // 根据 AST 生成 render 函数
import { createCompilerCreator } from './create-compiler' //允许创建使用替代编译器,在这只使用默认部件导出默认编译器

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // parseHTML 的过程,导入配置,将template去掉空格,解析成AST ,最后返回AST元素对象
  const ast = parse(template.trim(), options)
  console.log(ast)
  
  // 默认开始优化标记处理,否则不进行优化
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 拿到最终的code。里面包含renderFn和静态renderFn
  const code = generate(ast, options)
  console.log(code.render)
  
  //抛出
  return { 
    ast, 
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

createCompilerCreator()принимает параметр функции,createCompilerИспользуется для создания компилятора, возвращаемое значениеcompileтак же какcompileToFunctions.

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,//模板
      options?: CompilerOptions // 编译配置
    ): CompiledResult {
    
    // 将finalOptions的隐式原型__proto__指向baseOptions对象
      const finalOptions = Object.create(baseOptions) 
      const errors = []
      const tips = []
      
      finalOptions.warn = (msg, tip) => {
        (tip ? tips : errors).push(msg)
      }
      
      // 如果导入了配置就将配置进行合并  
      if (options) {
        // 合并分支模块
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        // 合并自定义指令
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // 合并其他配置
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }
      // 将传入的函数执行,传入模板和配置项,得到编译结果
      const compiled = baseCompile(template, finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        errors.push.apply(errors, detectErrors(compiled.ast))
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }
    
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

Наконец вcompile()После выполнения уровня будет выброшена функция компиляции

compileкомпилятор, который будетtemplateпреобразовать в соответствующийASTДерево,renderFnтак же какstaticRenderFnsфункция

compileToFunctions, выполнивcreateCompileToFunctionFn(compile)получать,createCompileToFunctionFn()является кэшированным компилятором, аstaticRenderFnsтак же какrenderFnбудет преобразован вFuntion对象. в конечном итоге скомпилирует

Разные платформы имеют некоторые отличияoptions,такcreateCompilerСогласно платформе, один будет передан вbaseOptions, будет сcompileпрошел сам по себеoptionsобъединиться, чтобы получить финалfinalOptions.

export function createCompileToFunctionFn (compile: Function): Function {
  // 声明缓存器
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 合并配置
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn
    //开发环境下尝试检测CSP,类似于用户浏览器设置,需要放宽限制否则无法进行编译,一般情况下可以忽略
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an ' +
            'environment with Content Security Policy that prohibits unsafe-eval. ' +
            'The template compiler cannot work in this environment. Consider ' +
            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
            'templates into render functions.'
          )
        }
      }
    }
    //有缓存的时候优先读取缓存的结果,并且返回 ,
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // 没有缓存结果则直接编译 
    const compiled = compile(template, options)

    // 检查编译错误/提示 
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        warn(
          `Error compiling template:\n\n${template}\n\n` +
          compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
          vm
        )
      }
      if (compiled.tips && compiled.tips.length) {
        compiled.tips.forEach(msg => tip(msg, vm))
      }
    }

    // 将代码转换成功能 
    const res = {}
    const fnGenErrors = []
    // 将render转换成Funtion对象
    res.render = createFunction(compiled.render, fnGenErrors)
    // 将staticRenderFns全部转化成Funtion对象
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    //检查函数生成错误。只在编译器本身存在错误时才会发生,作者主要用于codegen开发使用
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\n\n` +
          fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
          vm
        )
      }
    }
    //最后存放在缓存中,下一次用可以进行读取
    return (cache[key] = res)
  }
}

Здесь есть кое-что интересное,

const cache = Object.create(null)

почему не напрямуюconst cache = {}Шерстяная ткань? мы чувствуем

Самое интуитивное ощущение, что неявный прототип __proto__ не равен нулю.const cache = {}унаследуетObject.prototypeВсе методы-прототипы выше. в то время как null не будет, другой используетObject.create(null)Причина в том, что использованиеfor..inВ цикле он проходит свойства в цепочке прототипов объектов, используяObject.create(null)Конечно, больше нет необходимости проверять свойства, мы также можем напрямую использовать Object.keys[]. Если вы не думаете, что вам нужен очень чистый и настраиваемый объект в качестве словаря данных или вы хотите сохранитьhasOwnPropertyнекоторая потеря производительности.

HTML для RenderFn

Давайте сначала напишем код

<div id="app"></div>
  <script>
      var vm = new Vue({
        el:'#app',
        template:`
          <div @click="changeName()">
            <span>{{name}}</span>
            <ul>
              <li v-for="(item,index) in like" :key="index">{{item}}</li>
            </ul>
          </div>`,
        data:{
          name:'Seven',
          like:['旅游','电影','滑雪']
        },methods:{
          changeName(){
            this.name = 'Floyd'
          }
        }
      })
    </script>

Давайте сначала посмотрим на его синтаксическое дерево AST,

Может быть, у вас немного кружится голова, все в порядке, нам не нужно заботиться об этом, абстракция, если вы можете ее понять, это называется абстракцией? Давайте снова посмотрим на функцию рендеринга

with(this){return _c('div',{on:{"click":function($event){changeName()}}},[_c('span',[_v(_s(name))]),_v(" "),_c('ul',_l((like),function(item,index){return _c('li',{key:index},[_v(_s(item))])}))])}

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

with(this) {
      return _c('div', 
                {
                  on: {
                    "click": function ($event) {
                      changeName()
                    }
                  }
                }, 
                [
                  _c('span', [ _v(_s(name)) ]), 
                  _v(" "), 
                  _c('ul', 
                      _l( (like), function (item, index) {
                      return _c('li', 
                                  {
                                    key: index
                                  }, 
                                  [
                                    _v( _s(item) )
                                  ]
                                )
                    })
                  )
                ]
              )
    }

Может быть, некоторые люди думают, что они не могут понять большего, но ничего страшного, эту логику можно понять.

_c(
    '标签名',
    {
        on:{//绑定
            属性1:值,
            属性2:值,
            ...
        }
    },
    [//子节点
       _c(
            '标签名',
            {
                on:{//绑定
                    子属性1:值,
                    子属性2:值,
                    ...
                }
            },
            [
                //子标签...
            ]
        }
    ]
)

Скомпилируйте renderFn в Vnode

Из-за используемого синтаксиса with(this) все переменные в функции зависят от переменной this,_cэквивалентноthis._cэквивалентноvm._c, мы печатаемvm._c

существуетСуть языка JavaScriptКак упоминалось в книге, старайтесь не использовать внутри своей функцииwith()синтаксис, это может сделать ваше приложение невозможным для отладки. Но You Yuxi использовал это так, используя замыкание, чтобы инкапсулировать его в функцию, так что не нужно беспокоиться об утечке.

ƒ (a, b, c, d) { return createElement(vm, a, b, c, d, false); }

/src/core/instance/render.js定义该方法
// 将 createElement 函数绑定到这个实例上以便在其中获得renderFn上下文。
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

направлениеcreateElement()функция, которая, в свою очередь, указывает на_createElement(), функция определена в/src/core/vdom/create-element.js. Окончательный возврат — это Vnode. Определение функции можно просмотреть в каталоге этой статьи. Другие функции, которые мы можем использовать в/rc/core/instance/render-helper/index.jsнайти определение в

export function installRenderHelpers (target: any) {
  target._o = markOnce   // v-once静态组件
  target._n = toNumber   // 判断是否数字,先parse再isNAN
  target._s = toString   // 需解析的文本,之前在parser阶段已经有所修饰
  target._l = renderList //  v-for节点
  target._t = renderSlot // slot节点
  target._q = looseEqual //  检测两个变量是否相等
  target._i = looseIndexOf // 检测数组中是否包含与目标变量相等的项
  target._m = renderStatic // 渲染静态内容
  target._f = resolveFilter // filters处理
  target._k = checkKeyCodes // 从config配置中检查eventKeyCode是否存在
  target._b = bindObjectProps // 合并v-bind指令到VNode中
  target._v = createTextVNode  // 创建文本节点
  target._e = createEmptyVNode // 注释节点
  target._u = resolveScopedSlots // 处理ScopedSlots
  target._g = bindObjectListeners // 处理事件绑定
}

createElement

var SIMPLE_NORMALIZE = 1;
var ALWAYS_NORMALIZE = 2;

function createElement (
  context,
  tag,
  data,
  children,
  normalizationType,
  alwaysNormalize
) {
  // 兼容不传data的情况
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children;
    children = data;
    data = undefined;
  }
  // 如果alwaysNormalize是true
  // 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE;
  }
   // 调用_createElement创建虚拟节点
  return _createElement(context, tag, data, children, normalizationType)
}

function _createElement (
  context,
  tag,
  data,
  children,
  normalizationType
) {
   /*
    * 如果存在data.__ob__,说明data是被Observer观察的数据
    * 不能用作虚拟节点的data
    * 需要抛出警告,并返回一个空节点
    * 
    * 被监控的data不能被用作vnode渲染的数据的原因是:data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作
    * 
    */
  if (isDef(data) && isDef((data).__ob__)) {
    "development" !== 'production' && warn(
      "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
      'Always create fresh vnode data objects in each render!',
      context
    );
    return createEmptyVNode()
  }
  // 当组件的is属性被设置为一个false的值
  if (isDef(data) && isDef(data.is)) {
    tag = data.is;
  }
  // Vue将不会知道要把这个组件渲染成什么,所以渲染一个空节点
  if (!tag) {
    return createEmptyVNode()
  }
  // 如果key是原始值,就警告key不能是原始值,必须string或者是number类型的值
  if ("development" !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      );
    }
  }
    // 作用域插槽
    // 如果子元素是数组并且第一个是renderFn,就将其转移到scopedSlots
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {};
    data.scopedSlots = { default: children[0] };
    children.length = 0;
  }
  // 根据normalizationType的值,选择不同的处理方法
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children);
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children);
  }
  var vnode, ns;
  //如果标签名是string类型
  if (typeof tag === 'string') {
    var Ctor;
    // 取到如果当前有自己的vnode和命名空间 或者 获取标签名的命名空间
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
    // 判断是否为保留标签
    if (config.isReservedTag(tag)) {
       // 如果是保留标签,就创建一个这样的vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      );// 如果不是保留标签,那么我们将尝试从vm实例的components上查找是否有这个标签的定义,自定义组件
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
        //  如果找到了这个标签的定义,就以此创建虚拟组件节点
      vnode = createComponent(Ctor, data, context, children, tag);
    } else {
       // 保底方案,正常创建一个vnode
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      );
    }
  } else {
    // direct component options / constructor
    // 当tag不是字符串的时候,就是组件的构造类,直接创建
    vnode = createComponent(tag, data, context, children);
  }
  // 如果vnode是数组,直接返回。
  if (Array.isArray(vnode)) {
    return vnode
              //如果有vnode
  } else if (isDef(vnode)) {
     // 如果有namespace,就应用下namespace,然后返回vnode
    if (isDef(ns)) { applyNS(vnode, ns); }
    // 如果定义了数据,就将其深度遍历,针对于class或者是style
    if (isDef(data)) { registerDeepBindings(data); }
    return vnode
  } else {
    //保底创建空VNode
    return createEmptyVNode()
  }
}

Посмотрите блок-схему

new Vue

оказатьсяsrc/core/instance/index.js

Создайте функцию Vue и определите, будет ли текущая среда разработки предупреждать, если Vue не создается экземпляром new. затем инициализируйтеthis._init(options). Почему (этот экземпляр Vue) может определить, используется ли новый оператор?

Вызов конструктора с новым проходит через 4 шага:

  • создать новый объект;
  • назначьте область конструктора новому объекту (чтобы это указывало на новый объект);
  • выполнить код в конструкторе (добавить свойства к этому новому объекту);
  • Вернуть новый объект. а такжеinstanceofИспользуется для обнаружения конструкторов Vue.prototypeСуществует ли вthisв цепочке прототипов, другими словами, если вы используетеnewПри создании экземпляраthisуказывает на этот вновь созданный объект, затемthis instanceof VueСмысл этого предложения в том, чтобы оценить, относится ли вновь созданный объект к типу Vue, что эквивалентно оценке нового экземпляра объекта.constructorЭто конструктор Vue.

Продолжение следует...Постоянное обновление