一步一步手撸一个miniVue(一)

326 阅读4分钟

本文适用于想探究vue工作原理的同学,大家不妨将其作为一个入门vue原理的文章,跟着一步一步实现,到最后看到结果,内心幸福感满满。这仅仅是我一些感悟,如果有不对的地方,希望各位大佬能指正。

1.vue的工作机制

我们在初始化vue实例之后,执行$mount,vue就会开始编译模板语法。

因为浏览器并不认识我们写的双大括号,v-xxx,自定义组件一类的指令,编译的过程就是将这些指令转化为符合浏览器标准的语法。这就是下图中所示的compile过程。由此可以vue在背后默默做了很多工作,才让我们工作起来上手这么容易。

在编译这个阶段我们会生成渲染函数,或者说是更新函数,我们会产生一个虚拟的dom树,之后我们更新的操作首先都是作用于这个虚拟的树上的。当更新之前我们会做一个diff算法的比较,从而计算出我们需要进行最小的更新,到patch()去打补丁,渲染出真实的页面。

浏览器的瓶颈出现在dom操作,页面渲染这个阶段。vue的核心目的就是用js计算时间去换dom操作的时间,减少页面渲染的次数

compile()除了生成渲染函数外还有一个最重要的功能,就是依赖收集。页面中元素是怎么和我们的数据模型发生关系的呢,当数据发生变化之后页面又是怎么响应式的更新的呢?这一部分是我们接下来要实现的重点,也是vue的核心部分。

好了,废话不多说,代码永远是别人的,只有自己撸一遍才是自己的

talk is cheap,show me the code.

新建一个index.html,我们先来认识一下vue2.x中进行数据侦测的一个方法:Object.defineProperty()。

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <p id="name"></p>
    </div>
    <script>
        var obj = {};
        Object.defineProperty(obj,"name",{
            get(){
                return document.getElementById("name").innerHTML
            },
            set(newVal){
                document.getElementById("name").innerHTML = newVal
            }
        })
        obj.name = "Link"
    </script>
</body>
</html>

输出:

我们在index.html同级目录下新建一个LinkVue.js,这个js就是我们用来进行数据监听的地方,名称可以取一个你喜欢的名字。我们在写之前先回顾一下Vue是怎么初始化实例的,都接收了什么参数。首先vue会接收一个el,这是挂载点,我们在编译的时候要编译里面的子元素。还要接受一个data,这里面的数据变化时视图要进行响应式更新。生命周期钩子我们也可以在这里接收。还有method。我们写一个类,LinkVue,构造函数里接受这几个值。然后我们需要一个observe(),用以对传入的data遍历。

class LinkVue {
    constructor(option) {
        this.$el = option.el;
        this.$data = option.data
        this.observe(this.$data)
    }
}

oberserve()接收的是一个对象,当然也可以是一个函数的返回对象,我这里没有做处理,简单的当作用户传入了一个对象处理。在对data进行遍历之后,我们写一个defineReactive(),这个函数用来定义响应式。

observe(data){
        //判断传入数据的合法性
        if(data&&typeof(data)=="object"){
            //对data遍历
            Object.keys(data).forEach((key)=>{
                this.defineReactive(data,key,data[key])
            })
        }
    }

在defineReactive()这个函数里面我们就用到了一开始讲到的Object.defineProperty()。这个方法对data的每一个属性都设置了getter和setter.当读取data中的属性时会触发get方法,当数据发生改变时会触发set()。另外为了能够深度监听data中的属性变化,我们要在defineReactive()里递归一下observe函数。

defineReactive(data, key, value) { 
    //循环遍历data,直至不是一个对象
    this.observe(value) 
    Object.defineProperty(data, key, {
        get() {
            return value
        },
        set(newVal) {
            if (value === newVal) {
                return
            }
            value = newVal 
            console.log(key + "更新成为了" + newVal)
        }
    })
}

我在set的时候加了一个log,这可以让我们阶段性的看到我们的结果。修改index.html如下:

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <script src="LinkVue.js"></script>
    <div id="app"> </div>
    <script>
        const app = new LinkVue({
            el: "#app",
            data: {
                firstdata: "have a try",
                fullname: {
                    fisrname: "Link"
                }
            }
        }) 
        app.$data.firstdata = "changeFirstData"
        app.$data.fullname.fisrname = "Lin"
    </script>
</body>
</html>

输出结果:

我们可以看到当data中的属性发生改变后,已经触发了set函数,也打印出的相应的消息。

接下来我们实现依赖收集的这部分工作。

我们需要新写一个类,用来保存收集的观察者(watcher)。可以新写一个js,也可以直接写在这个LinkVue.js里面。我们为了更加清晰,还是写在这个js里面。在构造函数里面我们初始化一个数组,这个数组用来存放watcher。这里的对应关系时data中的一个属性对应一个deps,template解析后出现几个这个data中的属性,那这个deps里面就有几个watcher。

Dep这个类里面有两个成员方法,addDep()就是添加操作,而notify()是通知操作,通知deps[]里面的watcher进行更新。

这里面可能有点绕,我一开始也不是很明白Dep是怎么和Watcher发生联系的,没事,我们先向下写,这部分我们稍后会说明。

class Dep {
    constructor() {
        //数组里面存放的时watcher
        this.deps = []
    }
    addDep(dep) {
        this.deps.push(dep)
    }
    notify() {
        this.deps.forEach((dep) => dep.update())
    }
}

我们还需要一个watcher类,我们在这个类里面进行update操作。其中将Dep与Watcher链接起来的是构造函数里面的Dep.target = this;将watcher实例指向Dep的静态属性target。成员方法update()我们暂时不需要它做什么,可以打印一下,验证一下我们到现在做的是否正确。

class Watcher {
    constructor() {
        //将当前watcher的实例指定到Dep的静态属性target下 
        Dep.target = this;
    }
    update() {
        console.log("执行了update方法");
    }
}

我们修改一下我们的 class LinkVue,在定义响应化的defineReactive()函数里面添加初始化依赖收集的方法。在执行get函数的时候我们将Dep.target通过addDep()添加到这个属性对应的deps(依赖收集者)中。在数据发生变化,执行set函数的时候,我们用notify()通知更新。

修改后的defineReactive()方法如下

defineReactive(data, key, value) {
    //循环遍历data,直至不是一个对象 
    this.observe(value)
    const dep = new Dep()
    Object.defineProperty(data, key, {
        get() {
            Dep.target && dep.addDep(Dep.target)
            return value
        },
        set(newVal) {
            if (value === newVal) {
                return
            }
            value = newVal
            dep.notify()
            // console.log(key+"更新成为了"+newVal)
        }
    })
}

好了,到现在我们的数据处理部分大体上已经完成了,我们可以修改一下index.html,看看码到现在的结果。修改index.html。在初始化LinkVue之后,我们初始化Watcher,而且想要触发get(),我们要在初始化Watcher之后读一下这个属性。虽然现在看起来有点笨,但是我们还没写编译函数,未来Watcher的初始化我们会放在编译这个class下。修改如下:

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <script src="LinkVue.js"></script>
    <div id="app"> </div>
    <script>
        const app = new LinkVue({
            el: "#app",
            data: {
                firstdata: "have a try",
                fullname: {
                    fisrname: "Link"
                }
            }
        })
        new Watcher()
        app.$data.firstdata
        new Watcher()
        app.$data.fullname.fisrname
        app.$data.firstdata =
            "changeFirstData"
        app.$data.fullname.fisrname = "Lin"
    </script>
</body>

</html>

输出结果如下:

到这一步,恭喜你,vue的响应式你已经完成了,下面我们将着手实现编译,将vue提供给我们的方便的模板语法编译成浏览器认识的语法。完成这一步能让你看到实实在在的效果,相信我,实现一遍会让你感到巨大的满足感。