Шаг за шагом к получению серии ВУЭ-МВВМ, резервирует навыки прохождения собеседования

внешний интерфейс Vue.js опрос MVVM

предисловие

Это запись моего обучения, потому что я готовлюсь к интервью в последнее время, и меня во многих случаях будут спрашивать: пожалуйста, кратко опишитеmvvm? Обычно я бы ответил так:mvvmэто разделение зрения и логики, этоmodel view view-modelАббревиатура двусторонней привязки данных через виртуальный дом (могу ответить вскользь)

Итак, вот вопрос, вы знаетеmvvmКак это достигается? отвечать:mvvmглавным образом черезObjectизdefinePropertyсвойство, переопределениеdataизsetа такжеgetфункцию для реализации. хорошо, ответ 60 баллов, так что вы знаете конкретный процесс реализации? Подумайте об этом, не было бы лучше, если бы он не спрашивал, а вы ответили? На предпосылке, не забудьте передать простойmvvmбудет впечатлен~

Нечего сказать, нижеследующее основано на обучающем видео г-на Чжана Рэньяна, использующегоES6Грамматика, которая также содержит мое личное понимание, я буду очень рад, если она вам поможет. Если есть ошибки, поправьте меня, буду очень признательна~~~

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

  • Скомпилированная блок-схема

  • Общий анализ

Его можно найтиnew MVVM()Основная часть процесса пост-компиляции делится на две части:

  1. Частью этого является составление шаблоновCompile
    • Скомпилируйте элементы и текст и, наконец, отобразите на странице
    • Только теги с директивами шаблона в тегах выполняют компиляцию.Например<div>我很帅</div>не компилировать
  2. Частично это захват данныхObserver
    • DepПубликуйте и подписывайтесь, всем нужно будет получать уведомления об измененияхdataдобавить в массив
    • WatcherЕсли данные изменяются, вObjectизdefinePropertyизsetвызов функцииWatcherизupdateметод

Уточните, для чего нужна бумага

  1. Процесс реализации компиляции шаблона завершенVueСвойства в экземпляре могут быть правильно привязаны к тегу и отображены на странице.
    • Работа: разбор инструкции, регулярная замена{{}}
    • содержимое узлаnode.textContentилиinputизvalueсоставлено
  2. Полная двусторонняя привязка данных
    • работа: пройтиobserveизменения данных захвата класса
    • Добавьте публикацию и подписку:Object.definePropertyсуществуетgetкрюкaddSub,setУведомлять об изменениях в хукахdep.notify()
    • dep.notify()называетсяWatcherизupdateметод, так сказатьinputвызов обновления при изменении

Давайте сначала уточним наши цели: просмотр рендеринга и двусторонняя привязка данных и уведомления об изменениях!шаг: Начните с пошагового анализа того, как использовать Vue, от начального класса Vue до цели компиляции [реализовать рендеринг представления], до этого следует наблюдать за захватом данных, а затем вызывать обновление представления, класс наблюдателя прослушивает изменения и, наконец, уведомляет обо всех обновлениях представления и т. д.

Разложение экземпляра Vue

С чего начать? Как использовать в первую очередьVueНачинать. Давайте разберем его шаг за шагомVueиспользование:

let vm = new Vue({
    el: '#app'
    data: {
        message: 'hello world'
    }
})

В приведенном выше коде можно увидеть использованиеVue, мы первыеnewОдинVueЭкземпляр, передайте параметр объекта, включаяelа такжеdata.

хорошо, вышеизложенная информация получена, давайте реализуем ее дальшеЦель 1:БудуVueпримерdataскомпилировано на странице

Процесс реализации Compile для компиляции шаблонов

Сначала посмотрите на использование страницы:index.html

<div id="app">
    <input type="text" v-model="jsonText.text">
    <div>{{message}}</div>
    {{jsonText.text}}
</div>
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./vue.js"></script>
<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'gershonv',
            jsonText:{
                text: 'hello Vue'
            }
        }
    })
</script>

Первым шагом, конечно же, является добавлениеVueКласс действует как входной файл.

Класс Vue — добавление файла записи

создать новыйvue.jsфайл со следующим кодом определяется в конструкторе$elа также$data, потому что последующая компиляция будет использовать

class Vue {
    constructor(options) {
        this.$el = options.el; // 挂载
        this.$data = options.data;

        // 如果有要编译的模板就开始编译
        if (this.$el) {
            // 用数据和元素进行编译
            new Compile(this.$el, this)
        }
    }
}
  • Перехват данных еще не добавленobeserve, для достижения цели 1 пока не используется и будет добавлено позже
  • Потребности в компиляцииelи сопутствующие данные, приведенный выше код будет скомпилирован после выполнения, поэтому мы создаем новый файл для скомпилированного класса

здесь, в файле вводаvue.jsсерединаnewвзял одинCompileЭкземпляр, поэтому создайте новый следующийcompile.js

Compile class — добавление шаблонной компиляции

CompileЧто я должен делать? Мы знаем операцию на страницеdomбудет потреблять производительность, поэтому вы можете поставитьdomПерейдите к обработке памяти:

  1. поставь настоящийdomПеремещаться в память (действовать в памятиdomБыстрее)
    • Как вставить в память? Может воспользоваться фрагментацией документаfragment
  2. компилироватьcompile(fragment){}
    • Извлеките нужные узлы элементов и текстовые узлыv-model {{}}, а затем выполните соответствующие операции.
  3. составленоfragmentвернуться на страницу
class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        if (this.el) {// 如果这个元素能获取到 我们才开始编译
            // 1.先把这些真实的DOM移入到内存中 fragment[文档碎片]
            let fragment = this.node2fragment(this.el)
            // 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
            this.compile(fragment)
            // 3.编译好的fragment在塞回页面里去
            this.el.appendChild(fragment)
        }
    }

    /* 专门写一些辅助的方法 */
    isElementNode(node) { // 判断是否为元素及节点,用于递归遍历节点条件
        return node.nodeType === 1;
    }

    /* 核心方法 */
    node2fragment(el) { // 将el的内容全部放入内存中
        // 文档碎片
        let fragment = document.createDocumentFragment();
        while (el.firstChild) { // 移动DOM到文档碎片中
            fragment.appendChild(firstChild)
        }
        return fragment;
    }
    
    compile(fragment) {
    }
}

Дополнение: будетelПереместить содержимое документа во фрагмент документаfragmentIn — это процесс входа и выхода из стека. дети Эль переезжают вfragmentПосле [хлопка],elСледующий дочерний элемент станетfirstChild.

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

Компиляция процесса компиляции (фрагмент)

  • Шаг 1: Получить узел элемента, извлечь в нем инструкцию или шаблон{{}}
    • Во-первых, вам нужно пройти по узлам, используярекурсивный метод, потому что существует отношение вложенности узлов,isElementNodeПредставитель является элементом узла, а также условием завершения рекурсии.
  • Шаг 2: Способ классификации инструкций компиляцииcompileElementИ составить текст{{}}Методы
    • compileElementправильноv-model,v-textразбор команд
    • compileTextскомпилировать текстовый узел{{}}
class Compile{
    // ...
    compile(fragment) {
        // 遍历节点 可能节点套着又一层节点 所以需要递归
        let childNodes = fragment.childNodes
        Array.from(childNodes).forEach(node => {
            if (this.isElementNode(node)) {
                // 是元素节点 继续递归
                // 这里需要编译元素
                this.compileElement(node);
                this.compile(node)
            } else {
                // 文本节点
                // 这里需要编译文本
                this.compileText(node)
            }
        })
    }
}
compileElement && compileText
  1. Получить атрибуты элементаnode.attributesСначала определите, включена ли команда
  2. Определить тип инструкции (v-html v-text v-model...) вызвать другой метод обновления данных
    • Скомпилированный объект инструмента извлекается здесьCompileUtil
    • Метод вызова:CompileUtil[type](node, this.vm, expr)
      • CompileUtil.类型(节点,实例,v-XX 绑定的属性值)
class Compile{
    // ...
    
    // 判断是否是指令 ==> compileElement 中递归标签属性中使用
    isDirective(name) {
        return name.includes('v-')
    }
    
    compileElement(node) {
        // v-model 编译
        let attrs = node.attributes; // 取出当前节点的属性
        Array.from(attrs).forEach(attr => {
            let attrName = attr.name;
            // 判断属性名是否包含 v-
            if (this.isDirective(attrName)) {
                // 取到对应的值,放到节点中
                let expr = attr.value;
                // v-model v-html v-text...
                let [, type] = attrName.split('-')
                CompileUtil[type](node, this.vm, expr);
            }
        })
    }
    compileText(node) {
        // 编译 {{}}
        let expr = node.textContent; //取文本中的内容
        let reg = /\{\{([^}]+)\}\}/g;
        if (reg.test(expr)) {
            CompileUtil['text'](node, this.vm, expr)
        }
    }
    
    // compile(fragment){...}
}
CompileUtil = {
    getVal(vm, expr) { // 获取实例上对应的数据
        expr = expr.split('.'); // 处理 jsonText.text 的情况
        return expr.reduce((prev, next) => { 
            return prev[next] // 譬如 vm.$data.jsonText.text、vm.$data.message
        }, vm.$data)
    },
    getTextVal(vm, expr) { // 获取文本编译后的结果
        return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            return this.getVal(vm, arguments[1])
        })
    },
    text(node, vm, expr) { // 文本处理 参数 [节点, vm 实例, 指令的属性值]
        let updateFn = this.updater['textUpdater'];
        let value = this.getTextVal(vm, expr)
        updateFn && updateFn(node, value)
    },
    model(node, vm, expr) { // 输入框处理
        let updateFn = this.updater['modelUpdater'];
        updateFn && updateFn(node, this.getVal(vm, expr))
    },
    updater: {
        // 文本更新
        textUpdater(node, value) {
            node.textContent = value
        },
        // 输入框更新
        modelUpdater(node, value) {
            node.value = value;
        }
    }
}

На данный момент привязка данных завершена, т.е.new Vueв случаеdataОн уже может корректно отображаться на странице, теперь решениеКак реализовать двустороннюю привязку

в сочетании с открытиемvueГрафик процесса компиляции показывает, что у нас на один меньшеobserveзахват данных,DepСообщить об изменениях, добавитьWatcherПрислушивайтесь к изменениям и, в конечном итоге, переписывайтеdataАтрибуты

Реализовать двустороннюю привязку

Класс Observer — добавление наблюдателей

  1. существуетvue.jsзахват данных
class Vue{
    //...
    if(this.$el){
       new Observer(this.$data); // 数据劫持
       new Compile(this.$el, this); // 用数据和元素进行编译
    }  
}
  1. новыйobserver.jsдокумент

Шаги кода:

  • Добавить прямо в конструкторobserve
    • судитьdataСуществует ли он, является ли он объектом (не может быть записан при использовании нового Vue)dataАтрибуты)
    • Захватите данные один за другим, получитеdataсерединаkeyа такжеvalue
class Observer {
    constructor(data) {
        this.observe(data)
    }

    observe(data) {
        // 要对这个数据将原有的属性改成 set 和 get 的形式
        if (!data || typeof data !== 'object') {
            return
        }
        // 将数据一一劫持
        Object.keys(data).forEach(key => {
            // 劫持
            this.defineReactive(data, key, data[key])
            this.observe(data[key]) //递归深度劫持
        })
    }

    defineReactive(obj, key, value) {
        let that = this
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() { // 取值时调用的方法
                return value
            },
            set(newValue) { // 当给data属性中设置的时候,更改属性的值
                if (newValue !== value) {
                    // 这里的this不是实例
                    that.observe(newValue) // 如果是对象继续劫持
                    value = newValue
                }
            }
        })
    }
}

Хотя естьobserver, но не связаны, и уведомления меняются. добавить нижеWatcherДобрый

Добавление класса Watcher

новыйwatcher.jsдокумент

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

запомни сначалаwatchПрименение:this.$watch(vm, 'a', function(){...})Параметры, которые нам нужно передать при добавлении подписчика публикации:экземпляр vm, свойства связаны v-XX, функция обратного вызова cb(getValметод скопирован ранееCompileUtilМетод, на самом деле, можно извлечь...)

class Watcher {
    // 观察者的目的就是给需要变化的那个元素增加一个观察者,当数据变化后执行对应的方法
    // this.$watch(vm, 'a', function(){...})
    constructor(vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;

        // 先获取下老的值
        this.value = this.get();
    }

    getVal(vm, expr) { // 获取实例上对应的数据
        expr = expr.split('.');
        return expr.reduce((prev, next) => { //vm.$data.a
            return prev[next]
        }, vm.$data)
    }

    get() {
        let value = this.getVal(this.vm, this.expr);
        return value
    }

    // 对外暴露的方法
    update(){
        let newValue = this.getVal(this.vm, this.expr);
        let oldValue = this.value

        if(newValue !== oldValue){
            this.cb(newValue); // 对应 watch 的callback
        }
    }
}

WatcherОпределено, но еще не вызвано. Когда шаблон скомпилирован, наблюдайте за ним, когда вам нужно настроить наблюдение.Compile

class Compile{
    //...
}
CompileUtil = {
    //...
    text(node, vm, expr) { // 文本处理 参数 [节点, vm 实例, 指令的属性值]
        let updateFn = this.updater['textUpdater'];
        let value = this.getTextVal(vm, expr)
        updateFn && updateFn(node, value)

        expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            new Watcher(vm, arguments[1], () => {
                // 如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
                updateFn && updateFn(node, this.getTextVal(vm, expr))
            })
        })
    },
    //...
    model(node, vm, expr) { // 输入框处理
        let updateFn = this.updater['modelUpdater'];
        // 这里应该加一个监控,数据变化了,应该调用watch 的callback
        new Watcher(vm, expr, (newValue) => {
            // 当值变化后会调用cb 将newValue传递过来()
            updateFn && updateFn(node, this.getVal(vm, expr))
        });

        node.addEventListener('input', e => {
            let newValue = e.target.value;
            this.setVal(vm, expr, newValue)
        })
        updateFn && updateFn(node, this.getVal(vm, expr))
    },
    
    //...
}

После осуществления мониторинга обнаруживается, что уведомление об изменении было отправлено не всем шаблонам, привязанным к инструкции или{{}}, поэтому нам нужноDepКласс для мониторинга, свойства публикации-подписки экземпляра, которые мы можем добавить вobserver.jsсередина

Добавление класса Dep

Обратите внимание, что он не будет вызываться при первой компиляции.Watcher,dep.targetне существует,new Watcherкогдаtargetстоит только Это немного сбивает с толку, посмотрите на следующий код:

class Watcher {
    constructor(vm, expr, cb) {
        //...
        this.value = this.get()
    }
    get(){
        Dep.target = this;
        let value = this.getVal(this.vm, this.expr);
        Dep.target = null;
        return value
    }
    //...
}

// compile.js
CompileUtil = {
    model(node, vm, expr) { // 输入框处理
        //...
        new Watcher(vm, expr, (newValue) => {
            // 当值变化后会调用cb 将newValue传递过来()
            updateFn && updateFn(node, this.getVal(vm, expr))
        });
    }
}
class Observer{
    //...
    defineReactive(obj, key, value){
        let that = this;
        let dep = new Dep(); // 每个变化的数据 都会对应一个数组,这个数组存放所有更新的操作
        Object.defineProperty(obj, key, {
            //...
            get(){
                Dep.target && dep.addSub(Dep.target)
                //...
            }
             set(newValue){
                 if (newValue !== value) {
                    // 这里的this不是实例
                    that.observe(newValue) // 如果是对象继续劫持
                    value = newValue;
                    dep.notify(); //通知所有人更新了
                }
             }
        })
    }
}
class Dep {
    constructor() {
        // 订阅的数组
        this.subs = []
    }

    addSub(watcher) {
        this.subs.push(watcher)
    }

    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}

Приведенный выше код выполненопубликовать подписчикаВыкройка, простая реализация. . То есть цель 2 двустороннего связывания достигнута.


Эпилог

У меня нет намерения выступать напоказ, это всего лишь статья из моих учебных записей. Если вы хотите поделиться им, вы можете добиться прогресса. Буду очень рад, если эта статья вам поможет. Есть вопросы, чтобы задатьissue, я надеюсь, что есть ошибки, и я надеюсь, что каждый может выдвинуть это, большое спасибо.

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