简单的响应式更新实现

158 阅读4分钟

把你了解的vue响应式原理阐述一下

三个核心类
    1.observer:给对象的属性添加gettersetter,用于依赖收集和派发更新
    2.Dep:用于收集当前响应式对象的依赖关系,每个响应式对象都有一个dep实例
        dep.subs = watcher[].
        当数据发生变更的时候,会通过dep.notify()通知各个watcher。
    3.watcher: 观察者对象,render watcher,computed watcher,user watcher
依赖收集
    1.initState,对computed属性初始化时,会触发computed wathcer 依赖收集。
    2.initState, 对监听属性初始化的时候,触发的user watcher 依赖收集。
    3.render.触发 render watcher 依赖收集
派发更新 Object.defineProperty.
    1、组件中的对响应数据进行修改,会触发setter逻辑
    2、dep.notify()
    3.遍历所有subs,调用每一个watcher的update方法。
总结原理
    当创建VUE实例时,vue 先遍历data的属性。Object.defineProperty为属性添加gettersetter对数据的读取进行劫持。
getter: 依赖收集
setter: 派发更新
每个组件的实例都会对应的watcher实例。

计算属性的实现原理

compute watcher, 计算属性的监听器。
computed watcher 持有一个dep实例,通过dirty属性标记计算属性是否需要重新求值
当computed 的依赖值改变后,就会通知订阅的watcher进行更新,对于computed watcher会将dietary 属性设为true,并对计算属性方法的调用
1、计算属性computed所谓的缓存是什么?
计算属性是基于它的响应式依赖进行缓存的, 只有依赖发生改变时才重新求值
2. 实际应用特性
    比如计算属性方法非常耗时,遍历一个特别大的数组,计算一次可能非常耗时
 3.一下情况, computed可以监听到数据的变化吗? 不可以
 computed: {
     storageMsg: function(){
         return sessionStorage.getItem('xxx');
     },
     time: function(){
           return Date.now();
     }
 }
 created(){
     sessionStorage.setItem('xxx', 111);
 }
 onClick(){
     sessionStorage.setItem('xxx', Math.random());
 }
 
 watch与computed区别(都做响应式变化监听)
 watch 用于监听到某个属性的变化后做什么动作或逻辑(复杂)才使用它,没有缓存
 computed 只有在data里初始化过的数据才会被监听到,响应式更新数据,用于格式转换等简单的操作,不适合做复杂的操作。

Vue.nextTick的原理

  Vue.nextTick(()=>{})
  await Vue.nextTick();
  vue 异步执行dom更新,一旦观察到数据的变化,把同一个event loop 中观察数据变化的watcher推送到这个队列。
  在下一个事件循环时,vue会清空异步队列,进行dom的更新。
  Promise.then > MutationObserver -> setImmediate => setTimeout.
  vm.someData = 'new value', dom并不会马上更新,而是在异步队列被清除时才会更新Dom
  宏任务-》清空微任务队列-〉UI渲染-》宏任务
  什么时候用到nextTick呢?
  在数据变化后要执行某个操作,因为数据改变而改变的Dom,这个操作应该放在vue.nextTick里面
  <template>
      <div v-if="loaded" ref="test"></div>
  </template>
  async showDiv(){
      this.loaded = true;
      await vue.nextTick();
      this.$refs.test.xxx();
  }
  
  

手写简单的vue, 实现简单的响应式更新

index.html 主页面

vue.js vue主文件

compiler.js 编译模板,解析指令, v-model v-html

dep.js 收集依赖关系,存储观察者。// 以发布订阅的形式实现

oberserver.js 数据劫持

watcher.js 观察者对象类

index.html

    <div id="app">
        <h1>模版表达式</h1>
        <h3>{{msg}}</h3>
        <br />
        <h1>v-text测试</h1>
        <div v-text="msg"></div>
        <br />
        <h1>v-html测试</h1>
        <div v-html="testHtml"></div>
        <br />
        <h1>v-model测试</h1>
         <input type="text" v-model="msg"/>
         <input type="text" v-model="count">
         <button v-on:click="handler">按钮</button>
    </div>
    <script src="./index.js" type="module"></script>

index.js

import Vue from './vue.js';
const vm = new Vue({
    el: '#app',
    data:{
        msg: 'hello vue',
        name: 'sb',
        count: 100,
        testHtml: '<ul><li>ddddd</li></ul>',
        obj: {
            width: 222,
            height: 111,
            child: {
                name: 111,
            }
        }
    },
    methods: {
        handler(){
            alert('111');
        }
    }
}) 

console.log(vm); 

vue.js

import Observer from './observer.js';
import Compiler from './compiler.js';
/** 
 * 包括vue的构造函数,及接收的各种配置参数
 * 
 */
export default class Vue{

    constructor(options = {}){
        this.$options = options;
        this.$data = options.data;
        this.$method = options.methods;

        this.initRootElement(options);

        this._proxyData(this.$data);
        // 实例化Object.defineProperty将data里的属性注入到vue实例中
        new Observer(this.$data);
        // 实例化compiler对象,解析指令和模板表达式 {{msg}}
        new Compiler(this);
    }

    initRootElement(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('传入数组参数有误');
        }
    }

    _proxyData(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get(){
                    return data[key];
                },
                set(newData){
                    if(data[key] === newData){
                        return ;
                    }
                    data[key] = newData;
                }
            })
        })
    }
}

observer.js

import Dep from "./dep.js";

export default class Observer{

    constructor(data){
        this.traverset(data);
    }
    // 递归 遍历data里的所有属性
    traverset(data){
        if(!data || typeof data !== 'object'){
            return;
        }
        Object.keys(data).forEach(key =>{
            this.defineReactive(data, key, data[key]);
        })
    }
    // 给传入的数据设置 getter/setter
    defineReactive(obj, key, val){
        this.traverset(val);
        const dep = new Dep();
        let that = this;
        Object.defineProperty(obj, key, {
            configurable: true,
            enumerable: true,
            get(){
                Dep.target && dep.addSub(Dep.target);
                return val;

            },
            set(newValue){
                if(newValue === val){
                    return;
                }
                val = newValue;
                that.traverset(newValue);// 设置的时候可能设置一个对象
                dep.notify();
            }
        })
    }


}

compile.js

import Watcher from './watcher.js';

export default class Compiler{
    constructor(vm){
        this.el = vm.$el;
        this.vue = vm;
        this.methods = vm.$methods;
        this.compile(vm.$el);
    }
    // 编译模版
    compile(el){
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach(node=>{
            // 文本节点
            // 元素节点
            if(this.isTxtNode(node)){
                this.compileText(node)
            }

            if(this.isElementNode(node)){
                this.compileElement(node);
            }
            // 子节点,递归调用
            if(node.childNodes && node.childNodes.length > 0){
                this.compile(node);
            }
        })
    }

    compileText(node){
        // {{ msg }}
        const reg = /\{\{(.+?)\}\}/;
        const value = node.textContent;

        if(reg.test(value)){
            const key = RegExp.$1.trim(); // msg
            node.textContent = value.replace(reg, this.vue[key]);

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

    compileElement(node){
        if(node.attributes.length){
            // 遍历元素节点的所有属性
            Array.from(node.attributes).forEach(attr=>{
                const attrName = attr.name;
                if(this.isDirective(attrName)){
                    let directiveName = attrName.indexOf(':')> -1 ? attrName.substr(5) : attrName.substr(2);
                    let key = attr.value;  
                    this.update(node, key, directiveName);
                }
            });
        }
    }

    update(node, key, directiveName){
        const updateFn = this[directiveName + 'Updater'];
        // v-model , v-text, v-html, v-on:click
        updateFn && updateFn.call(this, node, this.vue[key], key,directiveName);
    }

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

    modelUpdater(node, value, key){
        node.value = value;
        new Watcher(this.vue, key, (newValue)=>{
            node.value = newValue;
        })
        node.addEventListener('input', ()=>{
            this.vue[key] = node.value;
        })
    }

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

    clickUpdater(node, value, key, directiveName){
        node.addEventListener(directiveName, this.vue.$method[key])
    }

    /**
     * 判断元素属性是否是指令
     * @param {} node 
     */
    isDirective(attrName){
        // v-mode v-html
        return attrName.startsWith('v-');
    }

    isTxtNode(node){
        return node.nodeType === 3;
    }

    isElementNode(node){
        return node.nodeType === 1;
    }
}

sub.js

/**
 * 发布订阅的模式
 * 存储所有观察者, watcher
 * 每个watcher都有一个update
 * 通知subs里的每个watcher实例,触发update方法
 */
export default  class Dep{
    constructor(){
        // 存储所有观察者
        this.subs = [];
    }
    // 添加观察者
    addSub(watcher){
        if(watcher && watcher.update){
            this.subs.push(watcher)
        }
    }
    // 通知观察者
    notify(){
        this.subs.forEach(watcher=>{
            watcher.update();
        })
    }
}
// Dep 在哪里实例化, 在哪里addSub;
// Dep notify在哪里调用?

watch.js

import Dep from "./dep.js";

export default class Watcher{
    // vm =》vue实例
    /**
     * 
     * @param {*} vm vue实例
     * @param {*} key data中属性名
     * @param {*} cb  负责更新试图的回调函数
     */
    constructor(vm, key, cb){
        this.vm = vm;
        this.key = key;
        this.cb = cb;

        Dep.target = this; // 获取旧值
        // 触发get方法,在get方法里会做一些操作?
        this.oldValue = vm[key];// 当前的值
        Dep.target = null;
    }
    // 当数据变化当时候更新试图
    update(){
        let newValue = this.vm[this.key];
        if(this.oldValue === newValue){
            return;
        }

        this.cb(newValue);
    }

}
// watcher 初始化oldValue的时候,会去做一些什么操作?
// 通过vm[key]获取oldValue前,为什么要将当前的实例挂在Dep上,获取之后为什么为什么只为null
// update 在什么执行的