文章内容输出来源:拉勾教育-大前端高薪训练营
心得
不知不觉,已经连续学习两个多月了(这真是个奇迹,Amazing!)。到底是什么魔力让我这样平时上课就犯困,学习都是从入门到放弃的人,能一直坚持学习,并且还有了写博客的冲动?,我想在开头,非常有必要说一下这段时间参加“拉勾教育-大前端高薪训练营”学习的一些心得体会:
首先是课程,说实话从我看到这门课程的大纲之后,我就觉得,靠谱,就是它了。课程总共包含了8个大模块(详见截图),几乎包含了前端工程师必备的所有技能;而且课程结构非常合理,内容循序渐进,由浅而深,非常适合我这样有一定基础和开发经验,但是知识体系不够全面,不够深入的前端工程师。可谓是确认过眼神,这就是对的课程(爱了爱了),于是就果断下手啦。两个多月学习下来,目前已经学到模块3了,真的发现,自己无论从基础知识、技术认知还是工作方法,都有了质的提升。
好的课程千千万,不坚持到底都不算(这算不算双押?哈哈)。而让我坚持学习的动力,除了课程,还有就是老师和同学们了。让我映像最深,感触最多的当然是zce(汪磊)老师了。汪老师可谓是真的把课讲活了,枯燥的知识点,在他生动的课件演示和细心讲述下,真的是想不会都很难。就拿前端工程化课程来说,平时看起来那么复杂的东西,听完汪老师的课之后,现在我已经能把plop、gulp、webpack这些工具用到实际项目开发中了,甚至还用yeoman手写了自己的脚手架,提升了团队的开发效率。除了讲课有趣,最最最主要的还是责任感,为人师表、传道受业的责任感,每次直播课,汪老师都是讲到深夜12点才结束,不给我们讲明白,不罢休。大家也可以去看看他Github,从中能学到很多东西。当然除了汪老师,还有深夜“提刀”催学习的班主任-小雪老师, 颜值导师-熊熊老师,“美女”导师-小北老师以及天天一起水群、讨论、争辩的同学们,跟优秀的人在一起学习,真的让我体会到了学习的乐趣。
正文
在手写Vue之前,有三个必要的知识点需要先讲一下,那就是数据驱动的概念、发布/订阅模式和观察者模式。
1. 数据驱动
核心概念
- 数据响应式
- 以普通的JavaScript对象作为数据模型,修改数据的时候,视图会自动更新,避免了繁琐的dom操作,提高开发效率
- 双向绑定
- 数据改变,视图改变,视图改变 ,数据也随之改变
- 具体表现,可以通过v-model创建双向绑定
- 数据驱动
核心原理
- Vue2.x
- 基于Object.defineProperty,对数据进行劫持,在程序启动的时候,给data属性设置getter和setter,当数据发生变化的时候,自动进行视图的更新
const data = {
msg: 'Hello'
}
const vm = {}
Object.defineProperty(vm, 'msg', {
//监听数据的获取
get(key) {
console.log('data get')
return data.msg
},
//监听数据的赋值
set(val) {
//判断值是否相同,相同则不作任何操作
if (data.msg === val) return
//不相同的话,赋值,然后改变dom
data.msg = val;
document.getElementById('app').textContent = val;
}
})
//触发数据的变化,更新dom
function textInput(e) {
vm.msg = e.value;
}
- Vue3.x
- 使用Proxy代理对象,从而实现对数据的响应式处理(Proxy比defineProperty功能更强大,此外Proxy还是非入侵的数据劫持,感兴趣的可以进一步了解)
const data = {
msg: "",
};
const dataProxy = new Proxy(data, {
get(target, key) {
return target[key];
},
set(target, key, value) {
target[key] = value;
if (key == "msg") {
document.getElementById("app").textContent = value;
} else {
console.log(key + ":changed");
}
},
});
function textInput(target) {
dataProxy.msg = target.value;
}
2. 发布/订阅、观察者模式
发布/订阅模式
- 概念
- 以一个普通的JavaScript对象为“信号中心”, 记录注册的所有事件
- 通过$on函数注册一个事件,存储到对应的事件数组中
- 通过$emit函数触发一个事件
- 实现
class EventEmiter {
constructor() {
this.subs = Object.create(null);
}
// 注册事件
$on(eventType, eventHandler) {
this.subs[eventType] = this.subs[eventType] || [];
this.subs[eventType].push(eventHandler);
}
// 触发事件
$emit(eventType, datas) {
this.subs[eventType] && this.subs[eventType].forEach(handler => {
handler(datas);
})
}
}
//使用
const em = new EventEmiter();
//注册事件
em.$on('input', value => {
document.getElementById('app').textContent = value;
})
//触发事件
function onTextInput(target) {
em.$emit('input', target.value)
}
观察者模式
- 概念
- 观察者(订阅者,可以想象成租/购房客户,将自己的租/购房意愿告诉中介)–watcher
- update(): 当事件发生时,具体要做的事情
- 目标(发布者,可以想象成一个房屋中介,房屋降价、有新房源的时候发送通知给客户)–Dep
- subs数组:存储所有的观察者
- addSub():添加观察者
- notify(): 当事件发生,调用所有观察者的update方法
- 观察者(订阅者,可以想象成租/购房客户,将自己的租/购房意愿告诉中介)–watcher
- 实现
//Dep类,添加观察者,通知观察者
class Dep {
constructor() {
this.subs = []
}
add(sub) {
if (sub && sub.update && typeof sub.update == 'function') {
this.subs.push(sub)
}
}
notify(datas) {
this.subs.forEach(sub => {
sub.update(datas)
})
}
}
//观察者,接受一个回调参数,时间触发的时候调用回调函数
class Watcher {
constructor(callback) {
this.callback = callback;
}
update(datas) {
typeof this.callback == 'function' && this.callback(datas);
}
}
//实例化发布者
const dep = new Dep();
//实例化观察者并注册回调
const watcher = new Watcher(onInput);
//添加观察者到发布者中
dep.add(watcher);
//事件函数,事件发生是通过发布者通知所有观察者
function onTextInput(target) {
dep.notify(target.value)
}
//回调函数
function onInput(datas) {
document.getElementById('app').textContent = datas;
}
两者区别
- 观察者模式是由具体的目标调度,比如当事件触发,Dep就会调用观察者的方法,所以观察者模式的订阅者与发布者之间存在依赖
- 发布/订阅模式由统一的调度中心调用,因此发布者和订阅者不需要知道对方的存在
3. Vue响应式原理模拟
- 分析过程
- Vue的基本结构
const vueInstance = new Vue({
el: "#app",
router,
render: h => h(App)
}).$mount("#app");
- 打印Vue实例并观察其属性
- 整体结构
- 实现Vue
a. 功能
b. Vue类
- 属性
- $options -构造函数传入的参数
- $el - 挂载的元素(选择器或者dom元素)
- $data - 数据
- 方法
- _proxyData() - 将数据转换成getter/setter
- 实现
/**
* 属性
* - $el:挂载的dom对象
* - $data: 数据
* - $options: 传入的属性
* 方法:
* - _proxyData 将数据转换成getter/setter形式
*/
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data || Object.create(null);
this.$el =
typeof options.el === "string"
? document.querySelector(options.el)
: options.el;
this._proxyData(this.$data);
// 监测数据的变化,渲染视图
new Observer(this.$data);
}
// 将数据代理到vue(this)中,并使数据是响应式的。使数据能通过this.xxx访问并复制
_proxyData(data) {
Reflect.ownKeys(data).forEach((key) => {
Reflect.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(newValue) {
if (newValue == data[key]) {
return;
}
data[key] = newValue;
},
});
});
}
}
c. Observer
- 功能:
- 把$data中的属性,转换成响应式数据
- 如果$data中的某个属性也是对象,把该属性转换成响应式数据
- 数据变化的时候,发送通知
- 类图
- 方法
- walk(data) - 遍历data属性,调用defineReactive将数据转换成getter/setter
- defineReactive(data, key, value) - 将数据转换成getter/setter
- 方法
import Dep from './Dep.js'
class Observer {
constructor(data) {
this.walk(data);
}
walk(data) {
// 如果data为空或者或者data不是对象
if (!data || typeof data !== "object") {
return;
}
Reflect.ownKeys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
defineReactive(data, key, value) {
const that = this;
// 给每个data添加一个观察者
const dep = new Dep();
// 递归检测属性值是否对象,是对象的话,继续将对象转换为响应式的
this.walk(value);
Reflect.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
// 实例化Wathcer的时候,会获取并缓存对应的值,触发get,此时将watcher添加到dep
// 获取watcher实例,并添加到dep中
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) {
if (newValue == value) {
return;
}
// 此处形成了闭包,延长了value的作用域
value = newValue;
// 属性被赋予新值的时候,将检查属性是否对象,是对象则将属性转换为响应式的
that.walk(newValue);
// 数据变化,发送通知,触发watcher的pudate方法
dep.notify();
},
});
}
}
export default Observer
d. Compiler
- 功能
- 编译模板,解析指令/插值表达式
- 负责页面的首次渲染
- 数据变化后,重新渲染视图
- 属性
- el -app元素
- vm -vue实例
- 方法
- compile(el) -编译入口
- compileElement(node) -编译元素(指令)
- compileText(node) 编译文本(插值)
- isDirective(attrName) -(判断是否为指令)
- isTextNode(node) -(判断是否为文本节点)
- isElementNode(node) - (判断是否问元素节点)
/**
. 属性
• el -app元素
• vm -vue实例
• 方法
• compile(el) -编译入口
• compileElement(node) -编译元素(指令)
• compileText(node) 编译文本(插值)
• isDirective(attrName) -(判断是否为指令)
• isTextNode(node) -(判断是否为文本节点)
• isElementNode(node) - (判断是否问元素节点)
*/
import Watcher from "./Watcher.js";
class Compiler {
constructor(vm) {
this.vm = vm;
this.el = vm.$el;
this.compile(this.el);
}
compile(el) {
if (!el) return;
const nodes = el.childNodes;
//收集
Array.from(nodes).forEach((node) => {
if (this.isTextNode(node)) {
this.compileText(node);
} else if (this.isElementNode(node)) {
this.compileElement(node);
}
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
update(node, value, attrName, key) {
const updateFn = this[`${attrName}Updater`];
updateFn && updateFn.call(this, node, value, key);
}
textUpdater(node, value, key) {
node.textContent = value;
}
modelUpdater(node, value, key) {
node.value = value;
node.addEventListener("input", (e) => {
this.vm[key] = node.value;
});
}
compileElement(node) {
Array.from(node.attributes).forEach((attr) => {
if (this.isDirective(attr.name)) {
const attrName = attr.name.substr(2);
const key = attr.value;
const value = this.vm[key];
this.update(node, value, attrName, key);
// 数据更新之后,通过wather更新视图
new Watcher(this.vm, key, (newValue) => {
this.update(node, newValue, attrName, key);
});
}
});
}
compileText(node) {
/**
* . 表示任意单个字符,不包含换行符
* + 表示匹配前面多个相同的字符
* ?表示非贪婪模式,尽可能早的结束查找
* */
const reg = /\{\{(.+?)\}\}/;
var param = node.textContent;
if (reg.test(param)) {
// $1表示匹配的第一个
const key = RegExp.$1.trim();
node.textContent = param.replace(reg, this.vm[key]);
// 编译模板的时候,创建一个watcher实例,并在内部挂载到Dep上
new Watcher(this.vm, key, (newValue) => {
// 通过回调函数,更新视图
node.textContent = newValue;
});
}
}
isDirective(attrName) {
return attrName && attrName.startsWith("v-");
}
isTextNode(node) {
return node && node.nodeType === 3;
}
isElementNode(node) {
return node && node.nodeType === 1;
}
}
export default Compiler;
4. Dep
- 功能
- 收集观察者
- 触发观察者
- 属性
- subs:Array
- target:Watcher
- 方法
- addSub(sub) -添加观察者
- notify() -触发观察者的update
/**
* 观察者类
*/
export default class Dep {
constructor() {
this.subs = [];
}
// 添加观察者
addSub(sub) {
if (sub && sub.update && typeof sub.update === "function") {
this.subs.push(sub);
}
}
// 发送通知
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
5. Watcher
- 功能
- 生成观察者更新视图
- 将观察者实例挂载到Dep类中
- 数据发生变化的时候,调用回调函数更新视图
- 属性
- vm -vue实例
- key -观察的键
- cb -回调函数
- 方法
- update()
/**
* 属性:
* vm -vue实例
* key -观察的元素的key
* cb -注册一个回调,变化的时候调用
*/
import Dep from "./Dep.js";
export default class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// oldValue缓存之前,将watcher实例挂载到Dep
Dep.target = this;
// 缓存旧值
this.oldValue = vm[key];
// get值之后,清除Dep中的实例
Dep.target = null;
}
update() {
// 调用update的时候,获取新值
const newValue = this.vm[this.key];
// 比较,相同则不更新
if (this.oldValue === newValue) {
return;
}
this.cb(newValue);
}
}
总结
那么到这里,我们的简版Vue写完了,其核心就是通过Object.defineProperty(),监听数据的获取和赋值。在获取数据(getter)的时候,给属性设置watcher,并添加到dep中;在数据赋值(setter)的时候,通过dep.notify()发送更新通知,遍历注册的watcher,调用watcher的update方法,实现视图的更新。具体代码可以从github中获取,有任何疑问欢迎在评论区留言。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 小灰🌾🐛 原文链接:https://juejin.im/post/6854573222533267463