vue2.x的响应式原理解析

258 阅读6分钟
本文章脱胎于x站的2021上半年各大厂web核心面试题解析(第二期)-路白,视频更加的通俗易懂!

看了关于vue响应式的原理,以这篇笔记记录下来,来加深对于vue响应式原理的印象


在解析vue2的响应式原理之前,首先我们得明白几个概念,或者说几个核心类他们分别的作用

  1. Observe类,对data函数里的变量属性添加getter/setter,进行数据劫持以及派发更新
  2. Watcher类,Watcher类有多种,比如computed watcher,user watcher(自己在watch里定义的需要监听数据变化的watcher)。
  3. Dep类, 用于收集当前响应式对象的依赖关系,每个响应式对象都有一个dep实例。dep.subs=watcher[],是当前响应式对象dep实例收集的watcher依赖。当数据发生改变的时候,就会触发dep.notify方法,该方法会遍历subs数组,调用每一个watcher的update方法,进行响应式的更新。

好,简单了解完各个核心类的作用,接下来就来逐个的实现,文件结构如下。

index.html  -------- html文件,提供根元素
index.js    -------- 引入vue.js,添加参数
watch.js    -------- watcher类
dep.js      -------- dep类
observe.js  -------- 观察者类
compiler.js -------- 用于编译指令,如v-html,v-model,v-text
vue.js      -------- 主文件

第一步 首先我们先将index.html与index.js文件的初始代码搞定

//index.js
import Vue from './vue.js'

const vm=new Vue({
    el:'#app',
    data:{
        msg:'hello word',
        text:'<div>哈哈哈哈</div>',
        html:'<h1>变大了</h1>'
    },
    methods:{
        fn(){
            alert("111");
        }
    }
})

console.log(vm);

//index.html
<body>
    <div id="app">
        {{msg}}

        <input type="text" v-model='msg'>
        <button type="button" v-on:click='fn'>按钮</button>

        <div v-text='text'></div>
        <div v-html='html'></div>
    </div>

    <script src="./index.js" type="module"></script>
</body>  
  1. 我们知道Vue是new出来的,所以可以知道Vue是用class来声明的类。
  2. html的js引入写上了type='module',意思是告诉浏览器我们使用的是ESM语法,主流浏览器支持这样的声明。

第二步 将vue.js的主体代码写出,有几点是需要实现的

  1. 将传入的options及相关属性绑定到vue实例上
  2. 对传入的el属性进行判定
  3. 通过对vue的vm实例可以知道,vue将data里的属性绑定了一份到vue实例上
import Observe from "./observe.js";
import Complier from "./compiler.js";

export default class Vue{
    constructor(options){
        this.$options=options;
        this.$data=options.data;
        this.$methods=options.methods;

        //初始化el,对传进来的根元素进行判定
        this.initElement(options);

        //将data元素绑定到vue实例上
        this._proxyData (this.$data);

        //数据劫持
        new Observe(this.$data);

        //编译模板
        new Complier(this);
    }

    initElement(options){
        if(typeof options.el==='string'){
            this.$el=document.querySelector(options.el);
        }else if(options.el instanceof HTMLElement){
            this.$el=options.el;
        }

        //兜底,传进来的根元素类型错误时
        if(!this.$el){
            throw new Error('请传入css selector或者HTMLElement');
        }
    }

    _proxyData(data){
        //通过Object.defineProperty将data里的属性绑定到vue实例上
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this, key, {
                //表示可以被枚举,也就是可以被循环出来
                enumerable:true,
                //表示可以进行相应配置
                configurable:true,
                get(){
                    return data[key];
                },
                set(newValue){
                    if(newValue===data[key]){
                        return
                    }

                    data[key]=newValue;
                }
            })
        })
    }
}

_proxyData函数用于将属性绑定在vue实例上,而不是用于进行依赖的收集和派发更新,所以不用递归的对每一个值进行劫持。

第三步 梳理架构

这个时候我们先不急着把其余的类实现,而是先将架构先写出来,能够更好地梳理我们的知识点,当然在写架构的时候注释要加上,不然等到写的时候,就忘记了这个是用来干什么的了,得不偿失。

  1. dep类
//dep类是用来保存watch类的,所以初始的时候就有一个保存watch的数组
export default class Dep{
    constructor(){
        //watch数组
        this.subs=[];
    }

    //要想放进subs数组,就需要一个add方法
    addSubs(watcher){
        
    }

    //用于派发更新
    notify(){

    }

  1. watch类
export default class Watcher{
    constructor(vm, key, cb){   
        
    }

    update(){

    }
}

这里需要三个参数,分别是vm的实例,data的属性名,回调函数

  1. observe类
export default class Observe{
    constructor(data){

    }

    transver(data){
        
    }

}

这里需要一个data参数,也就是传进来的data,使用Object.defineProperty将data里的每一个属性进行数据的劫持,加上getter/setter,而我们的依赖收集派发更新就是在这里实现

  1. complier类
export default class Complier{
    constructor(vm){
        
    }

    //遍历元素,对模板进行编辑
    compile(el){

    }
}

就这样就完成了四大类的基本构架,在此基础上添加相应的代码,距离完成又近了一步,是不是看起来还挺简单的。

第四步 dep类的整体实现

//dep类是用来保存watch类的,所以初始的时候就有一个保存watch的数组
export default class Dep{
    constructor(){
        //watch数组
        this.subs=[];
    }

    //要想保存进subs数组,就需要一个add方法
    addSubs(watcher){
        if(watcher&&watcher.update){
            this.subs.push(watcher);
        }
    }

    //用于派发更新
    notify(){
        //遍历subs数组,调用每一个watcher的updatae方法
        console.log(this.subs);
        this.subs.forEach(watcher=>{
            watcher.update();
        })
    }

}

看起来是不是很简单
不过可以先思考两个问题。

  1. dep类在什么时候实例化?在哪里addSubs?
  2. dep类在什么时候调用notify方法?

第五步 watcher类的整体实现

import Dep from "./dep.js";

export default class Watcher{
    /**
     * 
     * @param {*} vm vm的实例,拿到所需值
     * @param {*} key data的属性名,拿到新值,旧值
     * @param {*} cb 更新时调用回调函数
     */
    constructor(vm, key, cb){   
        this.vm=vm;
        this.key=key;
        this.cb=cb;

        //为什么要往dep.target上添加watcher实例
        Dep.target=this;

        //拿到旧值
        //同时注意一点,在这里会触发变量的get方法
        this.oldValue=vm[key];

        Dep.target=null;
    }

    update(){
        //因为数据已经更新,所以拿到的就是最新的值
        let newValue=this.vm[this.key];
        if(newValue===this.oldValue){
            return;
        }

        this.cb(newValue);
    }   
}

是不是看起来也很简单,别着急,真正复杂的还在compiler模块中,在这里也有个问题。

  1. 为什么要往dep.target上添加watcher实例?是为了能够将在同一时间只维持一份watcher,因为在computed里,watch里用到时,会添加多个watcher,容易造成数据紊乱,所以在一个时间里只有一个watcher,保证watcher是正常添加的。

第六步 compiler的整体实现

这一步会比较复杂,主要就是对于元素的节点进行分析,遍历。代码很长,不要惊慌。

import Watcher from "./watch.js";

export default class Complier{
    constructor(vm){
        this.vm=vm;
        this.compile(vm.$el);
    }

    //遍历元素,对模板进行替换
    compile(el){
        //拿到元素的子节点,一个类数组
        const childNodes=el.childNodes;
        Array.from(childNodes).forEach(node=>{
            //判断节点类型
            if(this.isTextNode(node)){
                //文本节点
                this.compileText(node);
            }else if(this.isElementType(node)){
                //元素节点
                this.compileElement(node);
            }

            if(node.childNodes&&node.childNodes.length>0){
                this.compile(node);
            }

        })
    }

    //判断文本节点
    isTextNode(node){
        return node.nodeType===3;
    }

    //判断元素节点
    isElementType(node){
        return node.nodeType===1;
    }

    //文本节点编译
    compileText(node){
        let reg=/\{\{(.+?)\}\}/;
        let value=node.textContent;

        if(reg.test(value)){
            const key=RegExp.$1.trim();//拿到msg
            node.textContent=value.replace(reg, this.vm[key]);//替换成功

            //数据需要动态改变,所以需要依赖收集
            new Watcher(this.vm, key, (newValue)=>{
                node.textContent=value.replace(reg, newValue);;
            })
        }

    }

    //元素节点编译
    compileElement(node){
        if(node.attributes.length>0){
            Array.from(node.attributes).forEach(attr=>{
                //属性名
                const attrName=attr.name;

                //判断是否v-开头
                if(this.isVStartsWith(attrName)){
                    //判断是否有:号,例如v-on:click        v-model
                    const directiveName=attrName.indexOf(':')>-1?attrName.substr(5):attrName.substr(2);
                    //拿到值,例如v-model='msg'的msg
                    let key=attr.value;

                    this.update(node, key, directiveName);
                }
            })
        }
    }

    //判断v-开头
    isVStartsWith(attr){
        return attr.startsWith('v-');
    }

    //进行拼接,使之对应函数
    update(node, key, directiveName){
        const updaterFn=this[directiveName+'Updater'];

        updaterFn && updaterFn.call(this, node, this.vm[key], key, directiveName);
    }

    //v-model
    modelUpdater(node, value, key){
        //对应于input
        node.value=value;
        new Watcher(this.vm, key, (newValue)=>{
                node.value=newValue;
        })

        node.addEventListener('input', ()=>{
            //会触发setter
            this.vm[key]=node.value;
        })
    }

    //v-text
    textUpdater(node, value, key){
        node.textContent=value;

        new Watcher(this.vm, key, (newValue)=>{
            node.textContent=newValue;
        })
    }

    //v-html
    htmlUpdater(node, value, key){
        node.innerHTML=value;

        new Watcher(this.vm, key, (newValue)=>{
            node.innerHTML=newValue;
        })
    }

    //v-on:click
    clickUpdater(node, value, key, directiveName){
        node.addEventListener(directiveName, this.vm.$methods[key]);
    }
}

啊,搞定了这一步,接下来就是最后一步了,加油,快成功了!!!

第七步 observe类的实现

import Dep from './dep.js'

export default class Observe{
    constructor(data){
        this.transver(data);
    }

    transver(obj){
        if(!obj || typeof obj!=='object'){
            return
        }

        //遍历监听数据
        Object.keys(obj).forEach(data=>{
            this.defineReactive(obj, data, obj[data]);
        })
    }

    defineReactive(obj, data, value){
        //可能又是一个object
        this.transver(value);

        //在这里实例化dep
        let dep=new Dep();

        //保存this
        const that=this;

        Object.defineProperty(obj, data, {
            enumerable:true,
            configurable:true,
            get(){
                //在这里添加依赖,拿到绑定在Dep身上的watcher
                Dep.target && dep.addSubs(Dep.target);

                //注意,这一步不能够用obj[data],会造成循环的get这个值
                return value;

            },
            set(newValue){
                if(value===newValue){
                    return;
                }

                value=newValue;

                //可能是一个object
                that.transver(newValue);

                //在这里派发更新
                dep.notify();
            }
        })

    }
}

至此,大功造成,能完整的看下来真的是毅力可嘉,不妨跟着视频手写一遍,记忆会更加深刻。

以上添加有个人理解,如有不对,请留言