从0到1实现VUE

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啦~下一节会详细讲解,下面是下一节的原理图。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值