最简vue.js原理教程,适合初学者

简介: 早就想写这个了,和csdn高校俱乐部约好了有个直播,想着反正要备课,我不如直接把要讲的东西写成博客算了。

1.我们要做什么?

早就想写这个了,和csdn高校俱乐部约好了有个直播,想着反正要备课,我不如直接把要讲的东西写成博客算了。

说到vue,我们自然就想到数据绑定。

说到数据绑定,自然就想到MVVM。

6c2401e32d4949fcbcab811d4e643b50.png

什么是MVVM呢,大家可以看下上面这张图 。


MVVM采用了双向数据绑定的思想,基本可以分为三层:


M(Model,模型层),负责业务数据相关,比如vue里面的data就是典型的Model。

V(View,视图层),视图相关,展示给用户的交互界面,同时捕获用户的操作,我们可以理解为页面的DOM元素。

VM(ViewModel, V与M连接的桥梁,也可以看做控制器)。在Vue中,Vue从一开始就利用ViewModel与view,model进行交互。


bc9e9f92aaa24e348cf7895f975c83c1.png

Vue在实例化的时候,我们一般会写这样的代码:

var vm = new Vue({
    /** 需要被vue控制的根元素ID * */
    el: 'app' ,     
    /** Model模型层 * */
    data: {
        username: 'jack',
        password: '123456'
    }
});

这段代码展示了Vue实例化的过程,当你写完这段代码,其实Vue内部已经完成了ViewModel的创建,ViewModel的作用就是连接Model(模型层)和View(视图层),它是一个桥梁。

让我们引用《三分钟手写一个迷你jQuery,附源码》的页面代码:

<input id="username" placeholder="请输入用户名" autocomplete="off"> <span id="username2"></span> 
<br>
<input id="password" placeholder="请输入密码" autocomplete="off"> <span id="password2"></span>

页面效果:

9a19990f618c41c194b7ba38cd35930e.png

在上一节中,我们用迷你jQuery完成的功能,这一节需要用MVVM的思想来重做一遍,即在输入框右边同步显示左边输入框的值。

761192f35268443884cebf38a5cdf063.png


MVVM用的是 数据驱动页面 的思想,当前端页面足够复杂,其效率和可维护性都是远远大于jQuery的,这也是jQuery没落的原因。哦不,我们应该说jQuery光荣地完成了自己的历史使命!


怎么叫数据驱动页面呢?比如这个例子,当我们修改data中的值,页面上span的innerHTML就会同步变化。即数据变了,DOM也跟着变了。

3401a980b3cf40838a9540d6d6e2e6c2.png


对应图中的这个过程:

970d3dcb99a246cc833e02b05b38ceb8.png


看到这里,有人看你会疑惑,这不是单向绑定么,vue.js不是号称双向数据绑定吗?哈哈,不要着急吗,vue自然是双向数据绑定的。还是这个例子,当我们修改input框的value,就会触发监听(input事件),从而导致Model层的对应数据发生变化。

对应图中的这个过程:

4a5fa77e062346b78d12e23205a22dba.png

好了,接下来我们再明确一下,我们需要做什么?那就是,完成VM的逻辑。vue.js其实最主要的,就是完成ViewModel。


2.Vue是一个函数

为了尽快实现VM的逻辑,我故意省去了vue中一些复杂的部分,看到这里,你只需要记住,Vue是一个函数。


ViewModel在Vue中,我们可以这样理解,它就是Vue里面的两个阶段而已,分别为初始化阶段和编译阶段。


所以,我们在编写Vue这个函数的时候,就要完成这两个阶段。

function Vue(){
    this.init();        //初始化
    this.compile();     //编译
}

在函数里面,我们竟然看到了this,这个在《JavaScript百炼成仙》的函数七重关有讲过,这说明我们如果要使用这个函数,就得把它new出来。


init方法和compile方法是这个函数的两个实例方法。


关于创建函数的实例方法,这边我们介绍一个新的方式,那就是用prototype。


prototype很迷惑,你可以这样理解它:prototype是函数的一个公共未来对象!


只要这个函数将来在什么地方被new了,prototype有的,那个实例一定有!


所以,我们这样写

Vue.prototype.init = function(){
    console.log('所有的Model数据,都被我劫持了!');
}
Vue.prototype.compile = function(){
    console.log('页面所有的DOM,都被我窃听了!');
}

怎么用?

这么用:

var vm = new Vue({
    /** 需要被vue控制的根元素ID * */
    el: 'app' ,     
    /** Model数据层 * */
    data: {
        username: 'jack',
        password: '123456'
    }
});

效果:

964ef3554d3d4ea89cf386049d2d8802.png

我在init方法和compile方法中都打印了可爱的语句,现在你不妨猜一猜这两个阶段分别干了什么,对应下面这张图的哪两个部分?

e42983976ce048e09aacda05fba36695.png


3.初始化阶段

init:所有的Model数据,都被我劫持了!


init函数劫持了Model中所有的数据,当Model中数据发生了set操作,就自动去更新页面。所有,init起到的作用就是 M -> VM -> V


好了,我要劫持数据,那么请问,数据从哪来?


答:Vue函数的参数传进来,哈哈。

function Vue(options){
    /** Model层 * */
    this.$data = options.data;
    this.init(); 
    this.compile(); 
}

$data是啥,不就是这个:

/** Model数据层 * */
data: {
    username: 'jack',
    password: '123456'
}

ok,怎么遍历呢?

答:还记得《JavaScript百炼成仙》中叶老教授叶小凡如何遍历对象的法术么?

看代码:

for(let key in this.$data){
}

这段代码是写在init函数里面的,因为是实例方法,它可以直接调用Vue中的实例对象$data。

怎么劫持?

答:用Object.defineProperty,这个方法的意思就是对某个对象的某个key进行劫持。

for(let key in this.$data){
    Object.defineProperty(this.$data,key,{
        get:function(){
            return this.$data[key];
        },
        set:function(newVal){
            this.$data[key] = newVal;
        }
    })
}

,代码还未写完,毕竟如果就这么点东西,那还劫持个锤子!

我们希望看到的是,当data的某个key发生变化,就去更新DOM。怎么更新呢?再来看下这个图:3401a980b3cf40838a9540d6d6e2e6c2 (1).png

从图中可以看到,假如username改变了,页面上有两个地方要变。那我凭什么知道是这两个要变呢?

答:因为vue指令(其实就是元素的属性)

代码如下,我们去掉了id,毕竟不用jQuery那一套了,改成v-model和v-bind:

<div id="app">
    <input v-model="username" placeholder="请输入用户名" autocomplete="off"> 
    <span  v-bind="username"></span>     <br>
    <input v-model="password" placeholder="请输入密码" autocomplete="off"> 
    <span  v-bind="password"></span>
</div> 

到这一步,我们发现一个username可能有2个指令(甚至更多),所以需要给每一个key配置一个指令集,用数组比较合适。

function Vue(options){
    ...
    /** 用来给每一个Model属性配置指令集 * */
    this.$bindings = {}
    ...
}

遍历Model属性的时候,初始化指令集

let _this = this;
for(let key in _this.$data){
    _this.$bindings[key] = {
        directions: []
    }
}

最后,在set的时候,去遍历所有指令,更新DOM

let value = _this.$data[key]; 
Object.defineProperty(_this.$data,key,{
    get:function(){
        return value;
    },
    set:function(newVal){
        value = newVal;
        /** 更新DOM * */
        _this.$bindings[key].directions.forEach(watcher => {
            watcher.update();
        });
    }
})

这边我们用了forEach,watcher是数组中的某一个对象,它有一个update方法,目的在于更新DOM元素。上面的代码还有一个小技巧,就是要把_this.$data[key]单独放到上面用value存起来,不然会有循环调用的问题。

演示循环调用:

Object.defineProperty(_this.$data,key,{
    get:function(){
        return _this.$data[key];
    },
    ...
})

因为_this.$data[key]会触发get函数,所以产生了循环调用,直接给你报错:

46cd2ba407404e4bb2c95b390a7e2cec.png


可是我们只要在外面用value锁住 _this.$data[key]就不会有这个问题了。(emmm......很眼熟是不是,这也算是闭包的一种,即用函数锁住变量)


然后是watcher,_this.$bindings[key].directions中我们会放置很多Watcher对象,这是一个专门用来更新DOM的对象。


Watcher代码如下:

/** 口诀:什么DOM的什么 = 某个Model属性的值* */
function Watcher(dom,expression,vm,dataKey){
    this.dom = dom;
    this.expression = expression;
    this.vm = vm;
    this.dataKey = dataKey;
    this.update();
}
Watcher.prototype.update = function(){
    this.dom[this.expression] =  this.vm.$data[this.dataKey];
}

至此,init函数开发完毕,可能有的同学会问,这弄了半天,DOM的指令怎么和Model绑定呢?别急,这就是下一步【编译阶段】该做的事情。


4. 编译阶段

编译阶段要做的事情很简单,就是根据el(你传进来的根节点元素),遍历所有的子节点(为了简单我们不做递归),挨个检查vue支持的指令,如果找到了,就将这个元素与Model进行绑定。

function Vue(options){
    /** 根节点 * */
    this.$el = document.getElementById(options.el);
    ...
}


上面的代码是获取根节点的DOM对象,然后是具体的编译函数:

Vue.prototype.compile = function(){
    let _this = this;
    let nodes = this.$el.children;
    for (let i = 0; i < nodes.length; i++) {
        let node = nodes[i];
        if(node.hasAttribute('v-model')){
            let dataKey = node.getAttribute('v-model');
            /** 如果是v-model指令,就监听DOM的input事件 * */
            node.addEventListener('input',function(){
                /** 更新Model * */
                _this.$data[dataKey] = node.value; 
            });
                /** 添加watcher * */
                _this.$bindings[dataKey].directions.push(new Watcher(
                    node,
                    'value',
                    _this,
                    dataKey
                ));
        }else if(node.hasAttribute('v-bind')){
            /** 如果是v-bind指令,只需要添加watcher即可 * */
            let dataKey = node.getAttribute('v-bind');
            _this.$bindings[dataKey].directions.push(new Watcher(
                    node,
                    'innerHTML',
                    _this,
                    dataKey
                ));
        }
    }
}

5.成果展示

刷新页面,会看到这个效果:

c30d0b0430d740008fd26c61517d45a3.png

当我们随便修改input框的值,右边会产生联动效果,很有趣,建议自己把代码撸一遍尝试一下。

6. 一定要用let,不要用var

用var最大的问题就是变量提升,比如init方法中,假如我们把这个地方改成var

279ef7fd60f747a5902ac9ac171b457a.png


你猜会发生什么?这个我在书里写过,可以根据抽象语法树去判断,当你运行这个函数时,var定义的变量会自动提升到顶端,其实是这样的。

5e83f620a7254a33a20edac1f976c20d.png

这就会导致我们去使用的时候,value已经变成循环的最后一个了,效果如图:

9cefe87e551c4463a7e96852a6d25426.png

当data的循环结束,value因为变量提升就自动变成了123456,导致data中所有属性get方法都受到了影响!

get:function(){
    return value;
},


value就变成123456了,不管是哪个key。

这与我们预期的效果相悖,虽然可以用闭包来解决这个问题,但是代码会变得很复杂。所以,我们尽量要使用let,不要用var了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <input v-model="username" placeholder="请输入用户名" autocomplete="off"> 
        <span  v-bind="username"></span>     <br>
        <input v-model="password" placeholder="请输入密码" autocomplete="off"> 
        <span  v-bind="password"></span>
    </div>   
    <script>
        function Vue(options){
            /** 根节点 * */
            this.$el = document.getElementById(options.el);
            /** Model层 * */
            this.$data = options.data;
            /** 用来给每一个Model属性配置指令集 * */
            this.$bindings = {}
            this.init(); 
            this.compile(); 
        }
        Vue.prototype.compile = function(){
            let _this = this;
            let nodes = this.$el.children;
            for (let i = 0; i < nodes.length; i++) {
                let node = nodes[i];
                if(node.hasAttribute('v-model')){
                    let dataKey = node.getAttribute('v-model');
                    /** 如果是v-model指令,就监听DOM的input事件 * */
                    node.addEventListener('input',function(){
                        /** 更新Model * */
                        _this.$data[dataKey] = node.value; 
                    });
                        /** 添加watcher * */
                        _this.$bindings[dataKey].directions.push(new Watcher(
                            node,
                            'value',
                            _this,
                            dataKey
                        ));
                }else if(node.hasAttribute('v-bind')){
                    /** 如果是v-bind指令,只需要添加watcher即可 * */
                    let dataKey = node.getAttribute('v-bind');
                    _this.$bindings[dataKey].directions.push(new Watcher(
                            node,
                            'innerHTML',
                            _this,
                            dataKey
                        ));
                }
            }
        }
        Vue.prototype.init = function(){
            let _this = this;
            for(let key in _this.$data){
                _this.$bindings[key] = {
                    directions: []
                }
                let value = _this.$data[key]; 
                Object.defineProperty(_this.$data,key,{
                    get:function(){
                        return value;
                    },
                    set:function(newVal){
                        value = newVal;
                        /** 更新DOM * */
                        _this.$bindings[key].directions.forEach(watcher => {
                            watcher.update();
                        });
                    }
                })
            }
            //console.log('所有的Model数据,都被我劫持了!');
        }
        /** 口诀:什么DOM的什么 = 某个Model属性的值* */
        function Watcher(dom,expression,vm,dataKey){
            this.dom = dom;
            this.expression = expression;
            this.vm = vm;
            this.dataKey = dataKey;
            this.update();
        }
        Watcher.prototype.update = function(){
            this.dom[this.expression] =  this.vm.$data[this.dataKey];
        }
        let vm = new Vue({
            /** 需要被vue控制的根元素ID * */
            el: 'app' ,     
            /** Model数据层 * */
            data: {
                username: 'jack',
                password: '123456'
            }
        });
    </script>
</body>
</html>

这可能是全网最易懂的Vue原理教程了吧,喜欢的话给兔子来个一键三连哦,哈哈。

相关文章
|
2月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
27天前
|
Web App开发 JavaScript 前端开发
2024年5月node.js安装(winmac系统)保姆级教程
本篇博客为2024年5月版Node.js安装教程,适用于Windows和Mac系统。作者是一名熟悉JavaScript与Vue的大一学生,分享了Node.js的基本介绍、下载链接及简单安装步骤。安装完成后,通过终端命令`node -v`验证版本即可确认安装成功。欢迎关注作者,获取更多技术文章。
27 2
2024年5月node.js安装(winmac系统)保姆级教程
|
24天前
|
JavaScript 前端开发 API
介绍一下Vue中的响应式原理
介绍一下Vue中的响应式原理
27 1
|
24天前
|
缓存 前端开发 JavaScript
JavaScript前端路由的实现原理及其在单页应用中的重要性,涵盖前端路由概念、基本原理、常见实现方式
本文深入解析了JavaScript前端路由的实现原理及其在单页应用中的重要性,涵盖前端路由概念、基本原理、常见实现方式(Hash路由和History路由)、优点及挑战,并通过实际案例分析,帮助开发者更好地理解和应用这一关键技术,提升用户体验。
61 1
|
27天前
|
监控 JavaScript 算法
深度剖析 Vue.js 响应式原理:从数据劫持到视图更新的全流程详解
本文深入解析Vue.js的响应式机制,从数据劫持到视图更新的全过程,详细讲解了其实现原理和运作流程。
|
29天前
|
JavaScript
Vue 双向数据绑定原理
Vue的双向数据绑定通过其核心的响应式系统实现,主要由Observer、Compiler和Watcher三个部分组成。Observer负责观察数据对象的所有属性,将其转换为getter和setter;Compiler解析模板指令,初始化视图并订阅数据变化;Watcher作为连接Observer和Compiler的桥梁,当数据变化时触发相应的更新操作。这种机制确保了数据模型与视图之间的自动同步。
|
1月前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
70 9
|
24天前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
51 0
|
2月前
|
前端开发 JavaScript
深入理解JavaScript中的事件循环(Event Loop):从原理到实践
【10月更文挑战第12天】 深入理解JavaScript中的事件循环(Event Loop):从原理到实践
42 1
|
2月前
|
JavaScript 前端开发
js教程——函数
js教程——函数
43 4