🚩Исходный код Vue — как шаблоны и данные преобразуются в окончательный DOM

Vue.js
🚩Исходный код Vue — как шаблоны и данные преобразуются в окончательный DOM

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

предисловие

Почему вы спрашиваете исходный код Vue на собеседовании?Многие обычно задают этот вопрос.Исходный код Vue редко используется в обычной работе, а API Vue часто используется в работе. Если вы можете использовать Vue API, вы можете удовлетворить потребности работы. Разве это не собеседование для постройки ракеты и работа по завинчиванию гаек? На самом деле, я действительно не хочу спрашивать вас об исходном коде Vue, а просто использую исходный код Vue, чтобы оценить, надежна ли ваша основа JavaScript, например, владение рабочим механизмом JS, область действия, замыкание, цепочка прототипов, рекурсия, каррирование и другие знания. Для того, чтобы отличить уровень способностей интервьюируемого. так вВажно освоить исходный код фреймворка на собеседовании старших фронтенд-инженеров..

1. Как читать исходный код

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

1. Возьмитесь за основную линию, временно игнорируя ответвление

Рим не строился за один день, и код не писался за день. Любой код разрабатывается по сценарию приложения, и по мере того, как сценарий приложения постоянно добавляется, код постоянно совершенствуется. Например, чтобы понять, как шаблоны и данные рендерятся в окончательный DOM в Vue.Прежде всего, нам нужно написать простейшую демонстрацию, на основе этой заданной сцены для изучения.. В процессе исследования код, относящийся к заданной сцене, является основной линией, а код, не относящийся к заданной сцене, — ответвлением.

Демонстрационный код выглядит следующим образом

<!DOCTYPE html>
<html>
    <head>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    </head>
    <body>
        <div id="app">
          <p>{{aa}}<span>{{bb}}</span></p>
        </div>
    </body>
    <script>
        var app = new Vue({
            el: '#app',
            data(){
            	return{
                  aa:'欢迎',
                  bb:'Vue'
                }
            }
        })
    </script>
</html>

2. Используйте инструменты разработчика Chrome для отладки точек останова

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

3. Логическая схема

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

2. новый Вью()

Чтобы использовать Vue, сначалаnew Vue(), то Vue — это конструктор класса.

function Vue(options) {
    if (!(this instanceof Vue)) {
        warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
}

Вы можете видеть, что Vue можно вызывать только с ключевым словом new.Если он не вызывается с новым, в консоли будет напечатано предупреждение, означающее, что Vue является конструктором и должен вызываться с ключевым словом «new».

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

Три, это ._инит

Vue.prototype._init = function(options) {
    var vm = this;
    if (options && options._isComponent) {
    	//...
    } else {
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        );
    }
    initProxy(vm);
    initRender(vm);
    initState(vm);
    if (vm.$options.el) {
    	vm.$mount(vm.$options.el);
    }
}

воплощать в жизньmergeOptionsсливатьсяoptionsи различные инициализации в заданной сценеvm.$options.elценность#app, поэтому выполнитеvm.$mount(vm.$options.el),передачаvm.$mountМетод монтирует экземпляр в DOM, а затем анализирует процесс монтирования Vue.

существуетvm.$mount(vm.$options.el)Сделайте здесь точку останова и нажмите F11, чтобы войтиvm.$mountметод выполнен.

1. Параметры слияния

// 合并options
if (options && options._isComponent) {
} else {
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    );
}

вoptions._isComponentдляtrueВ этом смысл компонента, и понятно, что сеттинг не является компонентом, поэтомуfalse,идтиelseчасть кода.

оставить это в покоеmergeOptionsвнутренняя логика, только помните, что после слияния можно использоватьvm.$optionsдоступ черезnew Vue(options)входящие параметрыoptionsПросто сделай это.

2. инициализация прокси

function initProxy(vm) {
    if (hasProxy) {
        var options = vm.$options;
        var handlers = options.render && options.render._withStripped ? 
            getHandler : hasHandler;
        vm._renderProxy = new Proxy(vm, handlers);
    } else {
        vm._renderProxy = vm;
    }
}

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

Помните здесь,vmПосле выполнения прокси назначьте егоvm._renderProxy,доступvm._renderProxyдовольно доступныйvm, который впоследствии генерируетсяvnode(Виртуальный DOM).

3. initRender

function initRender(vm) {
    vm._vnode = null;
    vm._c = function(a, b, c, d) {
        return createElement(vm, a, b, c, d, false);
    };
    vm.$createElement = function(a, b, c, d) {
        return createElement(vm, a, b, c, d, true);
    };
}

Вспомним здесь, что определениеvm._cа такжеvm.$createElementДва метода, сгенерированные в последующемvnodeПолезно во время.

4. состояние инициализации()

function initState(vm) {
    vm._watchers = [];
    var 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,watch, в сет-сцене просто понять инициализациюdataпроцесс

на съемочной площадкеopts.dataсуществует, поэтому выполнитеinitData(vm).

function initData(vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function' ?
        getData(data, vm) :
        data || {};
    if (!isPlainObject(data)) {
        data = {};
        warn(
            'data functions should return an object:\n' +
            'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
            vm
        );
    }

    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    while (i--) {
        var key = keys[i]; {
            if (methods && hasOwn(methods, key)) {
                warn(//..);
            }
        }
        if (props && hasOwn(props, key)) {
            warn(//...);
        } else if (!isReserved(key)) {
            proxy(vm, "_data", key);
        }
    }
    observe(data, true /* asRootData */ );
}
function getData(data, vm) {
    pushTarget();
    try {
        return data.call(vm, vm)
    } catch (e) {
        handleError(e, vm, "data()");
        return {}
    } finally {
        popTarget();
    }
}
function 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);
}
new Vue({
    el: '#app',
    data() {
        return {
            aa: '欢迎',
            bb: 'Vue'
        }
    }
})

существуетinitData(vm)в, получитьvm.$optionsАтрибут DATA также является опцией Vue, если Data является функцией, сgetDataметод, чтобы получить возвращаемое значение и присвоить егоvm._data, опять такиvm._data, является ли это объектом и значение ключа не может совпадать с параметромprops, опцииmethodsКлюч тот же суд.

затем используйтеisReservedМетод параvm._dataданные фильтруются, а не$или_начать сproxyметод в качестве прокси для него, в котором используетсяObject.definePropertyПерехват данных, таких как доступvm.aa, на самом деле доступvm._data.aaПосле агента посетитеvmможет получить доступvm._dataценность .

Вот почему его можно использоватьvm.aaдоступ к определенным в опции данныхaa, имейте это в виду при созданииvnode(Виртуальный DOM) полезен в процессе.

4. vm.$mount

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el, hydrating) {
    el = el && query(el);
    if (el === document.body || el === document.documentElement) {
        warn("Do not mount Vue to <html> or <body> - mount to normal elements instead.");
        return this
    }
    var options = this.$options;
    if (!options.render) {
        var template = options.template;
        if (template) {
            //...
        } else if (el) {
            template = getOuterHTML(el);
        }
    }
    if (template) {
        var ref = compileToFunctions(template, {
            outputSourceRange: "development" !== 'production',
            shouldDecodeNewlines: shouldDecodeNewlines,
            shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
        }, this);
        var render = ref.render;
        var staticRenderFns = ref.staticRenderFns;
        options.render = render;
        options.staticRenderFns = staticRenderFns;
    }
    return mount.call(this, el, hydrating)
}

почему первыйVue.prototype.$mountназначить наmount, а затем переопределитьVue.prototype.$mount. Потому что$mountМетоды зависят от платформы и сборки. при разных обстоятельствах$mountМетоды разные. Итак, чтобы кэшировать исходный прототип$mountметод, определяющий различные$mountметод и, наконец, вызвать исходный прототип$mountметод.Эта технология называется перехватом функций..

1. скомпилировать функции

if (template) {
    var ref = compileToFunctions(template, {
        outputSourceRange: "development" !== 'production',
        shouldDecodeNewlines: shouldDecodeNewlines,
        shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
    }, this);
    var render = ref.render;
    options.render = render;
}

Не определено в заданной сценеrenderметод, который требуется в Vuerenderметод созданияvnode(Виртуальный DOM), поэтому используйтеcompileToFunctionsметод созданияrenderметод и смонтировать вvm.$options.

а такжеcompileToFunctionsметод, используйте шаблонtemplate, в заданной сцене шаблон не определенtemplateопределяется только цель монтированияel.那么要先通过elгенерироватьtemplate.

el = el && query(el);
if (el === document.body || el === document.documentElement) {
    warn("Do not mount Vue to <html> or <body> - mount to normal elements instead.");
    return this
}
var options = this.$options;
if (!options.render) {
    var template = options.template;
    if (template) {
        //...
    } else if (el) {
        template = getOuterHTML(el);
    }
}

использоватьqueryметод полученияelЗатем соответствующий объект DOM присваиваетсяel,правильноelПосле принятия решения Vue не может быть смонтирован на корневых узлах, таких как body и html.

this.$options.templateне существует, поэтому выполнитеtemplate = getOuterHTML(el),templateценностьelСоответствующий HTML-контент.

2. компонент монтирования

существуетvm.$mountпоследнее исполнениеreturn mount.call(this, el, hydrating), который вызывает$mountметод,Сделайте здесь точку останова, нажмите F11, чтобы войти в исходный прототип$mountметод.

Vue.prototype.$mount = function(el,hydrating) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating)
};

на оригинальном прототипе$mountметод, последний вызовmountComponentметод, параметрhydratingСвязанный с рендерингом на стороне сервера, в среде браузераfalse, можно игнорировать.

return mountComponent(this, el, hydrating)Сделайте здесь точку останова и нажмите F11, чтобы войтиmountComponentметод.

function mountComponent(vm,el,hydrating) {
    vm.$el = el;
    var updateComponent;
    updateComponent = function() {
        vm._update(vm._render(), hydrating);
    };
    new Watcher(vm, updateComponent, noop, {
        before: function before() {
            if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, 'beforeUpdate');
            }
        }
    }, true /* isRenderWatcher */ );
    hydrating = false;
    if (vm.$vnode == null) {
        vm._isMounted = true;
        callHook(vm, 'mounted');
    }
    return vm
}

В настоящее времяelобъект DOM цели монтирования, назначенныйvm.$el,Запомнить здесьvm.$elявляется объектом DOM цели монтирования экземпляра Vue..

Создайте экземпляр Watcher рендеринга, вторым параметром которого является значение функции обратного вызова. Здесь есть две функции: одна для выполнения функции обратного вызова во время инициализации, а другая — для выполнения функции обратного вызова при изменении отслеживаемых данных в экземпляре vm.

Функция обратного вызоваupdateComponentфункция, которая выполняет функцию обратного вызова, а не выполняетvm._update(vm._render(), hydrating), вvm._render()Как параметр он выполняется первым,vm._update(vm._render(), hydrating)Сделайте точку останова в методе и нажмите F11, чтобы войтиvm._render()метод.

Пять, VM._Render.

vm._renderОсновная функция – генерироватьvnode(Дерево виртуального DOM) и вернуться.

Vue.prototype._render = function(){
    var vm = this;
    var ref = vm.$options;
    var render = ref.render;
    var _parentVnode = ref._parentVnode;
    if (_parentVnode) {
        //...
    }
    vm.$vnode = _parentVnode;
    var vnode;
    try{
        vnode = render.call(vm._renderProxy, vm.$createElement);
    }catch(e){
        //...
    }
    return vnode
}

vm.$vnodeПредставляет родительский виртуальный DOM экземпляра Vue в заданной сцене,_parentVnodeне определено, поэтомуvm.$vnodeне определено.

renderметод находится вvm.$mountпрошедшийcompileToFunctionsсгенерированный метод, код выглядит следующим образом.

(function anonymous() {
    with(this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        }, [_c('p', [_v(_s(aa)), _c('span', [_v(_s(bb))])])])
    }
})

Роль оператора with заключается в том, чтобы указать объект по умолчанию для оператора или группы операторов, напримерwith(this){ a + b }эквивалентthis.a + this.b.

потому чтоrenderчерезcall(vm._renderProxy, vm.$createElement)звонить, такthisдаvm._renderProxy, представленный ранееinitProxyсерединаvm._renderProxyэквивалентvm, так вотthisто естьvm.

ТакrenderМетод также эквивалентен следующему коду

function (){
    return vm._c('div', {
            attrs: {
                "id": "app"
            }
        }, [vm._c('p', 
            [	
                vm._v(vm._s(vm.aa)), 
                vm._c('span', [vm._v(this._s(vm.bb))])
            ]
        )]
    )
}

vm._cвведенный ранееinitRenderопределено в , а также определеноvm.$createElement, не используется в заданной сцене.

vm._c = function(a, b, c, d) { return createElement(vm, a, b, c, d, false) }

vm._cа такжеvm.$createElementвнутренние звонкиcreateElementметод, единственная разница между ними заключается в том, что последний параметрtrueилиfalse;

vm._cОн используется для метода рендеринга, скомпилированного в шаблон,vm.$createElementОн используется написанным пользователем методом рендеринга.

const vm = new Vue({
    el:'#app',
    render: h => h(App)
})

в приведенном выше кодеhто естьvm.$createElement.

vnode = render.call(vm._renderProxy, vm.$createElement), сделайте здесь точку останова, дважды нажмите F11, чтобы ввестиcreateElementметод.

var SIMPLE_NORMALIZE = 1;
var ALWAYS_NORMALIZE = 2;
function createElement(context, tag, data, children, normalizationType, alwaysNormalize) {
    if (Array.isArray(data) || isPrimitive(data)) {
        normalizationType = children;
        children = data;
        data = undefined;
    }
    if (isTrue(alwaysNormalize)) {
        normalizationType = ALWAYS_NORMALIZE;
    }
    return _createElement(context, tag, data, children, normalizationType);
}
  • параметрcontext: Объект контекста, то есть это;
  • параметрtag: имя тега HTML, объект опций компонента;
  • параметрdata: объект данных узла;
  • параметрchildren: дочерние виртуальные узлы (VNodes), также строки могут использоваться для создания «текстовых виртуальных узлов».
  • параметрnormalizationType alwaysNormalize: контролирует, каким образом обрабатыватьchildren, установленная сцена не используется, игнорируйте ее.
if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children;
    children = data;
    data = undefined;
}

Обработайте проблему совместимости при передаче параметров и оцените второй параметрdataЯвляется ли это массивом или базовым типом, если да, то указывает второй параметрdataдолжен быть третьим параметромchildren, так будетchildrenназначить наnormalizationType, а потомdataназначить наchildren, а потомdataустановлен вundefined.

последний звонок_createElementметод,Сделайте здесь точку останова и нажмите F11, чтобы войти_createElementметод.

1. _createElement

function _createElement(context, tag, data, children, normalizationType){
  var vnode;
  if (typeof tag === 'string') {
      if (config.isReservedTag(tag)) {
          vnode = new VNode(config.parsePlatformTagName(tag), data, 
          children, undefined, undefined, context);
      }
  }
  return vnode
}

Игнорировать предыдущую логику суждения для ряда параметров, и посколькуvm._cПоследний параметр методаflase,такnormalizationTypeдляundefined, не будет заниматьсяchildren, посмотрите непосредственно на основную логику.

В заданной сцене,tagэто строка, послеconfig.isReservedTag(tag)судитьtagявляется зарезервированным символом HTML, выполнитьnew VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context)Создайте экземпляр VNode для созданияvnode

2. новый виртуальный узел ()

new VNode()Роль заключается в создании экземпляра VNode для созданияvnode, который является виртуальным DOM. Краткое введение в виртуальный DOM.Объекты DOM в браузере очень большие.Например, вы можете увидеть, что объекты DOM очень большие, когда нажимаете Enter на консоли. Стандарты браузера делают объекты DOM очень сложными. Поэтому, когда мы часто манипулируем объектами DOM, возникают определенные проблемы с производительностью. Хотя виртуальный DOM использует собственный JS-объект для описания объекта DOM, он не содержит методов для управления DOM, поэтому стоимость управления им намного меньше, чем управление объектами DOM.

const div = document.createElement('div');
let str= ''
for(let key in div){
    str += key + ';'
}

function VNode(tag, data, children, text, elm, context, componentOptions, asyncFactory) {
    this.tag = tag;
    this.data = data;
    this.children = children;
    this.text = text;
    this.elm = elm;
    this.context = context;
    this.key = data && data.key;
    this.parent = undefined;
}

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

  • tag: название ярлыка
  • data:данные
  • children: дочерний узел
  • text: текст текущего узла
  • elm: соответствующий реальный объект DOM
  • parent: родительский узел

В установленной сцене сгенерированный объект DOM, как показано на рисунке ниже, представляет собой древовидную структуру, тогда соответствующий виртуальный DOM также должен быть древовидной структурой, затем следуйтеVNodeтретий параметр функцииchildrenСвязанный,childrenчерез_createElementчетвертый параметр функцииchildrenпройти мимо,_createElementфактически сгенерированныйrenderвызывается метод, возвращаемся к сгенерированномуrenderметод (кrenderметод трактовался эквивалентно).

function (){
    return vm._c('div', {
        attrs: {
                "id": "app"
            }
        }, [vm._c('p', 
            [	
                vm._v(vm._s(vm.aa)), 
                vm._c('span', [vm._v(this._s(vm.bb))])
            ]
        )]
    )
}

При выполнении функции, если параметр является функцией, он должен быть выполнен первым.

  • воплощать в жизньvm._c('div'..когда встречается параметр[vm._c('p', [vm._v(vm._s(vm.aa)), vm._c('span', [vm._v(vm._s(vm.bb))])])];

  • воплощать в жизньvm._c('p', [vm._v(vm._s(vm.aa)), vm._c('span', [vm._v(vm._s(vm.bb))])]), параметры встречи[vm._v(vm._s(vm.aa)), vm._c('span', [vm._v(vm._s(vm.bb))])];

  • воплощать в жизньvm._v(vm._s(vm.aa))а такжеvm._c('span', [vm._v(vm._s(vm.bb))]), встретив параметрvm._s(vm.aa)а также[vm._v(vm._s(vm.bb))]

  • воплощать в жизньvm._s(vm.aa)а такжеvm._v(vm._s(vm.bb)), параметры встречиvm._s(vm.bb)

  • воплощать в жизньvm._s(vm.bb)

Согласно вышеприведенному анализу, первое, что нужно сделать, этоvm._s(vm.aa)а такжеvm._s(vm.bb), затем выполнитеvm._v(vm._s(vm.bb)).

существуетinstallRenderHelpersопределяется в функцииvm._vа такжеvm._s. Когда Vue.js загружен, он выполняетrenderMixin(Vue), при котором выполнениеinstallRenderHelpers(Vue.prototype),Vue.prototypeто естьvm. такvm._vсоответствуетcreateTextVNodeметод,vm._sсоответствуетtoStringметод.

function renderMixin (Vue) {
    installRenderHelpers(Vue.prototype)
}
function installRenderHelpers(target) {
    target._s = toString;
    target._v = createTextVNode;
}
function createTextVNode (val) {
    return new VNode(undefined, undefined, undefined, String(val))
}
function toString (val) {
  return val == null ? '' : 
	Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
	? JSON.stringify(val, null, 2) : String(val)
}
  • createTextVNodeМетод используется для создания виртуального DOM текстовых узлов, обратите внимание, что его параметры передаются вnew VNodeчетвертый параметрtext(текст текущего узла).

  • toStringметод используется для преобразования любого значения в строку.

затем выполнитьvm._v(vm._s(vm.bb))получить одинvnode, обозначаемый как vnode1, а затем выполнитьvm._c('span', [vm._v(vm._s(vm.bb))]), который выполняет довольноvm._c('span', [vnode]), возьми другойvnode, обозначаемый как vnode2, в это время значение vnode1childrenСтоимость свойства[vnode2].

И так далее, послойное выполнение. при исполненииvm._c('div'..Вы получаете виртуальное дерево DOM, как показано на рисунке.

существуетvm._renderСоздайте виртуальное дерево DOM из шаблона и данных, а затемvm._updateПревратите виртуальное дерево DOM в настоящее дерево DOM.

6. vm._update

Vue.prototype._update = function(vnode, hydrating) {
    var vm = this;
    var 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);
    }
};

воплощать в жизньvar prevVnode = vm._vnode,vm._vnodeЭто виртуальный DOM, сгенерированный текущим экземпляром Vue. Это первый рендеринг в заданной сцене.vm._vnodeне определено, поэтомуprevVnodeДля undefined выполнить сноваvm._vnode = vnode, назначьте Virtual DOM, сгенерированный текущим экземпляром Vue, наvm._vnode.

потому чтоprevVnodeне определено, поэтому выполнитеvm.$el = vm.__patch__(vm.$el, vnode, hydrating, false), Введите первую операцию рендеринга DOM.

Vue.prototype.__patch__ = inBrowser ? patch : noop;

В среде браузераVue.prototype.__patch__ даpatch.

1, патч

var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });

patchЗависит отcreatePatchFunctionгенерировать. его параметрыnodeOpsМетод, который инкапсулирует ряд операций DOM, параметрыmodulesЭто функция-ловушка, которая определяет некоторые модули, я не буду здесь ее подробно представлять, давайте посмотрим.createPatchFunctionреализация.

export function createPatchFunction(backend) {
    const {modules,nodeOps} = backend
    return function patch(oldVnode, vnode, hydrating, removeOnly) {
       //...
    }
}

Перед этим подумайте, почемуcreatePatchFunctionсоздатьpatchметод.

потому чтоpatchзависит от контекста, как в веб-контексте, так и в контексте Weex, каждый имеет свой собственныйnodeOpsа такжеmodules.但是不同环境的patchОсновная логика та же, а дифференцированную часть нужно только различать по параметрам, здесь используется прием каррирования функций.createPatchFunctionЗаранее закрепите дифференцированные параметры, не звоня каждый разpatchпри прохождении соответствующегоnodeOpsа такжеmodules, эту технику программирования стоит изучить.

затем выполнитьvm.$el = vm.__patch__(vm.$el, vnode, hydrating, false), в конце концов звонитpatchметод,Сделайте здесь точку останова и нажмите F11, чтобы войтиpatchметод.

function patch(oldVnode, vnode, hydrating, removeOnly) {
    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {} else {
        const isRealElement = isDef(oldVnode.nodeType)
        if (!isRealElement && sameVnode(oldVnode, vnode)) {} else {
            if (isRealElement) {
                oldVnode = emptyNodeAt(oldVnode)
            }
            const oldElm = oldVnode.elm
            const parentElm = nodeOps.parentNode(oldElm)
            createElm(vnode, insertedVnodeQueue, parentElm, nodeOps.nextSibling(oldElm))
            if (isDef(parentElm)) {
                removeVnodes(parentElm, [oldVnode], 0, 0)
            } else if (isDef(oldVnode.tag)) {}
        }
    }
    return vnode.elm
}
  • параметрoldVnode: Последний виртуальный DOM, значение в заданной сцене равноvm.$elявляется DOM-объектом;
  • параметрvnode: На этот раз виртуальный DOM;
  • параметрhydrating: в случае несерверного рендерингаfalse,Можно игнорировать;
  • параметрremoveOnly: вtransition-groupОн используется в сцене, если его нет в сцене настройки, онfalse, Можно игнорировать.

еслиoldVnodeНе виртуальный DOM, а объект DOM, поместитеoldVnodeиспользоватьemptyNodeAtв виртуальный DOM и в его свойствахelmприсвоение преобразованному объекту DOM, поэтомуoldElmэквивалентvm.$el,С использованиемnodeOps.parentNode(oldElm)ПолучатьoldElmРодительский узел DOM элемента , в данном случае body.

function emptyNodeAt (elm) {
    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
//nodeOps.parentNode
function parentNode (node) {
    return node.parentNode
}

воплощать в жизньcreateElm(vnode, insertedVnodeQueue, parentElm, nodeOps.nextSibling(oldElm))генерировать настоящий DOM,Сделайте здесь точку останова и нажмите F11, чтобы войтиcreateElmметод,nodeOps.nextSibling(oldElm)ВыбиратьoldElmследующий брат .

2. создатьвяз

function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
    vnode.isRootInsert = !nested;
    var data = vnode.data;
    var children = vnode.children;
    var tag = vnode.tag;
    if (isDef(tag)) {
        vnode.elm = nodeOps.createElement(tag, vnode);
        createChildren(vnode, children, insertedVnodeQueue);
        if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue);
        }
        insert(parentElm, vnode.elm, refElm);
    } else if (isTrue(vnode.isComment)) {
    } else {
        vnode.elm = nodeOps.createTextNode(vnode.text);
        insert(parentElm, vnode.elm, refElm);
    }
}
  • параметрvnode: виртуальный дом;
  • параметрinsertedVnodeQueue: Перехват очереди функций;
  • параметрparentElm: параметрvnodeОбъект DOM родительского узла, соответствующий реальному объекту DOM;
  • параметрrefElm: объект узла-заполнителя, например, параметрыvnodeСоответствует следующему родственному узлу объекта DOM;
  • параметрnested:судитьvnodeЯвляется ли это виртуальным DOM корневого экземпляра;
  • параметрownerArray: массив собственных дочерних узлов
  • параметрindex: Нижний индекс массива дочерних узлов

параметрownerArray,indexэто решитьvnodeОн был отрендерен раньше, и теперь как новый виртуальный DOM, ошибка, вызванная перезаписью его вяза, установленной сцены, является первым рендерингом, который можно игнорировать.

  • vnode.tagЕсли он существует, выполнитеnodeOps.createElement(tag, vnode)Создайте реальный узел DOM и назначьте егоvnode.elm,nodeOps.createElementсоответствуетcreateElementметод.
function createElement (tagName, vnode) {
    var elm = document.createElement(tagName);
    if (tagName !== 'select') {
      return elm
    }
    if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
      	elm.setAttribute('multiple', 'multiple');
    }
    return elm
}
  • vnode.tagЕсли он не существует, это, вероятно, комментарий или простой текстовый узел, выполнитеnodeOps.createTextNode(vnode.text)Создайте комментарий или простой текстовый узел и назначьте егоvnode.elm,nodeOps.createTextNodeсоответствуетcreateTextNodeметод.
function createTextNode (text) {
    return document.createTextNode(text)
}

createChildren(vnode, children, insertedVnodeQueue)используется для обработкиvnodeдочерний виртуальный дом,Сделайте здесь точку останова и нажмите F11, чтобы войтиcreateChildrenметод

3. создать детей

function createChildren(vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
        checkDuplicateKeys(children);
        for (var i = 0; i < children.length; ++i) {
            createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
        }
    } else if (isPrimitive(vnode.text)) {
        nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
    }
}

createChildrenЛогика очень проста, на самом деле это обходvnodeДочерний виртуальный DOM, рекурсивно называемыйcreateElm, который является широко используемым алгоритмом обхода в глубину.vnode.elmкакchildren[i](Виртуальный DOM) Передается родительский узел, соответствующий реальному DOM.

когдаchildrenкогда не массив. судитьvnode.textЯвляется ли он базовым типом, если он называетсяnodeOps.createTextNodeСоздайте простой текстовый узел, затем вызовитеnodeOps.appendChildвставить вvnode.elmсередина.

4. вставить

воплощать в жизньinsert(parentElm, vnode.elm, refElm)поместите полученный DOM (vnode.elm) вставляется в соответствующий родительский узел (parentElm), так как это рекурсивный вызов, дочерний виртуальный DOM будет вызываться первым.insert, поэтому порядок вставки всего дерева Virtual DOM после создания реального DOM сначала дочерний, а затем родительский.

существуетinsert(parentElm, vnode.elm, refElm)Нажмите точку останова и нажмите F11, чтобы войтиinsertметод.

function insert(parent, elm, ref$$1) {
    if (isDef(parent)) {
        if (isDef(ref$$1)) {
            if (nodeOps.parentNode(ref$$1) === parent) {
                nodeOps.insertBefore(parent, elm, ref$$1);
            }
        } else {
            nodeOps.appendChild(parent, elm);
        }
    }
}
  • параметрparent: родительский узел узла, который нужно вставить
  • параметрelm: вставить узел
  • параметрref$$1: Ссылочный узел, будет вставлен перед ссылочным узлом.

nodeOps.insertBeforeвести перепискуinsertBeforeметод,nodeOps.appendChildвести перепискуappendChildметод,

function insertBefore (parentNode, newNode, referenceNode) {
     parentNode.insertBefore(newNode, referenceNode);
}
function appendChild (node, child) {
     node.appendChild(child);
}

insertBeforeМетоды иappendChildНа самом деле метод заключается в вызове API собственного DOM для выполнения операций DOM.

5. Вернитесь к созданию Elm

при звонкеcreateChildren,ПучокvnodeПосле обхода дочернего виртуального DOM вернитесь к исходному вызовуcreateElmметод, затем выполнитеinsert(parentElm, vnode.elm, refElm), его параметрparentElmЗначение находится вpatchвыполнение метода

const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)

приобретенный, на съемочной площадкеoldElmдля<div id="app"><p>{{aa}}<span>{{bb}}</span></p></div>этот объект DOM, поэтомуparentElmдля объекта тела.vnode.elmСоздайте соответствующий реальный DOM для дерева Virtual DOM.

Фактически весь процесс представляет собой рекурсивный вызовcreateElmНастоящее DOM-дерево создается и вставляется в тело.

Увидев это, возможно, вы вдруг поймете, что Vue — это такой динамически создаваемый DOM.

Также вinvokeCreateHooksМетод выполнит ряд функций-ловушек, таких какdataсерединаattrs: {id: "app"}Как атрибуты добавляются к тегу div, здесь не рассматривается.

if (isDef(parentElm)) {
    removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
}

потому чтоparentElmсуществуют, вpatchпоследний вызов методаremoveVnodesпоставить оригинальный шаблон<div id="app"><p>{{aa}}<span>{{bb}}</span></p></div>удален из своего родительского узла. Это также подтверждает, почему цель монтирования экземпляра Vue должна быть ограничена, и она не может быть смонтирована на теле или html-объекте.

назадvm._updataметод, потому чтоpatchМетод вернет объект DOM новой текущей цели монтирования экземпляра Vue, поэтому обновите его.vm.$el.

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);

6. Вернитесь к mountComponent

if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, 'mounted');
}
return vm

vm.$vnodeПредставляет родительский виртуальный DOM экземпляра Vue. Если он равен null, это означает, что в данный момент это корневой экземпляр Vue.vm._isMounted 为 true, указывая на то, что экземпляр смонтирован и выполняется в то же времяmountedфункция крючка.

7. Резюме

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

существуетvm._renderСоздайте виртуальное дерево DOM от дочернего к родительскому вvm._updateРеальный DOM генерируется от родителя к дочернему, а затем от дочернего к родительскому, сгенерированный реальный DOM вставляется в соответствующий родительский узел, создается реальное дерево DOM и, наконец, вставляется в тело.