title: 手写 Vue2.0 源码 description: 核心逻辑以及功能的实现
一.使用 Rollup 搭建开发环境
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,
rollup.js
更专注于Javascript
类库打包。
1.安装 rollup 环境
1
npm install @babel/preset-env @babel/core rollup rollup-plugin-babel rollup-plugin-serve cross-env -D
2.rollup.config.js 编写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import babel from 'rollup-plugin-babel';
import serve from 'rollup-plugin-serve';
// 常见的模块规范 import export (ESModule) module.exports require (CommonJS)
// AMD 比较老的模块规范 systemjs 模块规范
// ES6Module commonjs umd 支持amd 和 cmd Vue)
export default {
input: './src/index.js', // 打包项目的入口文件
output: {
format: 'umd', // 打包后的结果是umd模块规范
file: 'dist/vue.js', // 打包出的文件结果放在哪个目录
name: 'Vue', // 打包后的全局变量的名字
sourcemap: true,
},
plugins: [
babel({
exclude: 'node_modules/**',
}),
process.env.ENV === 'development'
? serve({
open: true,
openPage: '/public/index.html',
port: 3000,
contentBase: '',
})
: null,
],
};
3.配置.babelrc
1
2
3
{
"presets": ["@babel/preset-env"]
}
4.执行脚本配置
1
2
3
4
5
// package.json
"scripts": {
"build:dev": "rollup -c",
"serve": "cross-env ENV=development rollup -c -w"
}
二.Vue 响应式原理
导出 Vue 构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 整个自己编写的Vue入口
import { initMixin } from './init';
// es6的类, 要求所有的扩展都在类的内部来进行扩展
function Vue(options) {
this._init(options);
}
initMixin(Vue); // 后续再扩展都可以采用这种方式
// 给Vue添加原型方法我们通过文件的方式来添加, 防止所有的功能都在一个文件中来处理
export default Vue;
init 方法初始化 Vue 状态
1
2
3
4
5
6
7
8
9
10
11
12
13
import { initState } from './state';
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this;
// vue中会判断如果是$开头的属性不会被变成响应式数据
this.$options = options; // 所有后续的扩展方法都有一个$options选项可以获取用户的所有选项
// 对于实例的数据源 props data methods computed watch
// prop data
initState(vm);
};
}
根据不同属性进行初始化操作
1
2
3
4
5
6
7
8
9
10
11
export function initState(vm) {
// 获取传入的数据对象
const options = vm.$options;
// 后续实现计算属性 watch props methods
if (options.data) {
// 初始化data
initData(vm);
}
}
function initData(vm) {}
1. 初始化数据
1
2
3
4
5
6
7
8
import { observe } from './observer/index';
function initData(vm) {
let data = vm.$options.data;
// 如果是函数就拿到函数的返回值, 否则就直接采用data作为数据源
data = vm._data = typeof data === 'function' ? data.call(vm) : data;
// 属性劫持, 采用defineProperty将所有的属性进行劫持
observe(data);
}
2. 递归属性劫持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import arrayPrototype from './array';
class Observer {
constructor(data) {
// 如果有__ob__属性, 说明被观测过了
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false, // 不可枚举
writable: true,
configurable: true,
});
if (Array.isArray(data)) {
// 如果是数组的话也是用defineProperty回浪费很多性能, 并且很少用户会通过索引操作 arr[666] = 777
// vue3中的Polyfill直接就给数组做代理了
// 改写数组的方法, 勇敢用户调用了可以改写数组方法的api, 那么就去劫持这个方法
// 变异方法 push pop shift unshift reverse sort splice
// 修改数组的索引和长度无法更新视图
data.__proto__ = arrayPrototype;
// 如果数组里面放的是对象类型, 期望它也会被变成响应式的
this.observeArray(data);
} else {
this.walk(data);
}
}
observeArray(data) {
data.forEach((item) => observe(item)); // 如果是对象才观测
}
walk(data) {
// 循环对象, 尽量不用for in 会遍历原型链
let keys = Object.keys(data);
keys.forEach((key) => {
// 没有重写数组里的每一项
defineReactive(data, key, data[key]);
});
}
}
// 性能不好的原因, 所有的属性都被重新定义了一遍
// 一上来需要将对象深度代理, 性能差
function defineReactive(data, key, value) {
observe(value); // 递归代理属性
// 属性会全部被重写添加了get和set
Object.defineProperty(data, key, {
get() {
return value;
},
set(newValue) {
observe(newValue); // 赋值一个对象, 也可以实现响应式数据
if (newValue !== value) {
value = newValue;
}
},
});
}
export function observe(data) {
if (typeof data !== 'object' || data == null) {
// 如果不是对象类型, 那么不需要做任何处理
return;
}
if (data.__ob__) {
// 说明这个属性已经被代理过了
return;
}
// 如果一个对象已经被观测了, 就不要再次被观测了
// __ob__ 标识是否又被观测过
return new Observer(data);
}
3. 数组方法劫持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Observer {
constructor(data) {
if (Array.isArray(data)) {
// 如果是数组的话也是用defineProperty回浪费很多性能, 并且很少用户会通过索引操作 arr[666] = 777
// vue3中的Polyfill直接就给数组做代理了
// 改写数组的方法, 勇敢用户调用了可以改写数组方法的api, 那么就去劫持这个方法
// 变异方法 push pop shift unshift reverse sort splice
// 修改数组的索引和长度无法更新视图
data.__proto__ = arrayPrototype;
// 如果数组里面放的是对象类型, 期望它也会被变成响应式的
this.observeArray(data);
} else {
this.walk(data);
}
}
observeArray(data) {
data.forEach((item) => observe(item)); // 如果是对象才观测
}
}
重写数组原型方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let oldArrayPrototype = Array.prototype;
// arrayPrototype.__proto__ = Array.prototype
let arrayPrototype = Object.create(oldArrayPrototype);
let methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'];
methods.forEach((method) => {
// 用户调用push方法会先自己重写的方法, 之后调用数组原来的方法
arrayPrototype[method] = function (...args) {
let inserted;
const ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift':
inserted = args; // 数组
break;
case 'splice': // arr.splice(1, 1, xxx)
inserted = args.slice(2);
break;
default:
break;
}
if (inserted) {
// 对新增的数据再次进行观测
ob.observeArray(inserted);
}
return oldArrayPrototype[method].call(this, ...args);
};
});
export default arrayPrototype;
增加ob属性
1
2
3
4
5
6
7
8
9
10
11
class Observer {
constructor(data) {
// 如果有__ob__属性, 说明被观测过了
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false, // 不可枚举
writable: true,
configurable: true,
});
}
}
给所有响应式数据增加标识,并且可以在响应式上获取
Observer
实例上的方法
4. 数据代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function proxy(vm, source, key) {
Object.defineProperty(vm, key, {
get() {
return vm[source][key];
},
set(newValue) {
vm[source][key] = newValue;
},
});
}
function initData(vm) {
let data = vm.$options.data;
// 如果是函数就拿到函数的返回值, 否则就直接采用data作为数据源
data = vm._data = typeof data === 'function' ? data.call(vm) : data;
// 期望用户可以直接通过 vm.xxx 获取值, 也可以这样取值 vm._data.xxx
for (let key in data) {
proxy(vm, '_data', key);
}
// 属性劫持, 采用defineProperty将所有的属性进行劫持
observe(data);
}
三.模板编译
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Vue.prototype._init = function (options) {
const vm = this;
// vue中会判断如果是$开头的属性不会被变成响应式数据
this.$options = options; // 所有后续的扩展方法都有一个$options选项可以获取用户的所有选项
// 对于实例的数据源 props data methods computed watch
// prop data
initState(vm);
// 状态初始化完毕后需要进行页面挂载
if (vm.$options.el) {
// el属性和直接调用$mount是一样的
vm.$mount(vm.$options.el);
}
};
Vue.prototype.$mount = function (el) {
const vm = this;
el = document.querySelector(el);
const options = vm.$options;
if (!options.render) {
let template = options.template;
if (!template && el) {
template = el.outerHTML;
}
// 将template变成render函数
// 创建render函数 => 虚拟dom => 渲染真实dom
const render = compileToFunctions(template); // 开始编译
options.render = render;
}
// 将当前组件实例挂载到真实的el节点上面
return mountComponent(vm, el);
};
将 template 编译成 render 函数
1.解析标签和内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名 形如 abc-123
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配特殊标签 形如 abc:234 前面的abc:可有可无
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签开始 形如 <abc-123 捕获里面的标签名
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束 >
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾 如 </abc-123> 捕获里面的标签名
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性 形如 id="app"
export function parse(html) {
let root; // 树的操作,需要根据开始标签和结束标签产生一棵树
// 如何构建树的父子关系
let stack = [];
while (html) {
// 一个个字符来解析将结果跑出去
let textEnd = html.indexOf('<');
if (textEnd === 0) {
const startTagMatch = parseStartTag(); // 解析开始标签
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
let matches;
if ((matches = html.match(endTag))) {
// </div> 不是开始就会走到结束
end(matches[1]);
advance(matches[0].length);
continue;
}
}
let text;
if (textEnd >= 0) {
text = html.substring(0, textEnd);
}
if (text) {
advance(text.length);
chars(text);
}
}
function createASTElement(tagName, attrs) {
return {
tag: tagName,
attrs,
children: [],
parent: null,
type: 1,
};
}
function start(tagName, attrs) {
const element = createASTElement(tagName, attrs);
if (root == null) {
root = element;
}
const parent = stack[stack.length - 1]; // 取到栈中最后一个
if (parent) {
element.parent = parent; // 让这个元素记住自己的父亲是谁
parent.children.push(element); // 让父亲记住儿子是谁
}
// 将原素放到栈中
stack.push(element);
}
function end(tagName) {
stack.pop();
}
function chars(text) {
text = text.replace(/\s/g, '');
if (text) {
const parent = stack[stack.length - 1];
parent.children.push({
type: 3,
text,
});
}
}
function parseStartTag() {
const matches = html.match(startTagOpen);
if (matches) {
const match = {
tagName: matches[1],
attrs: [],
};
advance(matches[0].length);
// 继续解析开始标签的属性
let end, attr;
while (
!(end = html.match(startTagClose)) &&
(attr = html.match(attribute))
) {
// 只要没有匹配到结束标签就一直匹配
match.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5] || true,
});
advance(attr[0].length); // 解析一个属性删除一个
}
if (end) {
advance(end[0].length);
return match;
}
}
}
function advance(n) {
html = html.substring(n); // 每次根据传入的长度截取html
}
return root;
}
2.生成 ast 语法树
语法树就是用对象描述 js 语法
1
2
3
4
5
6
7
{
tag:'div',
type:1,
children:[{tag:'span',type:1,attrs:[],parent:'div对象'}],
attrs:[{name:'pf',age:10}],
parent:null
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
let root; // 树的操作,需要根据开始标签和结束标签产生一棵树
// 如何构建树的父子关系
let stack = [];
while (html) {
// 一个个字符来解析将结果跑出去
let textEnd = html.indexOf('<');
if (textEnd === 0) {
const startTagMatch = parseStartTag(); // 解析开始标签
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
let matches;
if ((matches = html.match(endTag))) {
// </div> 不是开始就会走到结束
end(matches[1]);
advance(matches[0].length);
continue;
}
}
let text;
if (textEnd >= 0) {
text = html.substring(0, textEnd);
}
if (text) {
advance(text.length);
chars(text);
}
}
function createASTElement(tagName, attrs) {
return {
tag: tagName,
attrs,
children: [],
parent: null,
type: 1,
};
}
function start(tagName, attrs) {
const element = createASTElement(tagName, attrs);
if (root == null) {
root = element;
}
const parent = stack[stack.length - 1]; // 取到栈中最后一个
if (parent) {
element.parent = parent; // 让这个元素记住自己的父亲是谁
parent.children.push(element); // 让父亲记住儿子是谁
}
// 将原素放到栈中
stack.push(element);
}
function end(tagName) {
stack.pop();
}
function chars(text) {
text = text.replace(/\s/g, '');
if (text) {
const parent = stack[stack.length - 1];
parent.children.push({
type: 3,
text,
});
}
}
3.生成代码
template 转换成 render 函数的结果
1
2
3
4
<div style="color:red">hello <span></span></div>
render(){
return _c('div',{style:{color:'red'}},_v('hello'+_s(name)),_c('span',undefined,''))
}
实现代码生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括号 捕获花括号里面的内容
// 处理attrs属性
function genProps(attrs) {
let str = '';
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
// 特殊属性 style
if (attr.name === 'style') {
const obj = {};
attr.value.split(';').reduce((memo, current) => {
const [key, value] = current.split(':');
memo[key] = value;
return memo;
}, obj);
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0, -1)}}`;
}
function gen(node) {
if (node.type === 1) {
return generate(node);
} else {
const text = node.text;
if (!defaultTagRE.test(text)) {
return `_v(${JSON.stringify(text)})`; // 不带表达式
} else {
const tokens = [];
let match;
// exec 遇到全局匹配会有 lastIndex 问题,每次匹配谦虚要将lastIndex置为0
let startIndex = (defaultTagRE.lastIndex = 0);
while ((match = defaultTagRE.exec(text))) {
const endIndex = match.index; // 匹配到的索引 abc {{aa}} cd
if (endIndex > startIndex) {
tokens.push(JSON.stringify(text.slice(startIndex, endIndex)));
}
tokens.push(`_s(${match[1].trim()})`);
startIndex = endIndex + match[0].length;
}
if (startIndex < text.length) {
// 将最后的尾巴也丢进去
tokens.push(JSON.stringify(text.slice(startIndex)));
}
return `_v(${tokens.join('+')})`; // 组合成最终的语法
}
}
}
// 生成子节点,递归创建
function genChildren(el) {
const children = el.children;
if (children) {
return `${children.map((child) => gen(child)).join(',')}`;
}
}
export function generate(el) {
// 字符串拼接
const children = genChildren(el);
let code = `_c('${el.tag}',${
el.attrs.length ? `${genProps(el.attrs)}` : 'undefined'
}${children ? `,${children}` : ''})`; // _c('div', {className: 'xxx'}, _v('hello world'))
return code;
}
4.生成 render 函数
1
2
3
4
5
6
7
8
9
10
11
// 将模板变成render函数,通过with+new Function的方式让字符串变成js语法来执行
export function compileToFunctions(template) {
const ast = parse(template);
// 通过ast语法树转换成render函数
const code = generate(ast);
// 使用with改变作用域为this
const renderFn = new Function(`with(this) {return ${code}}`);
return renderFn;
}
五.依赖收集
每个属性都要有一个dep
,每个dep
中存放着watcher
,同一个watcher
会被多个dep
所记录。
1. 在渲染是存储 watcher
1
2
3
4
5
6
get() {
// debugger;
Dep.target = this; // 静态属性就是只有一份
this.getter(); // 会去vm上取值 vm._update(vm._render) 取name 和age
Dep.target = null; // 渲染完毕后就清空
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let id = 0;
class Dep {
constructor() {
this.id = id++; // 属性的dep要收集watcher
this.subs = []; // 这里存放着当前属性对应的watcher有哪些
}
depend() {
// 这里我们不希望放重复的watcher,而且刚才只是一个单向的关系 dep -> watcher
// watcher 记录dep
// this.subs.push(Dep.target);
Dep.target.addDep(this); // 让watcher记住dep
// dep 和 watcher是一个多对多的关系 (一个属性可以在多个组件中使用 dep -> 多个watcher)
// 一个组件中由多个属性组成 (一个watcher 对应多个dep)
}
addSub(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach((watcher) => watcher.update()); // 告诉watcher要更新了
}
}
Dep.target = null;
export default Dep;
2. 对象依赖收集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 性能不好的原因, 所有的属性都被重新定义了一遍
// 一上来需要将对象深度代理, 性能差
// 闭包 属性劫持
function defineReactive(data, key, value) {
const childOb = observe(value); // 递归代理属性, childOb就是当前的实例
// 属性会全部被重写添加了get和set
let dep = new Dep(); // 每一个属性都有一个dep
Object.defineProperty(data, key, {
// 取值的时候 会执行get
get() {
if (Dep.target) {
dep.depend(); // 让这个属性的收集器记住当前的watcher
if (childOb) {
childOb.dep.depend(); // 让数组和对象本身也实现依赖收集
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
// 修改的时候 会执行set
set(newValue) {
// 修改的时候 会执行set
if (newValue === value) return;
observe(newValue);
value = newValue;
dep.notify(); // 通知更新
},
});
}
-
Dep 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
let id = 0; class Dep { constructor() { this.id = id++; // 属性的dep要收集watcher this.subs = []; // 这里存放着当前属性对应的watcher有哪些 } depend() { // 这里我们不希望放重复的watcher,而且刚才只是一个单向的关系 dep -> watcher // watcher 记录dep // this.subs.push(Dep.target); Dep.target.addDep(this); // 让watcher记住dep // dep 和 watcher是一个多对多的关系 (一个属性可以在多个组件中使用 dep -> 多个watcher) // 一个组件中由多个属性组成 (一个watcher 对应多个dep) } addSub(watcher) { this.subs.push(watcher); } notify() { this.subs.forEach((watcher) => watcher.update()); // 告诉watcher要更新了 } } Dep.target = null; export default Dep;
-
watcher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
import Dep from './dep'; let id = 0; // 1)当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target上 // 2) 调用_render() 会取值 走到get上 // 每个属性有一个dep(属性就是被观察者),watcher就是观察者(属性变化了会通知观察者来更新) -> 观察者模式 class Watcher { // 不同组件有不同的watcher 目前只有一个渲染根实例的 constructor(vm, fn, options) { this.id = id++; this.renderWatcher = options; // 是一个渲染watcher this.getter = fn; // getter意味着调用这个函数可以发生取值操作 this.deps = []; // 后续我们实现计算属性,和一些清理工作需要用到 this.depsId = new Set(); this.get(); } addDep(dep) { // 一个组件 对应着多个属性 重复的属性也不用记录 let id = dep.id; if (!this.depsId.has(id)) { this.deps.push(dep); this.depsId.add(id); dep.addSub(this); // watcher已经记住了dep了而且去重了,此时让dep也记住watcher } } get() { // debugger; Dep.target = this; // 静态属性就是只有一份 this.getter(); // 会去vm上取值 vm._update(vm._render) 取name 和age Dep.target = null; // 渲染完毕后就清空 } update() { console.log('update'); queueWatcher(this); // 把当前的watcher 暂存起来 // this.get(); // 重新渲染 } run() { this.get(); // 渲染的时候用的是最新的vm来渲染的 } } export default Watcher;
3. 数组依赖收集
-
递归收集数组依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
function defineReactive(data, key, value) { const childOb = observe(value); // 递归代理属性, childOb就是当前的实例 // 属性会全部被重写添加了get和set let dep = new Dep(); // 每一个属性都有一个dep Object.defineProperty(data, key, { // 取值的时候 会执行get get() { if (Dep.target) { dep.depend(); // 让这个属性的收集器记住当前的watcher if (childOb) { childOb.dep.depend(); // 让数组和对象本身也实现依赖收集 if (Array.isArray(value)) { dependArray(value); } } } return value; }, // ... } // 深层次嵌套会递归, 递归多了性能差, 不粗案子属性监控不到, 存在的属性要重写方法 function dependArray(value) { for (let i = 0; i < value.length; i++) { let current = value[i]; current.__ob__ && current.__ob__.dep.depend(); if (Array.isArray(current)) { dependArray(current); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
// 获取数组原型 let oldArrayPrototype = Array.prototype; // arrayPrototype.__proto__ = Array.prototype let arrayPrototype = Object.create(oldArrayPrototype); // 数组变异方法 let methods = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort', ]; methods.forEach((method) => { // 用户调用push方法会先自己重写的方法, 之后调用数组原来的方法 arrayPrototype[method] = function (...args) { // 需要对新增的数据再次进行劫持 let inserted; const ob = this.__ob__; switch (method) { case 'push': case 'unshift': inserted = args; // 数组 break; case 'splice': // arr.splice(1, 1, xxx) inserted = args.slice(2); break; default: break; } if (inserted) { // 对新增的数据再次进行观测 ob.observeArray(inserted); } const result = oldArrayPrototype[method].call(this, ...args); ob.dep.notify(); // 数据变化了, 通知对应的watcher实现更新逻辑 return result; }; }); export default arrayPrototype;
六.Vue 异步更新之 nextTick
1. 实现队列机制
1
2
3
4
5
update() {
console.log('update');
queueWatcher(this); // 把当前的watcher 暂存起来
// this.get(); // 重新渲染
}
-
scheduler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// watcher.js let queue = []; let has = {}; let pending = false; // 防抖 function flushSchedulerQueue() { let flushQueue = queue.slice(0); queue = []; has = {}; pending = false; flushQueue.forEach((q) => q.run()); // 在刷新的过程中可能还有新的watcher,重新放到queue中 } function queueWatcher(watcher) { const id = watcher.id; if (!has[id]) { queue.push(watcher); has[id] = true; // 不管我们的update执行多少次 ,但是最终只执行一轮刷新操作 if (!pending) { nextTick(flushSchedulerQueue, 0); pending = true; } } }
2. nextTick 实现原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// watcher.js
let callbacks = [];
let waiting = false;
function flushCallbacks() {
let cbs = callbacks.slice(0);
waiting = false;
callbacks = [];
cbs.forEach((cb) => cb()); // 按照顺序依次执行
}
// nextTick 没有直接使用某个api 而是采用优雅降级的方式
// 内部先采用的是promise(ie不兼容)MutationObserver(h5的api) 可以考虑ie专享的 setImmediate setTimeout
let timerFunc;
if (Promise) {
timerFunc = () => {
Promise.resolve().then(flushCallbacks);
};
} else if (MutationObserver) {
let observer = new MutationObserver(flushCallbacks); // 这里传入的回调是异步执行的
let textNode = document.createTextNode(1);
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
textNode.textContent = 2;
};
} else if (setImmediate) {
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
timerFunc = () => {
setTimeout(flushCallbacks);
};
}
// 异步任务分为两种,宏任务/微任务
// 宏任务 setTimeout setImmediate(IE下支持性能优于setTimeout)
// 微任务 promise.then mutationObserver
// Vue在更新的时候希望尽快的更新页面 Promise.then MutationObserver setImmediate setImmediate
// Vue3不再考虑兼容性问题了, 所以后续Vue3中直接使用Promise.then
export function nextTick(cb) {
// 先内部还是先用户的?
callbacks.push(cb); // 维护nextTick中的cakllback方法
console.log(callbacks);
if (!waiting) {
timerFunc();
// Promise.resolve().then(flushCallbacks);
waiting = true;
}
}
七.Vue 中 Diff 算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// index.js
// ------------- 为了方便观察前后的虚拟节点-- 测试的-----------------
let vm0 = new Vue({
data: {
name: 'pf',
age: 22,
},
});
let render0 = compileToFunctions(
`<div class="a" style="color: lightblue;" b="1"><span></span><span></span></div>`
);
let oldVnode = render0.call(vm0);
let ele = createElm(oldVnode);
document.body.appendChild(ele);
let vm = new Vue({
data: {
message: 'hello world',
},
});
let render = compileToFunctions(
`<div class="b" style="color: red" c=33><span></span></div>`
);
let newVnode = render.call(vm);
let render1 = compileToFunctions(`<ul a="1" style="color:blue">
<li key="A">a</li>
<li key="B">b</li>
<li key="C">c</li>
<li key="D">d</li>
</ul>`);
let vm1 = new Vue({ data: { name: 'pf' } });
let prevVnode = render1.call(vm1);
let el = createElm(prevVnode);
document.body.appendChild(el);
let render2 = compileToFunctions(`<ul a="1" style="color:red;">
<li key="C">c</li>
<li key="A">a</li>
<li key="D">d</li>
<li key="E">e</li>
<li key="Q">q</li>
</ul>`);
let vm2 = new Vue({ data: { name: 'zf' } });
let nextVnode = render2.call(vm2);
// 直接将新的节点替换掉了老的,不是直接替换 而是比较两个人的区别之后在替换 diff算法
// diff算法是一个平级比较的过程 父亲和父亲比对,儿子和儿子比对
// 主要比对标签名和key来判断是不是同一个元素, 如果标签和key都一样说明两个元素使同一个元素
setTimeout(() => {
// patch(oldVnode, newVnode);
patch(prevVnode, nextVnode);
}, 2000);
我们想掌握 vue 中的 diff 算法就先构建出两个虚拟
dom
之后做 patch
diff 算法:采用双指针的方式比较两个节点
- 比较开头节点
- 比较结尾节点
- 比较尾头节点
- 比较头尾节点
- 乱序比对:根据老的列表做一个映射关系 用新的去找 找到则移动 找不到则添加 最后多余的删除
1.基本 Diff 算法
比对标签
1
2
3
4
// case1: 前后两个虚拟节点不是相同节点 直接替换掉即可
if (!isSameVnode(oldVnode, vnode)) {
return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
}
在 diff 过程中会先比较标签是否一致,如果标签不一致用新的标签替换掉老的标签
1
2
3
4
5
6
// case2: 两个元素虚拟节点都是文本的情况下 用新文本替换到老文本
if (!oldVnode.tag) {
if (oldVnode.text !== vnode.text) {
return (el.textContent = vnode.text);
}
}
如果标签一致,有可能都是文本节点,那就比较文本的内容即可
比对属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// case3: 两个都是标签 比对标签属性
patchProps(el, oldVnode.data, vnode.data);
function patchProps(el, oldProps = {}, props = {}) {
// 老的属性中有 新的没有 要删除老的
const oldStyles = oldProps.style || {};
const newStyles = props.style || {};
for (let key in oldStyles) {
// 老的样式有 新的没有则删除
if (!newStyles[key]) {
el.style[key] = '';
}
}
for (let key in oldProps) {
// 老的属性有 新的没有删除属性
if (!props[key]) {
el.removeAttribute(key);
}
}
for (let key in props) {
// 用新的覆盖老的
if (key === 'style') {
for (let styleName in props.style) {
el.style[styleName] = props.style[styleName];
}
} else {
el.setAttribute(key, props[key]);
}
}
}
当标签相同时,我们可以复用老的标签元素,并且进行属性的比对
比对子元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// case4: 比较儿子节点 一方有儿子 一方没儿子 两方都有儿子
const oldChildren = oldVnode.children || [];
const newChildren = vnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
// 完整diff算法 比对两个儿子 一层层比较, 不涉及夸级比较
updateChildren(el, oldChildren, newChildren);
} else if (newChildren.length > 0) {
// 没有老的 有新的
mountChildren(el, newChildren);
} else if (oldChildren.length > 0) {
// 新的没有 老的有 直接删除
el.innerHTML = ''; // 可以循环删除
}
function mountChildren(el, newChildren) {
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
el.appendChild(createElm(child));
}
}
这里要判断新老节点儿子的状况
2.Diff 优化策略
在开头和结尾新增元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function isSameVnode(n1, n2) {
// 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
return n1.tag === n2.tag && n1.key === n2.key;
}
// Vue2.0中采用双指针的方式比较两个节点
// 操作列表经常会有push shift pop unshift reverse sort这些方法(针对镇邪情况做一些优化)
function updateChildren(el, oldChildren, newChildren) {
// Vue中创建了4个指针 分别指向老孩子和新孩子的头尾
// 分别一次进行比较有一方先比较完毕就结束
let oldStartIndex = 0;
let newStartIndex = 0;
let oldEndIndex = oldChildren.length - 1;
let newEndIndex = newChildren.length - 1;
let oldStartVnode = oldChildren[0];
let newStartVnode = newChildren[0];
let oldEndVnode = oldChildren[oldEndIndex];
let newEndVnode = newChildren[newEndIndex];
// 有任何一个不满足则停止 || 有一个为true就继续走
// 双方有一方的头指针 大于尾部指针则停止循环
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (isSameVnode(oldStartVnode, newStartVnode)) {
// 说明两个是同一个元素 要比较属性和它的儿子
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
// 比较开头节点
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
// 说明两个是同一个元素 要比较属性和它的儿子
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
// 比较结尾节点
}
}
if (newStartIndex <= newEndIndex) {
// 新的多了 多余的就插入进去
for (let i = newStartIndex; i <= newEndIndex; i++) {
const childEl = createElm(newChildren[i]);
// 这里可能向后追加 还可能是向前追加
let anchor = newChildren[newEndIndex + 1]
? newChildren[newEndIndex + 1].el
: null; // 获取下一个元素
el.insertBefore(childEl, anchor); // anchor为null的时候则会认为是appendChild
}
}
}
头移尾、尾头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
else if (isSameVnode(oldEndVnode, newEndVnode)) {
// 说明两个是同一个元素 要比较属性和它的儿子
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
// 比较结尾节点
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
// insertBefore具备移动性 会将原来的元素移动走
el.insertBefore(oldEndVnode.el, oldStartVnode.el); // 将老的尾巴移动到老的前面去
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex];
// 比较尾头节点
}
操作列表经常会有 push shift pop unshift reverse sort 这些方法(针对镇邪情况做一些优化)。以上四个条件对常见的 dom 操作进行了优化
暴力比对
1
2
3
4
5
6
7
8
function makeIndexByKey(children) {
const map = {};
children.forEach((child, index) => {
map[child.key] = index;
});
return map;
}
const map = makeIndexByKey(oldChildren);
对所有的孩子元素进行编号
1
2
3
4
5
6
7
8
9
10
11
12
13
// 根据老的列表做一个映射关系 用新的去找 找到则移动 找不到则添加 最后多余的删除
// 给动态列表添加key的时候 要尽量避免使用索引 因为索引前后都是从0开始 可能会发生错误复用
const moveIndex = map[newStartVnode.key]; // 如果拿到则说明是要移动的索引
if (moveIndex !== undefined) {
const moveVnode = oldChildren[moveIndex]; // 找到对应的虚拟节点 复用
el.insertBefore(moveVnode.el, oldStartVnode.el);
oldChildren[moveIndex] = undefined; // 表示这个节点已经移动走了
patchVnode(moveVnode, newStartVnode); // 比对属性和子节点
} else {
el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
}
newStartVnode = newChildren[++newStartIndex];
// 乱序比对
3. 更新操作
1
2
3
4
5
6
7
8
9
10
11
12
Vue.prototype._update = function (vnode) {
// 将虚拟节点变成真实节点
// 将vnode渲染到el元素中
const vm = this;
const prevVnode = vm._vnode; // 上一次的vNode
if (!prevVnode) {
vm.$el = patch(vm.$el, vnode); // 可以初始化渲染,后续更新也走这个patch方法
} else {
vm.$el = patch(prevVnode, vnode);
}
vm._vnode = vnode; // 渲染完毕后重新更新vnode
};
八.生命周期合并
1.Mixin 原理
1
2
3
4
5
6
7
8
// gloablAPI.js
export default function initGlobalAPI(Vue) {
Vue.options = {};
Vue.mixin = function (options) {
this.options = mergeOptions(this.options, options);
return this;
};
}
2.合并生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// gloablAPI.js
LIFECYCLE.forEach((hook) => {
strats[hook] = function (p, c) {
if (c) {
// 如果儿子有 父亲有 让父亲和儿子拼在一起
if (p) {
return p.concat(c); // 最终八生命周期都合并在一起了
} else {
return [c]; // 儿子有父亲没有 则将儿子包装秤数组
}
} else {
return p; // 如果儿子没有则用父亲即可
}
};
});
export function mergeOptions(parent, child) {
const options = {};
for (let key in parent) {
// 循环老的
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;
}
3.调用生命周期
1
2
3
4
5
6
7
8
9
// lifecycle.js
export function callHook(vm, hook) {
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm);
}
}
}
4.初始化流程中调用生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// init.js
Vue.prototype._init = function (options) {
const vm = this;
// vue中会判断如果是$开头的属性不会被变成响应式数据
this.$options = mergeOptions(vm.constructor.options, options); // 所有后续的扩展方法都有一个$options选项可以获取用户的所有选项
// 对于实例的数据源 props data methods computed watch
// prop data
callHook(vm, 'beforeCreate');
initState(vm);
callHook(vm, 'created');
// 状态初始化完毕后需要进行页面挂载
if (vm.$options.el) {
// el属性和直接调用$mount是一样的
vm.$mount(vm.$options.el);
}
};
九.Watch & Computed
1.Watch 实现原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const vm = new Vue({
el: '#app',
data: {
name: 'pf',
a: { a: 1 },
arr: ['1', '2', '3'],
age: 22,
address: '广州',
},
methods: {
handler(newVal, oldVal) {
console.log(newVal, oldVal);
},
},
watch: {
name(newVal, oldVal) {
console.log(newVal, oldVal);
},
a(newVal, oldVal) {
console.log(newVal, oldVal);
},
arr: [
(newVal, oldVal) => {
console.log(newVal, oldVal);
},
],
age: [
'handler',
{
handler: (newVal, oldVal) => {
console.log(newVal, oldVal);
},
immediate: true,
},
],
},
});
vm.$watch('a.a', (newVal, oldVal) => {
console.log(newVal, oldVal);
});
setTimeout(() => {
vm.a.a = 22;
vm.name.a = 'ricardopang';
vm.a = { b: 2 };
vm.arr.push('123');
vm.age = 33;
}, 1000);
选项中如果有 watch 则对 watch 进行初始化
1
2
3
4
5
6
7
// state.js
export function initState(vm) {
// ...
if (options.watch) {
initWatch(vm);
}
}
选项中如果有 watch 则对 watch 进行初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// state.js
function initWatch(vm) {
const watch = vm.$options.watch;
for (let key in watch) {
const handler = watch[key]; // 字符串 数组 函数
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatch(vm, key, handler[i]);
}
} else {
// 对当前属性进行创建watcher 存放的回调是handler 取数据是从vm上获取
createWatch(vm, key, handler);
}
}
}
function createWatch(vm, key, handler) {
// 判断如果handler是一个字符串 可以采用实例上的方法
let options;
if (typeof handler === 'object' && handler !== null) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(key, handler, options);
}
这里涉及了 watch 的三种写法,1.值是对象、2.值是数组、3.值是字符串 (如果是对象可以传入一些 watch 参数),最终会调用 vm.$watch 来实现
扩展 Vue 原型上的方法,都通过 mixin 的方式来进行添加
1
2
3
4
5
6
7
8
9
10
11
// state.js
export function stateMixin(Vue) {
Vue.prototype.$nextTick = nextTick;
Vue.prototype.$watch = function (exprOrFn, cb, options = {}) {
options.user = true; // 标记为用户watcher
const watcher = new Watcher(this, exprOrFn, options, cb);
if (options.immediate) {
cb.call(this, watcher.value);
}
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// observer/watcher.js
class Watcher {
// 不同组件有不同的watcher 目前只有一个 渲染根实例的
constructor(vm, exprOrFn, options, cb) {
// ...
// 如果给的是一个字符串 需要去通过字符串取值
if (typeof exprOrFn === 'string') {
this.getter = function () {
const path = exprOrFn.split('.'); // [a.b]
return path.reduce((vm, current) => {
vm = vm[current];
return vm;
}, vm);
};
} else {
this.getter = exprOrFn; // getter意味着调用这个函数可以发生取值操作
}
// ...
this.cb = cb;
this.vm = vm;
this.user = options.user; // 标识是否是用户自己的watcher
this.value = this.get(); // 实现页面渲染
}
addDep(dep) {
// 一个组件 对应着多个属性 重复的属性也不用记录
let id = dep.id;
if (!this.depsId.has(id)) {
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this); // watcher已经记住了dep了而且去重了,此时让dep也记住watcher
}
}
evaluate() {
this.value = this.get(); // 获取到用户函数的返回值 并且还要标识为脏
this.dirty = false;
}
get() {
pushTarget(this); // 静态属性就是只有一份
let value = this.getter.call(this.vm); // 会去vm上取值 vm._update(vm._render) 取name 和age
popTarget(); // 渲染完毕后就清空
return value;
}
run() {
let oldValue = this.value;
let newValue = this.get(); // 渲染的时候用的是最新的vm来渲染的
this.value = newValue;
if (this.user && this.cb) {
this.cb.call(this.vm, newValue, oldValue);
}
}
}
还是借助 vue 响应式原理,默认在取值时将 watcher 存放到对应属性的 dep 中,当数据发生变化时通知对应的 watcher 重新执行
2.Computed 实现原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// state.js
if (options.computed) {
initComputed(vm);
}
function initComputed(vm) {
const computed = vm.$options.computed;
const watchers = (vm._computedWatchers = {}); // 将计算属性watcher保存到vm上
for (let key in computed) {
let userDef = computed[key];
// 监控计算属性中get的变化
const fn = typeof userDef === 'function' ? userDef : userDef.get;
// 如果直接new Watcher 默认就会执行fn 将属性和watcher对应起来
watchers[key] = new Watcher(vm, fn, { lazy: true });
defineComputed(vm, key, userDef);
}
}
每个计算属性也都是一个
watcher
,计算属性需要表示 lazy:true,这样在初始化 watcher 时不会立即调用计算属性方法
1
2
3
4
5
6
7
8
9
// watcher.js
class Watcher {
constructor(vm, exprOrFn, callback, options) {
this.vm = vm;
this.dirty = this.lazy;
// ...
this.value = this.lazy ? undefined : this.get(); // 调用get方法 会让渲染watcher执行
}
}
默认计算属性需要保存一个 dirty 属性,用来实现缓存功能
1
2
3
4
5
6
7
8
9
// state.js
function defineComputed(target, key, userDef) {
const setter = userDef.set || (() => {});
// 每次取值都会执行 可以通过实例拿到对应的属性
Object.defineProperty(target, key, {
get: createComputedGetter(key),
set: setter,
});
}
创建缓存 getter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// state.js
// 计算属性根本不会收集依赖 只会让自己的依赖属性去收集依赖
function createComputedGetter(key) {
return function () {
const watcher = this._computedWatchers[key]; // 获取到对应属性的watcher
if (watcher.dirty) {
// 如果是脏的就去执行 用户传入的函数
watcher.evaluate(); // 求值后 dirty变为了false 下次就不求值了
}
// 在求值的过程中 stack = [渲染watcher, 计算属性watcher]
// 在evaluate执行完毕后 stack = [渲染watcher] => Dep.target = 渲染watcher
if (Dep.target) {
// 让计算属性watcher对应的两个dep记录watcher即可
// 计算属性出栈后 还要渲染watcher 应该让计算属性watcher里面的属性 也去收集上一层watcher
watcher.depend();
}
return watcher.value; // 最后返回的是watcher上的值
};
}
watch.evaluate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// watcher.js
evaluate() {
this.value = this.get(); // 获取到用户函数的返回值 并且还要标识为脏
this.dirty = false;
}
update() {
if (this.lazy) {
// 如果是计算属性 依赖的值变化了 就标识计算属性是脏值了
this.dirty = true;
} else {
queueWatcher(this); // 把当前的watcher 暂存起来
// this.get(); // 重新渲染
}
}
当依赖的属性变化时,会通知 watcher 调用 update 方法,此时我们将 dirty 置换为 true。这样再取值时会重新进行计算
1
2
3
4
5
6
7
8
9
10
11
12
13
// state.js
if (watcher.dirty) {
// 如果是脏的就去执行 用户传入的函数
watcher.evaluate(); // 求值后 dirty变为了false 下次就不求值了
}
// 在求值的过程中 stack = [渲染watcher, 计算属性watcher]
// 在evaluate执行完毕后 stack = [渲染watcher] => Dep.target = 渲染watcher
if (Dep.target) {
// 让计算属性watcher对应的两个dep记录watcher即可
// 计算属性出栈后 还要渲染watcher 应该让计算属性watcher里面的属性 也去收集上一层watcher
watcher.depend();
}
return watcher.value; // 最后返回的是watcher上的值
1
2
3
4
5
6
7
8
9
// watcher.js
depend() {
// watcher的depend 就是让watcher中dep去depend
let i = this.deps.length;
while (i--) {
// 让dep记住渲染watcher
this.deps[i].depend(); // 让计算属性watcher 也收集渲染watcher
}
}
十.Vue 组件原理
1.全局组件的解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Vue.component内部会调用一个api Vue.extend() 产生一个组件的子类
Vue.component('outer-button', {
// Vue.options.components = {}
name: 'xxx',
template: '<button>全局button</button>',
}); // new Sub().$mount('某个元素上') 每个组件在使用的时候都是通过类来new
Vue.component(
'pf-button',
Vue.extend({
template: '<button>PF按钮</button>',
})
);
// 函数式组件没有类 就是每次调用函数返回一个虚拟节点 所以函数式组件是没有watcher的
const vm = new Vue({
el: '#app',
// 局部组件(定义后只能在当前组件中使用)
components: {
'my-button': {
beforeCreate() {
console.log('beforeCreate 1');
},
components: {
'my-ll': {
template: '<div>我很帅</div>',
beforeCreate() {
console.log('beforeCreate 2');
},
},
},
template: '<button>我的按钮1111 <my-ll></my-ll></button>',
},
},
});
我们可以通过
Vue.component
注册全局组件,之后可以在模板中进行使用
1.1.Vue.component 方法
1
2
// index.js
initGlobalAPI(Vue);
Vue.component
内部会调用Vue.extend
方法,将定义挂载到Vue.options.components
上。这也说明所有的全局组件最终都会挂载到这个变量上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// globalApi.js
export default function initGlobalAPI(Vue) {
Vue.options = {};
Vue.options._base = Vue;
Vue.mixin = function (options) {
this.options = mergeOptions(this.options, options);
return this;
};
Vue.options.components = {}; // 放的是全局组件 全局的指令 Vue.otpions.directives
Vue.component = function (id, definition) {
// 如果definition已经是一个函数了 说明用户自己调用了Vue.extend
definition =
typeof definition === 'function' ? definition : Vue.extend(definition);
Vue.options.components[id] = definition;
};
}
1.2.Vue.extend 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 组件核心方法 可以手动创造组件进行挂载
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;
};
extend
方法就是创建出一个子类,继承于Vue
,并返回这个类
1.3.属性合并
1
2
3
4
5
6
7
8
9
10
// globalAPI.js
function mergeField(key) {
// 策略模式 减少if-else
if (strats[key]) {
options[key] = strats[key](parent[key], child[key]);
} else {
// 如果不在测量中则以儿子为主
options[key] = child[key] || parent[key]; // 优先采用儿子 再采用父亲
}
}
1.4.初始化合并
1
Sub.options = mergeOptions(Vue.options, options); // 保存用户传递的选项
2.组件的渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vdom/index.js
const isReservedTag = (tag) => {
return ['a', 'div', 'p', 'button', 'ul', 'li', 'span'].includes(tag);
};
// 创建元素vnode 等于render函数里面的 h=>h(App)
export function createElement(vm, tag, data = {}, ...children) {
// 需要判断tag是元素还是组件
if (isReservedTag(tag)) {
return vnode(vm, tag, data, children, data.key, undefined);
} else {
// 创造组件的虚拟节点 组件需要找到组件的模版去进行渲染
const Ctor = vm.$options.components[tag]; // 罪案构造函数
// Ctor就是组件的定义 可能是一个Sub类 还有可能更是组件的obj选项
return createComponentVnode(vm, tag, data, children, data.key, Ctor);
}
}
在创建虚拟节点时我们要判断当前这个标签是否是组件,普通标签的虚拟节点和组件的虚拟节点有所不同
2.1.创建组件虚拟节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// vdom/index.js
// 创建元素vnode 等于render函数里面的 h=>h(App)
export function createElement(vm, tag, data = {}, ...children) {
// 需要判断tag是元素还是组件
if (isReservedTag(tag)) {
return vnode(vm, tag, data, children, data.key, undefined);
} else {
// 创造组件的虚拟节点 组件需要找到组件的模版去进行渲染
const Ctor = vm.$options.components[tag]; // 罪案构造函数
// Ctor就是组件的定义 可能是一个Sub类 还有可能更是组件的obj选项
return createComponentVnode(vm, tag, data, children, data.key, Ctor);
}
}
const init = (vnode) => {
// 组件的虚拟节点上有组件的实例 new Sub()._init()
const child = (vnode.componentInstance = new vnode.componentOptions.Ctor({})); // 组件的children {} 放插槽属性
child.$mount();
};
function createComponentVnode(vm, tag, key, data, children, Ctor) {
if (typeof Ctor === 'object' && Ctor !== null) {
Ctor = vm.$options._base.extend(Ctor); // 组件内部声明的components属性也会包装成类
}
data.hook = {
// 稍后创造真实节点的时候 如果是组件则调用此init方法
init,
};
return vnode(vm, tag, data, undefined, key, undefined, { Ctor, children });
}
2.2.创建组件的真实节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// // vdom/patch.js
export function patch(oldVnode, vnode) {
if (!oldVnode) {
// 这就是组件的挂载
return createElm(vnode); // vm.$el 对应的就是组件渲染的结果了
}
// ...
}
// 调用组件初始化方法
function createComponent(vnode) {
let i = vnode.data;
if ((i = i.hook) && (i = i.init)) {
i(vnode); // 初始化组件 找到init方法
}
if (vnode.componentInstance) {
return true; // 说明是组件
}
}
export function createElm(vnode) {
const { tag, data, children, text } = vnode;
if (typeof tag === 'string') {
// 元素
if (createComponent(vnode)) {
return vnode.componentInstance.$el;
}
vnode.el = document.createElement(tag);
patchProps(vnode.el, {}, data);
children.forEach((child) => {
vnode.el.appendChild(createElm(child)); // 递归渲染
});
} else {
// 文本
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
调用
init
方法,进行组件的初始化
1
2
3
4
5
6
7
8
9
10
data.hook = {
// 稍后创造真实节点的时候 如果是组件则调用此init方法
init,
};
const init = (vnode) => {
// 组件的虚拟节点上有组件的实例 new Sub()._init()
const child = (vnode.componentInstance = new vnode.componentOptions.Ctor({})); // 组件的children {} 放插槽属性
child.$mount();
};