zl程序教程

您现在的位置是:首页 >  前端

当前栏目

从实现讲解vue的原理

Vue原理 实现 讲解
2023-09-27 14:28:56 时间

首先我们上一张图

 

 

由图可知,MVVM是由两大块构成,Observer劫持监听响应式 以及 Compile指令解析。 下面我们就从这两方面来组合实现Vue。

响应式

Observer的实现

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配置的data进行数据劫持,defineProperty(obj, key, val)方法我们再次调用this.observe(val),这里是因为数据可能不止一层,我们需要把data下面的所以数据都拦截到。 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就相当简单了,只是两个方法一个addWathernotify两个方法, 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步:

  • 把传入Vue的options配置的el移动到文档片段
  • 编译文档片段
  • 编译好的文档添加到html

这里有一个重点,为什么要添加到文档片段,文档片段用作一个临时的占位符放置项目,然后用appendChild()添加到dom中,这里做到最小化现场更新(一次更新),提升了连续dom操作的性能瓶颈

我们重点来讲讲如何编译文档片段,首先遍历出我们所有的dom,然后分为标签文本两块来进行解析

标签

  • 首先获取出标签中的属性,我们用node.attributes获取当前node的所有属性

  • 取出所有的属性名以及属性值

  • 分情况处理p-@(本文只针对这两种情况做处理)

    p-text 改变node.textContent的值,并添加wather监听后续变化

    p-html 改变node.innerHTML的值,并添加wather监听后续变化

    p-model 改变node.value的值,并添加wather监听后续变化,并且给node添加input事件,实现表单的双向绑定

    @click 通过node.addEventListener('click', fn)添加click事件

    @dblclick 通过node.addEventListener('dblclick', fn)dblclick事件

文本

  • 改变node.textContent的值

最后再把编译完成的文档片段添加到dom完成整个流程,详细代码请参考github


作者:潘勇旭
链接:https://juejin.im/post/5e034b816fb9a0161f306657
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。