├── .gitignore
├── README.md
├── babel.config.js
├── html
├── 01-mixin.html
├── 02-computed.html
├── 03-watch.html
├── 04-array.html
├── 05-diff.html
└── 06-component.html
├── index.html
├── package-lock.json
├── package.json
├── rollup.config.js
└── src
├── compiler
├── index.js
├── parser.js
└── type.js
├── global-static-api.js
├── hooks
└── life-hook.js
├── index.js
├── init.js
├── initState.js
├── lifecycle.js
├── observe
├── array.js
├── dep.js
├── index.js
└── watcher.js
├── utils
├── index.js
├── merge.js
└── strategy.js
└── vdom
├── index.js
└── patch.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | dist
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-study
2 |
3 | ## vue2的常见源码实现
4 |
5 | ### rollup环境搭建
6 |
7 | #### 安装rollup及其插件
8 |
9 | ```shell
10 | npm i rollup rollup-plugin-babel @babel/core @babel/preset-env rollup-plugin-node-resolve -D
11 | ```
12 |
13 | #### 编写配置文件 rollup.config.js
14 |
15 | 这个可以直接使用es module
16 |
17 | ```js
18 | // rollup默认可以导出一个对象 作为打包的配置文件
19 | import babel from "rollup-plugin-babel";
20 | import resolve from 'rollup-plugin-node-resolve'
21 | export default {
22 | // 入口
23 | input: "./src/index.js",
24 | // 出口
25 | output: {
26 | // 生成的文件
27 | file: "./dist/vue.js",
28 | // 全局对象 Vue 在global(浏览器端就是window)上挂载一个属性 Vue
29 | name: "Vue",
30 | // 打包方式 esm commonjs模块 iife自执行函数 umd 统一模块规范 -> 兼容cmd和amd
31 | format: "umd",
32 | // 打包后和源代码做关联
33 | sourcemap: true,
34 | },
35 | plugins: [
36 | babel({
37 | // 排除第三方模块
38 | exclude: "node_modules/**",
39 | }),
40 | // 自动找文件夹下的index文件
41 | resolve()
42 | ],
43 | };
44 |
45 |
46 | ```
47 |
48 | ##### babel.config.js
49 |
50 | ```js
51 | // babel config
52 | module.exports = {
53 | // 预设
54 | presets: ["@babel/preset-env"],
55 | };
56 |
57 | ```
58 |
59 | #### 编写脚本
60 |
61 | ```json
62 | "scripts": {
63 | "dev": "rollup -cw"
64 | }
65 | ```
66 |
67 | -c表示使用配置文件,-w表示监控文件变化。
68 |
69 | #### element.outerHTML
70 |
71 | `outerHTML`属性获取描述元素(包括其后代)的序列化HTML片段。它也可以设置为用从给定字符串解析的节点替换元素。
72 |
73 | ```html
74 |
因为 /具有特殊含义
32 | */
33 | // ^<\\/((?:[a-zA-Z_][\\-\\.0-9a-zA-Z]*\\:)?[a-zA-Z_][\\-\\.0-9a-zA-Z]*)[^>]*>
34 | const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
35 | // `^<\\/${qnameCapture}[^>]*>`
36 | /**
37 | * 匹配属性 a="abc" a='abc' a=abc a
38 | * 分组一的值就是键key 分组3/4/5匹配到的是value
39 | */
40 | const attribute =
41 | /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
42 | /**
43 | * 匹配标签结束
44 | * 标签可能自闭合
/>
45 | */
46 | const startTagClose = /^\s*(\/?)>/;
47 | /**
48 | * 匹配 双花括号语法 {{}} 匹配到的是就是双花括号的 变量
49 | */
50 | export const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
51 | /**
52 | * 解析 模板
53 | * @param {string} html 模板字符串
54 | * vue2采用正则编译解析 vue3不是采用正则了
55 | */
56 | function parseHTML(html) {
57 | /**
58 | * 最终需要转换为一颗抽象语法树 ast abstract syntax tree
59 | * 可以借助栈思想
60 | * 栈中的最后一个标签元素 就是当前正在匹配的元素的父元素
61 | * @type {Array<{tag:string,type:number,children:Array}>}
62 | */
63 | const stack = [];
64 | // 栈帧 指向最后一个元素
65 | let curParent = null;
66 | let root = null; // 根元素
67 |
68 | /**
69 | * 创建 ast
70 | * @param {string} tag 标签名
71 | * @param {Array<{name:string,value:any}>} attrs 属性
72 | * @returns
73 | */
74 | function createASTElement(tag, attrs) {
75 | return {
76 | tag,
77 | type: ELEMENT_TYPE,
78 | children: [],
79 | attrs,
80 | parent: null,
81 | };
82 | }
83 | /**
84 | * 处理开始标签 并且开始构造抽象语法树
85 | * @param {string} tag
86 | * @param {Array<{name:string,value:any}>} attrs
87 | * @param {boolean} isSelfClose 是否自闭合
88 | */
89 | function start(tag, attrs, isSelfClose) {
90 | // console.log(tag, attrs);
91 | // 当前节点
92 | const node = createASTElement(tag, attrs);
93 | // 根节点
94 | root = root ?? node;
95 | // 更新当前节点的父节点 更新父元素的子元素节点
96 | curParent && ((node.parent = curParent), curParent.children.push(node));
97 | // TODO 是自闭合标签 不需要入栈的
98 | if (isSelfClose) return;
99 | // 新节点入栈
100 | stack.push(node);
101 | // 更新当前指向的最前面的父节点
102 | curParent = node;
103 | // console.log(node, root);
104 | }
105 | /**
106 | * 处理文本内容
107 | * @param {string} text
108 | */
109 | function chars(text) {
110 | // 去除空字符串
111 | text = text.replace(/^\s+|\s+$/gm, "");
112 | // console.log(text);
113 | // 文本节点 插入到父元素的孩子中
114 | text &&
115 | curParent.children.push({
116 | type: TEXT_TYPE,
117 | text,
118 | parent: curParent,
119 | });
120 | }
121 | /**
122 | * 处理结束标签
123 | * @param {string} tag 标签名称
124 | */
125 | function end(tag) {
126 | // console.log(tag);
127 | // 弹出最后一个栈元素 并更新指向的父节点
128 | const node = stack.pop();
129 | // TODO 可以根据tag和node.tag 校验标签是否合法等 也需要考虑自闭合标签
130 | if (tag !== node.tag) {
131 | // console.log("标签不合法---------",tag, node);
132 | }
133 | curParent = stack[stack.length - 1];
134 | }
135 | /**
136 | * 解析模板的开始标签
137 | * @param {string} html 模板字符串
138 | */
139 | function parseStartTag() {
140 | // 匹配标签起始位置
141 | const start = html.match(startTagOpen);
142 | if (start) {
143 | // 是开始标签
144 | const match = {
145 | // 标签名
146 | tagName: start[1],
147 | // 属性
148 | attrs: [],
149 | // 是否是自闭合标签
150 | isSelfClose: false,
151 | };
152 | advance(start[0].length);
153 | // 不是标签结束位置 一直匹配
154 | let attr, end;
155 | while (
156 | !(end = html.match(startTagClose)) &&
157 | (attr = html.match(attribute))
158 | ) {
159 | // 去除属性
160 | advance(attr[0].length);
161 | match.attrs.push({
162 | // 属性名
163 | name: attr[1],
164 | // 属性值 key="value" key='value' key=value
165 | // key 对于只有key的这种,我们给默认值true
166 | value: attr[3] || attr[4] || attr[5] || true,
167 | });
168 | }
169 | // 去除标签的右闭合箭头
中的 > 或者自闭合标签
/>
170 | if (end) {
171 | advance(end[0].length);
172 | // 自闭合
173 | if (end[0].endsWith("/>")) match.isSelfClose = true;
174 | }
175 | // console.log(match);
176 | return match;
177 | }
178 | // 不是开始标签
179 | return false;
180 | }
181 | /**
182 | * 字符串截取
183 | * @param {number} start 截取的起始位置
184 | */
185 | function advance(start) {
186 | html = html.substring(start);
187 | }
188 | // vue2中 html 开头肯定是 <
hello
189 | while (html) {
190 | // 如果indexOf中索引的值是 0 则说明是个开始标签 或者 结束标签
191 | // > 0 是文本的结束位置
192 | let textEnd = html.indexOf("<");
193 | if (textEnd === 0) {
194 | // 解析开始标签 开始标签及其标签内的属性等
195 | const startTagMatch = parseStartTag(); // 匹配结果
196 | if (startTagMatch) {
197 | // console.log(startTagMatch);
198 | start(
199 | startTagMatch.tagName,
200 | startTagMatch.attrs,
201 | startTagMatch.isSelfClose
202 | );
203 | continue;
204 | }
205 | // 去除结束标签 来到这里 肯定是
206 | const endTagMatch = html.match(endTag);
207 | if (endTagMatch) {
208 | advance(endTagMatch[0].length);
209 | // console.log(endTagMatch, html);
210 | end(endTagMatch[1]);
211 | continue;
212 | }
213 | }
214 | // 文本内容 adb
215 | if (textEnd > 0) {
216 | // 获取文本内容
217 | const text = html.substring(0, textEnd);
218 | if (text) {
219 | advance(text.length); // 解析到的文本
220 | chars(text);
221 | }
222 | // console.log(html);
223 | }
224 | }
225 | // console.log(root);
226 | // 返回 生成的vNode树 ast
227 | return root;
228 | }
229 |
230 | export { parseHTML };
231 |
--------------------------------------------------------------------------------
/src/compiler/type.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 毛毛
3 | * @Date: 2022-04-14 13:09:23
4 | * @Last Modified by: 毛毛
5 | * @Last Modified time: 2022-04-14 13:10:07
6 | * 节点类型
7 | */
8 | // 元素类型
9 | const ELEMENT_TYPE = 1;
10 | // 文本类型
11 | const TEXT_TYPE = 3;
12 |
13 | export{
14 | ELEMENT_TYPE,
15 | TEXT_TYPE
16 | }
--------------------------------------------------------------------------------
/src/global-static-api.js:
--------------------------------------------------------------------------------
1 | import { isFunction } from "./utils";
2 | import { mergeOptions } from "./utils/merge";
3 |
4 | /*
5 | * @Author: 毛毛
6 | * @Date: 2022-04-15 20:40:36
7 | * @Last Modified by: 毛毛
8 | * @Last Modified time: 2022-04-18 13:30:21
9 | * 全局静态 api
10 | */
11 | export function initGlobalStaticAPI(Vue) {
12 | Vue.options = {}; // 全局选项
13 | // 缓存 Vue构造函数
14 | Object.defineProperty(Vue.options, "_base", {
15 | value: Vue,
16 | // 为了可以混入到所有实例的选项中 需要可枚举
17 | enumerable: true,
18 | });
19 | // 混入
20 | Vue.mixin = function mixin(mixin) {
21 | // 我们期望将用户的选项和全局的options进行合并
22 | // {} + mixin {created(){}} => {created:[fn]}
23 | this.options = mergeOptions(Vue.options, mixin);
24 | // console.log(this.options);
25 | return this;
26 | };
27 | /**
28 | * 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
29 | * 返回值是一个构造函数 通过new可以创建一个vue组件实例
30 | * @param {{data:Function,el:string}} options
31 | * @returns
32 | */
33 | Vue.extend = function extend(options) {
34 | // 组合式继承 Vue
35 | function Sub(options = {}) {
36 | // 最终使用的组件 就是 new 一个实例
37 | this._init(options);
38 | }
39 | Sub.prototype = Object.create(Vue.prototype);
40 | Object.defineProperty(Sub.prototype, "constructor", {
41 | value: Sub,
42 | writable: true,
43 | configurable: true,
44 | });
45 | // 保存用户传递的选项 且和全局的配置合并
46 | Sub.options = mergeOptions(Vue.options, options);
47 | return Sub;
48 | };
49 | // 维护一个 全局组件对象
50 | Vue.options.components = {};
51 | /**
52 | * 定义或者获取全局组件 没有获取到组件时 返回 undefined
53 | * @param {string} id
54 | * @param {Function | object} definition
55 | */
56 | Vue.component = function component(id, definition) {
57 | // 获取全局组件
58 | if (!definition) return Vue.options[id];
59 | // 如果 definition 是一个函数,说明用户自己调用了 Vue.extend
60 | // 不是函数 就用 extend函数包装一下
61 | !isFunction(definition) && (definition = Vue.extend(definition));
62 | Vue.options.components[id] = definition;
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/src/hooks/life-hook.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 毛毛
3 | * @Date: 2022-04-15 21:16:58
4 | * @Last Modified by: 毛毛
5 | * @Last Modified time: 2022-04-15 21:20:16
6 | * 执行生命周期的hook
7 | */
8 |
9 | export function callHook(vm, hook) {
10 | const handles = vm.$options[hook];
11 | // 生命周期的钩子的this 都是当前实例
12 | handles?.forEach((handle) => handle.call(vm));
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 毛毛
3 | * @Date: 2022-04-12 22:45:40
4 | * @Last Modified by: 毛毛
5 | * @Last Modified time: 2022-04-17 20:15:15
6 | */
7 |
8 | import { initGlobalStaticAPI } from "./global-static-api";
9 | import { initMixin } from "./init";
10 | import { initStateMixin } from "./initState";
11 | import { initLifeCycle } from "./lifecycle";
12 |
13 | /**
14 | * Vue构造函数
15 | * @param {*} options 用户选项
16 | */
17 | function Vue(options) {
18 | // 初始化
19 | this._init(options);
20 | }
21 |
22 | initMixin(Vue); // 扩展_init方法
23 | // vm._update vm._render vm._c vm._v vm._s
24 | initLifeCycle(Vue); // 拓展生命周期 进行组件的挂载和渲染的方法
25 |
26 | // 静态方法
27 | initGlobalStaticAPI(Vue);
28 |
29 | // Vue.$nextTick vm.$watch
30 | initStateMixin(Vue);
31 |
32 |
33 |
34 | export default Vue;
35 |
--------------------------------------------------------------------------------
/src/init.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 毛毛
3 | * @Date: 2022-04-12 22:48:39
4 | * @Last Modified by: 毛毛
5 | * @Last Modified time: 2022-04-18 13:16:18
6 | */
7 | import { initState } from "./initState";
8 | import { compileToFunction } from "./compiler";
9 | import { mountComponent } from "./lifecycle";
10 | import { mergeOptions } from "./utils/merge";
11 | import { callHook } from "./hooks/life-hook";
12 | export function initMixin(Vue) {
13 | /**
14 | * 初始化操作
15 | * @param {*} options
16 | */
17 | Vue.prototype._init = function _init(options) {
18 | // console.log("init------------>", options);
19 | // vue app.$options = options 获取用户配置
20 | const vm = this;
21 | // 合并 Vue.options 和 传入的配置项
22 | // TODO 目前还只是可以合并生命周期和普通属性等,对于 data 这种选项还需要特殊的合并处理
23 | // 这种使用this获取其构造函数上的静态属性options,因为构造函数不一定直接是 Vue,也可以是Vue的子类(组件)
24 | vm.$options = mergeOptions(this.constructor.options, options); // vue认为 $xxx 就是表示vue的属性
25 | // console.log(vm.$options);
26 | // 执行初始化之前,执行 beforeCreate 的钩子
27 | callHook(vm, "beforeCreate");
28 | // 初始化状态
29 | // TODO computed methods watcher ....
30 | initState(vm);
31 | // 状态初始化完毕之后,执行 created 钩子
32 | callHook(vm, "created");
33 | // TODO 编译模板 等...
34 | // el vm挂载到的dom容器
35 | if (options.el) vm.$mount(options.el);
36 | };
37 | Vue.prototype.$mount = function $mount(el) {
38 | const vm = this;
39 | const ops = vm.$options;
40 | el = document.querySelector(el);
41 | let template;
42 | // 是否有render函数
43 | // 没有render
44 | if (!ops.render) {
45 | // 没有template选项 但是写了el 直接用el作为模板
46 | if (!ops.template && el) template = el.outerHTML;
47 | else template = ops.template; // 没有el 一般是组件的挂载
48 | }
49 | // 有template 直接用模板
50 | if (template) {
51 | console.log("------------------", /^[\.#a-zA-Z_]/i.test(template));
52 | if (/^[\.#a-zA-Z_]/i.test(template)) {
53 | // 模板标签
54 | template = document.querySelector(template).innerHTML;
55 | }
56 | // TODO 去除开头和结尾的空白符 m是忽略换行 进行多行匹配
57 | // template = template.trim();
58 | template = template.replace(/^\s+|\s+$/gm, "");
59 |
60 | // 编译模板 生成 render函数
61 | const render = compileToFunction(template);
62 | ops.render = render;
63 | }
64 | // console.log("$mount template-------------->", template);
65 | // 调用 render 实现页面渲染
66 | // console.log(ops.render);
67 | // 组件的挂载
68 | mountComponent(vm, el);
69 | /**
70 | * script 标签引用的是vue.global.js 这个编译过程是在浏览器运行的
71 | * runtime是不包含模板编译的,整个编译打包的时候是通过loader来转义.vue文件的
72 | * 用runtime的时候 不能使用模板template(可以使用.vue,loader处理就行了)
73 | */
74 | };
75 | }
76 |
--------------------------------------------------------------------------------
/src/initState.js:
--------------------------------------------------------------------------------
1 | import { isFunction } from "./utils";
2 | import { observe } from "./observe";
3 | import Watcher, { nextTick } from "./observe/watcher";
4 | import Dep from "./observe/dep";
5 | function proxy(vm, target, key) {
6 | Object.defineProperty(vm, key, {
7 | enumerable: true,
8 | get() {
9 | return vm[target][key];
10 | },
11 | set(newVal) {
12 | vm[target][key] = newVal;
13 | },
14 | });
15 | }
16 | /**
17 | * 初始化实例
18 | * @param {*} vm vue实例
19 | */
20 | function initState(vm) {
21 | const opts = vm.$options; // 获取所有选项
22 | if (opts.data) {
23 | // data 初始化
24 | initData(vm);
25 | }
26 | // computed
27 | if (opts.computed) {
28 | initComputed(vm);
29 | }
30 | // watch
31 | if (opts.watch) {
32 | initWatch(vm);
33 | }
34 | }
35 | /**
36 | * 初始化watch
37 | * @param {Vue} vm
38 | */
39 | function initWatch(vm) {
40 | const watch = vm.$options.watch;
41 | for (const key in watch) {
42 | // 字符串 数组 函数
43 | const handler = watch[key];
44 | if (Array.isArray(handler)) {
45 | for (let i = 0; i < handler.length; i++) {
46 | createWatch(vm, key, handler[i]);
47 | }
48 | } else {
49 | createWatch(vm, key, handler);
50 | }
51 | }
52 | }
53 | /**
54 | *
55 | * @param {*} vm
56 | * @param {string|Function} exprOrFn 侦听的值
57 | * @param {string|Function|object} handler 对象的情况没有考虑
58 | */
59 | // TODO handler 还可以考虑对象的情况 name:{ handler(){} ...}
60 | function createWatch(vm, exprOrFn, handler) {
61 | if (typeof handler === "string") {
62 | // name: "handler" -> methods["handler"]
63 | handler = vm[handler];
64 | }
65 | return vm.$watch(exprOrFn, handler);
66 | }
67 |
68 | /**
69 | * 初始化 data
70 | * @param {Vue} vm 实例
71 | */
72 | function initData(vm) {
73 | // data可能是函数 也可能是对象
74 | let data = vm.$options.data;
75 | // data是函数 执行一下
76 | if (isFunction(data)) data = data.call(vm);
77 | Object.defineProperty(vm, "_data", {
78 | configurable: true,
79 | // enumerable: false,
80 | writable: true,
81 | value: data,
82 | });
83 | console.log("initData------------>", data);
84 | // 数据劫持
85 | observe(data);
86 | // 把 vm._data 用vm来代理 访问 vm.name -> vm._data.name
87 | for (const key in data) {
88 | proxy(vm, "_data", key);
89 | }
90 | }
91 | /**
92 | * 初始化 computed
93 | * @param {Vue} vm 实例
94 | */
95 | function initComputed(vm) {
96 | const computed = vm.$options.computed;
97 | const watchers = (vm._computedWatchers = {});
98 | for (const key in computed) {
99 | const userDef = computed[key];
100 | // function -> get
101 | // object -> {get(){}, set(newVal){}}
102 | let setter;
103 | const getter = isFunction(userDef)
104 | ? userDef
105 | : ((setter = userDef.set), getter);
106 | // 监控计算属性中 get的变化
107 | // 每次data的属性发生改变 重新执行的就是这个get
108 | // 传入额外的配置项 标明当前的函数 不需要立刻执行 只有在使用到计算属性了 才计算值
109 | // 把属性和watcher对应起来
110 | watchers[key] = new Watcher(vm, getter, { lazy: true });
111 | // 劫持每一个计算属性
112 | defineComputed(vm, key, setter);
113 | }
114 | }
115 | /**
116 | * 定义计算属性
117 | * @param {*} target
118 | * @param {*} key
119 | * @param {*} setter
120 | */
121 | function defineComputed(target, key, setter) {
122 | Object.defineProperty(target, key, {
123 | // vm.key -> vm.get key this -> vm
124 | get: createComputedGetter(key),
125 | set: setter,
126 | });
127 | }
128 | /**
129 | * vue2.x 的计算属性 不会收集依赖,只是让计算属性依赖的属性去收集依赖
130 | * 创建一个懒执行(有缓存的)计算属性 判断值是否发生改变
131 | * 检查是否需要执行这个getter
132 | * @param {string} key
133 | */
134 | function createComputedGetter(key) {
135 | // this -> vm 因为返回值给了计算属性的 get 我们是从 vm上取计算属性的
136 | return function lazyGetter() {
137 | // 对应属性的watcher
138 | const watcher = this._computedWatchers[key];
139 | if (watcher.dirty) {
140 | // 如果是脏的 就去执行用户传入的getter函数 watcher.get()
141 | // 但是为了可以拿到get的执行结果 我们调用 evaluate函数
142 | watcher.evaluate(); // dirty = false
143 | }
144 | // 计算属性watcher出栈后 还有渲染watcher(在视图中使用了计算属性)
145 | // 或者说是在其他的watcher中使用了计算属性
146 | if (Dep.target) {
147 | // 让计算属性的watcher依赖的变量也去收集上层的watcher
148 | watcher.depend();
149 | }
150 | return watcher.value;
151 | };
152 | }
153 | /**
154 | *
155 | * 实现 $nextTick $watch
156 | * @export
157 | * @param {Vue} Vue
158 | */
159 | export function initStateMixin(Vue) {
160 | /**
161 | * $nextTick实现
162 | */
163 | Vue.prototype.$nextTick = nextTick;
164 | /**
165 | * 实现 $watch
166 | */
167 | // watch的底层实现 全是通过$watch
168 | Object.defineProperty(Vue.prototype, "$watch", {
169 | /**
170 | * watch的实现 也是使用观察者模式
171 | * @param {Function|string} exprOrFn 监控的值
172 | * @param {*} callback 回调函数
173 | * @param {*} options 选项
174 | */
175 | value(exprOrFn, callback, options = {}) {
176 | // console.log(exprOrFn, callback);
177 | // 创建观察者 user属性 表名这是用户自己定义的watch
178 | // 侦听的属性值发生改变 直接执行callback即可
179 | new Watcher(this, exprOrFn, { user: true, ...options }, callback);
180 | },
181 | });
182 | }
183 | export { initState };
184 |
--------------------------------------------------------------------------------
/src/lifecycle.js:
--------------------------------------------------------------------------------
1 | import { createElementVNode, createTextVNode } from "./vdom";
2 | import { patch } from "./vdom/patch";
3 | import Watcher from "./observe/watcher";
4 | /*
5 | * @Author: 毛毛
6 | * @Date: 2022-04-14 14:10:39
7 | * @Last Modified by: 毛毛
8 | * @Last Modified time: 2022-04-18 14:02:59
9 | * 组件挂载 生命周期
10 | * vm._render() 生成虚拟节点 vNode
11 | * vm._update() 虚拟节点变成真实节点 dom
12 | */
13 | export function mountComponent(vm, container) {
14 | // 记录需要挂载的容器 $el
15 | Object.defineProperty(vm, "$el", {
16 | value: container,
17 | writable: true,
18 | });
19 | // 这里把渲染逻辑封装到watcher中
20 | const updateComponent = () => {
21 | // 1.调用render 产生虚拟节点 vNode
22 | const vNodes = vm._render();
23 | // 2. 根据虚拟dom 产生真实dom
24 | vm._update(vNodes);
25 | };
26 | new Watcher(vm, updateComponent, true);
27 | // 3. 挂载到container上 _update中实现
28 | }
29 | /**
30 | * 扩展原型方法
31 | * @param {*} Vue
32 | */
33 | export function initLifeCycle(Vue) {
34 | Object.defineProperties(Vue.prototype, {
35 | _render: {
36 | // 当渲染的时候,会去实例中取值,我们就可以将属性和视图绑定在一起
37 | value: function _render() {
38 | const vm = this;
39 | // 绑定 this为组件实例
40 | return vm.$options.render.call(vm);
41 | },
42 | },
43 | _update: {
44 | /**
45 | * 将虚拟dom转为真实dom vnode -> dom
46 | * @param {*} vnode 虚拟dom节点
47 | */
48 | value: function _update(vnode) {
49 | const vm = this;
50 | // 挂载的容器
51 | const el = vm.$el;
52 | // 拿到上次的vnode
53 | const prevVnode = vm._vnode;
54 | // 记录每次产生的 vnode
55 | vm._vnode = vnode;
56 | if (prevVnode) {
57 | // 更新
58 | vm.$el = patch(prevVnode, vnode);
59 | } else {
60 | // 初渲染
61 | // patch 更新 + 初始化 + 组件的创建(el为null)
62 | vm.$el = patch(el, vnode);
63 | }
64 | // console.log("_update----------------->", vnode);
65 | },
66 | },
67 | // _c("div",{name:'zs'},...children) 元素 虚拟dom
68 | _c: {
69 | value: function _c() {
70 | // this -> vm
71 | return createElementVNode(this, ...arguments);
72 | },
73 | },
74 | // _v(text) 文本虚拟dom
75 | _v: {
76 | value: function _v() {
77 | return createTextVNode(this, ...arguments);
78 | },
79 | },
80 | // 就是变量字符串化
81 | _s: {
82 | value: function _s(value) {
83 | // 对于不是对象的字符串,没必要再次转字符串了,不然会多出引号 zs -> \"zs\"
84 | return typeof value === "object" ? JSON.stringify(value) : value;
85 | },
86 | },
87 | });
88 | }
89 |
--------------------------------------------------------------------------------
/src/observe/array.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 毛毛
3 | * @Date: 2022-04-13 10:02:33
4 | * @Last Modified by: 毛毛
5 | * @Last Modified time: 2022-04-16 17:40:23
6 | * @Description 重写数组中的变异方法
7 | */
8 | let oldArrayProto = Array.prototype;
9 |
10 | let newArrayProto = Object.create(oldArrayProto);
11 | /**
12 | * 七个变异方法 会改变数组本身的方法
13 | * @type {Array
}
14 | */
15 | const methods = [
16 | "push",
17 | "pop",
18 | "unshift",
19 | "shift",
20 | "reverse",
21 | "sort",
22 | "splice",
23 | ];
24 | methods.forEach((method) => {
25 | // 重写数组的方法 内部调用的还是原来的方法
26 | // 函数的劫持 切片编程
27 | newArrayProto[method] = function (...args) {
28 | // 如果新增的数组元素是对象 需要再次劫持
29 | let inserted;
30 | // Observe实例
31 | const ob = this.__ob__;
32 | switch (method) {
33 | case "push":
34 | case "unshift": // 插入元素
35 | // 新增的元素 可能是对象
36 | inserted = args;
37 | break;
38 | case "splice": // 数组最强方法 splice(start, delCount, ...新增元素)
39 | inserted = args.slice(2); // 新增的元素
40 | break;
41 | default:
42 | break;
43 | }
44 | console.log("新增的内容------------------>", inserted);
45 | if (inserted) {
46 | // 观测新增的内容
47 | ob.observeArray(inserted);
48 | }
49 | console.log(`重写的${method}方法被调用------> this = `, this);
50 | const res = oldArrayProto[method].call(this, ...args);
51 | // 通知更新 dep -> watcher -> 视图更新
52 | ob.dep.notify();
53 | return res;
54 | };
55 | });
56 | export default newArrayProto;
57 |
--------------------------------------------------------------------------------
/src/observe/dep.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 毛毛
3 | * @Date: 2022-04-15 09:31:54
4 | * @Last Modified by: 毛毛
5 | * @Last Modified time: 2022-04-16 09:50:12
6 | * 依赖收集 dep
7 | */
8 | let id = 0;
9 | class Dep {
10 | id = id++;
11 | constructor() {
12 | // 属性的dep要收集watcher
13 | this.subs = [];
14 | }
15 | /**
16 | * 收集当前属性 对应的视图 watcher
17 | */
18 | depend() {
19 | // 这里我们不希望收集重复的watcher,而且现在还只是单向的关系 dep -> watcher
20 | // watcher 也需要记录 dep
21 | // this.subs.push(Dep.target);
22 | // console.log(this.subs);
23 | // 这里是让watcher先记住dep
24 | Dep.target.addDep(this); // this -> dep
25 | }
26 | /**
27 | * dep 在反过来记录watcher
28 | * @param {*} watcher
29 | */
30 | addSub(watcher) {
31 | this.subs.push(watcher);
32 | // console.log(watcher);
33 | }
34 | /**
35 | * 更新视图
36 | */
37 | notify() {
38 | this.subs.forEach((watcher) => watcher.update());
39 | }
40 | // 当前的watcher
41 | static target = null;
42 | }
43 |
44 | // watcher queue 视图渲染栈
45 | const watcherStack = [];
46 | /**
47 | * watcher入栈
48 | * @param {Watcher} watcher
49 | */
50 | export function pushWatcherTarget(watcher) {
51 | watcherStack.push(watcher);
52 | Dep.target = watcher;
53 | }
54 | /**
55 | * watcher 出栈 且让 Dep.target 指向上一个入栈的 watcher
56 | */
57 | export function popWatcherTarget() {
58 | watcherStack.pop();
59 | Dep.target = watcherStack[watcherStack.length - 1];
60 | }
61 | export default Dep;
62 |
--------------------------------------------------------------------------------
/src/observe/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 毛毛
3 | * @Date: 2022-04-13 08:51:06
4 | * @Last Modified by: 毛毛
5 | * @Last Modified time: 2022-04-16 20:33:36
6 | */
7 | import { isObject } from "../utils";
8 | import arrayProto from "./array";
9 | import Dep from "./dep";
10 | class Observe {
11 | constructor(data) {
12 | // 让引用数据自身也实现依赖收集 这个dep是放在 data.__ob__ = this 上的
13 | // 也就是说 data.__ob__.dep 并不是 data.dep 所以不会发生重复
14 | this.dep = new Dep();
15 | // 记录this 也是一个标识 如果对象上有了该属性 标识已经被观测
16 | Object.defineProperty(data, "__ob__", {
17 | value: this, // observe的实例
18 | });
19 | // 如果劫持的数据是数组
20 | if (Array.isArray(data)) {
21 | // 重写数组上的7个方法 这7个变异方法是可以修改数组本身的
22 | Object.setPrototypeOf(data, arrayProto);
23 | // 对于数组元素是 引用类型的,需要深度观测的
24 | this.observeArray(data);
25 | } else {
26 | // Object.defineProperty 只能劫持已经存在的属性(vue提供单独的api $set $delete 为了增加新的响应式属性)
27 | this.walk(data);
28 | }
29 | }
30 | /**
31 | * 循环对象 对属性依次劫持 重新‘定义’属性
32 | * @param {*} data
33 | */
34 | walk(data) {
35 | Object.keys(data).forEach((key) => defineReactive(data, key, data[key]));
36 | }
37 | /**
38 | * 劫持数组元素 是普通原始值不会劫持
39 | * @param {Array} data
40 | */
41 | observeArray(data) {
42 | data.forEach((item) => observe(item));
43 | }
44 | }
45 | // vue2 应用了defineProperty需要一加载的时候 就进行递归操作,所以好性能,如果层次过深也会浪费性能
46 | // 1.性能优化的原则:
47 | // 1) 不要把所有的数据都放在data中,因为所有的数据都会增加get和set
48 | // 2) 不要写数据的时候 层次过深, 尽量扁平化数据
49 | // 3) 不要频繁获取数据
50 | // 4) 如果数据不需要响应式 可以使用Object.freeze 冻结属性
51 | /**
52 | * vue2 慢的原因 主要在这个方法中
53 | * 定义目标对象上的属性为响应式
54 | * @param {Object} obj
55 | * @param {string|symbol} key
56 | * @param {*} value
57 | */
58 | export function defineReactive(obj, key, value) {
59 | // 如果属性也是对象 再次劫持 childOb有值的情况下是Observe实例,实例上挂载了dep
60 | const childOb = observe(value);
61 | // 每个属性都有一个dep
62 | let dep = new Dep();
63 | Object.defineProperty(obj, key, {
64 | get() {
65 | // 判断 Dep.target
66 | if (Dep.target) {
67 | // 让数组自身 和 对象自身 都能实现依赖收集
68 | if (childOb) {
69 | // 来到这里,表名 value是引用类型,且如果是数组,循环看数组元素是否是数组
70 | // 如果还是数组 则需要收集依赖
71 | childOb.dep.depend();
72 | // TODO 深度实现依赖收集 对于数组元素还是数组的情况,需要让此元素自身也进行依赖收集
73 | if (Array.isArray(value)) dependArray(value);
74 | }
75 | // 当前属性 记住这个watcher 也就是视图依赖的收集
76 | dep.depend();
77 | }
78 | // console.log("----------------dep.get----------------",key)
79 | return value;
80 | },
81 | set(newVal) {
82 | if (newVal === value) return;
83 | // 新值是对象 则需要重新观测
84 | observe(newVal);
85 | value = newVal;
86 | // 更新数据 通知视图更新
87 | dep.notify();
88 | },
89 | });
90 | }
91 |
92 | /**
93 | * 数据劫持方法
94 | * @param {*} data 需要劫持的数据
95 | */
96 | export function observe(data) {
97 | // 不是对象 不需要劫持
98 | if (!isObject(data)) return;
99 | // 如果一个对象被劫持过了,那么不需要再次被劫持了
100 | if (data.__ob__ instanceof Observe) return data.__ob__;
101 | // console.log("observe---------------->", data);
102 | return new Observe(data);
103 | }
104 |
105 | /**
106 | * 给对象属性或者数组元素是数组的,进行依赖收集
107 | * 深层次嵌套会递归,递归太多性能差,不存在的属性监控不到,存在的属性要重写方法 vue3->proxy
108 | * @param {*} arr
109 | */
110 | function dependArray(arr) {
111 | // console.log(arr);
112 | for (let i = 0; i < arr.length; i++) {
113 | const cur = arr[i];
114 | // console.log(cur, cur.__ob__);
115 | // 数组元素可能不是数组了
116 | if (Array.isArray(cur)) {
117 | // 收集依赖
118 | cur.__ob__.dep.depend();
119 | dependArray(cur);
120 | }
121 | }
122 | }
123 | // 1.默认vue在初始化的时候 会对对象每一个属性都进行劫持,增加dep属性, 当取值的时候会做依赖收集
124 | // 2.默认还会对属性值是(对象和数组的本身进行增加dep属性) 进行依赖收集
125 | // 3.如果是属性变化 触发属性对应的dep去更新
126 | // 4.如果是数组更新,触发数组的本身的dep 进行更新
127 | // 5.如果取值的时候是数组还要让数组中的对象类型也进行依赖收集 (递归依赖收集)
128 | // 6.如果数组里面放对象,默认对象里的属性是会进行依赖收集的,因为在取值时 会进行JSON.stringify操作
129 |
--------------------------------------------------------------------------------
/src/observe/watcher.js:
--------------------------------------------------------------------------------
1 | import Dep, { popWatcherTarget, pushWatcherTarget } from "./dep";
2 |
3 | /*
4 | * @Author: 毛毛
5 | * @Date: 2022-04-15 09:09:45
6 | * @Last Modified by: 毛毛
7 | * @Last Modified time: 2022-04-16 15:57:02
8 | * 封装视图的渲染逻辑 watcher
9 | */
10 | let id = 0;
11 | /**
12 | * watcher 进行实际的视图渲染
13 | * 每个组件都有自己的watcher,可以减少每次更新页面的部分
14 | * 给每个属性都增加一个dep,目的就是收集watcher
15 | * 一个视图(组件)可能有很多属性,多个属性对应一个视图 n个dep对应1个watcher
16 | * 一个属性也可能对应多个视图(组件)
17 | * 所以 dep 和 watcher 是多对多关系
18 | *
19 | * 每个属性都有自己的dep,属性就是被观察者
20 | * watcher就是观察者(属性变化了会通知观察者进行视图更新)-> 观察者模式
21 | */
22 | class Watcher {
23 | // 目前只有一个watcher实例 因为我只有一个实例 根组件
24 | id = id++;
25 | /**
26 | *
27 | * @param {*} vm 组件实例
28 | * @param {Function|string} exprOrFn 渲染页面的回调函数 或者函数 或者字符串(需要把字符串转为函数) name:()=>{}, -> ()=>name,()=>{}
29 | * @param {boolean|object} options 额外选项 true表示初次渲染 对象是额外的配置
30 | * @param {Function} callback watch等的回调函数
31 | */
32 | constructor(vm, exprOrFn, options, callback) {
33 | // console.log(this,"--------------------------------------------")
34 | if (typeof options === "boolean") this.renderWatcher = true;
35 | // 记录vm实例
36 | this.vm = vm;
37 | this.options = options;
38 | // exprOrFn是字符串 变成函数 name -> ()=>vm.name
39 | if (typeof exprOrFn === "string") {
40 | this.getter = () => vm[exprOrFn];
41 | // TODO 有this问题在切换
42 | // this.getter = function () {
43 | // return vm[exprOrFn];
44 | // };
45 | } else {
46 | // 调用这个函数 意味着可以发生取值操作
47 | this.getter = exprOrFn;
48 | }
49 | // 标识用户自定义watch
50 | this.user = options?.user;
51 | // 收集 watch等的callback
52 | this.callback = callback;
53 | // 收集 dep watcher -> deps
54 | this.deps = []; // 在组件卸载的时候,清理响应式数据使用 还有实现响应式数据等都需要使用到
55 | this.depsId = new Set(); // dep id
56 | // 是否懒执行
57 | this.lazy = options?.lazy;
58 | // dirty 计算属性使用的
59 | this.dirty = this.lazy;
60 | this.value = this.lazy ? void 0 : this.get();
61 | }
62 | get() {
63 | /**
64 | * 1.当我们创建渲染watcher的时候 会把当前的渲染watcher放到Dep.target上
65 | * 2.调用_render()取值 走到值的get上
66 | */
67 | // Dep.target = this;
68 | pushWatcherTarget(this);
69 | // 去 vm上取值 这里的this不是vm了,所以取值需要绑定vm
70 | const val = this.getter.call(this.vm);
71 | // 渲染完毕后清空
72 | // Dep.target = null;
73 | popWatcherTarget();
74 | return val; // 计算属性执行的返回值
75 | }
76 | evaluate() {
77 | // 获取到用户函数的返回值(getter返回值) 并且标识数据不是脏的
78 | this.value = this.get();
79 | this.dirty = false;
80 | }
81 | /**
82 | * 一个组件对应多个属性 但是重复的属性 也不需要记录
83 | * 比如在组件视图中 用到了多次的name属性,那么需要记录每次用到name的watcher吗
84 | * @param {*} dep
85 | */
86 | addDep(dep) {
87 | // dep去重 可以用到 dep.id
88 | const id = dep.id;
89 | if (!this.depsId.has(id)) {
90 | // watcher记录dep
91 | this.deps.push(dep);
92 | this.depsId.add(id);
93 | // dep记录watcher
94 | dep.addSub(this);
95 | }
96 | }
97 | /**
98 | * 更新视图 本质重新执行 render函数
99 | */
100 | update() {
101 | // 是计算属性
102 | if (this.lazy) {
103 | // 依赖的值变化 就标识计算属性的值是脏值了
104 | return (this.dirty = true);
105 | }
106 | // 同步更新视图 改为异步更新视图
107 | // this.get();
108 | // 把当前的watcher暂存
109 | queueWatcher(this);
110 | console.log("update watcher.................");
111 | }
112 | /**
113 | * 实际刷新视图的操作 执行render用到的都是实例最新的属性值
114 | */
115 | run() {
116 | // console.log("run------------------");
117 | // 可以拿到watch最新的值
118 | const newVal = this.get();
119 | // watch的回调函数 传入最新的值 和上次还未更新的值
120 | this.user && this.callback.call(this.vm, newVal, this.value);
121 | this.value = newVal;
122 | }
123 | depend() {
124 | // 之前是属性dep记录watcher
125 | // 这里是watcher记录属性dep
126 | let i = this.deps.length;
127 | while (i--) {
128 | // 让计算属性watcher收集上层watcher
129 | // curr dep -> prev watcher -> curr dep -> prev watcher
130 | // dep.depend() -> watcher.addDep(dep) -> dep.addSub(watcher)
131 | this.deps[i].depend();
132 | }
133 | }
134 | }
135 | // watcher queue 本次需要更新的视图队列
136 | let queue = [];
137 | // watcher 去重 {0:true,1:true}
138 | let has = {};
139 | // 批处理 也可以说是防抖
140 | let pending = false;
141 | /**
142 | * 不管执行多少次update操作,但是我们最终只执行一轮刷新操作
143 | * @param {*} watcher
144 | */
145 | function queueWatcher(watcher) {
146 | const id = watcher.id;
147 | // 去重
148 | if (!has[id]) {
149 | queue.push(watcher);
150 | has[id] = true;
151 | console.log(queue);
152 | if (!pending) {
153 | // 刷新队列 多个属性刷新 其实执行的只是第一次 合并刷新了
154 | // setTimeout(flushSchedulerQueue, 0);
155 | // 将刷新队列的执行和用户回调的执行都放到一个微任务中
156 | nextTick(flushSchedulerQueue);
157 | pending = true;
158 | }
159 | }
160 | }
161 | /**
162 | * 刷新调度队列 且清理当前的标识 has pending 等都重置
163 | * 先执行第一批的watcher,如果刷新过程中有新的watcher产生,再次加入队列即可
164 | */
165 | function flushSchedulerQueue() {
166 | const flushQueue = [...queue];
167 | queue = [];
168 | has = {};
169 | pending = false;
170 | // 刷新视图 如果在刷新过程中 还有新的watcher 会重新放到queueWatcher中
171 | flushQueue.forEach((watcher) => watcher.run());
172 | }
173 | // 任务队列
174 | let callbacks = [];
175 | // 是否等待任务刷新
176 | let waiting = false;
177 | /**
178 | * 刷新异步回调函数队列
179 | */
180 | function flushCallbacks() {
181 | const cbs = [...callbacks];
182 | callbacks = [];
183 | waiting = false;
184 | cbs.forEach((cb) => cb());
185 | }
186 | /**
187 | * 优雅降级 Promise -> MutationObserve -> setImmediate -> setTimeout(需要开线程 开销最大)
188 | */
189 | let timerFunc = null;
190 | if (Promise) {
191 | timerFunc = () => Promise.resolve().then(flushCallbacks);
192 | } else if (MutationObserver) {
193 | // 创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用(异步执行callback)。
194 | const observer = new MutationObserver(flushCallbacks);
195 | // TODO 创建文本节点的API 应该封装 为了方便跨平台
196 | const textNode = document.createTextNode(1);
197 | console.log("observer-----------------");
198 | // 监控文本值的变化
199 | observer.observe(textNode, {
200 | characterData: true,
201 | });
202 | timerFunc = () => (textNode.textContent = 2);
203 | } else if (setImmediate) {
204 | // IE平台
205 | timerFunc = () => setImmediate(flushCallbacks);
206 | } else {
207 | timerFunc = () => setTimeout(flushCallbacks, 0);
208 | }
209 | /**
210 | * 异步批处理
211 | * 是先执行内部的回调 还是用户的? 用个队列 排序
212 | * @param {Function} cb 回调函数
213 | */
214 | export function nextTick(cb) {
215 | // 使用队列维护nextTick中的callback方法
216 | callbacks.push(cb);
217 | if (!waiting) {
218 | // setTimeout(flushCallbacks, 0); // 刷新
219 | // 使用vue的原理 优雅降级
220 | timerFunc();
221 | waiting = true;
222 | }
223 | }
224 |
225 | export default Watcher;
226 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 是否是函数
3 | * @param {*} source 对象
4 | * @returns
5 | */
6 | export function isFunction(source) {
7 | return typeof source === "function";
8 | }
9 |
10 | export const isObject = (source) => {
11 | return source != null && typeof source === "object";
12 | };
13 |
--------------------------------------------------------------------------------
/src/utils/merge.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 毛毛
3 | * @Date: 2022-04-15 20:43:57
4 | * @Last Modified by: 毛毛
5 | * @Last Modified time: 2022-04-17 23:46:24
6 | * 合并对象的方法
7 | */
8 | import { strategy } from "./strategy";
9 | /**
10 | * 合并选项
11 | * @param {...any} options
12 | * @returns
13 | */
14 | export function mergeOptions(...options) {
15 | const opts = {};
16 | const [source1, source2] = options;
17 | for (const key in source1) {
18 | mergeField(key);
19 | }
20 | for (const key in source2) {
21 | if (!source1.hasOwnProperty(key)) {
22 | mergeField(key);
23 | }
24 | }
25 | function mergeField(key) {
26 | // 策略模式 减少 if / else
27 | if (strategy[key]) {
28 | opts[key] = strategy[key](source1[key], source2[key]);
29 | }
30 | // 优先采用用户的选项 再采用全局已存在的
31 | else opts[key] = source2[key] === void 0 ? source1[key] : source2[key];
32 | }
33 | if (options.length > 2) {
34 | options.splice(0, 2)
35 | return mergeOptions(opts, ...options);
36 | }
37 | return opts;
38 | }
39 |
--------------------------------------------------------------------------------
/src/utils/strategy.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 毛毛
3 | * @Date: 2022-04-15 20:52:34
4 | * @Last Modified by: 毛毛
5 | * @Last Modified time: 2022-04-18 00:50:40
6 | * 导出mixin时用到的一些策略模式
7 | */
8 | // 策略模式
9 | export const strategy = {};
10 | // 生命周期
11 | const LIFE_CYCLE = [
12 | "beforeCreate",
13 | "created",
14 | "beforeMount",
15 | "mounted",
16 | "beforeUpdate",
17 | "update",
18 | ];
19 | LIFE_CYCLE.forEach((hook) => {
20 | strategy[hook] = function (s1, s2) {
21 | if (s2) {
22 | if (s1) {
23 | // 合并选项
24 | // return s1.concat(s2);
25 | return [...s1, s2];
26 | } else {
27 | // 全局options没有 用户传递的有 变成数组
28 | return [s2];
29 | }
30 | } else {
31 | return s1;
32 | }
33 | };
34 | });
35 |
36 | // 组件的合并策略
37 | strategy.components = function (parentVal, childVal) {
38 | // TODO 这里这种做法不一定很好 该条件是不是应该有还应该考究 有了该条件 全局的组件定义的位置不同 可能最后的结果不同
39 | // 已经和全局组件对象创建关系了,则不需要再次建立关系 直接返回
40 | // if (Object.getPrototypeOf(parentVal) === Vue.options.components)
41 | // return parentVal;
42 | // 通过父亲 创建一个对象 原型上有父亲的所有属性和方法
43 | const res = Object.create(parentVal); // {}.__proto__ = parentVal
44 | if (childVal) {
45 | for (const key in childVal) {
46 | // 拿到所有的孩子的属性和方法
47 | res[key] = childVal[key];
48 | }
49 | }
50 | console.log(res);
51 | return res;
52 | };
53 |
--------------------------------------------------------------------------------
/src/vdom/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 毛毛
3 | * @Date: 2022-04-14 14:35:48
4 | * @Last Modified by: 毛毛
5 | * @Last Modified time: 2022-04-18 13:51:26
6 | * 虚拟dom 需要的方法
7 | */
8 | const ReservedTags = [
9 | "div",
10 | "h1",
11 | "h2",
12 | "h3",
13 | "h4",
14 | "h5",
15 | "h6",
16 | "span",
17 | "ul",
18 | "ol",
19 | "li",
20 | "a",
21 | "table",
22 | "button",
23 | "input",
24 | ];
25 |
26 | const isReservedTag = (tag) => {
27 | return ReservedTags.includes(tag);
28 | };
29 | /**
30 | * 生成虚拟dom
31 | * 虚拟dom是和ast不一样的 -> ast是语法层面的转换,他描述的是语法本身(可以描述 js css html等等)
32 | * 我们的虚拟dom 是描述dom元素,可以增加自定义属性
33 | * @param {*} vm
34 | * @param {*} tag
35 | * @param {*} key
36 | * @param {*} props
37 | * @param {*} children
38 | * @param {*} text
39 | * @returns
40 | */
41 | function vnode(vm, tag, key, props, children, text, componentOptions) {
42 | return {
43 | vm,
44 | tag,
45 | key,
46 | props, // props -> data
47 | children,
48 | text,
49 | // 组件的选项 包含组件的构造函数
50 | componentOptions,
51 | };
52 | }
53 | // h函数 创建元素节点
54 | function createElementVNode(vm, tag, data = {}, ...children) {
55 | // if(data == null) data = {}
56 | const key = data?.key; // data可能是 null
57 | key && delete data.key;
58 | if (isReservedTag(tag)) return vnode(vm, tag, key, data, children);
59 | // 组件的虚拟节点 需要包含组件的构造函数等
60 | // 组件的构造函数 如果是局部组件 可能是一个对象 btn: {template:""}
61 | const CtorOrObj = vm.$options.components[tag];
62 | return createComponentVNode(vm, tag, key, data, children, CtorOrObj);
63 | }
64 | /**
65 | * 创建组件节点
66 | * @param {*} vm
67 | * @param {*} tag
68 | * @param {*} key
69 | * @param {*} data
70 | * @param {*} children
71 | * @param {*} CtorOrObj
72 | * @returns
73 | */
74 | function createComponentVNode(vm, tag, key, data, children, CtorOrObj) {
75 | if (CtorOrObj != null && typeof CtorOrObj === "object") {
76 | // Vue.extend -> 变成构造函数
77 | CtorOrObj = vm.$options._base.extend(CtorOrObj);
78 | }
79 | // TODO 构造 组件的钩子 组件data是不能为null的
80 | data = data ?? {};
81 | data.hook = {
82 | // 创建真实节点的时候,如果是组件 则调用此init方法
83 | init(vnode) {
84 | // new Sub -> 保存实例到虚拟节点上
85 | const instance = (vnode.componentInstance =
86 | new vnode.componentOptions.Ctor());
87 | // instance.$el = 组件渲染的真实节点
88 | instance.$mount(); // 没有传递挂载的dom 最后会去 patch方法
89 | console.log(vnode.componentOptions.Ctor, "----------init");
90 | },
91 | };
92 | return vnode(vm, tag, key, data, children, null, { Ctor: CtorOrObj });
93 | }
94 |
95 | // _v 函数 创建文本节点
96 | function createTextVNode(vm, text) {
97 | return vnode(vm, undefined, undefined, undefined, undefined, text);
98 | }
99 | /**
100 | *
101 | * @param {*} n1
102 | * @param {*} n2
103 | * @returns {boolean} 是否是同一个vnode
104 | */
105 | function isSameVNode(n1, n2) {
106 | return n1.tag === n2.tag && n1.key === n2.key;
107 | }
108 |
109 | export {
110 | createElementVNode,
111 | createTextVNode as h,
112 | createTextVNode,
113 | createTextVNode as _c,
114 | isSameVNode,
115 | };
116 |
--------------------------------------------------------------------------------
/src/vdom/patch.js:
--------------------------------------------------------------------------------
1 | /**
2 | * vue的核心流程:
3 | * 1. 创造响应式数据
4 | * 2. 模板编译 生成 ast
5 | * 3. ast 转为render函数 后续每次数据更新 只执行render函数(不需要再次进行ast的转换)
6 | * 4. render函数执行 生成 vNode节点(会使用到响应式数据)
7 | * 5. 根据vNode 生成 真实dom 渲染页面
8 | * 6. 数据更新 重新执行render
9 | */
10 |
11 | import { isSameVNode } from ".";
12 |
13 | /**
14 | * 更新 | 初渲染时 第一个节点的值是真实元素
15 | * @param {*} oldVNode 旧vnode
16 | * @param {*} vnode 最新的vnode
17 | */
18 | function patch(oldVNode, vnode) {
19 | // 组件的挂载 vm.$el 对应的就是组件的渲染结果了
20 | if (!oldVNode) return createEle(vnode);
21 |
22 | const isRealElement = oldVNode.nodeType;
23 | // 真实元素
24 | if (isRealElement) {
25 | const elm = oldVNode;
26 | // 获取父节点 1. 元素节点 2. 文档节点 3. 文档碎片节点
27 | const parentElm = elm.parentNode;
28 | // console.log(parentElm)
29 | const newEle = createEle(vnode);
30 | // 插入新dom 移除父节点上的老dom节点
31 | insertBefore(parentElm, newEle, elm.nextSibling);
32 | removeChild(parentElm, elm);
33 | // console.log(newEle)
34 | return newEle;
35 | }
36 | // ------------------- 更新节点 --------------------
37 | return patchVnode(oldVNode, vnode); // 返回更新后的 dom元素
38 | }
39 | /**
40 | * 直接将新节点替换老节点,很消耗性能
41 | * 所以我们不直接替换,而是在比较两个节点之间的区别之后在替换,这就是diff算法
42 | * diff算是 是一个平级比较的过程,父亲和父亲节点比对 儿子和儿子节点比对
43 | */
44 | function patchVnode(oldVNode, vnode) {
45 | /**
46 | * 1. 两个节点不是同一个节点,直接删除老的换上新的(不在继续对比属性等)
47 | * 2. 两个节点是同一个节点(tag,key都一致),比较两个节点的属性是否有差异
48 | * 复用老节点,将差异的属性更新
49 | */
50 | const el = oldVNode.el;
51 | // 不是同一个节点
52 | if (!isSameVNode(oldVNode, vnode)) {
53 | // tag && key
54 | // 直接替换
55 | const newEl = createEle(vnode);
56 | replaceChild(el.parentNode, newEl, el);
57 | return newEl;
58 | }
59 | // 文本的情况 文本我们期望比较一下文本的内容
60 | vnode.el = el;
61 | if (!oldVNode.tag) {
62 | if (oldVNode.text !== vnode.text) {
63 | textContent(el, vnode.text);
64 | }
65 | }
66 | // 是标签 我们需要比对标签的属性
67 | patchProps(el, oldVNode.props, vnode.props);
68 | // 有子节点
69 | /**
70 | * 1.旧节点有子节点 新节点没有
71 | * 2. 都有子节点
72 | * 3. 旧节点没有子节点,新节点有
73 | */
74 | const oldChildren = oldVNode.children || [];
75 | const newChildren = vnode.children || [];
76 | const oldLen = oldChildren.length,
77 | newLen = newChildren.length;
78 | if (oldLen && newLen) {
79 | // 完整的diff 都有子节点
80 | updateChildren(el, oldChildren, newChildren);
81 | } else if (newLen) {
82 | // 只有新节点有子节点 挂载
83 | mountChildren(el, newChildren);
84 | } else if (oldLen) {
85 | // 只有旧节点有子节点 全部卸载
86 | unmountChildren(el, oldChildren);
87 | }
88 | return el;
89 | }
90 | /**
91 | * 对比更新子节点
92 | * @param {*} el
93 | * @param {*} oldChildren
94 | * @param {*} newChildren
95 | */
96 | // TODO 对于出现重复的key,有bug,还未修复。。。。
97 | function updateChildren(el, oldChildren, newChildren) {
98 | // 我们为了比较两个儿子的时候,提高比较的性能(速度)
99 | /**
100 | * 1. 我们操作列表 经常会有 push pop shift unshift sort reverse 等方法 针对这些情况可以做一些优化
101 | * 2. vue2中采用双指针的方法 比较两个节点
102 | */
103 | let oldStartIndex = 0,
104 | oldEndIndex = oldChildren.length - 1,
105 | newStartIndex = 0,
106 | newEndIndex = newChildren.length - 1,
107 | oldStartVnode = oldChildren[oldStartIndex],
108 | oldEndVnode = oldChildren[oldEndIndex],
109 | newStartVnode = newChildren[newStartIndex],
110 | newEndVnode = newChildren[newEndIndex];
111 | // 乱序比较时 使用的映射表 {key:"节点在数组中的索引"} -> {a:0,b:1,...}
112 | const map = makeIndexByKey(oldChildren);
113 | // 循环比较 只要头指针不超过尾指针 就一直比较
114 | while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
115 | // 排除 undefined 的情况
116 | if (!oldStartVnode) oldStartVnode = oldChildren[++oldStartIndex];
117 | if (!oldEndVnode) oldEndVnode = oldChildren[--oldStartIndex];
118 | /**
119 | * 1. old head -> new head
120 | * 2. old tail -> new tail
121 | * 3. old head -> new tail
122 | * 4. old tail -> new head
123 | * 5. 前面都不符合的情况下,进行乱序比较 看当前节点是否出现在老节点上
124 | */
125 | // 进行节点比较
126 | else if (isSameVNode(oldStartVnode, newStartVnode)) {
127 | // 头结点相同
128 | // 从头指针开始比较两个节点
129 | // 相同节点 递归比较子节点
130 | patchVnode(oldStartVnode, newStartVnode);
131 | oldStartVnode = oldChildren[++oldStartIndex];
132 | newStartVnode = newChildren[++newStartIndex];
133 | } else if (isSameVNode(oldEndVnode, newEndVnode)) {
134 | // 尾节点相同
135 | // 从尾指针开始比较两个节点
136 | patchVnode(oldEndVnode, newEndVnode);
137 | oldEndVnode = oldChildren[--oldEndIndex];
138 | newEndVnode = newChildren[--newEndIndex];
139 | }
140 | // 交叉比对 两次头尾比较
141 | // a b c -> c a b 把尾节点移动到头结点之前
142 | else if (isSameVNode(oldEndVnode, newStartVnode)) {
143 | patchVnode(oldEndVnode, newStartVnode);
144 | console.log(oldEndVnode, newStartVnode);
145 | // 将老节点的尾节点插入到老节点头结点(头结点会变化)的前面去
146 | insertBefore(el, oldEndVnode.el, oldStartVnode.el);
147 | oldEndVnode = oldChildren[--oldEndIndex];
148 | newStartVnode = newChildren[++newStartIndex];
149 | }
150 | // a b c d -> d c b a 头结点移动到尾节点后面
151 | else if (isSameVNode(oldStartVnode, newEndVnode)) {
152 | patchVnode(oldStartVnode, newEndVnode);
153 | insertBefore(el, oldStartVnode.el, oldEndVnode.el.nextSibling);
154 | oldStartVnode = oldChildren[++oldStartIndex];
155 | newEndVnode = newChildren[--newEndIndex];
156 | } else {
157 | // 乱序比对 a b c -> d e a b f
158 | /**
159 | * 根据老的列表做一个映射关系,用新的去找,找到则移动节点,找不到就新增节点,最后移除多余节点
160 | */
161 | // 如有值:则是需要移动的节点的索引
162 | let moveIndex = map[newStartVnode.key];
163 | if (moveIndex !== undefined) {
164 | const moveVnode = oldChildren[moveIndex];
165 | // 移动节点到头指针所在节点的前面
166 | insertBefore(el, moveVnode, oldStartVnode.el);
167 | // 标识这个节点已经移动过
168 | oldChildren[moveIndex] = undefined;
169 | patchVnode(moveVnode, newStartVnode);
170 | } else {
171 | // 找不到 这是新节点 创建 然后插入进去 完事
172 | insertBefore(el, createEle(newStartVnode), oldStartVnode.el);
173 | }
174 | newStartVnode = newChildren[++newStartIndex];
175 | }
176 | }
177 | // 新节点的比旧节点多 挂载
178 | if (newStartIndex <= newEndIndex) {
179 | for (let i = newStartIndex; i <= newEndIndex; i++) {
180 | // 这里可能是向后追加 也可能是向前插入
181 | // 判断当前的虚拟dom后面是否还有节点 有节点则是插入到该节点前面
182 | const anchor = newChildren[newEndIndex + 1]?.el;
183 | // 注意:插入方法在 要插入的那个节点不存在的情况下,自动变为追加方法 appendChild
184 | insertBefore(el, createEle(newChildren[i]), anchor);
185 | }
186 | }
187 | // 旧节点比新节点多 卸载
188 | if (oldStartIndex <= oldEndIndex) {
189 | for (let i = oldStartIndex; i <= oldEndIndex; i++) {
190 | // 乱序比对时 可能已经标记为 undefined了
191 | oldChildren[i] && removeChild(el, oldChildren[i].el);
192 | }
193 | }
194 | }
195 | /**
196 | * 生成映射表
197 | * @param {*} children
198 | * @returns
199 | */
200 | function makeIndexByKey(children) {
201 | const map = {};
202 | children.forEach((child, index) => (map[child.key] = index));
203 | return map;
204 | }
205 |
206 | /**
207 | * 卸载dom
208 | * @param {*} el
209 | * @param {*} children
210 | */
211 | // TODO 不要直接使用innerHTML清空
212 | function unmountChildren(el, children) {
213 | children.forEach((child) => removeChild(el, child.el));
214 | }
215 |
216 | /**
217 | * 把子节点都变成真实dom 挂载到el上
218 | * @param {*} el
219 | * @param {*} children
220 | */
221 | function mountChildren(el, children) {
222 | for (let i = 0; i < children.length; i++) {
223 | const child = children[i];
224 | appendChild(el, createEle(child));
225 | }
226 | }
227 |
228 | function createEle(vnode) {
229 | const { tag, props, children, text } = vnode;
230 | if (typeof tag === "string") {
231 | // 区分真实节点和组件节点
232 | if (createComponent(vnode)) {
233 | return vnode.componentInstance.$el;
234 | }
235 | // 标签 div h2
236 | // 将虚拟节点和真实节点想管理 根据虚拟节点可以找到真实节点 方便修改属性
237 | vnode.el = createElement(tag);
238 | // 更新属性
239 | patchProps(vnode.el, {}, props);
240 | children.forEach((child) => {
241 | // 如果孩子是组件 会实例化组件 并且插入到父组件内部子节点的最后
242 | appendChild(vnode.el, createEle(child));
243 | });
244 | } else if (typeof tag === "object") {
245 | // 组件
246 | } else {
247 | // 创建文本节点
248 | vnode.el = createTextNode(text);
249 | }
250 | return vnode.el;
251 | }
252 | /**
253 | * 更新属性到dom节点上
254 | * @param {*} el
255 | * @param {*} oldProps 老节点上的属性
256 | * @param {*} props
257 | */
258 | function patchProps(el, oldProps, props) {
259 | // 老的属性中有的属性 新节点没有的 需要删除
260 | const oldStyle = oldProps?.style || {}; // oldProps 可能是null
261 | const newStyle = props?.style || {}; // props可能是null
262 | // 样式移除
263 | for (let key in oldStyle) {
264 | if (!newStyle[key]) {
265 | el.style[key] = "";
266 | }
267 | }
268 | // 属性移除
269 | for (const key in oldProps) {
270 | if (!props[key]) {
271 | removeAttribute(el, key);
272 | }
273 | }
274 | // 属性存在 则覆盖
275 | for (const key in props) {
276 | if (key === "style") {
277 | Object.keys(props[key]).forEach((k) => (el.style[k] = props["style"][k]));
278 | } else {
279 | setAttribute(el, key, props[key]);
280 | }
281 | }
282 | }
283 |
284 | function createComponent(vnode) {
285 | // init 初始化组件
286 | vnode.props?.hook?.init(vnode);
287 | return vnode.componentInstance;
288 | }
289 |
290 | function createElement(tag, type = "browser") {
291 | switch (type.toLowerCase()) {
292 | case "browser":
293 | return document.createElement(tag);
294 | }
295 | }
296 |
297 | function createTextNode(tag, type = "browser") {
298 | switch (type.toLowerCase()) {
299 | case "browser":
300 | return document.createTextNode(tag);
301 | }
302 | }
303 |
304 | function appendChild(parent, child, type = "browser") {
305 | switch (type.toLowerCase()) {
306 | case "browser":
307 | parent.appendChild(child);
308 | break;
309 | }
310 | }
311 |
312 | function setAttribute(el, key, value, type = "browser") {
313 | switch (type.toLowerCase()) {
314 | case "browser":
315 | el.setAttribute(key, value);
316 | break;
317 | }
318 | }
319 |
320 | function removeChild(parent, child, type = "browser") {
321 | switch (type.toLowerCase()) {
322 | case "browser":
323 | parent.removeChild(child);
324 | break;
325 | }
326 | }
327 |
328 | function insertBefore(parent, child, prevChild, type = "browser") {
329 | switch (type.toLowerCase()) {
330 | case "browser":
331 | // document.insertBefore
332 | parent.insertBefore(child, prevChild);
333 | break;
334 | }
335 | }
336 |
337 | function replaceChild(parent, child, oldChild, type = "browser") {
338 | switch (type.toLowerCase()) {
339 | case "browser":
340 | // document.insertBefore
341 | parent.replaceChild(child, oldChild);
342 | break;
343 | }
344 | }
345 |
346 | function removeAttribute(el, key, type = "browser") {
347 | switch (type.toLowerCase()) {
348 | case "browser":
349 | // document.insertBefore
350 | el.removeAttribute(key);
351 | break;
352 | }
353 | }
354 | /**
355 | * 修改元素的文本内容
356 | * @param {*} element
357 | * @param {*} text
358 | * @param {*} type
359 | */
360 | function textContent(element, text, type = "browser") {
361 | switch (type.toLowerCase()) {
362 | case "browser":
363 | // document.insertBefore
364 | element.textContent = text;
365 | break;
366 | }
367 | }
368 |
369 | export {
370 | patch,
371 | createEle,
372 | patchProps,
373 | createElement,
374 | createTextNode,
375 | appendChild,
376 | setAttribute,
377 | removeChild,
378 | insertBefore,
379 | };
380 |
--------------------------------------------------------------------------------