模拟一个简单的Vue

作者 : 开心源码 本文共6502个字,预计阅读时间需要17分钟 发布时间: 2022-05-13 共260人阅读

Vue是现在前台非常流行的一个前台框架了,理解它的实现原理现在基本已经快成为前台开发一个必备的基本功了,这篇文章将尝试写一个简单的Vue框架。

Vue数据监听架构

Vue主要架构分为三个部分CompileObserverWatcher结构图如下:

Vue数据监听架构
Obserer负责监听Vue中的数据,Compile负责Vue中涉及dom节点的渲染,Compile和Observer通过Watcher关联,当Observer监听到数据变化会通过watcher使Compile升级页面,反之亦然。
下边就一部分一部分拆解Vue数据监听架构。

Vue函数

这里简单模拟Vue函数,el为Vue作用的dom节点钩子,data为Vue主要监听的数据,option为Vue中dom交互事件函数放置的地方。

class Vue {    constructor(el, data, option) {        this.$el = el;        this.$data = data;        this.$option = option; // 绑定方法放在这里        if (this.$el) {            new Observer(this.$data)            new Compile(this.$el, this);        }    }}

Compile

构造函数

compile负责Vue数据在页面上的渲染,首先看构造函数:

constructor(el, vm) {        this.vm = vm;        if (el && el.nodeType === 1) {            this.$el = el;        } else {            this.$el = document.querySelector(el);        }        const fragment = this.createFragment(this.$el);        this.compile(fragment);        this.$el.appendChild(fragment);    }createFragment(el) {        const fragment = document.createDocumentFragment();        while (el.firstChild) {            fragment.appendChild(el.firstChild);        }        return fragment;    }

都是比较简单的功能,首先在Vue构造函数中将el与vue实例通过构造函数传递进来,其余值得一说的就是为了减少dom结构变化造成的重排,使用了fragment,先将el子节点缓存在fragment中,而后compile后一次性插入el子节点中。

compile

compile(fragment) {        fragment.childNodes.forEach((childNode) => {            if (childNode && childNode.nodeType === 1) {                this.compileElement(childNode)            } else {                this.compileText(childNode)            }            if (childNode && childNode.childNodes.length > 0) {                this.compile(childNode);            }        })    }

遍历子节点,发现假如是element节点进行子节点的递归调用,这里简单解决为子节点只有element与text类型节点。分别针对element与text节点做编译解决。

编译text与element类型子节点

 compileElement(node) {        const attributes = Array.from(node.attributes);        attributes.forEach((attribute) => {            const {name, value} = attribute;            if (this.isDirective(name)) {                const [, directive] = name.split('-');                const [directiveName, eventName] = directive.split(':');                CompileUtil[directiveName](node, value, this.vm, eventName);            }        })    }    compileText(node) {        if (node.textContent && node.textContent.includes('{{')) {            CompileUtil['text'](node, node.textContent, this.vm)        }    }    isDirective(name) {        if (typeof name !== 'string') {            return false;        }        return name.startsWith('v-');    }
编译element节点

编译element节点首先遍历节点属性,找出v-开头的属性,简单假定这些就是vue框架渲染节点的钩子属性。
而后拆分钩子属性获取到expr(获取data值的属性表达式),绑定的事件名称,而后开始渲染页面。
渲染页面部分是个很独立的一块工作,所以这里封装了一个工具对象。

编译text节点
    compileText(node) {        if (node.textContent && node.textContent.includes('{{')) {            CompileUtil['text'](node, node.textContent, this.vm)        }    }

文本类型节点主要判断出能否是{{template }}类型的节点,而后将textConten传递给CompileUtil渲染到页面。

CompileUtil
结构图

CompileUtil结构图
首先针对vue的几个常用指令v-text、v-html、v-modal与v-on对应了几个操作方法,update是对应渲染到页面方法的工具对象。
首先从text方法来开始看:

text(node, expr, vm) {        let value = null;        if (expr.includes('{{')) {            value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {                new Watch(args[1], vm, (newValue) => {                    this.update.textUpdate(node, newValue);                });                return this.getValue(args[1], vm);            })        } else {            value = this.getValue(expr, vm);            new Watch(expr, vm, (newValue) => {                this.update.textUpdate(node, newValue);            });        }        this.update.textUpdate(node, value);    },

首先通过expr区分出是模版渲染还是v-text渲染,假如是模版渲染就用replace抽取出表达式,而后通过公用的表达式获取值方法拿到值渲染到页面。
watch类通过表达式关联vm中的对象变化,而后通过回调函数重新渲染页面。
getValue方法很简单,表达式通过‘.’拆分为数组,进行reduce操作,而后将vue实例中的data作为起始值。

getValue(expr, vm) {        return expr.split('.').reduce((data, attr) => {            return data[attr];        }, vm.$data)    },

Watch与Dep

Dep

Dep类非常简单

class Dep {    constructor() {        this.subs = [];    }    add(watcher) {        this.subs.push(watcher);    }    notify() {        this.subs.forEach((sub) => {            sub.update();        })    }}

Dep对象中负责增加watcher,在需要的时候发起通知,让watcher升级页面

watch

class Watch {    constructor(expr, vm, callBack) {        this.expr = expr;        this.vm = vm;        this.callBack = callBack;        this.oldValue = this.getOldValue();    }    update() {        const newValue = CompileUtil.getValue(this.expr, this.vm);        if (this.oldValue !== newValue) {            this.callBack(newValue);            this.oldValue = newValue;        }    }    getOldValue() {        Dep.target = this; // 用这种方式就不能Dep类与Watch类分在两个文件,webpack打包target值会丢掉        const oldValue = CompileUtil.getValue(this.expr, this.vm); // 获取data中的值,在get中增加Watch入Dep        Dep.target = null;        return oldValue;    }}

watch类中在构造函数中传递expr vm 与跟新的回调函数,最重要的是getOldValue函数,在这里边在Dep类中增加了target属性,属性值存了Watch实例对象,这里的关键思想是在这里通过CompileUtil.getValue获取Vue中data值,并在Dep中上存了一个watch,获取data属性值的时候会调用这个属性的get方法,假如Dep对象上target有值,就在Dep对象上增加一个watch。
update方法通过CompileUtils.getValue获取watch中表达式值假如新值不等于老值就调用callback跟新页面

Observer

Observer类是核心对象,这里通过构造函数传递Vue需要监听的对象

class Observer {    constructor(data) {        this.observe(data);    }    observe(data) {        if (data && typeof data === 'object') {            for (const key of Object.keys(data)) {                this.defineReactive(data, key, data[key]);            }        }    }    defineReactive(data, key, value) {        this.observe(value);        const dep = new Dep();        Object.defineProperty(data, key, {            configurable: false,            enumerable: true,            get: () => {                Dep.target && dep.add(Dep.target)                return value;            },            set: (v) => {                this.observe(v);                if (v !== value) {                    value = v;                    dep.notify();                }            }        })    }}

在observe方法中遍历data对象,而后调用核心方法defineReactive,这里注意的是在方法中首先回调了observe方法,由于对象的属性值可能也是个对象,所以回调了一下observe方法进行深度监听,这里遍历对象的每个属性值,而后增加get 与set方法,get方法中与watch对象中的getOldValue进行联动,在set方法中由于新设置的值可能也是一个对象,所以也要回调一此observe方法,假如属性设置的值与老值不同就调用dep进行广播所有watch进行页面升级。
这里set方法有个小技巧,set方法构成一个闭包,v关联了data的属性值所以每次升级值都可以和data中的属性值进行比较。

测试

下边简单测试一下功能
html部分的代码

<input type="text" id="input">  <p v-text="text.value">    </p>    {{text.value}}

js部分的代码

var vue = new Vue(    '#box',    {        text: {            value: '文本'        },        html: '<h1>html</h1>',        inputValue: 'input'    },    {        clickButton() {            alert(this.$data.text.value);        }    })const input = document.getElementById('input');input.addEventListener('input', (e) => {    vue.$data.text.value = e.target.value;})为了测试效果给input绑定时间修改input值修改文本绑定的变量

测试结果

初始效果

改变input值后效果

改变变量后值

v-html效果

v-html比较简单,首先看CompileUtil部分代码:

html(node, expr, vm) {        const value = this.getValue(expr, vm);        this.update.htmlUpdate(node, value);        new Watch(expr, vm, (newValue) => {            this.update.htmlUpdate(node, newValue);        })    },...     htmlUpdate(node, value) {            node.innerHTML = value;        },...

思路很简单通过expr获取变量值而后渲染到页面,watch监听到变化后重新调用update

测试

html部分代码:

    <button id="changeHtmlBtn">修改html</button>    <div v-html="html">        html    </div>

js部分代码

const htmlBtn = document.getElementById('changeHtmlBtn');htmlBtn.addEventListener('click', (e) => {    vue.$data.html = '<h2>changeHtml</h2>'})

当点击button后修改div下的html

初始效果点击button后效果
修改后的html

v-modal

v-modal就是我们常说的双向绑定
一样我们先看CompileUtil部分代码

...  setValue(expr, vm, inputValue) {        expr.split('.').reduce((data, currentValue, currentIndex, array) => {            if (currentIndex === array.length - 1) {                // 最后一个属性值赋值input输入的值                data[currentValue] = inputValue;            }            return data[currentValue];        }, vm.$data)    },...modal(node, expr, vm) {        node.addEventListener('input', (e) => {            const value = e.target.value;            this.setValue(expr, vm, value);        }, false);        new Watch(expr, vm, (newValue) => {            this.update.modalUpdate(node, newValue);        });        this.update.modalUpdate(node, this.getValue(expr, vm));    }, update: {...               modalUpdate(node, value) {            node.value = value;        }...    }

其实也很简单给节点绑定一个input事件,事件回调函数给vue中的data赋值,watch监听框架中的变量变化后升级节点的value值,赋值操作封装一个setValue方法,setValue方法和getValue方法一样使用reduce方法,在最后一个属性赋值inputValue

测试

html代码
<input type=”text” v-modal = ‘inputValue’>
<div>{{inputValue}}</div>
inputValue初始值赋值为input

效果

初始效果

input初始值赋值为input

修改input输入框值后,页面动态发生变化

修改输入框值

结语

这里只是简单模拟vue框架,有很多地方存在缺陷,大家有选择的阅读思考就好,感谢阅读。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 模拟一个简单的Vue

发表回复