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);
    };
}
相关推荐
陈随易2 小时前
Postman 被曝自动往 Claude Code 和 Codex 装插件
前端·后端·程序员
Dream-Y.ocean4 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn5 小时前
性能监控
前端
Highcharts.js5 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn5 小时前
构建工具
前端
FRing036 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
Java小卷6 小时前
JuanForm vs FormCreate!Vue3 表单引擎联动配置 PK!
vue.js·低代码
kyriewen8 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员
IT_陈寒8 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端
工控丫丫9 小时前
帆软看板中经常使用的公式函数及经常搭的组合
前端·javascript·数据库