本篇文章大致的介绍一下new Vue的过程,
首先我们在生成一个Vue实例化对象的时候,一般会这样写:
<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
方法,
function Vue(options) {// options就是用户的选项this._init(options);
}
在实例化的时候,会对数据进行初始化,在初始化的过程中,会在Vue原型上面挂载上_init
方法,
initMixin(Vue); // 扩展了init方法
initLifeCycle(Vue); // vm._update vm._render
initGlobalAPI(Vue); // 全局 api 的实现
initStateMixin(Vue); // 实现了 nextTick $watch
initMixin
将_init
方法挂载到了原型上面,
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");// 初始化状态 初始化计算属性 watchinitState(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采用外部的templateif (!ops.template && el) {// 没有写模板,但是写了eltemplate = el.outerHTML;} else {// if (el) {// template = ops.template;// }template = ops.template;}// 写了template就用写了的templateif (template) {// 这里需要对模板进行编译const render = compileToFunction(template);ops.render = render;}}// console.log(ops.render); // 最终就可以获取render方法mountComponent(vm, el); // 组件的挂载};
}
其中,mergeOptions方法主要是用来合并两个对象
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/elseif (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方法就是调用生命周期
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的变化,模板的解析,主要用来处理这些东西
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主要是用来配置一些全局属性
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.prototypeSub.prototype.constructor = Sub; // 组合式继承要重新连接// 希望将用户传递的参数 和全局的 Vue.options 来合并Sub.options = mergeOptions(Vue.options, options); // 保存用户传递的选项;return Sub;}Vue.options.components = {} // 全局的指令 Vue.options.directivesVue.component = function(id, definition) {// 如果 definition 已经是一个函数了,说明用户自己调用了 Vue.extenddefinition = typeof definition === 'function' ? definition : Vue.extend(definition)Vue.options.components[id] = definition;console.log(Vue.options.components);}
}
initStateMixin主要实现$nextTick
和$watch
方法,其中$watch
方法极为重要,是整个响应式系统的核心
function initStateMixin(Vue) {Vue.prototype.$nextTick = nextTick;Vue.prototype.$watch = function (exprOrFn, cb) {// firstname 的值变化了,直接执行 cb 函数即可new Watcher(this, exprOrFn, { user: true }, cb);};
}