vue的原理相信不少同学都已经看过,源码分析也有许多大牛分析过。不过不知道大家有没有这种的感觉,原理看起来很明白了,但是源码一看就懵。最近在学习尤大在frontend masters的课。(Advanced Vue.js Features from the Ground Up)突然觉得有点豁然开朗。那为什么不能从Ground Up开始实现一个vue.js呢。所以决定按照尤大课上的思路写一写。作者说过vue是渐进式的框架,那么我想也用渐进式的思路去写写。每一步都去解决一点问题。
leason 1 响应式的实现
我们将响应式的实现分为3步,最终实现一个能够双向数据绑定的Vue
- 转化:这里我们将实现一个convert函数能够监听响应式的数据变化
- 依赖的处理:对数据的依赖进行存储,收集和触发
- 编译:将响应式的数据与视图进行绑定
1、转化:convert函数实现
目标:实现b的值能够跟随a值自动变化?
let data = {
a1 : 1,
a2 : 2
}
b = data.a1*10;
data.a1 = 2;
console.log(b) //20 b不需要重新复制就可以自动跟随data.a1变化
复制代码
分析:
- convert应该监听到a给b赋值,并且能够a变化后,同时更改b的值
- 可以利用Object.defineProperty()中的set和get去实现(vue.2x)
- 可以利用es6中的Proxy中的set和get去实现(vue.3x)
代码:
function convert(obj){
let arr = Object.keys(obj); // 将对象中的key值取出
arr.forEach((key)=>{ // 每个属性用 Object.defineProperty转化
let inertValue = obj[key]; // 保存初始值
Object.defineProperty(obj,key,{
get(){
return inertValue; // 访问时给出key的值
},
set(newValue){
inertValue = newValue; // 赋值时将新值赋给inertValue
b = data.a1*10; // 执行相关响应式操作
}
})
})
}
convert(data)
复制代码
- convert函数中重点是对赋值操作进行监听,当每次取值时,自动执行需要响应的操作
- 不难看出convert函数虽然实现了我们的需求,但显然是不够通用
问题:首先我们还需要把所有依赖data.a1的操作手动写到set函数中去,其次在给data中其他属性(如data.a2)赋值时也会触发set中的操作
2、依赖的处理
class Dep{
constructor(){
this.subscribers = []
}
depend(){
this.subscribers.push(saveFunction);
}
notify(){
this.subscribers.forEach(fn=>fn())
}
}
let saveFunction;
function autorun(fn){
saveFunction = fn;
fn();
saveFunction = null;
}
复制代码
- 我们定义了一个Dev的类,它需有存放,收集,和触发依赖的属性和方法
- 为了能够保存操作,我们需要将所有操作保存在函数中
- 所以我们定义了一个autorun函数,它参数fn用来接收来包裹依赖的操作
- fn是autorun的一个参数,我们在this.subscribers.push中无法访问,用一个全局变量saveFunction去传递
function convert(obj){
Object.keys(data).forEach((key)=>{
let dep = new Dep(); //为每个key创建一个dep实例
let inertValue = obj[key];
Object.defineProperty(obj,key,{
get(){
dep.depend() //当取值时,收集依赖
return inertValue;
},
set(newValue){
inertValue = newValue;
dep.notify() //当设置值时,触发依赖
}
})
})
}
let data = {
a1 : 1,
a2 : 2
}
convert(data)
autorun(()=>{b = data.a1*10;}) //将所有响应式的操作通过autorun去执行
data.a1 = 2;
console.log(b) //20, b值可以跟随a值去自动响应
复制代码
- 在对data数据转化时,我们为每个key创建一个dep实例
- 当get时,收集依赖,当set时,触发依赖
- 将所有响应式的操作通过autorun去执行
问题:当我们在set中触发依赖的函数执行时,就会重新出发get收集依赖?
depend(){
if(!saveFunction){
this.subscribers.push(saveFunction);
}
}
复制代码
- 我们需要在dep中去判断一下saveFunction是否为null
- saveFunction是一个全局变量,所以以后我们可以将它定义为Dep的属性——Dep.target
问题:现在我们已经很好的完成了对数据的响应式处理,如果操作的是视图我们该如何绑定呢?
3、编译:数据与视图进行绑定
目标:实现一个数据绑定
<div id = "app">
<div>{{message}}</div>
<input v-modle = "message" >
</div>
let vm = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
复制代码
分析:
- 首先我们data应该被转化成为响应式的数据
- 然后在我们需要将模版中的数据,事件进行编译
- 在编译的过程中,我们进行收集依赖
function Vue (options){
Observe(options.data,this) //将data数据进行转化
let dom = document.getElementById(options.el); //获模版的dom元素
let newDom = compile(dom,this); //对dom元素进行编译
dom.appendChild(newDom); //添加到文本中
}
复制代码
- 定义一个Vue的构造函数
- 我们将上文中的convert函数名字修改为Observe,对数据进行响应式转化
- 在上文中,我们通过data.a去访问,在vue中我们希望把data中的属性绑定在vue实例中,所以传入this
- 然后我们应该将模版中的dom对象与数据进行绑定,得到新的newDom
- 最后添加到dom中
- 所以接下来我们看看我们如何去进行编译的,在编译过程中如何收集的依赖?
function compile(node,vm){
let frag = document.createDocumentFragment();
let child = node.firstElementChild;
while(child){
compileElement(child,vm)
child = child.nextElementSibling;
}
return frag;
}
复制代码
- 首先我们创建一个节点片段,然后对每个子节点进行处理
- 这里我们只考虑一层子节点,因为今天主要写的是响应式,对dom树的遍历会在虚拟dom中实现
- 然后我们需要对子节点分别去处理通过compileElement(child,vm)函数
- 下面我们就来实现这个compileElement函数
function compileElement(node,vm){
let attr = node.attributes;
for(let i = 0; i<attr.length ; i++){
let name = attr[i].nodeValue;
console.log(name)
switch (attr[i].nodeName) {
case "v-model":
node.addEventListener("input",(e)=>{
vm[name] = e.target.value;
console.log(e.target.value)
})
autorun(()=>{ node.value = vm[name]})
break;
}
}
let reg = /\{\{(.*)\}\}/;
if(reg.test(node.innerHTML)){
var name = RegExp.$1;
name = name.trim();
autorun(()=>{ node.innerHTML = vm[name]})
}
}
复制代码
- 两次传进来的node应该分别是 div和 input
- so我只实现了对节点中{{message}}和"v-model"的判断~哈哈,偷偷懒啦
- 根据v-modle的所有我们应该给node绑定一个input事件,但输入时改变vm上绑定的值
- 我们将所有赋值的操作在autorun中执行,这样就能收集到依赖
- 当vm上的值改变时,就能重新执行autorun函数中的操作
- autorun函数中的操作,都是dom操作,对dom进行修改
问题:我们发现autorun函数执行都是对dom的属性值的修改,所以我们可以进行进一步的抽象
class Watcher{
constructor(node,type,vm,name){
Dep.target = this;
this.node = node;
this.name = name;
this.type = type;
this.vm = vm;
this.update();
Dep.target = null;
}
update(){
this.node[this.type] = this.vm[this.name];
}
}
new Watcher(node,"value" ,vm ,name )
//autorun(()=>{ node.value = vm[name]})
new Watcher(node,"innerHTML" ,vm ,name )
//autorun(()=>{ node.value = vm[name]})
复制代码
- 我们将原来autorun函数的功能封装成一个类
- 然后将参数传进行进行保存,然后执行update方法时对依赖进行执行
现在我们已经实现了Vue的响应式,是不是更加深刻理解vue的MVVM模式是如何实现的啦。
以上就是Vue响应式的核心原理,编译过程与vue1x版本类似,因为vue.2x引用了虚拟Dom过程会比较复杂。会在下一篇专门去讲。
问题:这里大家思考一个问题,在这一版本中,Vue直接用watcher操作的Dom, 如果我们绑定的数据是一个数组,当只更改数组中的一小部分,此时Dom重新渲染的效率会不会很低。我们该如何改进,哈哈哈,当然是使用虚拟dom啦~下一节会详细讲解,下面是下一节的原理图。