Vue演变史 —— 1.0(原理刨析、代码实现)

1,269 阅读4分钟

本文讲解vue1.0的核心原理,以及我们自己手写1.0版本的Vue。

Tips:后面陆续会出vue2.0vue3.0的核心原理以及代码实现

课前准备

  • Object.defineProperty(obj,key,attributes:PropertyDescriptor)
    • obj:想要设置的对象
    • key:想要添加的key
    • attributes:属性描述符

    注意:vue1.0和vue2.0的核心都是通过Object.defineProperty实现的,只有在vue3.0的时候代理换成了Proxy 后面我们讲到3.0时会用Proxy,以及他们的不同点。

小实验

我们做这个小实验的目的就是,问了让大家看清楚vue的雏形是什么样子的,以及他的核心设计理念。很简单,没有什么特殊的语法,大家仔细看!!!

页面

假设,我们的页面是这样的:

效果图 顺便贴一下代码:

<!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">
    <style>
        .container {
            display: flex;
            justify-content: center;
            flex-direction: column;
            align-items: center;
            margin: 0 auto;
        }

        button {
            font-size: 22px;
        }

        .box {
            font-size: 30px;
        }
    </style>
    <title>Vue源码实现</title>
</head>

<body>
    <div class="container">
        <p class="box">0</p>
        <div>
            <button class="add"></button> &nbsp; <button class="subtract"></button>
        </div>
    </div>
    <script>

        var add = document.querySelector(".add"); // 加
        var subtract = document.querySelector(".subtract"); // 减
        var content = document.querySelector(".box"); // 显式内容

        var data = {
            count: 1
        }

        function update() {
            content.innerHTML = data.count;
        }
        update();

        add.onclick = function () {
            data.count++;
        }
        subtract.onclick = function () {
            data.count--;
        }

    </script>
</body>

</html>

看到这里,使用过vue的同学都知道,是不是只要我们点击加减按钮(也就是改变count的值),页面的值随之改变,我们就实现了vue,真正的声明式渲染。

主角登场

  • 说一下思路

    思考一下,这时候是不是只要我们修改count的时候拦截到这一操作时再使用update函数,是不是就可以实现。这时候就用到了我们的主角Object.defineProperty()

  • 增加代码

    <!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">
        <title>Vue源码实现</title>
    </head>
    
    <body>
        <div class="container">
            <p class="box">0</p>
            <div>
                <button class="add"></button> &nbsp; <button class="subtract"></button>
            </div>
        </div>
        <script>
    
            var add = document.querySelector(".add"); // 加
            var subtract = document.querySelector(".subtract"); // 减
            var content = document.querySelector(".box"); // 显式内容
    
            var data = {
                count: 1
            }
    
            function update() {
                content.innerHTML = data.count;
            }
            update();
    
    
    
            add.onclick = function () {
                data.count++;
            }
            subtract.onclick = function () {
                data.count--;
            }
    
    
            // 核心代码
            function defineReactive(obj, key, value) {
                Object.defineProperty(obj, key, {
                    get() {
                        return value;
                    },
                    set(newVal) {
    
                        if (newVal !== value) {
                            value = newVal
                            update()
                        }
    
                    }
                })
            }
            Object.keys(data).forEach(key => {
                defineReactive(data, key, data[key]);
            })
    
        </script>
    </body>
    
    </html>
    
  • 代码解析

    这段代码中,我们定义了defineReactive方法,然后遍历data中的属性设置到data上面,再利用js语言中闭包的特性,将value保存下来,修改访问的时候,直接修改的是我们内存当中的值,从而实现了我们的需求

环境搭建

环境很简单,一个html文件,一个js文件足矣。

image.png

  • index.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源码实现</title>
    </head>
    
    <body>
        <div id="app">{{count}}</div>
        <script>
            const app = new Vue({
                el: "#app",
                data: {
                    count: 1
                }
            })
            setInterval(() => {
                console.log(app.count);
                app.count++
            }, 2000)
    
    
    
    
        </script>
    </body>
    
    </html>
    

是不是只要我们可以让html中的例子跑起来,也就是页面中的count每2s加1那么我们就成功实现了Vue!!!!想想是不是很激动,到这里是不是已经有点思路了呢?别急我们一步步来。

vue1.0实现

通过上面的小实验我们可以得出结论,当我们this.count = xxx(也就是set)的时候,就是调用页面中元素相关的update方法,从而触发页面更新,这也就是vue的核心机制。这句话不理解没关系,只需要记住就行。

我们Vue的实现分如下几步:

  • 接收用户参数
  • 响应式处理
  • 代理
  • 模板编译
  • 依赖收集

其实在源码中,初始化的时候核心就是这几步。 基础代码结构:

class Vue {
  constructor(options) {
    // 1.接收用户参数
    
    // 2.响应式处理
  
    // 3.代理

    // 4.模板编译
  }
}

1.接收用户参数

  • options就是用户传入的参数,也就是new Vue({})时传入构造函数的参数
class Vue {
  constructor(options) {
    // 1.接收用户参数
    this.$options = options;
    this.$data = options.data;
    
    // 2.响应式处理
    
    // 3.代理 
    
    // 4.模板编译
  }
}

2.响应式处理

  • 什么是响应式处理

    vue是典型的MVVM框架,当我们this.count = xxx时,页面会重新render,而想要实现这一功能,响应式处理是必不可少的一步。

  • 代码实现

    class Vue {
      constructor(options) {
        // 1.接收用户参数
        this.$options = options;
        this.$data = options.data;
    
        // 2.响应式处理
        observe(this.$data);
    
        // 3.代理
    
        // 4.模板编译
      }
    }
    
    function observe(obj) {
      if (Array.isArray(obj)) {
        // TODO 数组的响应式处理
      } else if (typeof obj === "object") {
        Object.keys(obj).forEach((key) => {
          defineReactive(obj, key, obj[key]);
        });
      }
    }
    function defineReactive(obj, key, value) {
      Object.defineProperty(obj, key, {
        get() {
          console.log("get", key);// 如果访问这个属性控制到会输出
          return value;
        },
        set(newVal) {
          if (newVal !== value) {
            console.log("set", key);// 如果设置这个属性控制台会输出
            value = newVal;
          }
        },
      });
    }
    
  • 成果测试

    此时我们的setInterVal函数一直在访问app.count。按照我们代码中的逻辑控制台会输出:

    • 期望

      image.png

    • 结果

      image.png 想一下我们为什么会出现这样的结果?

    因为我们还没做代理,app实例上面是没有count这个属性的,所以你是访问不到。

    我们可以修改一下代码,此时你就可以看到控制台正确输出了:

    //index.html文件中
     setInterval(() => {
          console.log(app.count)
          app.count++
      }, 1000)
    
    // 修改为
    
    setInterval(() => {
          console.log(app.$data.count)
          app.$data.count++
      }, 1000)
    
    

3.代理

  • 什么是代理

    我们可以通过app.xxx访问到data中的某个属性,这就是代理的作用成果

    const app = new Vue({
        count:1
    })
    
    console.log(app.count)//1
    
    
  • 代码实现

    class Vue {
      constructor(options) {
        // 1.接收用户参数
        this.$options = options;
        this.$data = options.data;
    
        // 2.响应式处理
        observe(this.$data);
    
        // 3.代理
        proxy(this);
    
        // 4.模板编译
      }
    }
    
    function observe(obj) {...
    }
    function defineReactive(obj, key, value) {...
    }
    
    function proxy(instance) {
      Object.keys(instance.$data).forEach((key) => {
        defineReactive(instance, key, instance.$data[key]);
      });
    }
    
    

4.模板编译

  • 什么是模板编译

    • {{}}我们称之为插值语法
    • <div>{{count}}</div>编译为<div>2</div>,这就是模板编译
  • 实现思路

    模板编译的核心就是,将template里面的{{}}(也就是插值),替换成data中的数据。就这么简单!!!

  • 代码实现

      class Vue {
         constructor(options) {
           // 1.接收用户参数
           this.$options = options;
           this.$data = options.data;
    
           // 2.响应式处理
           observe(this.$data);
    
           // 3.代理
           proxy(this);
    
           // 4.模板编译
           new Compile(this, this.$el);
    
         }
       }
    
       function observe(obj) {...
       }
       function defineReactive(obj, key, value) {...
       }
    
       function proxy(instance) {...
       }
    
       
       class Compile {
         constructor(vm, el) {
           this.$vm = vm;
           this.$el = document.querySelector(el);
           this.compile(this.$el);
         }
         compile(el) {
           el.childNodes.forEach((node) => {
             // 判断是元素  ||  插值语法 1元素 2方法
             if (node.nodeType === 1) {
               // TODO 处理元素上面的指令
               
               if (node.childNodes.length > 0) {
                 this.compile(node);
               }
               console.log("处理元素");
             } else if (this.isInter(node)) {
               this.compileText(node);
             }
           });
         }
    
         compileText(node) {
           node.textContent = this.$vm[RegExp.$1];
         }
         // 判断是否是插值语法
         isInter(node) {
           return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
         }
       }
    
    
  • 成果演示

    image.png

  • 代码讲解:

    • Compile这个类的构造函数接收俩个参数,vm(实例)、el(类名)
    • compile这个实例方法,主要是遍历元素节点,区分节点是插值还是元素,分别执行对应的编译方法。同时我们处理元素的时候,因为可能会有子元素,所以要递归处理

5.依赖收集

初始化的过程在我们的前四步已经完成,但是我们的count不能修改,接下来最后一步也是最重要的一步依赖收集

结合小实验,也就是收集我们的update函数,利用闭包的特性从而完成更新。

  • 代码实现步骤

    • 实现DepWatcher这俩个类(一个key对应一个Dep,一个Dep里面可能有多个watcher)
    • 在响应式处理的时候,实例化Dep
    • 模板编译的时候实例化Watcher
    • DepWatcher之间是发布订阅关系,利用这层关系,实现依赖收集。
  • 依赖收集初始化

    响应式处理时,实例化dep => get时,调用dep.addDep() => 模板编译实例化Watcher => Watcher实例化时通过访问vm[key]触发get函数完成收集

  • 依赖收集运行时

    当app.count++时 => 调用当前key对应Dep.notify()方法 => notify遍历内部的Watcher => Watcher调用内部的update方法

  • 代码实现

    class Vue {
      constructor(options) {
        // 1.接收用户参数
        this.$options = options;
        this.$data = options.data;
        this.$el = options.el;
    
        // 2.响应式处理
        observe(this.$data);
    
        // 3.代理
        proxy(this);
    
        // 4.模板编译
        new Compile(this, this.$el);
      }
    }
    
    /**
     * 响应式处理
     */
    function observe(obj) {
      if (Array.isArray(obj)) {
        // TODO 数组的响应式处理
      } else if (typeof obj === "object") {
        Object.keys(obj).forEach((key) => {
          defineReactive(obj, key, obj[key]);
        });
      }
    }
    function defineReactive(obj, key, value) {
      const dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          Dep.target && dep.addDep(Dep.target);
          return value;
        },
        set(newVal) {
          if (newVal !== value) {
            value = newVal;
            dep.notify();
          }
        },
      });
    }
    
    /**
     * 代理
     */
    function proxy(instance) {...
    }
    
    /**
     * 模板编译
     */
    class Compile {
      constructor(vm, el) {
        this.$vm = vm;
        this.$el = document.querySelector(el);
        this.compile(this.$el);
      }
      compile(el) {
        el.childNodes.forEach((node) => {
          // 判断是元素  ||  插值语法 1元素 2方法
          if (node.nodeType === 1) {
            // TODO 处理元素
            if (node.childNodes.length > 0) {
              this.compile(node);
            }
            console.log("处理元素");
          } else if (this.isInter(node)) {
            // TODO 处理模板插值
            this.compileText(node);
          }
        });
      }
    
      compileText(node) {
        const fn = this.textUpdater;
        new Watcher(this.$vm, RegExp.$1, function () {
          return fn(node, this.$vm[RegExp.$1]);
        }).update();
      }
      textUpdater = (node, val) => {
        console.log(this);
        node.textContent = val;
      };
    
      // 判断是否是插值语法
      isInter(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
      }
    }
    
    /**
     * 依赖收集
     */
    class Dep {
      constructor() {
        this.deps = [];
      }
      addDep(watcher) {
        this.deps.push(watcher);
      }
      notify() {
        this.deps.forEach((fn) => {
          fn && fn.update();
        });
      }
    }
    
    class Watcher {
      constructor(vm, key, fn) {
        this.$vm = vm;
        this.$fn = fn;
    
        Dep.target = this;
        this.$vm[key];
        Dep.target = null;
      }
      update() {
        this.$fn && this.$fn();
      }
    }
    
    
  • 成果展示:每2秒自动加1

    image.png

完整代码

class Vue {
  constructor(options) {
    // 1.接收用户参数
    this.$options = options;
    this.$data = options.data;
    this.$el = options.el;

    // 2.响应式处理
    observe(this.$data);

    // 3.代理
    proxy(this);

    // 4.模板编译
    new Compile(this, this.$el);
  }
}

/**
 * 响应式处理
 */
function observe(obj) {
  if (Array.isArray(obj)) {
    // TODO 数组的响应式处理
  } else if (typeof obj === "object") {
    Object.keys(obj).forEach((key) => {
      defineReactive(obj, key, obj[key]);
    });
  }
}
function defineReactive(obj, key, value) {
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      Dep.target && dep.addDep(Dep.target);
      return value;
    },
    set(newVal) {
      if (newVal !== value) {
        value = newVal;
        dep.notify();
      }
    },
  });
}

/**
 * 代理
 */
function proxy(instance) {
  Object.keys(instance.$data).forEach((key) => {
    defineReactive(instance, key, instance.$data[key]);
  });
}

/**
 * 模板编译
 */
class Compile {
  constructor(vm, el) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    this.compile(this.$el);
  }
  compile(el) {
    el.childNodes.forEach((node) => {
      // 判断是元素  ||  插值语法 1元素 2方法
      if (node.nodeType === 1) {
        // TODO 处理元素
        if (node.childNodes.length > 0) {
          this.compile(node);
        }
        console.log("处理元素");
      } else if (this.isInter(node)) {
        // TODO 处理模板插值
        this.compileText(node);
      }
    });
  }

  compileText(node) {
    const fn = this.textUpdater;
    new Watcher(this.$vm, RegExp.$1, function () {
      return fn(node, this.$vm[RegExp.$1]);
    }).update();
  }
  textUpdater = (node, val) => {
    console.log(this);
    node.textContent = val;
  };

  // 判断是否是插值语法
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }
}

/**
 * 依赖收集
 */
class Dep {
  constructor() {
    this.deps = [];
  }
  addDep(watcher) {
    this.deps.push(watcher);
  }
  notify() {
    this.deps.forEach((fn) => {
      fn && fn.update();
    });
  }
}

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

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

总结

文章可能总结的不是很好,如果有问题的话可以在评论区留言,我看到就会回复,也可以加我微信15735090985一起讨论。后续会出vue2.0以及vue3.0。