大家在面试vue岗位的时候,一定经常被问到一些原理性的问题。带大家手写一个微型的vue框架,彻底弄懂这类问题。
完整代码可以从我的github上获取,记得给个star
[手写一个微型的vue框架
github.com/ddhujiang/FVue](link.zhihu.com/?target=htt…)
我们平时在使用vue的时候,经常会new 一个vue实例,你知道它具体进行了那些操作吗?
其实在new Vue()之后,vue会调用进行初始化,会初始化生命周期、事件、props、methods、data、computed与watch等。其中最重要的是通过Object.defineProperty设置setter与getter,用来实现响应式以及依赖收集。
初始化之后调用$mount 挂载组件
接下来,我们主要从下面是三个方向,来仿写一个vue框架
一、响应式系统
当属性的值改变时,是如何触发视图更改的
我们Vue2用的defineProperty实现的数据响应,基于发布订阅模式,其主要包含三部分:Observer、Dep、Watcher。
一、Observer进行数据的遍历,利用Object.defineProperty()进行数据的劫持。但数据不止一种类型,所以需要进行递归判断
//判断data是否是对象,是对象则遍历
observe(value) {
if (!value || typeof value !== "object") {
return;
}
// 遍历该对象
Object.keys(value).forEach(key => {
this.defineReactive(value, key, value[key]);
});
}
// 数据响应化
defineReactive(obj, key, val) {
//递归解决数据嵌套
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
// console.log('get-----------------');
Dep.target && dep.addDep(Dep.target);
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
// console.log('set---------------');
val = newVal;
dep.notify();
}
});
}
二、Watcher 监听器
监听订阅数据data的属性,但它是怎样和Observe联系起来的呢?奥秘就在这行代码
this.vm[this.key]; // 触发getter,添加依赖
读取vue实例上的属性,在这之前data下的属性已经被我们劫持为访问器属性了,那这就表明我们能触发对应属性的get函数,那这就与observer产生了关联,那既然如此,那在触发get函数的时候能不能把触发者Watch给收集起来呢?此时就得需要一个桥梁Dep来协助了。
//Watcher 监听器
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// 将当前watcher实例指定到Dep静态属性target
Dep.target = this;
this.vm[this.key]; // 触发getter,添加依赖
Dep.target = null;
}
update() {
console.log("属性更新了");
//执行new Watcher传递过来的回调函数,即data值改变时,需要执行的函数。
this.cb.call(this.vm, this.vm[this.key]);
}
}
三、 Dep:用来管理watcher
思路应该是data下的每一个属性都有一个唯一的Dep对象,在get中收集仅针对该属性的依赖,然后在set方法中触发所有收集的依赖
class Dep {
constructor() {
// 这里存放每个依赖的1个或多个watcher
this.deps = [];
}
// 在deps中添加一个监听器对象
addDep(dep) {
this.deps.push(dep);
console.log('jjjjjjjjj', this.deps)
}
// 通知所有依赖去更新视图
notify() {
this.deps.forEach(dep => dep.update());
}
}
存在的问题
现在简易的响应式系统已经实现了,但它还是存在一定问题的。比如我们直接给对象添加新的属性是监听不到了,修改数组的元素值也是如此
- 对于对象:
Vue中提供了Vue.set和vm.$set这两个方法供我们添加新的属性,其原理就是先判断该属性是否为响应式的,如果不是,则通过defineReactive方法将其转为响式。 - 对于数组:直接使用下标修改值还是无效的,
Vue只hack了数组中的七个方法:pop','push','shift','unshift','splice','sort','reverse',使得我们用起来依旧是响应式的。其原理是:在我们调用数组的这七个方法时,Vue会改造这些方法,它内部同样也会执行这些方法原有的逻辑,只是增加了一些逻辑:取到所增加的值,然后将其变成响应式,然后再手动出发dep.notify()
但直接通过下标修改数组的元素,还是无法实现响应式。
二、编译Compile
vue 的模板语句html 根本就不能识别,所以需要通过编译解析一些特殊的指令,如v-if、v-model(双向绑定) 等。编辑的过程可以进行依赖收集,让数据模式和视图之间产生了联系。以后数据模式发生变化了,就可以通知视图更新。
对于特殊指令的解析,主要是利用正则表达式。我们主要看下v-model的解析,其它指令的解析可以直接看代码
把v-model放在inpute上,在编译的时候会解析v-model,把v-model所属的元素上添加一个事件监听,inpute变化的时候,就可以把最新的值设置到vue的实例上,vue实例已经实现了数据响应化,它的响应化的set函数会通知所有的依赖去做更新。
// 双绑
model(node, vm, exp) {
// 指定input的value属性
this.update(node, vm, exp, "model");
// 视图对模型响应
node.addEventListener("input", e => {
vm[exp] = e.target.value;
});
}
modelUpdater(node, value) {
node.value = value;
}
三、虚拟dom
虚拟dom的讲解,可以看我之前的这篇文章
❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章
关注公众号_前端码头_,获取独家学习路线+博主私人微信,更多前端小干货等着你喔