<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--
模板编译原理 通过AST语法树描述出html语法结构 ,在把这个语法转换成JS语法,包上with 生成render方法 ,调用这个方法返回虚拟dom, 再把虚拟dom变成真实dom渲染到页面上。
注意:虚拟dom和ast语法树不一样,虚拟dom是表示dom对象的,ast语法树这里表示html结构
1. 将模板变成一个render函数,
2. 去当前的的实例上取值
3. 虚拟dom(说白了就是一个对象)它的好处是可以描述DOM结构,diff算法,可以比对哪些属性变化了哪些属性没有变化,把需要变化的进行更新
4. 生成真实dom
-->
<div id="app">
</div>
<!-- {{arr2[0].name}}{{arr2}} -->
<script src="/dist/vue.js"></script>
<script>
// mergeOptions API实现的原理
Vue.mixin({ // mixin 是一个静态方法
beforeCreate() { // 组件创建之前
console.log('beforeCreate1')
},
created() { // 数据已经被劫持
console.log('created1')
}
})
Vue.mixin({ // mixin 是一个静态方法
beforeCreate() { // 组件创建之前
console.log('beforeCreate2')
},
created() { // 数据已经被劫持
console.log('created2')
}
})
const vm = new Vue({
el: '#app',
beforeCreate() { // 组件创建之前
console.log('beforeCreate3')
},
created() { // 数据已经被劫持
console.log('created3')
}
})
/*
Vue.mixin 会合并成一个数组
*/
</script>
</body>
</html>
# Mixin原理
// src/global-api/index.js
import { mergeOptions } from '../util/index.js';
export function initGlobalAPI(Vue) {
Vue.options = {}; // 用来存储全局的配置,如多次调用了Vue.mixin(),将里面的数据合并到这个对象
Vue.mixin = function(mixin) {
// 将属性合并到Vue.options上
this.options = mergeOptions(this.options, mixin);
return this;
};
}
# 合并生命周期
// src/util/index.js
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed'
];
export const isObject = (value) => typeof value === 'object' && value !== null;
const strats = {};
// 核心就是把生命周期钩子函数变成一个数组
function mergeHook(parentVal, childValue) {
if (childValue) {
if (parentVal) {
return parentVal.concat(childValue);
} else {
// 儿子有父亲没有
return [ childValue ];
}
} else {
// 如果儿子没有就用父亲的
return parentVal;
}
}
// 把这些钩子都放到策略上
LIFECYCLE_HOOKS.forEach((hook) => {
strats[hook] = mergeHook;
});
export function mergeOptions(parent, child) {
/*
合并策略:
如果父亲有的儿子也有,应该用儿子替换父亲, 如 父元素的数据是{a:1} 子元素的数据是{a:2}合并后,应该是 {a:2}
如果父元素有值子元素没有,则用父元素的 如 父{a:1} 子{} 合并后应该是 {a: 1}
*/
const options = {};
for (let key in parent) {
mergeField(key);
}
for (let key in child) {
// 父亲没有儿子有
if (!parent.hasOwnProperty(key)) {
mergeField(key);
}
}
function mergeField(key) {
// if (strats[key]) {
// options[key] = strats[key](parent[key], child[key]);
// } else {
// if (typeof parent[key] == 'object' && typeof child[key] == 'object') {
// options[key] = {
// ...parent[key],
// ...child[key]
// };
// } else {
// options[key] = child[key];
// }
// }
// 策略模式
if (strats[key]) {
return options[key] = strats[key](parent[key], child[key]);
}
if (isObject(parent[key]) && isObject(child[key])) {
options[key] = { ...parent[key], ...child[key] };
} else {
if (child[key]) {
// 如果儿子有值
options[key] = child[key];
} else {
options[key] = parent[key];
}
}
}
return options;
}
# 调用生命周期
// src/lifeCycle.js
import { patch } from './observer/patch';
import Watcher from './observer/watcher';
export function lifecycleMixin(Vue) {
Vue.prototype._update = function(vnode) {
console.log('_udpate', vnode);
// 将虚拟节点转换成真实dom
const vm = this;
console.log('vm.$options.el', vm.$options.el);
// 首次渲染需要用虚拟节点更新真实dom
// vm.$el = patch(vm.$options.el, vnode);
// 第一次渲染的完毕后,拿到新的节点,下次再次渲染时替换上次渲染的结果
vm.$el = patch(vm.$el, vnode);
};
}
export function mountComponent(vm, el) {
// 默认 vue 通过 watcher 来渲染,这个watcher可以叫做渲染watcher,每个组件都有一个渲染watcher
vm.$el = el;
let updateComponent = () => {
// 将虚拟节点 渲染到页面上
// vm._render() 返回虚拟节点
// vm._update() 将虚拟节点转换成真实节点
vm._update(vm._render());
};
new Watcher(
vm,
updateComponent,
() => {
// 回调函数
},
true
); // true 表示这是一个渲染watcher
}
// 参数的意思就是调用实例上的哪个hook
export function callHook(vm, hook) { // 发布模式
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm);
}
}
}
# 初始化流程中调用生命周期
// src/index.js
import { initGlobalAPI } from './global-api/index';
import { initMixin } from './init';
import { lifecycleMixin } from './lifeCycle';
import { renderMixin } from './render';
function Vue(options) {
this._init(options); // 当用户 new Vue 时,会调用 init 方法进行 vue 的初始化
}
// Vue.prototype._init = function(options) {
// const vm = this;
// vm.$options = options; // 实例上有个属性$options 表示是用户传入的属性
// };
// 这样写的好处是可以查分逻辑,便于代码维护
initMixin(Vue); // 给原型上新增_init方法
lifecycleMixin(Vue); // 更新逻辑 包含的主要方法_update()
renderMixin(Vue); // 调用render逻辑 包含的主要方法_render()
initGlobalAPI(Vue); // 混合全局的api
export default Vue;
// src/init.js
import { compileToFunctions } from './compiler/index';
import { callHook, mountComponent } from './lifeCycle';
import { initState } from './state';
import { mergeOptions } from './util/index';
import { nextTick } from './util/next-tick';
export function initMixin(Vue) {
Vue.prototype._init = function(options) {
const vm = this;
// 用用户传去的options和全局的进行合并
vm.$options = mergeOptions(vm.constructor.options, options)
console.log(vm.$options)
// vm.$options = options; // 实例上有个属性$options表示的是用户传入的属性,可以通过vm.$options获取到用户传入的所有属性
callHook(vm, 'beforeCreate');
// 初始化状态
initState(vm);
callHook(vm, 'created');
// 有el属性的话,说明数据可以挂在到页面上
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
Vue.prototype.$nextTick = nextTick
Vue.prototype.$mount = function(el) {
const vm = this;
const options = vm.$options;
el = document.querySelector(el);
vm.$el = el;
// 如果有render就直接用render,没有render,看看有没有template属性,如果也没有template属性的话,就直接找外部模板
// 如果没有render方法
if (!options.render) {
let template = options.template;
// 如果没有模板但是有el
if (!template && el) {
template = el.outerHTML;
}
// 将模板编译成render函数
const render = compileToFunctions(template);
options.render = render;
console.log('render', render);
}
mountComponent(vm, el); // 组件挂载
};
}
Vue生命周期钩子的核心是使用发布订阅模式先把用户传入的生命周期钩子函数订阅好,然后在创建组件实例的时候依次执行对应的钩子方法。