Javascript

以手写代码的方式解析 Vue 的工作过程

本文主要是介绍以手写代码的方式解析 Vue 的工作过程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Vue的工作过程解析

对于 Vue 的工作过程,我们可以从下面这张图中得到一点思路。

我们可以从两个方面来解析 Vue 的工作过程:初始化阶段、数据修改阶段。

在 Vue 初始化阶段,我们创建了一个 Vue 实例并将其挂载在了页面上:

  • 在创建实例的过程中,我们调用了一个init()方法。它做了什么事情呢?它将传入的props、事件、data等都做了初始化。
  • 我们通过调用$mount()方法,实现了 Vue 实例的挂载。这个$mount()方法,最主要做的事情是什么呢?它通过调用 render()函数生成了 virtual DOM,即虚拟DOM树。 render()函数在执行的时候,会touch一下 对应属性的getter,这一步即为触发getter进行依赖收集的过程。
  • 最后,调用patch()方法生成真实DOM,挂载在页面上。

数据修改阶段

  • 数据修改会触发对应属性的setter
  • 由于数据响应式,对应的监听器 Watcher 会执行更新 (update) 操作。
  • 通过调用patch()方法,对比新旧 virtual DOM,得到页面的最小修改,执行页面刷新。

手写Vue包含的功能

我想要试试自己实现一个简单的 Vue。它将会是怎样的呢:

  • 包含功能:它会包含数据响应式、依赖收集、数据更新这些核心过程。
  • 解析阶段:只解析最简单的文本自定义变量{{}}
  • 不包含功能:没有虚拟 DOM模块,也没有patch算法。一个变量对应一个Watcher的方式(Vue 1 阶段)。

文件会有五个:

  • 测试文件 index.html
  • 核心的 fVue.js
  • 监视器 watcher.js
  • 调度模块 dep.js
  • 编译器 compier.js

首先,给出作为测试用的 index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
        {{test}}
        <p k-text="test"></p>
        <p k-html="html"></p>
        <p>
            <input type="text" k-model="test">
        </p>
        <p>
            <button @click="onClick">按钮</button>
        </p>
    </div>

    <script src="fvue.js"></script>
    <script src="fcompile.js"></script>
    <script src="watcher.js"></script>
    <script src="dep.js"></script>
    <script>
      const fVue = new FVue({
        el: "#app",
        data: {
          test: "hello, frank",
          foo: { bar: "bar" },
          html: '<button>adfadsf</button>'
        },
        methods: {
            onClick() {
                alert('blabla')
            }
        },
      });
      //模拟数据修改
      setTimeout(function(){
        fVue.$data.test = "hello,fVue!";
        console.log("setTimeout : ",fVue.$data.test);
      }, 2000);
    </script>
  </body>
</html>
复制代码

代码实现

为了验证想法,写了这四个文件。代码尽量简单。

//fvue.js
class FVue {

    constructor(options){
        this.$data = options.data;
        this.$options = options;
        //数据响应化
        this.observe(this.$data);
        //解析页面模板
        new Compile(options.el, this);
    }

    observe(value){
        if(!value || typeof value !== 'object'){
            return;
        }
        Object.keys(value).forEach(key =>{
            this.defineReactive(value, key, value[key]);
            // 为vue的data做属性代理:this.xxx = this.$data.xxx
            this.proxyData(key);
        })
    }
    
    defineReactive(obj, key, val){
        //递归
        this.observe(val);
        //每一个 key 都有一个的Dep与之对应
        const dep = new Dep();

        Object.defineProperty(obj, key, {
            get(){
                //依赖收集
                Dep.target &&  dep.addDep(Dep.target)
                return val;
            },
            set(newVal){
                if(newVal === val) return;
                val = newVal;
                //执行更新操作
                dep.notify();
            }
        })
    }

    proxyData(key) {
        Object.defineProperty(this, key, {
            get(){
                return this.$data[key];
            },
            set(newVal){
                this.$data[key] = newVal;
            },
        });
    }
}
复制代码

fvue.js 核心文件实现了 observe 逻辑:即在初始化过程中,将传入的data属性做了初始化处理,通过 defineReactive()方法将data中每个属性都做了数据拦截,重新定义了每个属性的gettersetter。更详细的:

  • 每一个属性都有自己专有的调度模块 Dep。

  • getter中,定义了依赖收集的方式(只要有对应的 Watcher 触发了 getter 方法,那么将其放入到 Dep 的数组里)。

  • setter中,定义了响应数据变化的方法(只要对应的setter方法被触发,那么该 Dep 就会执行通知操作,让对应的 Watcher 执行更新)。

再来看 dep.js 与 watcher.js。

//dep.js
class Dep {
    constructor(){
        this.deps = []
    }

    addDep(dep){
        this.deps.push(dep)
    }

    notify(){
        this.deps.forEach(dep => dep.update())
    } 
}

//watcher.js
class Watcher{
    constructor(vm, key, cb){
        this.vm = vm;
        this.key = key;
        this.cb = cb;

        Dep.target = this; //将当前Watcher实例附加到Dep的静态属性上
        this.vm[this.key]; //主动触发 getter 属性,触发依赖收集
        Dep.target = null; //解除 Dep.target 这个静态变量的锁定
    }

    update(){
        this.cb.call(this.vm, this.vm[this.key]);
    }
}
复制代码

我们将 Dep 看成是一个调度模块,它只负责管理更新。而 Watcher 相当于是一个执行人,它负责执行具体的更新过程。

我们看到,在 Watcher 初始化的过程中,我们主动触发了 getter 属性,触发了依赖收集的过程。但是,还没有看到 Watcher 在哪里被初始化的。其实,在 解析 HTML 模板的过程中,当我们发现了自定义变量时,就会触发 Watcher 的初始化。

为了简化,验证可行性。此时我们的 fcompile.js 会写得非常简单,只处理文本自定义变量的情况(在例子中是{{test}})。

class Compile {
    //el是宿主元素或者选择器
    //vm 是vue实例
    constructor(el, vm){
        this.$vm = vm;
        this.$el = document.querySelector(el); // 简化:通过选择器来获取到文档元素

        this.compile(this.$el);
    }

    compile(el){
        const childNodes =  el.childNodes;
        Array.from(childNodes).forEach(node => {
            if(this.isTextParam(node)){
                this.compileText(node);
            }
            //递归
            this.compile(node);
        })
    }

    isTextParam(node){
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
    }

    compileText(node){
        let key = RegExp.$1;
        let currentValue = this.$vm[key];
        //解析后,需要将真实值挂载到真实页面上
        this.textUpdate(node, currentValue)
        //创建新的 Watcher 实例
        new Watcher(this.$vm, key, (newValue)=>{
            this.textUpdate(node, newValue)
        })
    }

    textUpdate(node, value){
        node.textContent =  value;
    }
}
复制代码

Compile 是在 FVue 中调用的。它的工作是最为繁重的:

  • 解析 HTML 模板,找出各式各样的自定义变量、事件等,将自定义变量对应的真实值展示在网页上。
  • 最为关键的是:创建新的 Watcher 实例,触发依赖收集。同时实时响应 Watcher 的 update 情况,将最新的数据响应式结果,展示在页面对应的位置上。

当然,为了简单起见,此处的 Compile 只处理了一个最简单的情况:文本自定义变量 ({{test}}) 的情况。一个完善的 compile 函数会非常周密且复杂,可查看 Vue 源码。

总结

将代码放在一起,它们是可以运转的。页面上的展示变量在定时器时间过后,会发生改变。

在文章最后,让我们来捋一捋整个 Vue 工作的过程:

  • 初始化阶段,observe 对传入 data 的每个属性都做了数据拦截,设置了数据响应化逻辑。
  • 模板解析阶段,compile 通过查找自定义变量、事件等,并为此创建新的 Watcher 实例,触发依赖收集。
  • 当数据发生变动的时候,属性上的 setter 触发 对应 Dep 的通知操作,让对应的 Watcher 实例执行更新。
  • Watcher 执行更新的时候, HTML 模板上的自定义变量也会随之发生改变。由此触发页面的刷新。

整个过程可以看做是 Vue 1.x 的工作方式极端简易版本,虽然与 Vue 2.x 不同,但希望不会影响各位读者对 Vue 的理解。

这篇关于以手写代码的方式解析 Vue 的工作过程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!