• vue的MVVM


    Vue的相关知识有

    • 字符串模板
    • MVVM
    • 虚拟dom和domdiff,查看下一篇笔记

    字符串模板

    function render(template, data) {
      const reg = /{{(w+)}}/; // 模板字符串正则
      if (reg.test(template)) { // 判断模板里是否有模板字符串
        const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
        template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
        return render(template, data); // 递归的渲染并返回渲染后的结构
      }
      return template; // 如果模板没有模板字符串直接返回
    }
    // 使用
    let template = '我是{{name}},年龄{{age}},性别{{sex}}';
    let data = {
      name: '姓名',
      age: 18
    }
    render(template, data); // 我是姓名,年龄18,性别undefined
    

    MVVM
    MVVM 设计模式,是由 MVC(最早来源于后端)、MVP 等设计模式进化而来

    • M - 数据模型(Model)
    • VM - 视图模型(ViewModel)
    • V - 视图层(View)

    在 Vue 的 MVVM 设计中,我们主要针对Compile(模板编译),Observer(数据劫持),Watcher(数据监听),Dep(发布订阅)几个部分来实现,核心逻辑流程可参照下图:

    image.png

    数据监听API

    • vue2.0和vue2.x是用defineProperty
    • vue3.0即将使用proxy

    为什么要改用proxy,因为defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,defineProperty需要判断如果是数组,需要重写他的原型方法,而proxy就不需要

    为什么还不上线,因为proxy的兼容性太差

    defineProperty监听

    // 监听普通属性
    function isKey(obj,key){
        return Object.defineProperty(obj,key,{
            get: function() {
                console.log('get :', key);
                return eval(key) || "";
            },
            set: function(newValue) {
                console.log('set :', newValue);
                key = newValue;
            }
        })
    }
    // 监听数组属性
    function toNewArray(data,key){
        // 实例具名回调函数
        window.eval("var callback = function "+key+" (args,k){console.log('数组'+k+'发生变化...');}")
        return new NewArray(data[key],callback)  // 注入回调函数
    }
    
    class NewArray extends Array{
        constructor(arr,callback){
            if(arguments.length === 1){
                return super()
            }  // 产生中间数组会再进入构造方法
            // let args = arr  // 原数组
            arr.length === 1 ? super(arr[0].toString()) : super(...arr)
            this.callback = callback  // 注入回调具名函数
        }
        push(...args){
            super.push(...args)
            this.callback(this, this.callback.name)  // 切面调用具名回调函数
        }
        pop(){
            super.pop()
            this.callback(this, this.callback.name)
        }
        splice(...args){
            super.splice(...args)
            this.callback(this, this.callback.name)
        }
    }
    
    var data = {
        arr:[1,2,3,4],
        name:"pdt"
    }
    function init(data){
      Object.keys(data).forEach(key => {
         let value = data[key]
         // 如果是obj就递归
         if(value是对象){
             init(value)  
         }else if(Array.isArray(value)){
             // 如果value是数组
             data[key] = toNewArray(data,key)
         }else{
             // 如果是普通的值
             isKey(data,key)
         }
      })
    }
    init(data)
    

    proxy监听

    var data = {
       arr:[1,2,3,4],
       name:"pdt"
    }
    
    function init(data){
      Object.keys(data).forEach(key => {
         let value = data[key]
         if(value 是对象){
           data[key] = init(value)
         }
      })
      data = newData(data)
    }
    
    init(data)
    
    function newData(data){
        return new Proxy(data, {
            get: function(target, key, receiver) {
                console.log(target, key, receiver)
                return Reflect.get(target, key, receiver);
            },
            set: function(target, key, value, receiver) {
                console.log(target, key, value, receiver);
                return Reflect.set(target, key, value, receiver);
            }
        })
    }
    

    defineProperty实现一个Vue

    <script>
    class MVVM {
        constructor (options) {
            // 一上来 先把可用的东西挂载在实例上
            this.$el = options.el;
            this.$data = options.data;
    
            // 如果有要编译的模板就开始编译
            if(this.$el) {
                // 数据劫持,就是把对象的所有属性添加 set 和 get 方法
                new Observer(this.$data);
                // 用数据和元素进行编译
                new Compile(this.$el, this);
            }
        }
    }
    class Observer {
        constructor (data) {
            this.observe(data);
        }
        observe (data) {
            // 验证 data
            if(!data || typeof data !== 'object') {
                return;
            }
            // 要对这个 data 数据将原有的属性改成 set 和 get 的形式
            // 要将数据一一劫持,先获取到 data 的 key 和 value
            Object.keys(data).forEach(key => {
                // 劫持(实现数据响应式)
                this.defineReactive(data, key, data[key]);
                this.observe(data[key]); // 深度劫持
            });
        }
        defineReactive (object, key, value) { // 响应式
            let _this = this;
            // 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
            let dep = new Dep();
    
            // 获取某个值被监听到
            Object.defineProperty(object, key, {
                enumerable: true,
                configurable: true,
                get () { // 当取值时调用的方法
                    Dep.target && dep.addSub(Dep.target);
                    console.log(dep.subs);
                    return value;
                },
                set (newValue) { // 当给 data 属性中设置的值适合,更改获取的属性的值
                    if(newValue !== value) {
                        _this.observe(newValue); // 重新赋值如果是对象进行深度劫持
                        value = newValue;
                        dep.notify(); // 通知所有人数据更新了
                    }
                }
            });
        }
    }
    
    class Dep {
        constructor () {
            // 订阅的数组
            this.subs = [];
        }
        addSub (watcher) { // 添加订阅
            this.subs.push(watcher);
        }
        notify () { // 通知
            this.subs.forEach(watcher => watcher.update());
        }
    }
    
    // 观察者的目的就是给需要变化的那个元素增加一个观察者,当数据变化后执行对应的方法
    class Watcher {
        constructor (vm, exp, callback) {
            this.vm = vm;
            this.exp = exp;
            this.callback = callback;
            // 先获取一下老的值
            this.value = this.get();
        }
        get () { // 获取实例上老值得方法
            Dep.target = this;
            let value = CompileUtil.getVal(this.vm, this.exp);
            Dep.target = null;
            return value;
        }
        update () {
            let newValue = CompileUtil.getVal(this.vm, this.exp);
            let oldValue = this.value;
            // 用新值和老值进行对比,如果变化,就调用更新方法
            if(newValue !== oldValue) {
                this.callback(newValue); // 如果修改后得新旧值不等就执行回调
            }
        }
    }
    
    class Compile {
        constructor (el, vm) {
            this.el = this.isElementNode(el) ? el : document.querySelector(el);
            this.vm = vm;
            if(this.el) {
                // 如果这个元素能获取到,我们才开始编译
                // 1.先把这些真实的 DOM 移动到内存种 fragment
                let fragment = this.nodeToFragment(this.el);
                // 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{message}}
                this.compile(fragment);
                // 把编译好的 fragment再塞回到页面中去
                this.el.appendChild(fragment);
            }
        }
    
        /* 专门写一些辅助方法 */
        isElementNode (node) { // 是不是 dom 节点
            return node.nodeType === 1;
        }
        isDirective (name) { // 是不是指令
            return name.includes('v-');
        }
    
        /* 核心方法 */
        nodeToFragment (el) { // 需要将 el 中的内容全部放到内存中
            // 文档碎片 内存中的 dom 节点
            let fragment = document.createDocumentFragment();
            let firstChild;
            while(firstChild = el.firstChild) {
                fragment.appendChild(firstChild);
            }
            return fragment; // 内存中的节点
        }
        compile (fragment) { // 编译文档碎片方法
            // 需要递归
            let childNodes = fragment.childNodes;
            Array.from(childNodes).forEach(node => {
                if(this.isElementNode(node)) {
                    // 是元素节点,还需要继续深入的检查
                    // console.log('element', node);
                    this.compile(node);
                    // 这里需要编译元素
                    this.compileElement(node);
                } else {
                    // 是文本节点
                    // console.log('text', node);
                    // 这里需要编译文本
                    this.compileText(node);
                }
            });
        }
        compileElement (node) { // 编译元素节点
            // 带 v-model 的
            let attrs = node.attributes; // 取出当前节点的属性
            Array.from(attrs).forEach(attr => {
                // 判断属性名字是不是包含 v-
                let attrName = attr.name;
                if(this.isDirective(attrName)) {
                    // 取到对应的值,放在节点中
                    let exp = attr.value;
                    let [, type] = attrName.split('-');
                    // node this.vm.$date exp
                    CompileUtil[type](node, this.vm, exp);
                }
            });
        }
        compileText (node) { // 编译文本节点
            // 带 {{}} 的
            let exp = node.textContent; // 获取文本中的内容
            let reg = /{{([^}]+)}}/g; // {{a}} {{b}} {{c}}
            if(reg.test(exp)) {
                // node this.vm.$date exp
                CompileUtil['text'](node, this.vm, exp);
            }
        }
    }
    
    CompileUtil = {
        getVal (vm, exp) { // 获取实例上对应的数据
            exp = exp.split('.');
            return exp.reduce((prev, next) => {
                return prev[next];
            }, vm.$data);
        },
        setVal (vm, exp, newVal) { // 设置实例上对应的数据
            exp = exp.split('.');
            return exp.reduce((prev, next, currentIndex) => {
                if(currentIndex === exp.length - 1) {
                    return prev[next] = newVal;
                }
                return prev[next];
            }, vm.$data);
        },
        getTextVal (vm, exp) { // 获取编译文本后的结果
            return exp.replace(/{{([^}]+)}}/g, (...arg) => {
                return this.getVal(vm, arg[1]);
            });
        },
        text (node, vm, exp) { //文本处理
            let updateFn = this.updater['textUpdater'];
            let value = this.getTextVal(vm, exp);
            exp.replace(/{{([^}]+)}}/g, (...arg) => {
                new Watcher(vm, arg[1], newValue => {
                    // 如果数据变化了,文本节点应该重新获取依赖的数据更新文本中的内容
                    updateFn && updateFn(node, newValue);
                });
            });
    
            updateFn && updateFn(node, value);
        },
        model (node, vm, exp) { // 输入框处理
            let updateFn = this.updater['modelUpdater'];
            let value = this.getVal(vm, exp);
            // 这里应该加一个监控,数据变化了,应该调用 watch 的回调
            new Watcher(vm, exp, newValue => {
                updateFn && updateFn(node, newValue);
            });
            // 添加输入框事件实现双向绑定
            node.addEventListener('input', e => {
                let newValue = e.target.value;
                this.setVal(vm, exp, newValue);
            });
            // 防止没有的指令解析时报错
            updateFn && updateFn(node, value);
        },
        updater: {
            // 文本更新
            textUpdater (node, value) {
                node.textContent = value;
            },
            // 输入框更新
            modelUpdater (node, value) {
                node.value = value;
            }
        }
    };
    </script>
    
    // 使用
    <div id="app">
       <!-- 双向数据绑定 靠的是表单 -->
       <input type="text" v-model="message">
       <div>{{message}}</div>
    </div>
    <script>
    let vm = new MVVM({
      el: '#app',
      data: {
    	message: 'hello world!'
      }
    });
    </script>
    

    使用proxy写一个简易版的vue

    <script type="text/javascript">
    class Vue {
        constructor(options) {
            this.$el = document.querySelector(options.el);
            this.$methods = options.methods;
            this._binding = {};
            this._observer(options.data);
            this._compile(this.$el);
        }
        _pushWatcher(watcher) {
            if (!this._binding[watcher.key]) {
                this._binding[watcher.key] = [];
            }
            this._binding[watcher.key].push(watcher);
        }
        /*
         observer的作用是能够对所有的数据进行监听操作,通过使用Proxy对象
         中的set方法来监听,如有发生变动就会拿到最新值通知订阅者。
        */
        _observer(datas) {
            const me = this;
            const handler = {
                set(target, key, value) {
                    const rets = Reflect.set(target, key, value);
                    me._binding[key].map(item => {
                        item.update();
                    });
                    return rets;
                }
            };
            this.$data = new Proxy(datas, handler);
        }
        /*
         指令解析器,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相对应的更新函数
        */
        _compile(root) {
            const nodes = Array.prototype.slice.call(root.children);
            const data = this.$data;
            nodes.map(node => {
                if (node.children && node.children.length) {
                    this._compile(node.children);
                }
                const $input = node.tagName.toLocaleUpperCase() === "INPUT";
                const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA";
                const $vmodel = node.hasAttribute('v-model');
                // 如果是input框 或 textarea 的话,并且带有 v-model 属性的
                if (($vmodel && $input) || ($vmodel && $textarea)) {
                    const key = node.getAttribute('v-model');
                    this._pushWatcher(new Watcher(node, 'value', data, key));
                    node.addEventListener('input', () => {
                        data[key] = node.value;
                    });
                }
                if (node.hasAttribute('v-html')) {
                    const key = node.getAttribute('v-html');
                    this._pushWatcher(new Watcher(node, 'innerHTML', data, key));
                }
                if (node.hasAttribute('@click')) {
                    const methodName = node.getAttribute('@click');
                    const method = this.$methods[methodName].bind(data);
                    node.addEventListener('click', method);
                }
            });
        }
    }
    /*
    watcher的作用是 链接Observer 和 Compile的桥梁,能够订阅并收到每个属性变动的通知,
    执行指令绑定的响应的回调函数,从而更新视图。
    */
    class Watcher {
    constructor(node, attr, data, key) {
        this.node = node;
        this.attr = attr;
        this.data = data;
        this.key = key;
    }
    update() {
        this.node[this.attr] = this.data[this.key];
    }
    }
    </script>
    
    // 使用
    <div id="app">
    <input type="text" v-model='count' />
    <input type="button" value="增加" @click="add" />
    <input type="button" value="减少" @click="reduce" />
    <div v-html="count"></div>
    </div>
    <script type="text/javascript">
    new Vue({
        el: '#app',
        data: {
            count: 0
        },
        methods: {
            add() {
                this.count++;
            },
            reduce() {
                this.count--;
            }
        }
    });
    </script>
    

    总结上面的代码
    可以看到上面的两个写法都需要这么几个构造函数

    • MVVM.js 把参数传给Observer和Compile
    • Observer.js 劫持,就是监听,每个key都需要搭配一个Dep,数据更新就执行Dep.updata
    • Dep.js 就是一个订阅发布的数组而已
    • Compile.js,解析dom元素,通过属性【v-xx】创建更新规则,每个规则创建一个watch,watch装在对应位置的Dep数组里
    • Watch.js,闭包存着所有的dom和对应的无数个更新规则,由Dep调用它的更新

    相关链接一
    相关链接三
    相关链接四
    相关链接五
    相关链接六

    真正的Vue

    • 不能保留原来的dom,遍历过程成vDom【生命周期是created】
    • 遍历虚拟dom数据,生成新的真实的dom,再结合data数据,methods,计算属性,watch,数据绑定到新的dom上
    • 当数组发生改变,生成新的vDom,对比新旧的vDom,更新到真实dom上【如果使用proxy,就不需要domdiff了】,就是一个真正的vue了

    参考资料
    vue源码解读一
    vue源码解读二
    Vue实现

  • 相关阅读:
    详解扩展欧几里得算法(扩展GCD)
    NOIP 2012 同余方程
    NOIP 2011 观光公交
    NKOJ4330 逛公园
    NKOJ 7.7练习题A IP地址
    NKOJ3777 卡牌操作
    NKOJ3772 看电影
    NKOJ3765 k个最小和
    NKOJ3775 数列操作
    NKOJ3768 数列操作
  • 原文地址:https://www.cnblogs.com/pengdt/p/12046431.html
Copyright © 2020-2023  润新知