# Vue2 个人笔记
# 01.手写 Vue 数据代理、数据劫持、模板编译、挂载
# 1.package.json
{
"name": "mvvm",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"serve": "rollup -c -w"
},
"devDependencies": {
"@babel/core": "^7.13.1",
"@babel/preset-env": "^7.13.5",
"rollup": "^2.39.1",
"rollup-plugin-babel": "^4.4.0"
}
}
# 2.rollup.config.js
import babel from 'rollup-plugin-babel';
export default {
input: './lib/index.js',
output: {
format: 'umd',
name: 'Vue',
file: 'dist/vue.js',
sourcemap: true,
},
plugins: [
babel({
exclude: 'node_modules/**',
}),
],
};
# 3.index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div
id="root"
name="zhengchangfu"
style="color: red;background-color: #fff;"
>
<p>
<span>我叫{{ name }}哈哈</span>
</p>
</div>
<script src="./dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#root',
data() {
return {
arr: [1, { b: 2 }],
name: '郑常富',
age: 18,
sex: {
a: 1,
b: 2,
c: {
d: 3,
},
},
};
},
});
console.log(vm.name);
const obj = {
name: 'zcf',
};
const fn = new Function(`with(this){return name}`);
console.log(fn.call(obj));
</script>
</body>
</html>
# 4.lib 目录介绍
文件夹/文件 | 含义 |
---|---|
compile 目录 | 编译层 |
observe 目录 | 响应式处理层 |
vdom 目录 | 虚拟 dom 处理/diff 算法层 |
index.js 文件 | 入口文件 |
init.js 文件 | 初始化方法 |
lifecycle.js 文件 | 挂载组件 |
render.js 文件 | 调用 render 函数、创建虚拟对象 |
state.js 文件 | 初始化 props/methods/data/computed/watch...等 |
utils.js 文件 | 通用工具方法 |
# 5.index.js 文件代码
import { initMixin } from './init';
import { renderMixin } from './render';
import { lifecycleMixin } from './lifecycle';
function Vue(options) {
// 初始化方法
this._init(options);
}
initMixin(Vue);
renderMixin(Vue);
lifecycleMixin(Vue);
export default Vue;
# 6. init.js 文件代码
import { initState } from './state';
import { mountComponent } from './lifecycle';
import { CompileToFunction } from './compile/index';
export function initMixin(Vue) {
// 初始化调用了这个方法
Vue.prototype._init = function(options) {
const vm = this;
vm.$options = options;
// 调用state方法
initState(vm);
};
// 组件挂载的方法
Vue.prototype.$mount = function(vm) {
let el = vm.$options.el;
if (!vm.$options.render) {
el = document.querySelector(el);
vm.$el = el;
el = el.outerHTML;
// 将字符串编译成函数
let render = CompileToFunction(el);
console.log(render, 'render');
vm.$options.render = render;
}
// 挂载组件
mountComponent(vm, el);
};
}
# 7. state.js 文件代码
import { observe } from './observe/index';
import { isFunction } from './utils';
export function initState(vm) {
initData(vm);
// 挂载
if (vm.$options.el) {
vm.$mount(vm);
}
}
function proxy(vm, source, key) {
Object.c(vm, key, {
get() {
return vm[source][key];
},
set(newVal) {
vm[source][key] = newVal;
},
});
}
function initData(vm) {
let data = vm.$options.data;
data = vm._data = isFunction(data) ? data.call(vm) : data;
// 所有的数据现在都在_data中,那么我们做一层数据代理,外界访问vm.xxx的时候我们去vm._data中读取xxx
for (let key in data) {
proxy(vm, '_data', key);
}
// 响应式处理
observe(data);
}
# 8.observe/index.js 文件代码
import { isArray, isObject } from '../utils';
import { arrayMethods } from './array';
class Observe {
constructor(data) {
// 响应式属性里都会有一个__ob__属性
// data.__ob__ = this // 这么写会导致爆栈,因为一直在调用walk方法,然后一直在循环调用new Observe
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false, // 不可被枚举(遍历)
});
// 如果数据为一个数组,那么也会进来,此时我们要改写数组中的方法
if (isArray(data)) {
// 改写数组的7个方法
data.__proto__ = arrayMethods;
this.observeArray(data); // 对数组中是对象的定义成响应式
} else {
this.walk(data);
}
}
observeArray(data) {
data.forEach((item) => {
observe(item);
});
}
walk(data) {
Object.keys(data).forEach((key) => {
const value = data[key];
defineReactive(data, key, value);
});
}
}
function defineReactive(data, key, value) {
// 如果对象嵌套对象,继续进行递归观测
observe(value);
Object.defineProperty(data, key, {
get() {
return value;
},
set(newVal) {
// 如果用户新赋值一个对象,那么新对象也要进行观测
observe(newVal);
value = newVal;
},
});
}
export function observe(data) {
if (!isObject(data)) return;
if (data.__ob__) return;
return new Observe(data);
}
# 9.observe/array.js 文件代码
const oldArrayProto = Array.prototype;
export const arrayMethods = Object.create(oldArrayProto);
let methods = ['shift', 'unshift', 'sort', 'pop', 'splice', 'push', 'reverse'];
methods.forEach((method) => {
// 对数组方法进行重写
arrayMethods[method] = function(...args) {
oldArrayProto[method].call(this, ...args);
let ob = this.__ob__; // 拿到Observe实例
// 如果方法是新增属性的,那么我们要对新增的属性再次进行响应式处理
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
default:
break;
}
if (inserted) {
// 说明调用了push/unshift/splice等方法
// 将新增的数据定义成响应式
ob.observeArray(inserted);
}
};
});
# 10.compile/index.js 文件代码
import { parserHtml } from './parser';
import { generate } from './generate';
export function CompileToFunction(html) {
// 将html编译成ast语法树
let root = parserHtml(html);
// 将ast语法树转换成代码
let code = generate(root);
console.log(code, 'code');
// 将code代码串转成函数
let render = new Function(`with(this){return ${code}}`);
// console.log(render, 'render')
/**
* ƒ anonymous(){
with(this){
return _c(
'div',
{
id:"root",
name:"zhengchangfu",
style:{
"color":" red",
"background-color":" #fff"
}
},
_c(
'p',
undefined,
_c(
'span',
undefined,
_v(
"我叫"+name+"哈哈"
)
)
)
)
}
}
*/
return render;
}
# 11.compile/parser.js 文件代码
import { compileTemplate } from '../utils';
// 树根
let root = null;
// 栈
const stack = [];
// 创建ast语法树
function createAstElement(tag, attrs) {
return {
tag,
parent: null,
children: [],
attrs,
type: 1,
text: '',
};
}
function start(tag, attributes) {
// console.log('标签开始', tag, attributes)
// 取出栈中最后一个
const parent = stack[stack.length - 1];
// 创建ast元素
const element = createAstElement(tag, attributes);
// 如果没有根元素,那么创建出来的ast元素就是根元素
if (!root) {
root = element;
}
if (parent) {
// 有父级元素
parent.children.push(element);
element.parent = parent;
}
stack.push(element);
}
function chars(text) {
// console.log('标签文本', text)
const parent = stack[stack.length - 1];
text = text.replace(/\s/g, '');
if (text) {
parent.children.push({
type: 3,
text,
});
}
}
function end(tag) {
// console.log('标签结束', tag)
const element = stack.pop();
if (element.tag !== tag) {
throw new Error('标签有误');
}
}
export function parserHtml(html) {
/**
* 用正则匹配标签开始,标签名,然后提取标签名放入容器中,然后移除掉匹配的部分
* 用正则匹配标签属性,然后提取属性放入容器中,然后移除掉匹配的部分
* 用正则匹配标签结束,然后移除掉匹配的部分
* 用正则匹配标签中的文本,然后提取文本放入容器中,然后移除掉匹配的部分
* 用正则匹配标签结束,然后提取标签结束名和标签开始的标签名进行匹配,如果相同,说明标签相同,不相同,抛错
*/
function advance(len) {
html = html.substring(len);
}
// 解析开始标签
function parseStartTag() {
let startTag = compileTemplate.startTag(html);
if (startTag) {
advance(startTag[0].length);
let match = {
tag: startTag[1],
attrs: [],
};
let text, end;
// 匹配属性,只要没有到结束标签并且一直都可以匹配到属性,就一直循环
while (
!(end = compileTemplate.tagClose(html)) &&
(text = compileTemplate.tagAttributes(html))
) {
match.attrs.push({
key: text[1],
value: text[2] || text[3] || text[4],
});
advance(text[0].length);
}
if (end) {
advance(end[0].length);
}
return match;
}
return false;
}
// 解析结束标签
function parseEndTag() {
let endTag = compileTemplate.closeTag(html);
if (endTag) {
advance(endTag[0].length);
return endTag[1];
}
return false;
}
while (html) {
let index = html.indexOf('<');
if (index === 0) {
// 不是标签开始位置就是标签结束位置
// 解析开始标签
let startMatch = parseStartTag();
if (startMatch) {
start(startMatch.tag, startMatch.attrs);
continue;
}
let endMatch = parseEndTag();
if (endMatch) {
end(endMatch);
continue;
}
}
// 拿到文本的所有内容
let text = html.slice(0, index);
if (text) {
chars(text);
advance(text.length);
}
}
return root;
}
# 12.compile/generate.js 文件代码
import { compileTemplate } from '../utils';
function gen(c) {
if (c.type === 1) {
// 元素
return generate(c);
} else {
// 文本 _v('hello')
const text = c.text;
const regRes = compileTemplate.qn(text);
// 如果没有匹配到插值语法 hello {{name}} world ==> 'hello' + name + 'world'
if (!regRes) {
return `_v('${text}')`;
} else {
const reg = compileTemplate.qn(text, 0);
let tokens = [];
let lastIndex = 0;
text.replace(reg, function(...args) {
if (text.slice(lastIndex, args[2])) {
tokens.push(JSON.stringify(text.slice(lastIndex, args[2])));
}
if (lastIndex < text.length - 1) {
tokens.push(args[1].trim());
}
lastIndex = args[2] + args[0].length;
});
if (lastIndex < text.length) {
if (text.slice(lastIndex)) {
tokens.push(JSON.stringify(text.slice(lastIndex)));
}
}
return `_v(${tokens.join('+')})`;
}
}
}
function genChildren(root) {
const children = root.children;
if (children && children.length) {
const res = children.map((c) => gen(c)).join('+');
return res;
}
return false;
}
function genProps(props) {
let str = '';
for (let i = 0; i < props.length; i++) {
let { key, value } = props[i];
if (key === 'style') {
let styleObj = {};
// "color: red;background-color: #fff"; 转成对象
value.replace(/([^:;]+):([^:;]+)/g, function(p, s1, s2) {
styleObj[s1] = s2;
});
value = styleObj;
}
str += `${key}:${JSON.stringify(value)},`;
}
return `{${str.slice(0, -1)}}`;
}
export function generate(root) {
// 将ast语法树拼接成字符串
/**
* _c('div',{id:'root',name:'zhengchangfu'},'hello')
* new Function + with语法
*/
const children = genChildren(root);
let code = `_c('${root.tag}',${
root.attrs.length ? genProps(root.attrs) : 'undefined'
}${children ? `,${children}` : ''})`;
//code ==> _c('div',{id:"root",name:"zhengchangfu",style:{"color":" red","background-color":" #fff"}},_c('p',undefined,_c('span',undefined,_v("我叫"+name+"哈哈"))))
return code;
}
# 13.render.js 文件代码
import { createElement, createText } from './vDom/index';
export function renderMixin(Vue) {
// 创建元素
Vue.prototype._c = function(...args) {
return createElement(this, ...args);
};
// 创建文本
Vue.prototype._v = function(text) {
return createText(this, text);
};
Vue.prototype._render = function() {
const vm = this;
const render = vm.$options.render;
// 让render函数中的this指向this,所以我们在模板中不需要写vm.xxxx
let vNode = render.call(vm);
console.log(vNode);
return vNode;
};
}
# 14. vdom/index.js 文件代码
export function createElement(vm, tag, data = {}, ...children) {
return createVDom(vm, tag, data, data.key, children, undefined);
}
export function createText(vm, text) {
return createVDom(vm, undefined, undefined, undefined, undefined, text);
}
function createVDom(vm, tag, data, key, children, text) {
return {
vm,
tag,
data,
key,
children,
text,
};
}
# 15. vdom/patch.js 文件代码
export function patch(oldVnode, vNode) {
// 新旧虚拟dom比较,暂时不做
const parent = oldVnode.parentNode;
if (parent.nodeType === 1) {
// 真实元素
const elm = createEl(vNode);
parent.insertBefore(elm, oldVnode.nextSibling);
parent.removeChild(oldVnode);
} else {
}
}
function createEl(vNode) {
const { vm, text, key, children, tag, data } = vNode;
console.log(vNode, '~~');
let elm = '';
if (tag) {
// 标签
elm = document.createElement(tag);
console.log(elm, 'elm');
if (children && children.length) {
for (let i = 0; i < children.length; i++) {
const item = children[i];
elm.appendChild(createEl(item));
}
}
} else {
// 文本
elm = document.createTextNode(text);
}
return elm;
}
# 16. utils.js 文件代码
export const isFunction = (val) => {
return typeof val === 'function';
};
export const isObject = (val) => {
return typeof val === 'object' && val !== null;
};
export const isArray = (val) => {
return Array.isArray(val);
};
export let compileTemplate = {
// 匹配标签名
tag: `[a-zA-Z][0-9a-zA-Z]*`,
// 匹配标签开始
startTag: (html) => {
const reg = new RegExp(`^<(${compileTemplate.tag})`);
return html.match(reg);
},
// 匹配标签属性
tagAttributes: (text) => {
// 标签属性有3种,a=b a='b' a="b",前后可能有多个空格
const reg = /\s*([^=\s'"\/<>]+)\s*=\s*(?:"([^"]+)")|(?:'([^']+)')|([^\s"'=<>`]+)?/;
return text.match(reg);
},
// 匹配标签结束
tagClose: (text) => {
// 可能为自结束标签,可以为> ,/>
const reg = /^\s*(\/?)>/;
return text.match(reg);
},
// 匹配结束标签
closeTag: (text) => {
// </div >
const reg = new RegExp(`^<\/(${compileTemplate.tag})*>`);
return text.match(reg);
},
// 匹配插值语法
qn: (text, count = 1) => {
const reg = /\{\{([\s\S]*?)\}\}/g;
return count ? text.match(reg) : reg;
},
};
# 02.手写响应式原理(Dep 和 Watcher)
# 1.lib 目录新增文件
文件夹/文件 | 含义 |
---|---|
observe/dep 文件 | 依赖收集文件 |
observe/watcher 文件 | 通知视图更新文件 |
# 2.lifecycle 文件代码改动如下
import { patch } from './vDom/patch';
import Watcher from './compile/watcher';
export function mountComponent(vm, el) {
const updateComponent = () => {
// 生成虚拟dom
const vNode = vm._render();
// 更新
vm._update(vNode);
};
// updateComponent() ==> 更新组件
// 这里渲染的时候我们创建一个Watcher,在Watcher中更新组件
new Watcher(
vm,
updateComponent,
() => {
console.log('update view');
},
true
); // true代表是一个渲染Watcher
}
export function lifecycleMixin(Vue) {
Vue.prototype._update = function(vNode) {
const vm = this;
vm.$el = patch(vm.$el, vNode);
};
}
# 3. observe/index 文件代码改动如下
import { isArray, isObject } from '../utils';
import { arrayMethods } from './array';
import Dep from '../compile/dep';
class Observe {
constructor(data) {
// 响应式属性里都会有一个__ob__属性
// data.__ob__ = this // 这么写会导致爆栈,因为一直在调用walk方法,然后一直在循环调用new Observe
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false, // 不可被枚举(遍历)
});
// 如果数据为一个数组,那么也会进来,此时我们要改写数组中的方法
if (isArray(data)) {
// 改写数组的方法
data.__proto__ = arrayMethods;
this.observeArray(data); // 对数组中是对象的定义成响应式
} else {
this.walk(data);
}
}
observeArray(data) {
data.forEach((item) => {
observe(item);
});
}
walk(data) {
Object.keys(data).forEach((key) => {
const value = data[key];
defineReactive(data, key, value);
});
}
}
function defineReactive(data, key, value) {
// 如果对象嵌套对象,继续进行递归观测
observe(value);
// 每个属性都有自己的dep
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
// 先执行的是pushTarget方法存放了watcher,然后挂载组件,模板中使用了vm上的属性,会触发get,进来
if (Dep.target) {
dep.depend(); // 存放watcher
}
return value;
},
set(newVal) {
if (value !== newVal) {
// 如果用户新赋值一个对象,那么新对象也要进行观测
observe(newVal);
value = newVal;
// 通知watcher去更新
dep.notify();
}
},
});
}
export function observe(data) {
if (!isObject(data)) return;
if (data.__ob__) return;
return new Observe(data);
}
# 4.observe/dep 文件代码
let id = 0;
class Dep {
constructor() {
this.id = id++;
this.subs = [];
}
depend() {
if (Dep.target) {
// Dep.target就是watcher
Dep.target.addDeps(this); // this是当前实例dep
}
}
addSubs(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach((watcher) => watcher.update());
}
}
Dep.target = null;
export function pushTarget(watcher) {
Dep.target = watcher;
}
export function popTarget() {
Dep.target = null;
}
export default Dep;
# 5.observe/watcher 文件代码
import { pushTarget, popTarget } from './dep';
let id = 0;
class Watcher {
/**
*
* @param {*} vm Vue实例
* @param {*} updateFnOrExpr 更新的方法或者表达式
* @param {*} cb 自定义回调函数
* @param {*} options 其他选项配置
*/
constructor(vm, updateFnOrExpr, cb, options) {
this.vm = vm;
this.id = id++; // 每个watcher都是单独的,用id来区分一下
this.updateFnOrExpr = updateFnOrExpr;
this.cb = cb;
this.options = options;
this.deps = [];
this.depsId = new Set();
this.get();
}
get() {
pushTarget(this);
// 这个方法会触发Object.defineProperty中的get,会去vm上面取值
this.updateFnOrExpr();
popTarget(); // 在外面用vm上的属性是不需要收集Watcher的
}
// Vue中是异步更新的,主要是做一个缓存等待
addDeps(dep) {
const id = dep.id;
if (!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep);
// 调用dep的addSubs方法来存放watcher
dep.addSubs(this);
}
}
update() {
this.get();
}
}
export default Watcher;
# 6.index.html 文件如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div
id="root"
name="zhengchangfu"
style="color: red;background-color: #fff;"
>
<p>
<span>我叫{{ name }}哈哈 {{ age }}</span>
</p>
</div>
<script src="./dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#root',
data() {
return {
arr: [1, { b: 2 }],
name: '郑常富',
age: 18,
};
},
});
// setTimeout(() => {
// vm.name = '程小惠'
// vm._update(vm._render())
// }, 1000)
setTimeout(() => {
vm.name = 'z';
vm.name = 'c';
vm.name = 'f';
vm.name = '程小惠';
vm.age = 2;
}, 1000);
/*
如果更新的话,我们重新调用vm._update(vm._render即可)
在_render里面我们做了什么事?
1. 将ast生成的语法树转换成了代码 _c('div',{key:value},'哈哈')
2. 通过new Function + with语法将代码变成了render函数
3. 调用render函数生成虚拟dom对象
在_update里面我们做了什么事?
1. patch进行diff算法对比,现在我们还没有比对
2. 将虚拟dom对象转换成真实dom,实现页面更新
*/
/*
我们更新的时候肯定是不需要用户主动去调用vm._update(vm._render())
Vue中是这么做的:
1. 每个组件都会有一个渲染Watcher,专门是渲染页面用的
2. 一个页面中的所有属性都有一个dep,dep中存放当前页面的watcher
3. 当前页面中的watcher存放当前页面中所有的dep
4. 当用户想要更新时,会触发Object.defineProperty中的set方法
5. 我们在set中通知dep去更新watcher
6. 组件化的代码中,一个组件就会对应一个Watcher,那么多个组件就会对应多个Watcher
7. 而有时候我们的属性会被共享到多个组件中,又因为每个属性都有一个自己的dep,所以也就是一个dep可以有多个Watcher的情况
8. 而一个页面中可能有多个属性,每个属性又有自己的dep,所以也就是一个Watcher可能有多个dep
9. 所以Dep和Watcher的关系可能会属于1 => 1 , 1 => 多, 多 = 1 , 多 => 多
*/
vm.name = 2;
</script>
</body>
</html>
# 03.手写异步更新原理
# 1.lib 目录新增文件
文件夹/文件 | 含义 |
---|---|
observe/scheduler 文件 | watcher 调度文件 |
# 2.observe/watcher 文件代码改动如下
import { pushTarget, popTarget } from './dep';
import { queueWatcher } from './scheduler';
let id = 0;
class Watcher {
/**
*
* @param {*} vm Vue实例
* @param {*} updateFnOrExpr 更新的方法或者表达式
* @param {*} cb 自定义回调函数
* @param {*} options 其他选项配置
*/
constructor(vm, updateFnOrExpr, cb, options) {
this.vm = vm;
this.id = id++; // 每个watcher都是单独的,用id来区分一下
this.updateFnOrExpr = updateFnOrExpr;
this.cb = cb;
this.options = options;
this.deps = [];
this.depsId = new Set();
this.get();
}
get() {
pushTarget(this);
// 这个方法会触发Object.defineProperty中的get,会去vm上面取值
this.updateFnOrExpr();
popTarget(); // 在外面用vm上的属性是不需要收集Watcher的
}
// 存放dep,如果模板中使用了2次 {{ name }} {{ name }},他们其实用的是一个id,那么我们就不需要存放到数组中,需要进行去重
addDeps(dep) {
const id = dep.id;
if (!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep);
// 调用dep的addSubs方法来存放watcher
dep.addSubs(this);
}
}
// Vue中是异步更新的,主要是做一个缓存等待
// 如果watcher的id都是一样,那么要进行去重,而且只需要更新一次即可 (防抖) ,同一个页面多个dep公共一个watcher
// 所以Vue内部更新原理是: 去重 + 防抖
update() {
queueWatcher(this);
}
run() {
console.log(111);
this.get();
}
}
export default Watcher;
# 3.observe/scheduler 文件代码
import { nextTick } from '../utils';
let queue = [];
let obj = {};
let pending = false;
export function queueWatcher(watcher) {
const id = watcher.id;
if (!obj[id]) {
queue.push(watcher);
obj[id] = true;
if (!pending) {
pending = true;
nextTick(flushSchedulerQueue);
}
}
}
function flushSchedulerQueue() {
for (let i = 0; i < queue.length; i++) {
queue[i].run();
}
queue = [];
obj = {};
pending = false;
}
# 4.utils 文件中新增方法(nextTick)代码如下
let callbacks = [];
let watting = false;
export function nextTick(cb) {
// 按照正常情况来说,肯定是内部源码调用的nextTick先进来,然后再是函数,因为代码是一行行执行的
callbacks.push(cb);
// 也要做防抖,不能每次调用nextTick都循环一次
if (!watting) {
watting = true;
// 这里我就不写兼容代码了,vue2中写了兼容
Promise.resolve().then(flushCallbacks);
}
}
function flushCallbacks() {
for (let i = 0; i < callbacks.length; i++) {
const cb = callbacks[i];
cb();
}
callbacks = [];
watting = false;
}
# 04.手写 watch
# 1.index.html 文件代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root">
<p>{{ hobby.a }}</p>
</div>
<script src="./dist/vue.js"></script>
<script>
/*
watch的原理:
其实就是一个用户Watcher,当key值发生变化的时候,会调用key对应的handler函数
其实在watch对象中书写和手动调用vm.$watch其实原理都是一样,watch对象中的handler函数
都会被转换成vm.$watch
watch中的key(表达式)其实最后会在vm中取值,然后当key值发生变化,会通知watcher去更新,
*/
const vm = new Vue({
el: '#root',
data() {
return {
name: 'zcf',
hobby: {
a: 1,
},
};
},
methods: {
fn(newVal, oldVal) {
console.log(newVal, oldVal, '6');
},
},
watch: {
// watch写法有好几种
// 第一种:
// name(newVal, oldVal) {
// console.log(newVal, oldVal, '1')
// },
// 第二种,可以写表达式,比如想监听hobby.a的变化 'hobby.a'(){}
'hobby.a'(newVal, oldVal) {
console.log(newVal, oldVal, '2');
},
// 第三种,数组的形式,当name值发生变化的时候,会循环依次调用数组中的每一个函数
name: [
function(newVal, oldVal) {
console.log(newVal, oldVal, '3');
},
function(newVal, oldVal) {
console.log(newVal, oldVal, '4');
},
],
// 第四种
// name: {
// handler: function (newVal, oldVal) {
// console.log(newVal, oldVal, '5')
// },
// immediate: true, // 是否立即触发
// deep: true // 是否深度监视 ... 还有一系列参数
// },
// // 第五种,可以抽取函数到methods中
// name: 'fn' // 当name值发生变化时,会调用methods中的fn函数
},
});
// 第六种
vm.$watch(
'name',
function(newVal, oldVal) {
console.log(newVal, oldVal, '7');
},
{
/* 可以传入一些配置项 */
}
);
setTimeout(() => {
// vm.name = 'cxh'
vm.hobby.a = 2;
}, 1000);
</script>
</body>
</html>
# 2.state.js 文件代码改动如下
import { observe } from './observe/index';
import { isFunction } from './utils';
import Watcher from './observe/watcher';
export function stateMixin(Vue) {
Vue.prototype.$watch = function(key, handler, options = {}) {
const vm = this;
options.user = true; // 表示是一个用户Watcher
new Watcher(vm, key, handler, options);
};
}
export function initState(vm) {
if (vm.$options.data) {
initData(vm);
}
if (vm.$options.watch) {
initWatch(vm);
}
if (vm.computed) {
initComputed(vm);
}
// 挂载
if (vm.$options.el) {
vm.$mount(vm);
}
}
function proxy(vm, source, key) {
Object.defineProperty(vm, key, {
get() {
return vm[source][key];
},
set(newVal) {
vm[source][key] = newVal;
},
});
}
function initData(vm) {
let data = vm.$options.data;
data = vm._data = isFunction(data) ? data.call(vm) : data;
// 所有的数据现在都在_data中,那么我们做一层数据代理,外界访问vm.xxx的时候我们去vm._data中读取xxx
for (let key in data) {
proxy(vm, '_data', key);
}
observe(data);
}
function initWatch(vm) {
const watch = vm.$options.watch;
Object.keys(watch).forEach((key) => {
const handler = watch[key];
// handler可能是数组,可能是字符串,可能是对象,可能是函数
// 暂时没实现methods和对象,所以先不考虑这两种
// 那就先考虑数组和函数的情况
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
const fn = handler[i];
// 没一个函数都是一个watcher,只不过这个watcher是用户的
createWatcher(vm, key, fn);
}
} else {
createWatcher(vm, key, handler);
}
});
}
function createWatcher(vm, key, handler) {
vm.$watch(key, handler);
}
# 3.observe/watcher.js 代码改动如下
import { pushTarget, popTarget } from './dep';
import { queueWatcher } from './scheduler';
let id = 0;
class Watcher {
/**
*
* @param {*} vm Vue实例
* @param {*} updateFnOrExpr 更新的方法或者表达式
* @param {*} cb 自定义回调函数
* @param {*} options 其他选项配置
*/
constructor(vm, updateFnOrExpr, cb, options = {}) {
this.vm = vm;
this.id = id++; // 每个watcher都是单独的,用id来区分一下
this.user = !!options.user; // 区分是否为用户Watcher
this.cb = cb;
this.options = options;
this.deps = [];
this.depsId = new Set();
if (typeof updateFnOrExpr === 'string') {
// 表示为一个表达式,因为等下要调用this.get,get方法中会调用updateFnOrExpr,所以我们这里重写updateFnOrExpr
// 等下调用this.get会存储用户Watcher,然后调用this.updateFnOrExpr会触发我们下面的函数,然后会触发响应式中的get方法
// 会去收集用户Watcher,然后返回vm.name的值
// 当我们去改变name值的时候,会通知Watcher更新,我们收集新值和老值然后去调用用户回调即可
this.updateFnOrExpr = function() {
// return vm[updateFnOrExpr]
// 外界可能传入这种格式,'hobby.a'(),我们需要取到a的值,就不能按照上面那种写法了
let obj = vm;
const arr = updateFnOrExpr.split('.');
for (let i = 0; i < arr.length; i++) {
obj = obj[arr[i]]; // {a}
}
return obj;
};
} else {
// 表示为渲染Watcher
this.updateFnOrExpr = updateFnOrExpr;
}
this.value = this.get();
}
get() {
pushTarget(this);
// 这个方法会触发Object.defineProperty中的get,会去vm上面取值,第一次的值就是最早的值,第二次的值就是最新的值
const value = this.updateFnOrExpr();
popTarget(); // 在外面用vm上的属性是不需要收集Watcher的
return value;
}
// 存放dep,如果模板中使用了2次 {{ name }} {{ name }},他们其实用的是一个id,那么我们就不需要存放到数组中,需要进行去重
addDeps(dep) {
const id = dep.id;
if (!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep);
// 调用dep的addSubs方法来存放watcher
dep.addSubs(this);
}
}
// Vue中是异步更新的,主要是做一个缓存等待
// 如果watcher的id都是一样,那么要进行去重,而且只需要更新一次即可 (防抖) ,同一个页面多个dep公共一个watcher
// 所以Vue内部更新原理是: 去重 + 防抖
update() {
queueWatcher(this);
}
run() {
// 更新
const newVal = this.get();
if (this.user) {
const oldValue = this.value;
// 下一次的老值是这一次的新值
this.value = newVal;
// 表示是用户watcher
this.cb.call(this.vm, newVal, oldValue);
}
}
}
export default Watcher;
# 05.手写 computed
# 1. index.html 文件如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root">
<p>{{ fullName }}</p>
</div>
<script src="./dist/vue.js"></script>
<script>
/*
computed的原理:
本质其实也是一个Watcher,但是具有缓存功能,默认是不触发的,computed中的每一个key都是被定义在vm上的
对应的value其实就是Object.defineProperty中的get或者get/set,当依赖的值发生变化的时候,会重新执行
如果依赖的值没有发生变化,不会重新执行,会返回上一次的值(缓存)
计算属性中的key是没有收集渲染Watcher的,只有计算Watcher,计算属性中所依赖的值都有一个Dep,我们应该让这些Dep去收集渲染Watcher
这样的话,当依赖的值发生了变化,会通知渲染Watcher去更新
因为当我们去定义计算属性的时候,默认并不会调用watcher中的get方法,也就不会再Dep.target上面存放任何Watcher,
接着当我们去挂载组件的时候,我们会去new一个渲染Watcher,然后在Dep.target上面存放了渲染Watcher,接着去渲染模板的
时候,会在vm上面取计算属性中的key值,然后又在Dep.target上面存放了计算Watcher,此时计算Watcher覆盖了渲染Watcher
所以当我们去修改计算属性依赖值的时候,页面并不会重新渲染
解决方案:
每次在Dep.target上面赋值的时候,在重新维护一个栈结构,把Watcher存放进去,当我们的计算Watcher覆盖了渲染Watcher的时候,
让计算属性依赖的值去收集渲染Watcher即可
*/
const vm = new Vue({
el: '#root',
data() {
return {
firstName: 'zcf',
lastName: 'cxh',
};
},
computed: {
// 第一种写法,就是默认的get
fullName() {
console.log('我进来了');
return this.firstName + '-' + this.lastName;
},
// 第二种写法,当需要set方法时,要这么写
// fullName:{
// get(){
// },
// set(){
// }
// }
},
});
// console.log(vm.fullName)
// console.log(vm.fullName)
// console.log(vm.fullName)
// console.log(vm.fullName)
// 当我们去修改计算属性中所依赖数据的值时,计算属性中的get要重新执行
setTimeout(() => {
vm.firstName = 'ZCF';
}, 1000);
</script>
</body>
</html>
# 2.state.js 文件代码改动如下
function initComputed(vm) {
const computed = vm.$options.computed;
vm._computedWatchers = {};
Object.keys(computed).forEach((key) => {
const value = computed[key];
// value有2种,一种是对象,一种是函数
let getter = typeof value === 'function' ? value : value.get;
// 这个getter就是用户getter
// vm._computedWatchers用于做一个映射表,方便后面我们拿到Watcher
// getter函数内部的this是指向vm的
vm._computedWatchers[key] = new Watcher(vm, getter.bind(vm), () => {}, {
lazy: true,
});
defineComputed(vm, key, value);
});
}
/**
*
* @param {*} key 计算属性对象中的每一个key,上面已经做了映射表
*/
function createComputedGetter(key) {
// 返回后的get回调
return function() {
// 这里的this是vm调用的,因为我们定义了Object.defineProperty,第一个参数是vm
// 因为我们的getter函数也被存到了Watcher中,所以我们需要拿到Watcher然后在特定时机调用即可
const watcher = this._computedWatchers[key];
// 计算属性的缓存
if (watcher.dirty) {
// 说明是脏的,需要调用用户的回调
watcher.computedFn();
}
// 此时的计算Watcher已经覆盖了渲染Watcher,栈中存放了2个Watcher,一个计算,一个渲染
if (Dep.target) {
// 向上收集渲染Watcher
// 找到相关依赖dep
watcher.depend();
}
return watcher.value;
};
}
function defineComputed(vm, key, value) {
let shareComputed = {};
if (typeof value === 'function') {
// 因为计算属性具有缓存功能,所以我们这里用高阶函数返回一个getter,getter是否能被调用取决于dirty属性
shareComputed.get = createComputedGetter(key);
} else {
shareComputed.get = createComputedGetter(key);
shareComputed.set = value.set;
}
// 把key定义到vm上面,用于渲染模板
Object.defineProperty(vm, key, shareComputed);
}
# 3.observe/dep 文件代码改动如下
let id = 0;
class Dep {
constructor() {
this.id = id++;
this.subs = [];
}
depend() {
if (Dep.target) {
// Dep.target就是watcher
Dep.target.addDeps(this); // this是当前实例dep
}
}
addSubs(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach((watcher) => watcher.update());
}
}
Dep.target = null;
let stack = [];
export function pushTarget(watcher) {
Dep.target = watcher;
stack.push(watcher);
}
export function popTarget() {
// Dep.target = null
stack.pop();
Dep.target = stack[stack.length - 1];
}
export default Dep;
# 4.observe/watcher 文件代码改动如下
import { pushTarget, popTarget } from './dep';
import { queueWatcher } from './scheduler';
let id = 0;
class Watcher {
/**
*
* @param {*} vm Vue实例
* @param {*} updateFnOrExpr 更新的方法或者表达式
* @param {*} cb 自定义回调函数
* @param {*} options 其他选项配置
*/
constructor(vm, updateFnOrExpr, cb, options = {}) {
this.vm = vm;
this.id = id++; // 每个watcher都是单独的,用id来区分一下
this.user = !!options.user; // 区分是否为用户Watcher
this.lazy = !!options.lazy; // 区分是否为计算Watcher,默认不执行
this.dirty = !!options.lazy; // 区分是否需要调用计算属性中的get回调
this.cb = cb;
this.options = options;
this.deps = [];
this.depsId = new Set();
if (typeof updateFnOrExpr === 'string') {
// 表示为一个表达式,因为等下要调用this.get,get方法中会调用updateFnOrExpr,所以我们这里重写updateFnOrExpr
// 等下调用this.get会存储用户Watcher,然后调用this.updateFnOrExpr会触发我们下面的函数,然后会触发响应式中的get方法
// 会去收集用户Watcher,然后返回vm.name的值
// 当我们去改变name值的时候,会通知Watcher更新,我们收集新值和老值然后去调用用户回调即可
this.updateFnOrExpr = function() {
// return vm[updateFnOrExpr]
// 外界可能传入这种格式,'hobby.a'(),我们需要取到a的值,就不能按照上面那种写法了
let obj = vm;
const arr = updateFnOrExpr.split('.');
for (let i = 0; i < arr.length; i++) {
obj = obj[arr[i]];
}
return obj;
};
} else {
// 表示为渲染Watcher
this.updateFnOrExpr = updateFnOrExpr;
}
this.value = this.lazy ? undefined : this.get();
}
get() {
pushTarget(this);
// 这个方法会触发Object.defineProperty中的get,会去vm上面取值,第一次的值就是最早的值,第二次的值就是最新的值
const value = this.updateFnOrExpr();
popTarget(); // 在外面用vm上的属性是不需要收集Watcher的
return value;
}
// 存放dep,如果模板中使用了2次 {{ name }} {{ name }},他们其实用的是一个id,那么我们就不需要存放到数组中,需要进行去重
addDeps(dep) {
const id = dep.id;
if (!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep);
// 调用dep的addSubs方法来存放watcher
dep.addSubs(this);
}
}
// Vue中是异步更新的,主要是做一个缓存等待
// 如果watcher的id都是一样,那么要进行去重,而且只需要更新一次即可 (防抖) ,同一个页面多个dep公共一个watcher
// 所以Vue内部更新原理是: 去重 + 防抖
update() {
if (this.lazy) {
this.dirty = true;
} else {
queueWatcher(this);
}
}
run() {
// 更新
const newVal = this.get();
if (this.user) {
const oldValue = this.value;
// 下一次的老值是这一次的新值
this.value = newVal;
// 表示是用户watcher
this.cb.call(this.vm, newVal, oldValue);
}
}
// 计算属性的缓存
computedFn() {
this.dirty = false;
// get函数的返回值就是外面用户在get函数中return的值
this.value = this.get();
}
depend() {
// 因为我们在计算属性中取了值,所以会去收集dep
let i = this.deps.length;
while (i--) {
const dep = this.deps[i];
dep.depend();
}
}
}
export default Watcher;
# 06.手写生命周期原理和手写 mixin 原理
# 1.index.html 文件代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="./dist/vue.js"></script>
<script>
/*
mixin原理:
Vue源码中在Vue上增加了一个静态属性options,默认为一个空对象,当我们去调用mixin函数进行混合的时候
内部会进行合并策略,不同的属性会应用不同的合并策略,内部使用了策略模式(设计模式)来解决if/else过多的
问题
生命周期原理:
用了策略模式,将所有的钩子函数以字符串的方式放入一个数组中,然后合并的时候会将外部存在的钩子重写成函数,
采用先进先出的方式来进行管理钩子函数,当外部调用钩子的时候,会触发callHooks函数,内部会按顺序依次调用,
并且将钩子函数内部的this修改为Vue的实例
主要的生命周期(8个)执行顺序:
beforeCreate: 在初始化数据之前,数据还没有被处理成响应式前调用
created: 初始化数据之后,初始化数据包括且不限于 初始化watch,computed,data,里面的数据已经是响应式了
beforeMount: 即将挂载组件之前,调用_render之前
mounted: 挂载完组件之后,页面渲染完毕,在触发mounted,只执行一次
beforeUpdate: 更新组件之前被调用
updated: 更新组件之后
beforeDestroy: 组件卸载之前
destroyed: 组件卸载之后
mixin中生命周期的合并策略:
将mixin配置对象中使用的钩子函数全部放入到一个数组中,先调用mixin函数的先放,调用的时候也是先进先出,
new Vue里面的配置项也会进行合并
*/
Vue.mixin({
beforeCreate() {
console.log('beforeCreate 1');
},
});
Vue.mixin({
beforeCreate() {
console.log('beforeCreate 2');
},
});
new Vue({
el: '#root',
data() {
return {
a: 1,
};
},
beforeCreate() {
console.log('beforeCreate 3');
},
});
</script>
</body>
</html>
# 2.lib 目录新增文件
global-api 目录 | 全局 Api 定义 |
---|---|
index.js 文件 | 新增 mixin 方法 |
# 3.global/index 文件代码
/**
* Vue的全局方法模块
*/
import { mergeOptions } from '../utils';
export function globalMixin(Vue) {
Vue.options = {};
Vue.mixin = function(options) {
// 每次调用mixin方法都会进行合并,然后记录下来,放到Vue.options属性上
this.options = mergeOptions(this.options, options);
};
}
# 4.utils 文件新增代码
const hooks = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
];
const stracts = {};
for (let i = 0; i < hooks.length; i++) {
const hook = hooks[i];
stracts[hook] = mergeHook;
}
function mergeHook(parentValue, childValue) {
if (parentValue) {
// 有值说明是类似这样{hook:[fn]},我们直接合并就行
// parentValue就是 [fn]
return parentValue.concat(childValue);
} else {
// 第一个parentValue是空对象,那么我们需要编程{hook:[fn]}
return [childValue];
}
}
export function mergeOptions(parent, child) {
let obj = {};
for (let key in parent) {
mergeField(key);
}
for (let key in child) {
mergeField(key);
}
function mergeField(key) {
/**
*
* 情况:
* parent : {a:1} child : {a:2} ==> {a:2}
* parent : {a:1,data:{c:1}} child : {b:2,data:{d:2}} ==> {a:1,data:{c:1,d:2},b:2}
* parent : {} child : {beforeCreated:fn} ==> {beforeCreated:[fn]}
* parent : {beforeCreated:[fn]} child : {beforeCreated:fn} ==> {beforeCreated:[fn1,fn2]}
* **/
const parentValue = parent[key];
const childValue = child[key];
// 生命周期策略
if (stracts[key]) {
obj[key] = stracts[key](parentValue, childValue);
} else {
// 如果value是对象,暂时不考虑递归合并,浅拷贝
// 如果value不是对象,那么以child数据为准
if (isObject(parentValue) && isObject(childValue)) {
obj[key] = { ...parentValue, ...childValue };
} else {
// 如果子没值,就取父的值
obj[key] = childValue || parentValue;
}
}
}
return obj;
}
# 5.init 文件代码改动如下
import { initState } from './state';
import { mountComponent } from './lifecycle';
import { CompileToFunction } from './compile/index';
import { callHooks } from './lifecycle';
import { mergeOptions } from './utils';
export function initMixin(Vue) {
Vue.prototype._init = function(options) {
const vm = this;
// vm.$options = options
// 将$options中的选项和Vue.options进行合并,这样的话,mixin中就可以混入数据到组件中
vm.$options = mergeOptions(vm.constructor.options, options);
// 初始化数据之前调用beforeCreate
callHooks(vm, 'beforeCreate');
initState(vm);
// 初始化数据之后调用created
callHooks(vm, 'created');
// 挂载
if (vm.$options.el) {
vm.$mount(vm);
}
};
Vue.prototype.$mount = function(vm) {
let el = vm.$options.el;
if (!vm.$options.render) {
el = document.querySelector(el);
vm.$el = el;
el = el.outerHTML;
let render = CompileToFunction(el);
vm.$options.render = render;
}
// 挂载组件
mountComponent(vm, el);
};
}
# 6.lifecycle 文件代码改动如下
import { patch } from './vDom/patch';
import { nextTick } from './utils';
import Watcher from './observe/watcher';
export function mountComponent(vm, el) {
// 挂载组件之前调用beforeMount钩子
callHooks(vm, 'beforeMount');
const updateComponent = () => {
// 生成虚拟dom
const vNode = vm._render();
// 更新
vm._update(vNode);
};
// updateComponent() ==> 更新组件
// 这里渲染的时候我们创建一个Watcher,在Watcher中更新组件
new Watcher(
vm,
updateComponent,
() => {
console.log('update view');
},
true
); // true代表是一个渲染Watcher
callHooks(vm, 'mounted');
}
export function lifecycleMixin(Vue) {
Vue.prototype._update = function(vNode) {
const vm = this;
vm.$el = patch(vm.$el, vNode);
};
Vue.prototype.$nextTick = nextTick;
}
// 调用指定的钩子函数
export function callHooks(vm, hook) {
// 拿到Vue.options上经过mixin合并的hook
// const handlers = vm.constructor.options[hook]
// 因为我们将Vue.options和vm.$options进行了合并,让mixin中的数据可以混入到组件中,所以我们直接调用组件的$options中的钩子就行(已经被合并过了)
const handlers = vm.$options[hook];
// console.log(handlers, hook)
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
const handler = handlers[i];
// 钩子内部的this指向的是当前组件实例
handler.call(vm);
}
}
}
# 07.手写子组件渲染原理和手写 extend 原理
# 1.index.html 文件代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root">
<my-button></my-button>
</div>
<script src="./dist/vue.js"></script>
<script>
/*
extend原理:
传入一个对象,可以返回一个类,类名叫VueComponent,这个类继承自Vue,可以拥有Vue原型上所有的方法
传入的配置项会和Vue.options进行合并
子组件渲染流程:
子组件会被Vue.extend包装成一个类,这个类继承Vue,然后当我们创建虚拟dom的时候,判断是否是原生标签,如果
是原生标签,就创建原生标签的虚拟dom,如果不是原生原生标签,就创建组件的虚拟节点,在转换成真实dom时,判断
是否为组件,如果是组件,会调用data.init方法,这个方法内部就会调用类(被Vue.extend包装的),然后在类中调用
组件初始化方法,_init(),然后渲染子组件的template模板,template模板的优先级比el的优先级高
*/
// 会被放入到Vue.options.componnets
Vue.component('my-button', {
template: '<button>click me 2</button>',
});
new Vue({
el: '#root',
beforeCreate() {
console.log('parent beforeCreate');
},
mounted() {
console.log('parent mounted');
},
// 这种方式创建的组件和全局创建的组件都会被extend包装成一个类,类继承Vue,他们彼此不会覆盖,会按照原型链的方式串联起来
// 如果组件内部没有,会去全局查找
components: {
'my-button': {
template: '<button>click me1</button>',
beforeCreate() {
console.log('child beforeCreate');
},
mounted() {
console.log('child mounted');
},
},
},
});
</script>
</body>
</html>
# 2.utils 文件新增代码
// 组件的合并策略
/**
* 会按照原型链的方式进行合并,如果自身上找不到,在找原型上的(Vue.component)
*/
stracts.components = function(parentValue, childValue) {
// options.__proto__ = parentValue
let options = Object.create(parentValue);
if (childValue) {
for (let key in childValue) {
options[key] = childValue[key];
}
}
return options;
};
/**
* 判断是否为原生标签
* @param {*} tag 标签
*/
export function isReverseTag(tag) {
// 这里我们意思一下就行
const str = 'a,b,i,img,p,ul,li,div,button';
return str.includes(tag);
}
# 3.global/index 文件新增代码
// 为了让以后所有的组件都可以拿到这个属性
Vue.options._base = Vue;
// 用component方法注册的组件会被放到这里
Vue.options.components = {};
Vue.component = function(id, options) {
options = this.options._base.extend(options);
this.options.components[id] = options;
};
Vue.extend = function(options) {
const Super = this;
let Sub = function VueComponent(options) {
this._init(options);
};
// 继承
// 相当于Sub.prototype.__proto__ = Super.prototype
Sub.prototype = Object.create(Super.prototype);
// 修正此继承bug
Sub.prototype.constructor = Sub;
// 合并,每个组件都有一个自己的options选项,options选项会和Vue.options选项进行合并操作
Sub.options = mergeOptions(Super.options, options);
return Sub;
};
# 4.vdom/index 文件改动如下
import { isReverseTag, isObject } from '../utils';
export function createElement(vm, tag, data = {}, ...children) {
// 判断是否为原生标签,如果是原生标签,就渲染原生的虚拟节点
// 反之,渲染组件的虚拟节点
if (isReverseTag(tag)) {
return createVDom(vm, tag, data, data.key, children, undefined);
} else {
// 说明是组件,应该返回组件的虚拟节点
const Ctor = vm.$options.components[tag];
return createComponent(vm, tag, data, data.key, children, Ctor);
}
}
function createComponent(vm, tag, data, key, children, Ctor) {
if (isObject(Ctor)) {
// 外面在new Vue中传入的components也会被extend,等同于Vue.component()
Ctor = vm.$options._base.extend(Ctor);
}
data.hook = {
init(vNode) {
// 调用子组件
if (Ctor) {
let vm = (vNode.componentInstance = new Ctor({ isComponent: true }));
vm.$mount();
}
},
};
// 组件的虚拟节点
// console.log(createVDom(vm, `vue-componnet-${tag}`, data, key, undefined, undefined, { Ctor, children }))
return createVDom(
vm,
`vue-componnet-${tag}`,
data,
key,
undefined,
undefined,
{ Ctor, children }
);
}
export function createText(vm, text) {
return createVDom(vm, undefined, undefined, undefined, undefined, text);
}
function createVDom(vm, tag, data, key, children, text, componentOptions) {
return {
vm,
tag,
data,
key,
children,
text,
componentOptions,
};
}
# 5.vdom/patch 文件改动如下
export function patch(oldVnode, vNode) {
if (!oldVnode) {
return createEl(vNode);
}
// 新旧虚拟dom比较,暂时不做
const parent = oldVnode.parentNode;
if (parent.nodeType === 1) {
// 真实元素
const elm = createEl(vNode);
parent.insertBefore(elm, oldVnode.nextSibling);
parent.removeChild(oldVnode);
return elm;
}
}
function createComponent(vNode) {
let i = vNode.data;
if ((i = i.hook) && (i = i.init)) {
// 调用vNode.data.hook.init
i(vNode);
}
if (vNode.componentInstance) {
// 有属性说明子组件new完毕了,并且组件对应的真实DOM挂载到了componentInstance.$el
return true;
}
}
function createEl(vNode) {
const { vm, text, key, children, tag, data } = vNode;
let elm = '';
if (typeof tag === 'string') {
if (createComponent(vNode)) {
// 返回组件对应的真实节点
return vNode.componentInstance.$el;
}
// 标签
elm = document.createElement(tag);
if (children && children.length) {
for (let i = 0; i < children.length; i++) {
const item = children[i];
elm.appendChild(createEl(item));
}
}
} else {
// 文本
elm = document.createTextNode(text);
}
return elm;
}
# 08.手写 diff 算法
# 1.index.html 文件代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<ul>
<li key="A">{{a}}</li>
</ul>
</div>
<script src="./dist/vue.js"></script>
<script>
/*
diff算法:
diff算法采用分层求异的方式,来控制粒度的最小变化,目的就是尽可能复用老的节点
只对比同层级节点,不会跨层级比较
注意:
初始化挂载不会进行diff算法比较,因为diff算法依赖的是新旧的虚拟dom,第一次挂载只会生成一次虚拟dom
在更新时会生成新的虚拟dom,这时候新旧虚拟dom会进行比较,差别化更新
*/
const vm = new Vue({
el: '#app',
data: {
a: 'hello',
},
});
setTimeout(() => {
vm.a = 'zcf';
}, 3000);
</script>
</body>
</html>
# 2.index.js 文件模板
import { CompileToFunction } from './compile/index'
import { createEl, patch } from './vDom/patch'
第一种:如果标签不一样,直接用新的标签替换掉老的标签
// let oldTemplate =
<p>message</p>
第二种:比对属性
let oldTemplate =
<div a="1" style="font-size:14px;">1</div>
第三种:如果没有标签,文本不一样,直接替换文本
// let oldTemplate =
message
第四种:如果标签一样,老的有孩子,新的没孩子
let oldTemplate =
<div><li>A</li></div>
第五种:如果标签一样,老的没孩子,新的有孩子
let oldTemplate =
<div></div>
第六种:都有孩子
let oldTemplate =
<div> // <li>A</li> // <li>B</li> // <li>C</li> // </div>
第七种:新增一个孩子(尾部添加)的情况
let oldTemplate =
<div> // <li>A</li> // <li>B</li> // <li>C</li> // </div>
第八种:新增一个孩子(头部添加)的情况
let oldTemplate =
<div> // <li key="A">A</li> // <li key="B">B</li> // <li key="C">C</li> // </div>
第九种:老头和新尾相同情况
let oldTemplate =
<div> // <li key="A">A</li> // <li key="B">B</li> // <li key="C">C</li> // <li key="D">D</li> // </div>
第十种:老尾和新头相同情况
let oldTemplate =
<div> // <li key="A">A</li> // <li key="B">B</li> // <li key="C">C</li> // <li key="D">D</li> // </div>
第十一种:删除一个的情况
let oldTemplate =
<div> // <li key="A">A</li> // <li key="B">B</li> // <li key="C">C</li> // <li key="D">D</li> // </div>
第十二种:乱序
let oldTemplate =
<div> // <li key="A">A</li> // <li key="B">B</li> // <li key="C">C</li> // <li key="D">D</li> // </div>
let render1 = CompileToFunction(oldTemplate);
let vm1 = new Vue({
data: {
// message: 1,
},
});
const oldNode = render1.call(vm1);
document.body.appendChild(createEl(oldNode));
第一种:如果标签不一样,直接用新的标签替换掉老的标签
// let newTemplate =
<div>message</div>
第二种: 标签一样,对比属性
let newTemplate =
<div a="b" style="color:red;">1</div>
第三种:如果没有标签,文本不一样,直接替换文本
// let newTemplate =
message
第四种:如果标签一样,老的有孩子,新的没孩子
let newTemplate =
<div></div>
第五种:如果标签一样,老的没孩子,新的有孩子
let newTemplate =
<div><li>A</li></div>
第六种:都有孩子
let newTemplate =
<div> // <li>A</li> // <li>D</li> // <li>C</li> // </div>
第七种:新增一个孩子(尾部添加)的情况
let newTemplate =
<div> // <li>A</li> // <li>B</li> // <li>C</li> // <li>D</li> // </div>
第八种:新增一个孩子(头部添加)的情况
let newTemplate =
<div> // <li key="D">D</li> // <li key="E">E</li> // <li key="A">A</li> // <li key="B">B</li> // <li key="C">C</li> // </div>
第九种:老头和新尾相同情况
let newTemplate =
<div> // <li key="B">B</li> // <li key="C">C</li> // <li key="D">D</li> // <li key="A">A</li> // </div>
第十种:老尾和新头相同情况
let newTemplate =
<div> // <li key="D">D</li> // <li key="A">A</li> // <li key="B">B</li> // <li key="C">C</li> // </div>
第十一种:删除一个的情况
let newTemplate =
<div> // <li key="A">A</li> // <li key="B">B</li> // <li key="C">C</li> // </div>
第十二种:乱序
let newTemplate =
<div> // <li key="B">B</li> // <li key="F">F</li> // <li key="D">D</li> // <li key="E">E</li> // </div>
let render2 = CompileToFunction(newTemplate);
let vm2 = new Vue({
data: {
// message: 2,
},
});
const newNode = render2.call(vm2);
setTimeout(() => {
patch(oldNode, newNode);
}, 2000);
# 3.vdom/patch 文件代码
export function patch(oldVnode, vNode) {
if (!oldVnode) {
return createEl(vNode);
}
const parent = oldVnode.parentNode;
if (parent && parent.nodeType === 1) {
// 真实元素
const elm = createEl(vNode);
parent.insertBefore(elm, oldVnode.nextSibling);
parent.removeChild(oldVnode);
return elm;
} else {
// diff算法比较
// console.log(oldVnode, vNode)
// 第一种,如果标签名不一样,直接新的覆盖老的
if (oldVnode.tag !== vNode.tag) {
return oldVnode.el.parentNode.replaceChild(createEl(vNode), oldVnode.el);
}
// 说明标签肯定一样,复用老的节点
let el = (vNode.el = oldVnode.el);
// 如果文本不一样,用新的文本替换老的文本
if (oldVnode.tag === undefined) {
if (oldVnode.text !== vNode.text) {
el.textContent = vNode.text;
}
return;
}
// 比对属性
patchProps(vNode, oldVnode.data);
// 比对孩子
const oldChildren = oldVnode.children || [];
const newChildren = vNode.children || [];
// 都有孩子
if (oldChildren.length > 0 && newChildren.length > 0) {
// 比对孩子
patchChildren(el, oldChildren, newChildren);
} else if (oldChildren.length > 0) {
// 老节点有孩子,新节点没孩子,应该删除孩子
el.innerHTML = '';
} else if (newChildren.length > 0) {
// 新节点有孩子,老节点没孩子,应该添加孩子
for (let i = 0; i < newChildren.length; i++) {
el.appendChild(createEl(newChildren[i]));
}
}
return el;
}
}
function patchChildren(el, oldChildren, newChildren) {
// 双指针
let oldStartIndex = 0;
let oldStartNode = oldChildren[oldStartIndex];
let oldEndIndex = oldChildren.length - 1;
let oldEndNode = oldChildren[oldEndIndex];
let newStartIndex = 0;
let newStartNode = newChildren[newStartIndex];
let newEndIndex = newChildren.length - 1;
let newEndNode = newChildren[newEndIndex];
// 乱序情况的映射表
const makeIndexForKey = oldChildren.reduce((p, c, i) => {
// 用户key
const key = c.key;
p[key] = i;
return p;
}, {}); // {A:0,B:1,C:2,D:3}
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// 只要新老节点 有一方没有,循环就结束
// 如果老节点没值,说明已经被移动走了,直接跳过即可
if (!oldStartNode) {
oldStartNode = oldChildren[++oldStartIndex];
} else if (!oldEndNode) {
oldEndNode = oldChildren[--oldEndIndex];
}
if (isSameNode(oldStartNode, newStartNode)) {
// 头头比较
// 递归比较文本,标签,属性,子节点
patch(oldStartNode, newStartNode);
// 移动指针,更改节点
oldStartNode = oldChildren[++oldStartIndex];
newStartNode = newChildren[++newStartIndex];
} else if (isSameNode(oldEndNode, newEndNode)) {
// 尾尾比较
patch(oldEndNode, newEndNode);
oldEndNode = oldChildren[--oldEndIndex];
newEndNode = newChildren[--newEndIndex];
} else if (isSameNode(oldStartNode, newEndNode)) {
// 老头和新尾比较
// 如果一样,应该插入到最后一个元素的下一项元素前面reverse
patch(oldStartNode, newEndNode);
el.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling);
oldStartNode = oldChildren[++oldStartIndex];
newEndNode = newChildren[--newEndIndex];
} else if (isSameNode(oldEndNode, newStartNode)) {
// 老尾和新头比较
// 如果一样,应该插入到第一个元素的前面
patch(oldEndNode, newStartNode);
el.insertBefore(oldEndNode.el, oldStartNode.el);
oldEndNode = oldChildren[--oldEndIndex];
newStartNode = newChildren[++newStartIndex];
} else {
// 乱序比较
// 将老的节点形成一个映射表,然后用新元素去老的里面找,如果找到了,就给他插入到老开头指针的前面,没找到,就在老指针前面插入一个元素
// 找到了,就在映射表里面将找到的那一项填个坑,赋值为null,然后递归比较子节点
const newKey = newStartNode.key;
// 如果newKey在映射表里面不存在,就说明是新节点,要创建的,创建的节点要放在老节点开始的前面
if (!makeIndexForKey[newKey]) {
el.insertBefore(createEl(newStartNode), oldStartNode.el);
} else {
const moveIndex = makeIndexForKey[newKey];
const moveNode = oldChildren[moveIndex];
// oldChildren中的这项值填充null,表示被移动走了,也防止数组塌陷
oldChildren[moveIndex] = null;
// 移动
el.insertBefore(moveNode.el, oldStartNode.el);
// 递归比较子节点
patch(moveNode, newStartNode);
}
// 移动新指针
newStartNode = newChildren[++newStartIndex];
}
}
// 新增一个的情况,可能是头部增加,可能是尾部增加,这个时候我们就需要用key来进行判断了
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
// insertBefore可能实现appendChild功能
// 看尾指针元素的下一个元素是否存在,如果存在,就代表是往前追加,如果不存在,代表是往后追加
const nextNode = newChildren[newEndIndex + 1];
const anthor = nextNode ? nextNode.el : null;
el.insertBefore(createEl(newChildren[i]), anthor);
}
}
// 删除一个的情况
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
if (oldChildren[i]) {
el.removeChild(oldChildren[i].el);
}
}
}
}
function isSameNode(oldNode, newNode) {
return oldNode.tag === newNode.tag && oldNode.key === newNode.key;
}
function patchProps(vNode, oldProps = {}) {
let el = vNode.el;
let newProps = vNode.data || {};
let newStyle = newProps.style || {};
let oldStyle = oldProps.style || {};
for (let key in oldStyle) {
// 如果老的样式在新的上面没有,应该删除老的样式
if (!newStyle[key]) {
el.style[key] = '';
}
}
for (let key in oldProps) {
// 如果老的属性新的上面没有,应该删除老的
if (!newProps[key]) {
if (key === 'style') {
for (let attr in oldProps[key]) {
el.style[attr] = '';
}
} else {
el.removeAttribute(key);
}
}
}
// 循环添加新属性
for (let key in newProps) {
if (key === 'style') {
for (let attr in newProps[key]) {
el.style[attr] = newProps[key][attr];
}
} else {
el.setAttribute(key, newProps[key]);
}
}
}
function createComponent(vNode) {
let i = vNode.data;
if ((i = i.hook) && (i = i.init)) {
// 调用vNode.data.hook.init
i(vNode);
}
if (vNode.componentInstance) {
// 有属性说明子组件new完毕了,并且组件对应的真实DOM挂载到了componentInstance.$el
return true;
}
}
export function createEl(vNode) {
const { vm, text, key, children, tag, data } = vNode;
let elm = '';
if (typeof tag === 'string') {
if (createComponent(vNode)) {
// 返回组件对应的真实节点
return vNode.componentInstance.$el;
}
// 标签
elm = vNode.el = document.createElement(tag); // 虚拟节点上面都有一个el属性,对应真实元素,在做diff算法比较的时候有用
patchProps(vNode);
if (children && children.length) {
for (let i = 0; i < children.length; i++) {
const item = children[i];
elm.appendChild(createEl(item));
}
}
} else {
// 文本
elm = vNode.el = document.createTextNode(text);
}
return elm;
}
# 09.手写 vue-lazyload
# 1.使用 vue-cli 创建项目模板
# 2.随便写一个组件,使用 v-lazy 指令进行图片懒加载
# 3.v-lazy 指令代码如下
let plugin = {
install(Vue, options) {
const LazyClass = lazy(Vue);
const instance = new LazyClass(options);
Vue.directive('lazy', {
bind: instance.add.bind(instance),
unbind: instance.remove.bind(instance),
});
},
};
const render = (instance, status) => {
let el = instance.el;
let src = '';
// 根据状态设置不同src
switch (status) {
case 'loading':
src = instance.options.loading;
break;
case 'loaded':
src = instance.src;
break;
case 'error':
src = instance.options.error;
}
el.setAttribute('src', src);
};
const loadImg = (src, resolve, reject) => {
const img = new Image();
img.src = src;
img.onload = resolve;
img.onerror = reject;
};
const lazy = (Vue) => {
class ReactiveImg {
constructor({ el, src, options }) {
this.el = el;
this.options = options;
this.src = src;
this.state = { loading: false };
}
checkInView() {
const { top } = this.el.getBoundingClientRect();
return top < window.innerHeight * this.options.preload;
}
load() {
// 图片加载中...
// 图片加载成功显示真实图片,图片加载失败显示失败信息
render(this, 'loading');
setTimeout(() => {
loadImg(
this.src,
() => {
this.state.loading = true;
render(this, 'loaded');
},
() => {
this.state.loading = true;
render(this, 'error');
}
);
}, 1000);
}
}
return class LazyClass {
constructor(options) {
this.options = options;
this.handler = false;
this.listerens = [];
}
add(el, bindings) {
Vue.nextTick(() => {
const listeren = new ReactiveImg({
el,
src: bindings.value,
options: this.options,
});
// 找到父级元素,绑定滚动事件
const elm = scrollParent(el);
this.listerens.push(listeren);
if (!this.handler) {
elm.addEventListener('scroll', this.scroll.bind(this));
this.handler = true;
}
// 初始化显示图片
this.scroll();
});
}
scroll() {
this.listerens.forEach((listeren) => {
if (listeren.state.loading) return;
// 判断是否需要显示
listeren.checkInView() && listeren.load();
});
}
remove() {}
};
};
const scrollParent = (el) => {
let parentNode = el.parentNode;
// 找到滚动元素,这里我意思一下,只要找到overflow属性我就默认找到了
while (parentNode) {
if (/scroll/.test(getComputedStyle(parentNode)['overflow'])) {
return parentNode;
}
parentNode = parentNode.parentNode;
}
return parentNode;
};
export default plugin;
# 10.ssr(待更新)
# 11.手写 vuex
# 1.vuex 核心目录如下
vuex 目录/文件 | 含义 |
---|---|
modules 目录 | 循环模块/重新整合模块 |
module/module 文件 | 模块信息/循环模块 |
module/module-collection 文件 | 重新整合模块/namespace 拼接 |
helpers 文件 | 辅助函数相关 |
index 文件 | 入口文件 |
install 文件 | 插件的 install 方法 |
store 文件 | 核心逻辑模块 |
utils 文件 | 通用工具方法 |
# 2.install 文件代码
export let _Vue;
export default function insall(Vue) {
_Vue = Vue;
// 给每个组件都混入一个钩子函数,目的是给每个组件都提供一个$store选项
Vue.mixin(initMixin());
}
function initMixin() {
return {
beforeCreate() {
let options = this.$options;
if (options.store) {
// 根组件
this.$store = options.store;
} else {
// 子组件
if (this.$parent && this.$parent.$store) {
this.$store = this.$parent.$store;
}
}
},
};
}
# 3.index 文件代码如下
import install from './install';
import Store from './store';
import { mapState, mapActions, mapMutations, mapGetters } from './helpers';
export { mapState, mapActions, mapMutations, mapGetters };
export default {
install,
Store,
};
# 4.utils 文件代码如下
export function forEachValue(obj, fn) {
Object.keys(obj).forEach((key) => {
fn(obj[key], key);
});
}
export function isArray(data) {
return Array.isArray(data);
}
# 5.module/module 文件代码如下
import { forEachValue } from '../utils';
export default class Module {
constructor({ _raw, state, _children }) {
this._raw = _raw;
this.state = state;
this._children = _children;
}
get namespaced() {
return !!this._raw.namespaced;
}
getChild(name) {
return this._children[name];
}
forEachGetters(cb) {
this._raw.getters && forEachValue(this._raw.getters, cb);
}
forEachMutaions(cb) {
this._raw.mutations && forEachValue(this._raw.mutations, cb);
}
forEachActions(cb) {
this._raw.actions && forEachValue(this._raw.actions, cb);
}
forEachChild(cb) {
this._children && forEachValue(this._children, cb);
}
}
# 6.module/module-collection 文件代码如下
import { forEachValue } from '../utils';
import Module from './module';
// 格式化模块
export default class ModuleCollection {
constructor(options) {
this.root = null;
this.register([], options);
}
getNameSpace(path) {
let module = this.root;
return path.reduce((p, c) => {
module = module.getChild(c);
return module.namespaced ? p + c + '/' : p;
}, '');
}
/**
*
* @param {*} path 区分父子关系
* @param {*} options 模块
*/
register(path, module) {
let newModule = new Module({
_raw: module, // 当前模块
state: module.state, // 当前模块的状态
_children: {}, // 当前模块的子模块集合
});
if (path.length === 0) {
// 根模块
this.root = newModule;
} else {
// 子模块
// 找到父级模块
// 比如[a,c] 注册c模块的时候应该插入到a里面
// 递归回去的时候会把路径重置
let parent = path.slice(0, -1).reduce((p, c) => {
return p.getChild(c);
}, this.root);
parent._children[path[path.length - 1]] = newModule;
}
if (module.modules) {
// 递归注册模块
forEachValue(module.modules, (module, key) => {
this.register(path.concat(key), module);
});
}
}
}
# 7.store 文件代码如下
import { _Vue } from './install';
import { forEachValue } from './utils';
import ModuleCollection from './module/module-collection';
export default class Store {
constructor(options) {
// 格式化模块
this._modules = new ModuleCollection(options);
this.wrapperGetters = {};
this.getters = {};
this.mutations = {};
this.actions = {};
const computed = {};
const state = options.state;
// 安装模块
installModule(this, state, [], this._modules.root);
forEachValue(this.wrapperGetters, (getter, key) => {
computed[key] = getter;
Object.defineProperty(this.getters, key, {
get: () => this._vm[key],
});
});
this._vm = new _Vue({
data: {
$$state: state,
},
computed,
});
}
get state() {
return this._vm._data.$$state;
}
commit(type, payload) {
this.mutations[type] &&
this.mutations[type].forEach((mutation) => mutation(payload));
}
dispatch() {
this.actions[type] &&
this.actions[type].forEach((action) => action(payload));
}
}
function installModule(store, state, path, module) {
const ns = store._modules.getNameSpace(path);
// 子模块状态要安装到对应的父级模块中
if (path.length > 0) {
// [a]
let newState = path.slice(0, -1).reduce((p, c) => {
return p[c];
}, state);
newState[path[path.length - 1]] = module.state;
}
module.forEachGetters((fn, key) => {
key = ns + key;
store.wrapperGetters[key] = function() {
// 用户getter函数绑定this指向实例,传入模块状态
return fn.call(store, module.state);
};
});
module.forEachMutaions((fn, key) => {
key = ns + key;
// mutations和actions会被合并成数组,依次调用
store.mutations[key] = store.mutations[key] || [];
store.mutations[key].push((payload) => {
return fn.call(store, module.state, payload);
});
});
module.forEachActions((fn, key) => {
key = ns + key;
store.actions[key] = store.actions[key] || [];
store.actions[key].push((payload) => {
return fn.call(store, store, payload);
});
});
module.forEachChild((childModule, childKey) => {
return installModule(store, state, path.concat(childKey), childModule);
});
}
# 8.helpers 文件代码如下
import { isArray } from './utils';
export function mapState(states) {
let obj = {};
if (isArray(states)) {
for (let i = 0; i < states.length; i++) {
const stateName = states[i];
obj[stateName] = function() {
return this.$store.state[stateName];
};
}
} else {
// 对象格式
Object.keys(states).forEach((stateName) => {
const fn = states[stateName];
obj[stateName] = function() {
return fn(this.$store.state);
};
});
}
return obj;
}
export function mapGetters(getters) {
let obj = {};
if (isArray(getters)) {
for (let i = 0; i < getters.length; i++) {
const getterName = getters[i];
obj[getterName] = function() {
return this.$store.getters[getterName];
};
}
} else {
// 对象格式
Object.keys(getters).forEach((getterName) => {
const fn = getters[getterName];
obj[getterName] = function() {
return fn(this.$store.state);
};
});
}
return obj;
}
export function mapMutations(mutations) {
let obj = {};
if (isArray(mutations)) {
for (let i = 0; i < mutations.length; i++) {
const mutationName = mutations[i];
obj[mutationName] = function(...args) {
return this.$store.commit(mutationName, args);
};
}
} else {
// 对象格式
Object.keys(mutations).forEach((methodName) => {
const type = mutations[methodName];
obj[methodName] = function(...args) {
return this.$store.commit(type, args);
};
});
}
return obj;
}
export function mapActions(actions) {
let obj = {};
if (isArray(actions)) {
for (let i = 0; i < actions.length; i++) {
const actionName = actions[i];
obj[actionName] = function(...args) {
return this.$store.dispatch(actionName, args);
};
}
} else {
// 对象格式
Object.keys(actions).forEach((methodName) => {
const type = actions[methodName];
obj[methodName] = function(...args) {
return this.$store.dispatch(type, args);
};
});
}
return obj;
}
# 12.手写 vue-router
# 1.目录介绍
目录/文件 | 含义 |
---|---|
components 目录 | 全局组件(router-view/router-link) |
history 目录 | 路由模式 |
create-matcher 文件 | 包含 addRoute,match 等方法 |
create-route-map 文件 | 扁平化路由,路由映射表 |
index 文件 | 核心 |
install 文件 | 插件入口 |
# 2.install 文件代码如下
import RouterLink from './components/link';
import RouterView from './components/view';
export let _Vue;
export default function install(Vue) {
_Vue = Vue;
Vue.mixin({
beforeCreate() {
const router = this.$options.router;
if (router) {
// 根组件
this._router = router;
this._routerRoot = this;
this._router.init(this);
// 响应式属性current
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
// 子孙组件
if (this.$parent && this.$parent._routerRoot) {
this._routerRoot = this.$parent._routerRoot;
}
}
},
});
Object.defineProperty(Vue.prototype, '$router', {
get() {
// 方法
return this._routerRoot._router;
},
});
Object.defineProperty(Vue.prototype, '$route', {
get() {
// 属性current
return this._routerRoot._route;
},
});
Vue.component('router-link', RouterLink);
Vue.component('router-view', RouterView);
}
# 3.create-matcher 文件代码如下
import createRouteMap from './create-route-map';
export default function createMatcher(routes) {
let { pathMap } = createRouteMap(routes);
console.log(pathMap, '处理后的');
function match(path) {
return pathMap[path];
}
function addRoutes(routes) {
createRouteMap(routes, pathMap);
}
return {
match,
addRoutes,
};
}
# 4.create-route-map 文件代码如下
export default function createRouteMap(routes, oldPathMap) {
let pathMap = oldPathMap ? oldPathMap : {};
routes.forEach((route) => {
createRouteRecord(route);
});
function createRouteRecord(route, parent) {
let path = parent ? `${parent.path}/${route.path}` : route.path;
let record = {
path,
component: route.component,
props: route.props || {},
parent,
};
pathMap[path] = record;
if (route.children && route.children.length > 0) {
route.children.forEach((cRoute) => createRouteRecord(cRoute, record));
}
}
return {
pathMap,
};
}
# 5.history/base 文件代码如下
function createRoute(record, location) {
let matched = [];
if (record) {
while (record) {
if (record) {
matched.unshift(record);
}
record = record.parent;
}
}
return {
...location,
matched,
};
}
export default class History {
constructor(router) {
this.router = router;
this.current = createRoute(null, {
path: '/',
}); // {path:'/',matched:[]}
}
listen(cb) {
this.callback = cb;
}
// 核心方法
transtionTo(path, cb) {
// 当路径发生变化,渲染组件,那我们要收集匹配到的组件
// 比如跳转到/about/a,我们要收集/about和/aboutA组件
// 找到匹配的记录
const record = this.router.match(path);
// current属性变化,要渲染视图,所以我们要将current属性变成响应式属性
const route = createRoute(record, { path });
// 防止重复跳转
if (
route.path === this.current.path &&
route.matched.length === this.current.matched.length
)
return;
this.current = route;
this.callback && this.callback(this.current);
cb && cb();
}
}
# 6.history/hash 文件代码如下
import History from './base';
function ensurehash() {
if (!window.location.hash) {
window.location.hash = '/';
}
}
function gethash() {
return window.location.hash.slice(1);
}
export default class HashHistory extends History {
constructor(router) {
super(router);
ensurehash();
}
getCurrentLocation() {
return gethash();
}
setUplisten() {
window.addEventListener('hashchange', () => {
// 当路径发生变化后,要跳转
this.transtionTo(gethash());
});
}
}
# 7.history/html5 文件代码如下
import History from './base';
export default class HTML5History extends History {
constructor(router) {
super(router);
}
getCurrentLocation() {}
setUplisten() {}
}
# 8.index 文件代码如下
import install from './install';
import createMatcher from './create-matcher';
import HTML5History from './history/html5';
import HashHistory from './history/hash';
export default class VueRouter {
constructor(options) {
this.mode = options.mode || 'hash';
console.log(options.routes, '原来的');
// 扁平化数据
// matcher上面有match方法和addRoutes方法
this.matcher = createMatcher(options.routes || []);
switch (this.mode) {
case 'history':
this.history = new HTML5History(this);
break;
case 'hash':
this.history = new HashHistory(this);
break;
default:
console.error(`invalid mode: ${this.mode}`);
break;
}
}
init(app) {
// 默认要跳转
const history = this.history;
// 根据模式不同,要调用不同的方法
const setUpListen = () => {
history.setUplisten();
};
history.transtionTo(history.getCurrentLocation(), setUpListen);
history.listen((route) => {
app._route = route;
});
}
match(path) {
return this.matcher.match(path);
}
push(path) {
this.history.transtionTo(path, () => {
window.location.hash = path;
});
}
}
VueRouter.install = install;
# 9.components/link 文件代码如下
export default {
functional: true,
props: {
to: {
type: String,
required: true,
},
},
render: (h, context) => {
const click = () => {
// 点击a标签,要跳转
context.parent.$router.push(context.props.to);
};
return <a onClick={click}>{context.slots().default}</a>;
},
};
# 10.components/view 文件代码如下
export default {
functional: true,
render: (h, { parent, data }) => {
let route = parent.$route;
let depth = 0;
while (parent) {
// $vnode ==> 组件占位符
const vnodeData = parent.$vnode ? parent.$vnode.data : {};
if (vnodeData.routerView) {
// 找到父级router-view就加1,就渲染下一个组件
depth++;
}
parent = parent.$parent;
}
let record = route.matched[depth];
if (!record) {
// 没有匹配到,空渲染
return h();
}
data.routerView = true;
return h(record.component, data);
},
};
# 13.树形结构的增删改查
# 1.index.html 文件代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script type="module">
import DelayerTree from './index.js';
const treeList = [
{
title: '菜单1',
id: '11111',
parentId: '0',
children: [
{
title: '菜单1-1',
parentId: '11111',
id: '222222',
},
],
},
{
title: '菜单2',
id: '333',
parentId: '0',
children: [
{
title: '菜单2-1',
id: '444',
parentId: '333',
children: [
{
title: '菜单2-1-1',
id: '6666',
parentId: '444',
},
],
},
{
title: '菜单2-2',
id: '555',
parentId: '333',
},
],
},
];
// 格式化后的树实例
const _tree1 = new DelayerTree(treeList);
// console.log(_tree1)
// 增加一条数据
const _tree2 = _tree1.add('222222', {
title: '我是新增的数据1',
});
// console.log(_tree2)
// 增加一条数据
const _tree3 = _tree1.add('6666', {
title: '我是新增的数据2',
});
const _tree4 = _tree1.add('555', {
title: '我是新增的数据3',
});
const _tree5 = _tree1.add('555', {
title: '我是新增的数据4',
});
// console.log(_tree3)
// 重新状态成树形结构
const tree5 = _tree1.resetLoad(_tree5);
console.log(tree5);
</script>
</body>
</html>
# 2.index.js 文件代码如下
/**
* 很多时候,后台都会返回给我们一个树形结构的数据,前端递归渲染
* 那如果这个渲染出来的树要进行增删改查,我们怎么操作呢?
*
* 常规思路:
* 增的时候递归找到当前的父级元素,在父级元素中增加一条数据
* 查的时候递归查找当前元素,返回当前元素信息
* 删的时候递归查找到当前元素id
* 改的时候递归找到当前元素
*
* 以上可得:
* 性能开销非常大,当我们树形结构嵌套越深,数据越多,开销就越大,可能会导致卡死/崩溃/缓慢的效果
*
* 优化思路:
* 当我们前端递归渲染菜单的时候,对递归的数据进行扁平化处理,形成映射表,那么这样的话就方便很多了
*/
class DelayerTree {
/**
*
* @param {*} options 后台返回的数据
*/
constructor(options) {
this.options = options;
this._treeMap = {};
this.dirty = false; // 为true就表示脏了,就重新装载树,false就返回老树
this.delayerTree(options);
}
/**
*
* @param {string} parentId 要添加到哪个父级菜单中
* @param {...any} rest 额外参数
*/
add(parentId, { ...rest }) {
this.dirty = true;
const uid = new Date() + Math.floor(Math.random() * 10000);
// 找到对应的父级
let parent = this._treeMap[parentId];
const record = {
id: uid,
parentId,
parent,
isAdd: true, // 标识是新增的节点
...rest,
};
parent.children = parent.children ? parent.children : [];
parent.children.push(record);
this._treeMap[uid] = record;
// 修改父级引用
let topParent = this._treeMap[parent.parentId];
if (typeof topParent.parent === 'undefined') {
topParent.children[parent.inx] = parent;
topParent = this._treeMap[topParent.parentId];
parent = topParent;
} else {
while (topParent && typeof topParent.parent !== 'undefined') {
// debugger
topParent.children[parent.inx] = parent;
topParent = this._treeMap[topParent.parentId];
parent = topParent;
}
}
return this._treeMap;
}
/**
*
* @param {*} id 要删除子项的id
*/
remove(id) {
this.dirty = true;
delete this._treeMap[id];
return this._treeMap;
}
/**
*
* @param {*} id 要更新子项的id
* @param {...any} rest 其他参数
*/
update(id, ...rest) {
this.dirty = true;
const record = this._treeMap[id];
this._treeMap[id] = {
...record,
...rest,
};
return this._treeMap;
}
/**
*
* @param {*} id 获取对应子项id的匹配记录
*/
get(id) {
return this._treeMap[id];
}
/**
*
* @param {*} tree[] 树形菜单数据 => 扁平化处理
*/
delayerTree(tree) {
for (let i = 0; i < tree.length; i++) {
const ctree = tree[i];
this.createTreeMap(ctree, undefined, i);
}
}
/**
*
* @param {*} ctree 树的子节点
*/
createTreeMap(ctree, parent, inx) {
const { id, parentId, ...other } = ctree;
let record = {
id: id,
parentId,
parent,
inx,
...other,
};
this._treeMap[id] = record;
if (ctree.children && ctree.children.length > 0) {
for (let i = 0; i < ctree.children.length; i++) {
let child = ctree.children[i];
this.createTreeMap(child, record, i);
}
}
}
/**
*
* @param {*} _treeMap 扁平化数据 ==> 树形菜单数据
*/
resetLoad(_treeMap) {
// 看有没有被操作过,如果没有被操作过,说明可以返回初始化的老树
if (!this.dirty) return this.options;
// 将映射表重新装载成树形结构
let res = [];
Object.keys(_treeMap).forEach((key) => {
const record = _treeMap[key];
if (typeof record.parent === 'undefined') {
// 说明是顶层菜单
res.push(record);
}
});
return res;
}
}
export default DelayerTree;
# 14.字典树
# 1.index.html 文件代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
list-style: none;
}
</style>
</head>
<body>
<h1>请输入要搜索的数据</h1>
<input type="text" id="input" />
<ul id="ul"></ul>
<script>
// let results = []
// 字典树 + 节流 + 虚拟列表
function fetch(url, method = 'get') {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (
xhr.readyState === 4 &&
xhr.status >= 200 &&
xhr.status <= 299
) {
resolve(JSON.parse(xhr.response));
}
};
xhr.open(method, url);
xhr.send();
});
}
fetch('http://localhost:3000/getUsers').then((results) => {
// results = []十万条数据
class Leaf {
constructor(id = '', value = '') {
this.ids = id ? [id] : [];
this.value = value;
this.children = {};
}
share(id) {
this.ids.push(id);
}
// key = zc
getChildIds(keys) {
let children = this.children;
let res = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i]; // z , c
if (children[key]) {
res = children[key].ids
? [...children[key].ids, ...children[key].childrenIds]
: children[key].childrenIds;
children = children[key].children;
} else {
res = [];
break;
}
}
return res;
}
}
// 模拟请求返回了100000条数据,根据关键字查询
const root = new Leaf();
// 构建字典树
for (let i = 0; i < results.length; i++) {
const userinfo = results[i];
const name = userinfo.name;
let templateRoot = root;
for (let j = 0; j < name.length; j++) {
const charStr = name[j];
const reachEnd = j === name.length - 1;
if (!templateRoot.children[charStr]) {
// 说明树中没有,在树中添加
templateRoot.children[charStr] = new Leaf(
reachEnd ? userinfo.id : '',
charStr
);
} else {
if (reachEnd) {
// 如果值相同,就追加id标识
templateRoot.children[charStr].share(userinfo.id);
}
}
templateRoot = templateRoot.children[charStr];
}
}
console.log(root, 'root');
// 优化
budget(root);
function childrenCollectionIds(root) {
let ids = [];
Object.keys(root).forEach((key) => {
const value = root[key];
ids = value.ids ? [...ids, ...value.ids] : ids;
if (Object.keys(value.children).length) {
ids = ids.concat(childrenCollectionIds(value.children));
}
});
return ids;
}
// 预计算
function budget(root) {
const { children } = root;
root.childrenIds = childrenCollectionIds(root.children);
Object.keys(children).forEach((key) => {
const leaf = children[key];
leaf.childrenIds = childrenCollectionIds(leaf.children);
budget(leaf);
});
}
input.oninput = function(e) {
const keyword = e.target.value;
const ids = root.getChildIds(keyword);
const res = ids.reduce((p, c) => {
let count = 0;
while (true) {
if (results[count].id === c) {
p.push(results[count]);
break;
}
count++;
if (count >= results.length) break;
}
return p;
}, []);
// console.log(res)
innerHtml(ul, res);
};
function innerHtml(el, data = []) {
let str = '';
data.map((item) => {
str += `<li>${item.name}</li>`;
});
console.log(str, 'str');
el.innerHTML = str;
}
});
// 模糊搜索
// 收集叶子节点下的所有子节点id
// function searchBlur(root, keyword) {
// let results = []
// let templateRoot = root
// for (let i = 0; i < keyword.length; i++) {
// // 拿到每一个关键字
// const charStr = keyword[i]
// // 判断是否在树中,不过首字母就已经不再树中了,那么直接跳出循环,因为我们是按首字母构建树的
// if (!templateRoot.children[charStr]) break
// else {
// templateRoot = templateRoot.children[charStr]
// }
// if (i === keyword.length - 1) {
// results = templateRoot.ids
// // results = [
// // ...templateRoot.ids,
// // // 收集该叶子节点下的所有子节点id集合,要优化
// // ...childrenCollectionIds(templateRoot.children)
// // ]
// // 优化,预计算,因为树一旦生成,不会发生变化,提前计算好所有叶子节点的子节点id集合
// }
// }
// // console.log(results, 'results')
// return results
// }
</script>
</body>
</html>
# 2.server.js 文件代码如下
const http = require('http');
const fs = require('fs');
const path = require('path');
const strs = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'g',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
];
function randomUserName(nums) {
let cnums = nums;
let res = [],
str = '',
max = 5;
while (cnums--) {
for (let i = 0; i < max; i++) {
const randomNum = Math.floor(Math.random() * strs.length);
const charStr = strs[randomNum];
str += charStr;
}
res.push({
id: cnums,
name: str,
// message: 'random testing',
});
str = '';
}
return res;
}
const server = http.createServer((req, res) => {
// const users = randomUserName(100000)
// console.log(users, 'users')
if (req.url === '/index.html') {
res.end(fs.readFileSync(path.resolve(__dirname, './index.html')));
}
if (req.url === '/getUsers') {
res.end(JSON.stringify(randomUserName(100000)));
}
});
server.listen(3000);