手动实现Vue(1.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后续会更新

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容