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

Vue的工做过程解析

对于 Vue 的工做过程,咱们能够从下面这张图中获得一点思路。javascript

咱们能够从两个方面来解析 Vue 的工做过程:初始化阶段、数据修改阶段。html

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

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

数据修改阶段java

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

手写Vue包含的功能

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

  • 包含功能:它会包含数据响应式、依赖收集、数据更新这些核心过程。
  • 解析阶段:只解析最简单的文本自定义变量{{}}
  • 不包含功能:没有虚拟 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>
复制代码

代码实现

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

//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 的理解。

相关文章
相关标签/搜索