Объясните принцип vue из реализации

внешний фреймворк

Во-первых, давайте сделаем снимок

Как видно из рисунка, МВВМ состоит из двух блоков.Observerперехват слушателя реактивного иCompileРазбор инструкции. Давайте объединим эти два аспекта для реализации Vue.

Отзывчивый

Реализация наблюдателя

class Vue {
    constructor(options) {
        this.$options = options
        this.$data = options.data
        this.observe(this.$data)

        // 运行created生命周期
        this.$options.created && this.$options.created.call(this)
        
    }
    observe(data) {
        if (!data || typeof data !== 'object') {
            return
        }
        Object.keys(data).forEach((key) => {
            this.defineProperty(data, key, data[key])
            this.poxyData(key)
        })
    }
    defineProperty(obj, key, val) {
       const dep = new Dep()
        // 递归遍历
        this.observe(val)
        Object.defineProperty(obj, key, {
            get() {
                // 初始化时添加wather进行观察
                Dep.target && dep.addWather(Dep.target)
                return val
            },
            set(newVal) {
                val = newVal
                // 被赋值时,通知更新
                dep.notify()
            }
        })
    }
    poxyData(key) {
        this.$data[key] && Object.defineProperty(this, key, {
            get() {
                return this.$data[key]
            },
            set(newVal) {
                this.$data[key] = newVal

            }
        })
    }

}
const pan = new Vue({
            el: '#app',
            data: {
                name: "I am test.",
                age: 12,
                fag: {
                    bar: 'bar'
                },
                html: '<button @click="sex1">这是一个按钮</button>'
            }
        })

Здесь мы сначала используемObject.defineProperty()Этот метод выполняет захват данных, которые мы передаем в конфигурацию Vue.defineProperty(obj, key, val)метод, который мы вызываем сноваthis.observe(val), Вот поскольку данные могут быть более чем на один слой, нам нужно перехватить все данные ниже данных.poxyData(key)Этот метод заключается в проксировании данных в data наthisвверх, мы можемpan.htmlдоступ кpan.$data.htmlданные в.

Реализация Dep && Watcher

class Dep {
    constructor() {
        this.wathers = []
    }
    addWather(wather) {
        this.wathers.push(wather)
    }
    notify() {
        this.wathers.forEach(wather => wather.update())
    }
}

class Wather {
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb
        // 把新生成的wather附加到Dep.target 
        Dep.target = this
        // 访问一次被代理的属性
        this.vm[this.key]
        // Dep.target 置为空,等待下一个wather的生成
        Dep.target = null
    }

    update() {
        this.cb && this.cb.call(this.vm, this.vm[this.key])
    }
}

DepЭто довольно просто, всего два метода одинaddWatherа такжеnotifyдва метода,WatherПросто метод обновления. Это то, что мы часто называем паттерном наблюдателя.Depсохранить несколькоwather,когдаDepОбнаружитьWatherКогда будет обновление,Depпозвонюnotifyспособ уведомить всехwatherметодupdateобновить.

Компиляция шаблона

class Compile {
    constructor(el, vm) {
        this.$el = document.querySelector(el)
        this.$vm = vm
        // 模板移动到文档片段
        this.$fragment = this.node2Fragment(this.$el)
        // 编译
        this.compile(this.$fragment)
        // 把编译好的文档片段添加到el
        this.$el.appendChild(this.$fragment)
    }
    node2Fragment(el) {
        const fragment = document.createDocumentFragment()
        let firstNode
        while (firstNode = el.firstChild) {
            fragment.appendChild(firstNode)
        }
        return fragment
    }
    compile(el) {
        const nodes = el.childNodes
        Array.from(nodes, (node) => {
            // 标签
            if (node.nodeType === 1) {
                this.compileElement(node)
            }
            // 文本 
            else if (this.isInter(node)) {
                this.compileText(node)
            }
            // 编译子节点
            if (node.children && node.childNodes.length > 0) {
                this.compile(node)
            }
        })
    }

    isInter(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }
    compileElement(node) {
        const nodeAttrs = node.attributes;
        Array.from(nodeAttrs, (nodeAttr) => {
            // 获取到标签内的属性名以及属性值
            const attrName = nodeAttr.name
            const attrValue = nodeAttr.value
            // 匹配p-开头的属性名
            if (attrName.includes('p-')) {
                const dir = attrName.substring(2)
                this[dir] && this[dir](node, attrValue)
            }
            // 匹配@开头的属性名
            if (attrName.includes('@')) {
                const dir = attrName.substring(1)
                this[dir] && this[dir](node, attrValue)
            }
        })

    }
    compileText(node) {
        // 拿取到文本标签里的{{xxx}}

        // console.log(RegExp.$1); // {{name}}花括号内匹配的值
        this.update(node, RegExp.$1, 'text')
    }
    update(node, key, dir) {
        const updator = this[dir + 'Updator'].bind(this)
        updator && updator(node, this.$vm[key])

        new Wather(this.$vm, key, (value) => {
            updator && updator(node, value)
        })
    }
    eventListener(node, key, type) {
        const options = this.$vm.$options
        // 处理this指向问题
        const eventFn = options.methods[key].bind(this.$vm)
        eventFn && this.addEventListener(node, type, eventFn)

    }
    textUpdator(node, value) {
        node.textContent = value
    }

    htmlUpdator(node, value) {

        node.innerHTML = value

        this.compile(node)

    }
    modelUpdator(node, value) {
        node.value = value

    }
    text(node, key) {
        this.update(node, key, 'text')
    }

    html(node, key) {
        this.update(node, key, 'html')
    }
    model(node, key) {
        this.update(node, key, 'model')
        // 通过input 事件双向绑定表单数据
        node.addEventListener('input', () => {
            this.$vm[key] = node.value
        })
    }
    click(node, key) {
        this.eventListener(node, key, 'click')
    }
    dblclick(node, key) {
        this.eventListener(node, key, 'dblclick')
    }
    addEventListener(node, key, fn) {
        node.addEventListener(key, fn)
    }
}

Идея компиляции нашего шаблона здесь в основном состоит из 3 шагов:

  • Переместите el, переданный в конфигурацию параметров Vue, во фрагмент документа.
  • Составление фрагментов документа
  • Скомпилированная документация добавляется в html

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

Давайте сосредоточимся на том, как скомпилировать фрагменты документа, сначала пройтись по всему нашему dom, а затем разделить наЭтикеткаа такжетекстдве штуки для анализа

Этикетка

  • Сначала получите атрибуты в теге, мы используемnode.attributesПолучить все свойства текущего узла

  • Получить все имена атрибутов и значения атрибутов

  • Индивидуальное рассмотрениеp-а также@(Эта статья касается только этих двух случаев)

    p-textИзменятьnode.textContentзначения и добавить воды для отслеживания последующих изменений

    p-htmlИзменятьnode.innerHTMLзначения и добавить воды для отслеживания последующих изменений

    p-modelИзменятьnode.valueзначение и добавить воды, чтобы отслеживать последующие изменения, и добавить к узлуinputсобытие, реализующее двустороннюю привязку формы

    @clickпройти черезnode.addEventListener('click', fn)добавить событие клика

    @dblclickпройти черезnode.addEventListener('dblclick', fn)событие dblclick

текст

  • Изменятьnode.textContentзначение

Наконец, добавьте фрагмент скомпилированного документа в dom, чтобы завершить весь процесс. Подробный код см.github.

Добро пожаловать, чтобы оставить сообщение для руководства, спасибо.