5 }
6
7 // 订阅
8 addSub(watcher) {
9 this.subs.push(watcher);
10 }
11
12 // 通知
13 notify() {
14 this.subs.forEach(watcher => watcher.update());
15 }
16}
4、依赖收集
在我们更新视图的时候进行依赖收集,给每个属性创建一个发布订阅的功能,当我们的值在 set 中改变时,我们就触发订阅者的通知,让各个依赖该数据的视图进行更新。
1defineReactive(obj, key, value) {
2 // 递归创建 响应式数据,性能不好
3 this.observer(value);
4 let dep = new Dep(); // 给每一个属性都加上一个具有发布订阅的功能
5 Object.defineProperty(obj, key, {
6 get() {
7 // 创建 watcher 时,会取到响应内容,并且把 watcher 放到了全局上
8 Dep.target && dep.addSub(Dep.target); // 增加观察者
9 return value;
10 },
11 set: newValue => {
12 if (newValue !== value) {
13 // 设置某个 key 的时候,可能是一个对象
14 this.observer(value);
15 value = newValue;
16 console.log(‘-------------------------视图更新-----------------------------’)
17 dep.notify(); // 通知
18 }
19 }
20 });
剩下的就是我们调用 new Watcher 地方了,这个过程在编译模板里边。
三、编译模板
对于模板的编译,我们首先需要判断传入的 el 类型,然后拿到页面的结点到内存中去,把节点上有数据编译的地方,比如:v-model、v-on、{{student.name}} 进行数据的替换,然后再塞回页面,就完成的页面的显示。
1// 编译类
2class Compile {
3 constructor(el, vm) {
4 // 判断 el 传入的类型
5 this.el = this.isElementNode(el) ? el : document.querySelector(el);
6 this.vm = vm;
7
8 // 把当前节点放到内存中去 —— 之所以塞到内存中,是因为频繁渲染造成回流和重绘
9 let fragment = this.nodefragment(this.el);
10
11 // 把节点在内存中将表达式和命令等进行数据替换
12 this.compile(fragment);
13
14 // 把内容塞回页面
15 this.el.appendChild(fragment);
16 }
17}
1、将 DOM 拿到内存
首先我们之前已经声明好 data 了,如下:
1 let vm = new Vue({
2 el: ‘#app’,
3 data: {
4 student: {
5 name: ‘小鹿’,
6 age: 20,
7 },
8 }
9 })
然后我们需要拿到页面的模板,将页面中的一些指令(v-model=“student.name”)或者表达{{student.name}} 的结点替换成我们对应的属性值。
我们需要通过传入的 el 属性值先拿到页面的 dom 到内存中。
1/**
2 * 将 DOM 拿到内存中
3 * @param {*} node DOM
4 */
5nodefragment(node) {
6 let fragment = document.createDocumentFragment();
7 let firstChild;
8 while ((firstChild = node.firstChild)) {
9 fragment.appendChild(firstChild);
10 }
11 return fragment;
12}
2、数据替换
我们下一步需要将页面中的这些表达式,替换成相对应的 data 中的属性值,那么页面就将完成的呈现出带有数据的视图来。
1
2
3 {{student.age}}
4
通过上边的方法,已经将所有的页面结点循环遍历拿到。下一步开始进行一层层的遍历,将数据在内存中进行替换。
1/**
2 * 核心编译方法
3 * 编译内存中的 DOM 节点
4 * @param {*} node
5 */
6compile(node) {
7 let childNodes = node.childNodes;
8 […childNodes].forEach(child => {
9 // 判断当前的是元素还是文本节点
10 if (this.isElementNode(child)) {
11 this.compileElement(child);
12 // 如果是元素的话,需要把自己传进去,再去遍历子节点
13 this.compile(child);
14 } else {
15 this.compileText(child); // 文本节点有 {{student.age}}
16 }
17 });
18}
19
20/**
21 * 判断当前传入的节点是不是元素节点
22 * @param {*} node 节点
23 */
24isElementNode(node) {
25 return node.nodeType == 1; // 1 代表元素节点
26}
this.isElementNode(child)
页面是由很多的 node 结点构成,在上边的页面中,v-model=“student.name” 主要存在与元素节点中,{{student.age}} 表达式的值存在于文本节点中,所以我们需要通过 this.isElementNode(child) 进行判断当前是否为元素节点,然后对当前节点进行不同的处理。
对于元素节点,我们调用 compileElement(child)方法,当然,元素节点中可能存在子节点的情况,所以我们需要递归判断元素节点里是否还有子节点,再次调用 this.compile(child); 方法。
我们以解析 v-model 指令为例,开始对节点进行解析判断赋值。
1
1/**
2 * 编译元素节点 —— 判断是否存在 v- 指令
3 * @param {*} node
4 */
5compileElement(node) {
6 let attributes = node.attributes;
7 […attributes].forEach(attr => {
8 // type = “text” v-model=“student.name”
9 let { name, value: expr } = attr; // name:v-model expr:“student.name”
10 // 判断当前是否存在属性为 v- 的指令
11 if (this.isDirective(name)) {
12 // v-html v-bind v-model
13 let [, directive] = name.split(“-”);
14 let [directiveName, eventName] = directive.split(“:”); // v-on:click
15 // 调用不同的指令来处理
16 CompileUtildirectiveName;
17 }
18 });
19}
20
21/**
22 * 判断是够是 v- 开头的指令
23 * @param {*} attrName
24 */
25isDirective(attrName) {
26 return attrName.startsWith(“v-”);
27}
同时我们还有一个工具类 CompileUtil,主要用于把对应的 data 数据插入到对应节点中。
上一步中,我们通过 let [directiveName, eventName] = directive.split(“:”) 解析出了 directiveName= v-model ,eventName = student.name。
然后我们将两个参数 directiveName 和 eventName 传入工具类对象中。
1// node: 当前节点 expr:当前表达式(student.name) vm:当前 vue 实例
2CompileUtildirectiveName;
通过调用不同的指令进行不同的处理。
1/**
2 * 工具类(把数据插入到 DOM 中)
3 * expr: 指令的值(v-model=“student.name” 中的 student.name)
4 */
5let CompileUtil = {
6 // ---------------------- 匹配指令或者表达式的函数 ----------------------
7 // 匹配 v-model
8 model(node, expr, vm) {
9 let fn = this.updater[“modelUpdater”];
10 new Watcher(vm, expr, newValue => {
11 // 给输入框添加一个观察者,如果数据更新了,会触发此方法,将新值付给 input
12 fn(node, newValue);
13 });
14 // 给 input 绑定事件
15 node.addEventListener(“input”, e => {
16 let value = e.target.value; // 获取用户输入的内容
17 this.setValue(vm, expr, value);
18 });
19 let value = this.getValue(vm, expr);
20 fn(node, value);
21 },
22
23 // ---------------- 其他用到的工具函数 -------------------
24 // $data取值 [student, name]
25 getValue(vm, expr) {
26 return expr.split(“.”).reduce((data, current) => {
27 return data[current];
28 }, vm.$data);
29 },
30
31 // 给 vm.$data 中数据赋值
32 setValue(vm, expr, value) {
33 expr.split(“.”).reduce((data, current, index, arr) => {
34 // 如果遍历取到最后一个,我就给赋值
35 if (index == arr.length - 1) {
36 return (data[current] = value);
37 }
38 return data[current];
39 }, vm.$data);
40 },
41
42 // -------------- 给对应的 dom 进行赋值 -------------------
43 updater: {
44 modelUpdater(node, value) {
45 // 处理指令结点 v-model
46 node.value = value;
47 }
48 }
49};
以上就会触发这个函数:
1// 匹配 v-model
2model(node, expr, vm) {
3 let fn = this.updater[“modelUpdater”];
4 new Watcher(vm, expr, newValue => {
5 // 给输入框添加一个观察者,如果数据更新了,会触发此方法,将新值付给 input
6 fn(node, newValue);
7 });
8 // 给 input 绑定事件
9 node.addEventListener(“input”, e => {
10 let value = e.target.value; // 获取用户输入的内容
11 this.setValue(vm, expr, value);
12 });
13 let value = this.getValue(vm, expr);
14 fn(node, value);
15},
同时我们看到了 new Watch 对该属性创建一个观察者,用于以后数据更新时,通知视图进行相应的更新的。
1new Watcher(vm, expr, newValue => {
2 // 给输入框添加一个观察者,如果数据更新了,会触发此方法,将新值付给 input
3 fn(node, newValue);
4});
同时又给 input 绑定了一个事件,用于实现对 input 框的监听,相对应的 data 也要更新,这就实现了v-model输入框的双向绑定功能。
1// 给 input 绑定事件
2node.addEventListener(“input”, e => {
3 let value = e.target.value; // 获取用户输入的内容
4 this.setValue(vm, expr, value);
5});
每当 data 数据被改变,我们就触发 this.updater 中的视图更新函数。
1let fn = this.updater[“textUpdater”];
2fn(node, value);
1// 给 dom 文本结点赋值数据
2updater: {
3 modelUpdater(node, value) {
4 // 处理指令结点 v-model
5 node.value = value;
6 }
7}
对于文本节点,调用 this.compileText(child) 方法和以上同样的实现方法。这一部分的整体实现代码如下:
1**
2 * 工具类(把数据插入到 DOM 中)
3 * expr: 指令的值(v-model=“school.name” 中的 school.name)
4 */
5let CompileUtil = {
6 // $data取值 [school, name]
7 getValue(vm, expr) {
8 return expr.split(“.”).reduce((data, current) => {
9 return data[current];
10 }, vm.$data);
11 },
12
13 // 给 vm.$data 中数据赋值
14 setValue(vm, expr, value) {
15 expr.split(“.”).reduce((data, current, index, arr) => {
16 // 如果遍历取到最后一个,我就给赋值
17 if (index == arr.length - 1) {
18 return (data[current] = value);
19 }
20 return data[current];
21 }, vm.$data);
22 },
23
24 // 匹配 v-model
25 model(node, expr, vm) {
26 let fn = this.updater[“modelUpdater”];
27 new Watcher(vm, expr, newValue => {
28 // 给输入框添加一个观察者,如果数据更新了,会触发此方法,将新值付给 input
29 fn(node, newValue);
30 });
31 // 给 input 绑定事件
32 node.addEventListener(“input”, e => {
33 let value = e.target.value; // 获取用户输入的内容
34 this.setValue(vm, expr, value);
35 });
36 let value = this.getValue(vm, expr);
37 fn(node, value);
38 },
39
40 html(node, expr, vm) {
41 //xss
42 let fn = this.updater[“htmlUpdater”];
43 new Watcher(vm, expr, newValue => {
44 console.log(newValue);
45 fn(node, newValue);
46 });
47 let value = this.getValue(vm, expr);
48 fn(node, value);
49 },
50
51 // 获取 {{a}} 中的值
52 getContentValue(vm, expr) {
53 // 遍历表达式 将内容 重新特换成一个完整的内容 返还出去
54 return expr.replace(/{{(.+?)}}/g, (…args) => {
55 return this.getValue(vm, args[1]);
56 });
57 },
58
59 // v-on:click=“change”
60 on(node, expr, vm, eventName) {
61 node.addEventListener(eventName, e => {
62 vm[expr].call(vm, e);
63 });
64 },
65
66 // 可能存在 {{a}} {{b}} 多个样式
67 text(node, expr, vm) {
68 let fn = this.updater[“textUpdater”];
69 let content = expr.replace(/{{(.+?)}}/g, (…args) => {
70 // 给表达式 {{}} 中的值添加一个观察者,如果数据更新了,会触发此方法
71 new Watcher(vm, args[1], () => {
72 fn(node, this.getContentValue(vm, expr)); // 返回一个全新的字符串
73 });
74 return this.getValue(vm, args[1]);
75 });
76 fn(node, content);
77 },
78
79// 给 dom 文本结点赋值数据
80updater: {
81 modelUpdater(node, value) {
82 // 处理指令结点 v-model
83 node.value = value;
84 },
85 textUpdater(node, value) {
86 // 处理文本结点 {{}}
87 node.textContent = value;
88 },
89 htmlUpdater(node, value) {
90 // 处理指令结点 v-html
91 node.innerHTML = value;
92 }
93}
94};
3、塞回页面
此时,我们将渲染好的 fragment 塞回到真实 DOM中就可以正常显示了。
1this.el.appendChild(fragment);
当我们在输入框中输入数据时,相对应的视图上 {{student.name}} 的地方进行实时的更新;当我们通过 vm.$data.student.name 改变数据时,输入框内的数据也会发生改变。
从头到尾我们实现了一个双向绑定。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
总结
-
框架原理真的深入某一部分具体的代码和实现方式时,要多注意到细节,不要只能写出一个框架。
-
算法方面很薄弱的,最好多刷一刷,不然影响你的工资和成功率😯
-
在投递简历之前,最好通过各种渠道找到公司内部的人,先提前了解业务,也可以帮助后期优秀 offer 的决策。
-
要勇于说不,对于某些 offer 待遇不满意、业务不喜欢,应该相信自己,不要因为当下没有更好的 offer 而投降,一份工作短则一年长则 N 年,为了幸福生活要慎重选择!!!
喜欢这篇文章文章的小伙伴们点赞+转发支持,你们的支持是我最大的动力!
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-jdnPX115-1712263890823)]
总结
-
框架原理真的深入某一部分具体的代码和实现方式时,要多注意到细节,不要只能写出一个框架。
-
算法方面很薄弱的,最好多刷一刷,不然影响你的工资和成功率😯
-
在投递简历之前,最好通过各种渠道找到公司内部的人,先提前了解业务,也可以帮助后期优秀 offer 的决策。
-
要勇于说不,对于某些 offer 待遇不满意、业务不喜欢,应该相信自己,不要因为当下没有更好的 offer 而投降,一份工作短则一年长则 N 年,为了幸福生活要慎重选择!!!
喜欢这篇文章文章的小伙伴们点赞+转发支持,你们的支持是我最大的动力!
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算