Vue 源码(一):从 new Vue() 开始,尝试自己写一个 Vue

让我们忘记 Vue 的所有代码,仅仅从 new Vue() 开始分析,尝试着能不能自己也来写一个类似 Vue 的框架。

<div id="app"></div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            msg: 'Hello Vue'
        },
        template: '<div>{{msg}}</div>'
    })
</script>

首先,我们需要创建一个闭包,然后给 window 上面扩展一个 Vue 的接口,因为我们在外层是需要创建一个 Vue 的实例,所以我们需要定义一个 Vue 的构造函数,然后在 return 出去。

(function(root, factory) {
    root.Vue = factory()
})(this, function() {
    function Vue() { }

    return Vue
})

我们在创建一个Vue实例的时候,会传入一个对象,这个对象包含了很多我们在Vue实例创建的时候它要用到配置项,那么这些配置项怎么接收呢?我们可以用 options来接收。

(function(root, factory) {
    root.Vue = factory()
})(this, function() {
    function Vue( options ) {
        this.$options = options || {}
    }
    return Vue
})

然后我们知道,通过 vm,也是可以直接改变 data 里面的值的,比如 vm.msg = 'Hello 大木',那么是怎么做的呢?

(function(root, factory) {
    root.Vue = factory()
})(this, function() {
    function Vue( options ) {
        this.$options = options || {}
        
        // 我们需要将 data 上面的属性代理到 Vue 的实例上面去
        var data = this._data = options.data
        var _this = this

        Object.keys(data).forEach(function(key) {
            // 我们让 Vue 的实例通过 proxy 来代理在遍历 data 的时候它所有可枚举的属性
            _this._proxy(key)
        })
    }

    Vue.prototype._proxy = function(key) {
        // 给 Vue 的实例(this) 上所有的 key 增加钩子
        Object.defineProperty(this, key, {
            get: function() {
                return this._data[key]
            },
            set: function(newVal) {
                this._data[key] = newVal
            }
        })
    }
    return Vue
})

然后我们需要做的就是去监听这个 data 数据的变化,也就是响应式系统。

(function(root, factory) {
    root.Vue = factory()
})(this, function() {
    function observer(value) {
        // 要保证传过来的 data 是个对象
        if (!value || typeof value !== 'object') return
        
        // 如果有值,并且又是对象,那么我们就创建一个 Observer 的实例
        return new Observer(value)
    }

    function Observer(value) {}

    function Vue( options ) {
        this.$options = options || {}
        
        // 我们需要将 data 上面的属性代理到 Vue 的实例上面去
        var data = this._data = options.data
        var _this = this

        Object.keys(data).forEach(function(key) {
            // 我们让 Vue 的实例通过 proxy 来代理在遍历 data 的时候它所有可枚举的属性
            _this._proxy(key)

            // 监听 data 数据,加入响应式系统
            observer(data)
        })
    }

    Vue.prototype._proxy = function(key) { ... }
    return Vue
})

所以我们可以看到,在整个设计 Vue 的过程中,我们的 data 它里面要加入响应式系统,其实就是靠 Observer 来做的。然后我们继续来完善 Observer 这个构造函数。

(function(root, factory) {
    root.Vue = factory()
})(this, function() {
    function observer(value) {
        // 要保证传过来的 data 是个对象
        if (!value || typeof value !== 'object') return
        
        // 如果有值,并且又是对象,那么我们就创建一个 Observer 的实例
        return new Observer(value)
    }

    function Observer(value) {
        this.value = value

        // 用 walk 方法来遍历 value
        this.walk(value)
    }
    Observer.prototype = {
        walk: function(value) {
            var _this = this
            Object.keys(value).forEach(function(key) {
                _this.convert(key, value[key])
            })
        },
        convert: function(key, val) {
            // this.value 就是 data,key 就是 data 上面的属性,val 是对应的值
            this.defineReactive(this.value, key, val)
        },
        // 监听对象属性值的变化
        defineReactive: function(obj, key, val) {
            Object.defineProperty(obj, key, {
                get: function() {
                    return val
                },
                set: function(newVal) {
                    // 如果我们的值发生了改变,做这个判断,原值 === 新值,说明没有更改,则终止
                    if (val === newVal) return
                    
                    // 记录新的值
                    val = newVal
                }
            })
        }
    }

    function Vue( options ) {
        this.$options = options || {}
        
        // 我们需要将 data 上面的属性代理到 Vue 的实例上面去
        var data = this._data = options.data
        var _this = this

        Object.keys(data).forEach(function(key) {
            // 我们让 Vue 的实例通过 proxy 来代理在遍历 data 的时候它所有可枚举的属性
            _this._proxy(key)

            // 监听 data 数据,加入响应式系统
            observer(data)
        })
    }

    Vue.prototype._proxy = function(key) { ... }
    return Vue
})

那么我们通过给 vm.msg 赋值为 'Hello 大木' 的过程中,我们首先会触发 Vue.prototype._proxy 上面的代理的 set 钩子函数,然后我们会把新的值给到 this.data.msg,也就意味着 data 它上面的钩子函数触发了,也就是 Observer.prototype.defineReactive 上面的 set 钩子。然后我们在打印 vm.msg 的时候,就通过它的 get 返回出去,也就是 'Hello 大木' 了。

其实我们只是给 Vue 的实例增加了一层代理,我们先不管你有没有拓展,先给你代理起来,万一哪天你要更改我,那么我就把你这个更改后的值,通知 data 上面所对应的属性,你的值发生了改变,然后你在去通知视图就 OK 了。

我们要这么做的原因是因为,我们想要在一些生命周期的钩子函数,或者说一些计算属性里面,我们想要去更改 data 里面所对应的属性的时候,我们可以直接通过 Vue 的实例的方式来给它进行改变,我们不需要去找到 data 它里面所对应的 msg 等等,这样找会非常的麻烦,那如果说我把它里面这些响应式的属性,我们都挂载在 vm 上面了,那我们在一些钩子里面进行访问或更改,那是不是更加的方便呢?这就是我们要做一层代理的原因。

那么除了这个东西之外,我们是不是还要思考另外的事情?比如初始化一些事件,依赖注入,还有生命周期等等,那么接下来也就意味,在我们创建一个 Vue 的实例的时候,我们除了通过 observer 来监听 data 数据加入响应式系统,我们还需要调用一个 init 的方法。如果你看过源码就知道 Vue 在初始化的时候 init 了很多方法,那么我们先来完成第一个。

(function(root, factory) {
    root.Vue = factory()
})(this, function() {
    // 定义对象默认的属性值
    // obj对象 prop属性 value值 def默认值
    function defineProperty(obj, prop, value, def) {
        // 没值就用 el 所挂载的 DOM 元素做为我们要编译的模板, 就是$el.outerHTML
        // 有值就用该值
        if(value === undefined) {
            obj[prop] = def
        } else {
            obj[prop] = value
        }
    }

    var noop = function() { }

    function observer(value) { ... }

    function Observer(value) { ... }
    Observer.prototype = { ... }

    function Vue( options ) {
        this.$options = options || {}
        
        // 我们需要将 data 上面的属性代理到 Vue 的实例上面去
        var data = this._data = options.data
        var _this = this
        
        // 我们在初始化的时候,让它生成一个 render 的函数
        defineProperty(this, '$render', this.$options.render, noop)

        Object.keys(data).forEach(function(key) {
            // 我们让 Vue 的实例通过 proxy 来代理在遍历 data 的时候它所有可枚举的属性
            _this._proxy(key)

            // 监听 data 数据,加入响应式系统
            observer(data)

            this.init()
        })
    }

    Vue.prototype.init = function() {
        // init 的主要作用是来做一些初始化的操作,通过官方的生命周期图表我们知道,首先就是 $mount 的挂载,那么我们就需要来检测是否有 el 这个选项
        var el = this.$options.el
        if (el !== undefined) {
            this.$mount(el)
        }
    }
    
    Vue.prototype.$mount = function(el) {
        // el 只是一个字符串,但是我们需要获取 DOM
        this.$el = typeof el === 'string' ? document.querySelector(el) : document.body

        if (this.$el == null) {
            new Error('元素' + this.$options.el + '没有被找到')
        }

        // 检测完 el 之后,我们还在要检测 template 模板是否配置
        // 通过 defineProperty 方法来检测 this 有没有 'template' 这个属性
        // 如果有,就把 this.$options.template 的值赋值给你
        // 如果没有,就找到 this.$el.outerHTML 的值当作一个模板进行编译
        defineProperty(this, '$template', this.$options.template, this.$el.outerHTML)

        if (this.$render == noop) {
            // 编译当前模板
            // 如果有传 template 属性, 就编译
            // 如果没传, 就编译实例所挂载的元素, 也就是把 #app 当作一个模板来编译
            // 将编译完成后的值再赋值给 $render, 也就是我们所说的 render function
            this.$render = Vue.compile(this.$template)
        }
    }

    Vue.compile = function(temp) {
        // 编译...
    }

    Vue.prototype._proxy = function(key) { ... }
    return Vue
})

以上就是我们通过 new Vue() 做的一个即兴发挥,根据 Vue 实例是怎么创建的,然后按照我们自己的想法和思路去写一部分代码。

扫描二维码关注公众号,回复: 8836477 查看本文章

如果上面的代码你发了很多时间去看的话,希望你不要打我\(^o^)/,因为上面的代码很多地方都是有问题的。

那么从下一篇博客开始,我们就对照着源码,一步步分析 + 编写,看看 Vue 到底是怎么做的架构。

发布了61 篇原创文章 · 获赞 3 · 访问量 4391

猜你喜欢

转载自blog.csdn.net/weixin_43921436/article/details/99347457