vue2.x源码刨析-new Vue的时候做了什么(手写简易版01)

本篇文章大致的介绍一下new Vue的过程,

首先我们在生成一个Vue实例化对象的时候,一般会这样写:

js 复制代码
<div id="app" style="color: red">{{name}} dep {{age}} dep {{name}}</div>
const vm = new Vue({
    data() {
        return {
            // 代理数据
            name: "zf",
            age: 20,
            address: {
                num: 30,
                content: "回龙观",
            },
            hobby: ["eat", "drink", { a: 1 }],
        };
    },
    created() {
        // console.log(this.xxx); // 数据来源不明确
        console.log("created");
    },
    el: "#app", // 我们要将数据  解析到el元素上
    // template: "<div>111</div>",
});
// vue.mixin 混合  可以混入一些公共方法

那么在new Vue的过程中,都做了些什么,是怎么把数据做到响应式的呢?

其实,在实例化Vue的时候,主要执行的是,_init方法,

js 复制代码
function Vue(options) {
    // options就是用户的选项
    this._init(options);
}

在实例化的时候,会对数据进行初始化,在初始化的过程中,会在Vue原型上面挂载上_init方法,

js 复制代码
initMixin(Vue); // 扩展了init方法
initLifeCycle(Vue); // vm._update vm._render
initGlobalAPI(Vue); // 全局 api 的实现
initStateMixin(Vue); // 实现了 nextTick $watch

initMixin_init方法挂载到了原型上面,

js 复制代码
export function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        // 用于初始化操作
        const vm = this;
        // Vue vm.$options 就是获取用户的配置
        // 我们定义的全局指令和过滤器.... 都会挂载到实例上
        // this.constructor 不能写成 Vue,可能是子组件
        vm.$options = mergeOptions(this.constructor.options, options);
        callHook(vm, "beforeCreate");
        // 初始化状态 初始化计算属性 watch
        initState(vm);
        callHook(vm, "created");
        if (options.el) {
            vm.$mount(options.el); // 实现数据的挂载
        }
    };

    Vue.prototype.$mount = function (el) {
        const vm = this;
        el = document.querySelector(el);
        let ops = vm.$options;
        if (!ops.render) {
            // 先进行查找有没有render函数
            let template; // 没有render看一下是否写了template, 没写template采用外部的template
            if (!ops.template && el) {
                // 没有写模板,但是写了el
                template = el.outerHTML;
            } else {
                // if (el) {
                //     template = ops.template;
                // }
                template = ops.template;
            }
            // 写了template就用写了的template
            if (template) {
                // 这里需要对模板进行编译
                const render = compileToFunction(template);
                ops.render = render;
            }
        }
        // console.log(ops.render); // 最终就可以获取render方法
        mountComponent(vm, el); // 组件的挂载
    };
}

其中,mergeOptions方法主要是用来合并两个对象

js 复制代码
const strats = {};
const LIFECYCLE = ["beforeCreate", "created"];
LIFECYCLE.forEach((hook) => {
    strats[hook] = function (p, c) {
        // {} {created:function(){}} => {created:[fn]}
        // {created:[fn]} {created: function(){}} => {created: [fn,fn]}
        if (c) {
            // 如果儿子有  父亲有  让父亲和儿子拼在一起
            if (p) {
                return p.concat(c);
            } else {
                return [c]; // 儿子有父亲没有,则将儿子包装成数组
            }
        } else {
            return p; // 如果儿子没有,则用父亲即可
        }
    };
});

strats.components = function(parentVal, childVal) {
    const res = Object.create(parentVal)
    if(childVal) {
        for(let key in childVal) {
            res[key] = childVal[key];  // 返回的是构造的对象  可以拿到父亲原型上的属性,并且将儿子的都拷贝到自己身上
        }
    }
    return res;
}



export function mergeOptions(parent, child) {
    const options = {};
    for (let key in parent) {
        // 循环老的 {a:1}
        mergeField(key);
    }
    for (let key in child) {
        // 循环新的 {}
        if (!parent.hasOwnProperty(key)) {
            mergeField(key);
        }
    }
    function mergeField(key) {
        // 策略模式,用策略模式减少 if/else
        if (strats[key]) {
            options[key] = strats[key](parent[key], child[key]);
        } else {
            options[key] = child[key] || parent[key]; // 优先采用儿子,再采用父亲
        }
    }
    return options;
}

在这里首先用child的属性替换掉parent的属性,其次使用策略模式,整合各个生命周期,这里将生命周期整合成一个数组,是因为如果我们使用了mixin方法,可能会存在一个生命周期被调用两次的情况。在这里我们可以看出components其实就是父数据的一个复制版,先使用Object.create实现父数据的继承,然后循环子数据,替换掉父数据的数据。

callhook方法就是调用生命周期

js 复制代码
export function callHook(vm, hook) {
    const handlers = vm.$options[hook];
    if (handlers) {
        handlers.forEach((handler) => handler.call(vm));
    }
}

在前面我们已经知道,生命周期通过mergeOptions方法策略模式,已经是一个数组的形式,所以在这里对数组进行循环,调用数组的方法。

当然这里面主要还有$mount方法,这个方法可以解析模板中的数据,将响应式数据进行渲染,这个方法我在后面会主要进行分析。

_init方法中,其实还有对状态进行初始化(data,computed,watch),这个后面谈到响应式数据的时候,可以再说。

在初始化的时候还有几个初始化的方法,主要还是在Vue的原型上面挂载方法。

initLifeCycle方法,其中涉及到数据的更新引起dom的变化,模板的解析,主要用来处理这些东西

js 复制代码
export function initLifeCycle(Vue) {
    Vue.prototype._update = function (vnode) {
        const vm = this;
        const el = vm.$el;

        const prevVnode = vm._vnode;
        vm._vnode = vnode; // 把组件第一次产生的虚拟节点保存到 _vnode 上
        if (prevVnode) {
            // 之前渲染过了
            vm.$el = patch(prevVnode, vnode)
        } else {
            // patch既有初始化的功能,又有更新的逻辑
            vm.$el = patch(el, vnode);
        }
    };
    // _c('div', {}, ...children)
    Vue.prototype._c = function () {
        return createElementVNode(this, ...arguments);
    };
    // _v(text)
    Vue.prototype._v = function () {
        return cretaeTextNode(this, ...arguments);
    };
    Vue.prototype._s = function (value) {
        if (typeof value !== "object") return value;
        return JSON.stringify(value);
    };
    Vue.prototype._render = function () {
        const vm = this;
        // 让 with 中的this指向vm
        // 当渲染的时候会去实例中取值,我们就可以将属性和视图绑定在一起
        return vm.$options.render.call(vm); // 通过ast语法树转义后生成的render方法
    };
}

initGlobalAPI主要是用来配置一些全局属性

js 复制代码
function initGlobalAPI(Vue) {
    // 静态方法
    Vue.options = {
        _base: Vue
    };
    Vue.mixin = function (mixin) {
        // 我们期望将用户的选项和全局的 options 进行合并
        // {} {created:function(){}} => {created:[fn]}
        // {created:[fn]} {created: function(){}} => {created: [fn,fn]}
        this.options = mergeOptions(this.options, mixin);
        return this;
    };
    Vue.extend = function(options) {
        // 就是实现根据用户的参数 返回一个构造函数而已
        function Sub(options = {}) { // 最终使用一个组件 就是 new 一个实例
            this._init(options); // 就是默认对子类进行初始化操作
        }  

        Sub.prototype = Object.create(Vue.prototype);  // Sub.prototype.__proto__ === Vue.prototype
        Sub.prototype.constructor = Sub;  // 组合式继承要重新连接
        // 希望将用户传递的参数 和全局的 Vue.options 来合并
        Sub.options = mergeOptions(Vue.options, options); // 保存用户传递的选项;
        return Sub;
    }

    Vue.options.components = {} // 全局的指令  Vue.options.directives
    Vue.component = function(id, definition) {

        // 如果 definition 已经是一个函数了,说明用户自己调用了 Vue.extend

        definition = typeof definition === 'function' ? definition : Vue.extend(definition)
        Vue.options.components[id] = definition;
        console.log(Vue.options.components);
    }
}

initStateMixin主要实现$nextTick$watch方法,其中$watch方法极为重要,是整个响应式系统的核心

js 复制代码
function initStateMixin(Vue) {
    Vue.prototype.$nextTick = nextTick;
    Vue.prototype.$watch = function (exprOrFn, cb) {
        // firstname 的值变化了,直接执行 cb 函数即可
        new Watcher(this, exprOrFn, { user: true }, cb);
    };
}
相关推荐
成都被卷死的程序员16 分钟前
响应式网页设计--html
前端·html
fighting ~19 分钟前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录25 分钟前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
abments26 分钟前
JavaScript逆向爬虫教程-------基础篇之常用的编码与加密介绍(python和js实现)
javascript·爬虫·python
mon_star°35 分钟前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf219131845539 分钟前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
老码沉思录1 小时前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js
文军的烹饪实验室2 小时前
ValueError: Circular reference detected
开发语言·前端·javascript
Martin -Tang3 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发3 小时前
解锁微前端的优秀库
前端