本篇文章大致的介绍一下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: "111",
});
// 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");
// 初始化状态 初始化计算属性 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方法主要是用来合并两个对象
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方法就是调用生命周期
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.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
方法极为重要,是整个响应式系统的核心
function initStateMixin(Vue) {
Vue.prototype.$nextTick = nextTick;
Vue.prototype.$watch = function (exprOrFn, cb) {
// firstname 的值变化了,直接执行 cb 函数即可
new Watcher(this, exprOrFn, { user: true }, cb);
};
}