手动实现Vue(1.0)

作者: 有一种感动叫做丶只有你懂 | 来源:发表于2021-02-24 17:27 被阅读0次

现在vue3.0版本已经发布了,各种工具也已经对vue3.0做了支持,最近有时间,想学习下源码,这边文章主要是对vue1.0版本的实现,后续会更新出vue2.0和vue3.0

所有的代码可以复制粘贴,即可正常运行

前言

我们在面试的时候,面试官经常问的一个问题就是,你知道vue响应式的原理吗,说不定还会让你手写一个vue,今天的文章就带你实现一个1.0版本的vue,吊打面试官。

代码分割

我们将代码分为以下几个模块,依次去实现:

  • 1.响应式处理
  • 1.5设置代理
  • 3.模板编译
  • 4.响应式更新

需要具备技能

  • Object.defineProperty的使用。
  • 闭包的使用

前期准备

文件目录:我们今天实现的是vue1.0版本的关注vue1.0的文件夹即可


image.png

reactive.html内容

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="./vue.js"></script>
    <title>vue 1.0 源码实现</title>
  </head>
  <body>
    <div id="app">{{counter}}</div>
    <script>
      const app = new Vue({
        el: "#app",
        data: {
          counter: 0,
        },
      });
      setInterval(() => {
        app.counter += 1;
      }, 1000);
    </script>
  </body>
</html>

编写代码

1.响应式处理
class Vue {
  constructor (options) {
    this.$options = options; //接收用户传来的参数
    this.$data = options.data;
    // 1、对data进行响应式处理
    observe (this.$data);
    // 1.5、设置代理
    // 2.模板编译
  }
}

/**
 * 将传入的值进行响应式处理
 * @param {*} value 
 */
function observe (value) {
  if (Array.isArray (value)) {
    // TODO数组的响应式处理
  } else {
    // 对象的响应式处理
    Object.keys (value).forEach (key => {
      defineReactive (value, key, value[key]);
    });
  }
}

/**
 * 利用defineReactive,将对象的属性进行拦截,也就是响应式处理 
 * @param {*} obj 
 * @param {*} key 
 * @param {*} val 
 */
function defineReactive (obj, key, val) {
  Object.defineProperty (obj, key, {
    get () {
      console.log ('get', key);
      return val;
    },
    set (newVal) {
      if (newVal !== val) {
        console.log ('set', key);
        val = newVal;
      }
    },
  });
}

理解:
1.我们创建了Vue类,将用户传入的options赋值给$options,将options.data赋值给$data
2.声明了observe函数,这个函数主要是判断传入的值是对象还是数组(数组的响应式处理暂时没做,只做了对象的响应式处理),如果是对象就遍历这个对象传入到defineReactive函数中,defineReactive是闭包
验证:
响应式处理做完了,此时按照我们reactive.html文件中的代码,定时器访问定时访问app.counter,会触发get方法,从而我们可以在控制台看到输出
结果:
没有任何输出,此时我们还缺一步,设置代理,因为app是Vue类这个实例,实例上并没有counter这个属性

1.5设置代理

我们在上面的代码,继续编写

class Vue {
  constructor (options) {
    this.$options = options; //接收用户传来的参数
    this.$data = options.data;
    // 1、对data进行响应式处理
    observe (this.$data);
    // 1.5、设置代理
    proxy (this);
    // 2.模板编译
  }
}

/**
 * 将传入的值进行响应式处理
 * @param {*} value 
 */
function observe (value) {
  if (Array.isArray (value)) {
    // TODO数组的响应式处理
  } else {
    // 对象的响应式处理
    Object.keys (value).forEach (key => {
      defineReactive (value, key, value[key]);
    });
  }
}

/**
 * 利用defineReactive,将对象的属性进行拦截,也就是响应式处理 
 * @param {*} obj 
 * @param {*} key 
 * @param {*} val 
 */
function defineReactive (obj, key, val) {
  Object.defineProperty (obj, key, {
    get () {
      console.log ('get', key);
      return val;
    },
    set (newVal) {
      if (newVal !== val) {
        console.log ('set', key);
        val = newVal;
      }
    },
  });
}

/**
 * 设置代理
 */
function proxy (vm) {
  Object.keys (vm.$data).forEach (key => {
    Object.defineProperty (vm, key, {
      get () {
        return vm.$data[key];
      },
      set (newVal) {
        if (newVal !== vm.$data[key]) {
          vm.$data[key] = newVal;
        }
      },
    });
  });
}

理解:
1.创建proxy方法,将当前的Vue实例传进去,利用defineProperty方法,将data中的属性,添加到当前实例上面
2.proxy方法是闭包,当访问app.counter的时候,最终会指向defineReactive中的get方法,返回val
验证:
访问app.counter的时候,是否有输出
结果
输出get counter

2.模板编译

接着上面的代码继续

class Vue {
  constructor (options) {
    this.$options = options; //接收用户传来的参数
    this.$data = options.data;
    // 1、对data进行响应式处理
    observe (this.$data);
    // 1.5、设置代理
    proxy (this);
    // 2.模板编译
    new Compile (options.el, this);
  }
}

/**
 * 将传入的值进行响应式处理
 * @param {*} value 
 */
function observe (value) {
  if (Array.isArray (value)) {
    // TODO数组的响应式处理
  } else {
    // 对象的响应式处理
    Object.keys (value).forEach (key => {
      defineReactive (value, key, value[key]);
    });
  }
}

/**
 * 利用defineReactive,将对象的属性进行拦截,也就是响应式处理 
 * @param {*} obj 
 * @param {*} key 
 * @param {*} val 
 */
function defineReactive (obj, key, val) {
  Object.defineProperty (obj, key, {
    get () {
      console.log ('get', key);
      return val;
    },
    set (newVal) {
      if (newVal !== val) {
        console.log ('set', key);
        val = newVal;
      }
    },
  });
}

/**
 * 设置代理
 */
function proxy (vm) {
  Object.keys (vm.$data).forEach (key => {
    Object.defineProperty (vm, key, {
      get () {
        return vm.$data[key];
      },
      set (newVal) {
        if (newVal !== vm.$data[key]) {
          vm.$data[key] = newVal;
        }
      },
    });
  });
}

/**
 * 模板编译
 */
class Compile {
  constructor (el, vm) {
    this.$el = document.querySelector (el);
    this.$vm = vm;
    this.compile (this.$el);
  }
  compile (node) {
    node.childNodes.forEach (node => {
      if (this.isInter (node)) {
        this.update (node);
      }
    });
  }
  isInter (node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test (node.textContent);
  }
  // node {{counter}}
  update (node) {
    const updater = () => {
      node.textContent = this.$vm[RegExp.$1];
    };
    updater ();
  }
}

理解:
1.我们新增了compile这个类,这个类接收一个宿主元素和当前的vm实例
2.利用document.querySelector查找到这个元素,遍历里面所有的子节点,通过isInter这个函数校验的子节点,必然是{{***}}这种类型的节点,将这个节点传入到update函数中,利用正则读取到当前data里面的key,从当前的$vm实例上面读取到key对应的value,然后直接设置node.textContent完成模板编译
验证:
html界面显示0
结果
模板编译成功,成功显示

响应式更新

我们都知道,响应式更新,是vue里面的精髓,我会竭尽所能,给大家描述清楚,如果有看不懂的,可以私信我,也算是一个交流的过程

class Vue {
  constructor (options) {
    this.$options = options; //接收用户传来的参数
    this.$data = options.data;
    // 1、对data进行响应式处理
    observe (this.$data);
    // 1.5、设置代理
    proxy (this);
    // 2.模板编译
    new Compile (options.el, this);
  }
}

/**
 * 将传入的值进行响应式处理
 * @param {*} value 
 */
function observe (value) {
  if (Array.isArray (value)) {
    // TODO数组的响应式处理
  } else {
    // 对象的响应式处理
    Object.keys (value).forEach (key => {
      defineReactive (value, key, value[key]);
    });
  }
}

/**
 * 利用defineReactive,将对象的属性进行拦截,也就是响应式处理 
 * @param {*} obj 
 * @param {*} key 
 * @param {*} val 
 */
function defineReactive (obj, key, val) {
  const dep = new Dep ();
  Object.defineProperty (obj, key, {
    get () {
      console.log ('get', key);
      Dep.target && dep.addDep (Dep.target);
      return val;
    },
    set (newVal) {
      if (newVal !== val) {
        console.log ('set', key);

        val = newVal;
        dep.notify ();
      }
    },
  });
}

/**
 * 设置代理
 */
function proxy (vm) {
  Object.keys (vm.$data).forEach (key => {
    Object.defineProperty (vm, key, {
      get () {
        return vm.$data[key];
      },
      set (newVal) {
        if (newVal !== vm.$data[key]) {
          vm.$data[key] = newVal;
        }
      },
    });
  });
}

/**
 * 模板编译
 */
class Compile {
  constructor (el, vm) {
    this.$el = document.querySelector (el);
    this.$vm = vm;
    this.compile (this.$el);
  }
  compile (node) {
    node.childNodes.forEach (node => {
      if (this.isInter (node)) {
        this.update (node);
      }
    });
  }
  isInter (node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test (node.textContent);
  }
  // node {{counter}}
  update (node) {
    const updater = () => {
      node.textContent = this.$vm[RegExp.$1];
    };
    updater ();
    new Watcher (updater, this.$vm, RegExp.$1);
  }
}

/**
 * 响应式更新
 */
class Dep {
  constructor () {
    this.watchers = [];
  }
  addDep (watcher) {
    this.watchers.push (watcher);
  }
  notify () {
    this.watchers.forEach (v => {
      v.update ();
    });
  }
}

class Watcher {
  constructor (updateFn, vm, key) {
    this.$updateFn = updateFn;
    this.$vm = vm;
    this.$key = key;

    Dep.target = this;
    this.$vm[this.$key];
    Dep.target = null;
  }
  update () {
    this.$updateFn ();
  }
}

理解:
1.创建了Dep和Watcher俩个类
2.vue1.0是一个key对应的一个dep,一个dep对应的多个watcher
3.响应式处理的时候,defineReactive是闭包环境,不会被回收,直接在当前环境new Dep(),在compile的时候,new Watcher()将当前的updater方法传递给Watcher,Watcher在实例话的时候,会访问一下当前的key,从而触发get方法,Watcher又把自己加到Dep.target属性上面,(这就是是经典的发布订阅模式,)之后在get方法中,调用dep.addDep()方法把watcher添加到当前dep的肚子里面,每次set的时候,调用notify方法,从而可以触发更新
验证:
counter一直在加加
结果:
counter一直在变

结束

代码已上传gitee,地址:https://gitee.com/DayLoveNight/vue-code-edit.git,2.0,3.0后续会更新

相关文章

  • 手动实现Vue(1.0)

    现在vue3.0版本已经发布了,各种工具也已经对vue3.0做了支持,最近有时间,想学习下源码,这边文章主要是对v...

  • uni-app使用Vue.js

    说明 :uni-app基于Vue 2.0实现,开发者需注意Vue 1.0 -> 2.0 的使用差异,详见从 Vue...

  • vue 智能检索提示--手动定位

    手动定位,输入关键词,实现智能提示 要实现的效果 vue 引入vue-baidu-map 使用npm 引入 在v...

  • Vue 核心之数据劫持

    Vue 核心之数据劫持 Angular、Regular、Vue、React等等可以实现数据绑定,再也不需要手动进行...

  • vue综合讲解

    vue2.0和1.0模板渲染的区别 Vue 2.0 中模板渲染与 Vue 1.0 完全不同,1.0 中采用的 Do...

  • vue.js配合axios发送Ajax请求

    vue.js与ajax vue本身是不支持发送ajax请求,需要通过其他库来实现的(比如vue1.0版本官方推荐的...

  • vue插件--滚动信息插件

    基础要求 知道如何使用vue-cli熟悉vue1.0、了解sass 准备工作 使用vue-cli安装vue1.0 ...

  • TodoMVC

    TodoMVC1.0 实现的思路: 1.首先引入vue.js,创建vue的实例对象vm 2.在实例vm的data属...

  • vue1.0与vue2.0路由的区别

    个人总结——vue1.0与vue2.0路由的区别 vue1.0 html部分———— 主页 跳转链接 ...

  • Vue 实现手动刷新组件

    开发过程遇到了一个问题,就是我的 router-view 里面渲染出来的组件输入数据之后,我点击 路由视图外边的导...

网友评论

    本文标题:手动实现Vue(1.0)

    本文链接:https://www.haomeiwen.com/subject/grksfltx.html