zoukankan      html  css  js  c++  java
  • Vue 响应式原理学习

    根据官方文档描述实现的属性劫持和观察者双向绑定实现

    package.json

    {
      "name": "vue-app",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "dev": "webpack --mode development",
        "build": "webpack --mode production",
        "start": "webpack serve"
      },
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "clean-webpack-plugin": "^3.0.0",
        "css-loader": "^5.0.1",
        "html-webpack-plugin": "^5.0.0-alpha.14",
        "less": "^3.12.2",
        "less-loader": "^7.1.0",
        "style-loader": "^2.0.0",
        "vue": "^2.6.12",
        "webpack-cli": "^4.2.0",
        "webpack-dev-server": "^3.11.0",
        "webpack-hot-middleware": "^2.25.0"
      },
      "dependencies": {}
    }
    
    webpack.config.js
    const { resolve } = require("path");
    const webpack = require("webpack");
    const { CleanWebpackPlugin } = require("clean-webpack-plugin")
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    module.exports = {
        entry: [
            // 由于 index.html 等html文件
            // 不在依赖跟踪里面所以不会进行 HMR
            "./src/index.js",
        ],
        output: {
            filename: "[name]-bundle-[fullhash:10].js",
            path: resolve(__dirname, "dist")
        },
        module: {
            rules: [
                {
                    test: /.css$/,
                    use: [
                        "style-loader",
                        "css-loader"
                    ],
                },
                {
                    test: /.less$/,
                    use: [
                        "style-loader",
                        "css-loader",
                        // 需要安装 less 和 less-loader
                        "less-loader"
                    ]
                }
            ]
        },
        plugins: [
    注意,这里的插件是顺序执行的
    new webpack.progressplugin(),
            new CleanWebpackPlugin(),
            new HtmlWebpackPlugin({
                template: resolve(__dirname, "src", "static", "index.html")
            }),
            // HMR modules
            // new webpack.HotModuleReplacementPlugin(),
            // new webpack.NoEmitOnErrorsPlugin()
        ],
        mode: "development",
        // webpack-dev-server
        devServer: {
            host: "localhost",
            port: 5001,
            // recompile [JS module] when js file modified
            hot: true,
            // refresh page when recompile [JS module]
            inline: true,
            open: true,
            compress: true,
            watchContentBase: true,
            contentBase: resolve(__dirname, "dist"),
            // watchOptions: {
            //     poll: true
            // }
        }
    }
    

    src/index.js

    import "./index.less"
    console.log("init index.js")
    import './mvue/MVue'
    // if (module.hot) {
    //     module.hot.accept()
    // }
    

    src/index.less

    #app {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 500px;
        flex-direction: column;
        font-size: 30px;;
    }
    .hidden {
        display: block;
        border: 1px solid black;
    }
    

    src/static/index.html

    <!DOCTYPE html>
    <head>
        <!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> -->
        <!-- <script src="../MVue.js"></script> -->
    </head>
    <body>
        <div id="app">
            {{person.name}}---{{person.age}}
            <div>普通的 div 分隔新</div>
            <!-- <div v-text="person.age">{{person.name}}</div> -->
            <div v-text="msg"></div>
            <div v-html="htmlStr"></div>
            <input type="text" v-model="person.name"/>
            <!-- v-for -->
            <!-- { hidden: isActive } -->
            <div v-bind:class="isActive">box shown</div>
            <div :class="isActive">box shown</div>
            <button v-on:click="onClickButton">测试事件命令</button>
            <button @click="onClickButton">测试 @</button>
            <!-- isActive=!isActive -->
        </div>
    </body>
    </html>
    <script  defer>
        // defer 对内联无效
        // 等待 外部 MVue 加载完成
        document.addEventListener('DOMContentLoaded',function(){
            const vm = new MVue({
                el: "#app",
                data: function () {
                    return {
                        msg: "v text 测试",
                        person: {
                            name: "markaa",
                            age: 80
                        },
                        htmlStr: "<h2>测试 v-html</h2>",
                        isActive: "hidden"
                    }
                },
                methods: {
                    onClickButton(e) {
                        this.$data.msg += "1"
                        console.log(this.$data.msg);
                    }
                }
            });
            window.vm = vm;
        });
    </script>
    

    src/mvue/MVue.js

    const {Observer, Watcher, Dep} = require("./Observer");
    class MVue {
        constructor(options) {
            this.$options = options;
            this.$data = options.data;
            if (typeof this.$data == 'function') {
                this.$data = this.$data();
            }
            this.$el = options.el;
            new Observer(this.$data);
            new Compiler(this.$el, this);
        }
    }
    class Compiler {
        constructor(el, vm) {
            this.vm = vm;
            const node = this.isElementNode(el) ? el : document.querySelector(el);
            const fragments = this.node2Fragment(node);
            this.Compile(fragments);
            node.appendChild(fragments);
        }
        getValue(expr, data) {
            return expr.split(".").reduce((value, field) => {
                return value[field];
            }, data);
        }
        setValue(expr, newValue, data) {
            let findObj = data;
            const fields = expr.split(".");
            for(let i = 0 ; i < fields.length - 1; i++){
                findObj = findObj[fields[i]];
            }
            findObj[fields[fields.length - 1]] = newValue;
            // return expr.split(".").reduce((value, field) => {
            //     return value[field];
            // }, data);
        }
        compute() {
            // 目前只能支持 变量绑定,不支持 js 表达式
            return {
                text: (expr, eventName) => {
                    const viewData = this.getValue(expr, this.vm.$data);
                    return viewData;
                },
                html: (expr, eventName) => {
                    const viewData = this.getValue(expr, this.vm.$data);
                    return viewData;
                },
                model: (expr, eventName) => {
                    const viewData = this.getValue(expr, this.vm.$data);
                    return viewData;
                },
                bind: (expr, eventName) => {
                    const viewData = this.getValue(expr, this.vm.$data);
                    return viewData;
                },
                on: (expr, eventName) => {
                    return this.vm.$options.methods[expr];
                },
                textExpr: (expr) => {
                    return expr.replace(/{{(.+?)}}/g, (...args) => {
                        return this.getValue(args[1], this.vm.$data);
                    });
                }
            };
        }
        updaters() {
            return {
                text: (node, value) => {
                    node.textContent = value;
                },
                html: (node, value) => {
                    node.innerHTML = value
                },
                model: (node, value) => {
                    node.value = value;
                },
                bind: (node, value, eventName) => {
                    node.setAttribute(eventName, value);
                },
                on: (node, value, eventName) => {
                    node.addEventListener(eventName, value.bind(this.vm));
                }
            };
            // 目前只能支持 变量绑定,不支持 js 表达式
        }
        Compile(fragments) {
            // 对 frag 里面的元素进行编译,根据类型
            fragments.childNodes.forEach(node => {
                if (node.nodeType == 1) {
                    // 元素节点
                    // 查找是否拥有指令、
                    const attributes = node.attributes;
                    [...attributes].forEach(attr => {
                        // 检测如果是 v- 指令,就使用指定的编译方法
                        const { name, value } = attr;
                        let directive, directiveName, eventName, viewData;
                        // 判断分支里面仅仅做 获取 事件名 和 属性值 的功能,细节功能在下一级别
                        if (this.isDirective(name)) {
                            [, directive] = name.split("v-");
                            // 可能拥有事件名称,例如 v-on:click
                            [directiveName, eventName] = directive.split(":");
                            // 不能统一在这里面使用 getValue,因为属性值不仅仅是 data 数据绑定,还有 on 的 来自与 methods 里面的函数名字或者的 js 表达式
                            // 统一传到 updater 里面 分别 处理   viewData = this.getValue(value, this.vm.$data);
                            console.log(directiveName, eventName, value);
                            // 在这里创建 观察者, 获取 监听的 expr 和 更新回调函数
                            // 由于在 构造函数里面将 watcher 挂到 target,所以不会被垃圾回收
                            new Watcher(this.vm.$data, node, value, (newValue) => {
                                this.updaters()[directiveName](node, newValue, eventName);
                            });
                            // 在这步,会调用在 observer 里面劫持定义的 get 方法。
                            viewData = this.compute()[directiveName](value, eventName);
                            this.updaters()[directiveName](node, viewData, eventName);
                            this.clearDirective(node, name);
                            // 如果是输入类型控件,需要进行双向绑定
                            if (directiveName == "model") {
                                node.addEventListener("input", (event) => {
                                    const newValue = event.target.value;
                                    this.setValue(value, newValue, this.vm.$data);
                                });
                            }
                        } else if (this.isAtDirective(name)) {
                            [, eventName] = name.split("@");
                            // 可能拥有事件名称,例如 v-on:click
                            directiveName = "on";
                            // console.log(directiveName, eventName, value);
                            viewData = this.compute()[directiveName](value, eventName);
                            this.updaters()[directiveName](node, viewData, eventName);
                            this.clearDirective(node, name);
                        } else if (this.isBindDirective(name)) {
                            [, eventName] = name.split(":");
                            // 可能拥有事件名称,例如 v-on:click
                            directiveName = "bind";
                            // console.log(directiveName, eventName, value);
                            new Watcher(this.vm.$data, node, value, (newValue) => {
                                this.updaters()[directiveName](node, newValue, eventName);
                            });
                            viewData = this.compute()[directiveName](value, eventName);
                            this.updaters()[directiveName](node, viewData, eventName);
                            this.clearDirective(node, name);
                        }
                        // 是正常属性,不做处理
                    });
                } else if (node.nodeType == 3) {
                    // 文本节点
                    let viewData = node.textContent;
                    // 查找是否包含差值表达式
                    if (viewData.indexOf("{{") >= 0) {
                        new Watcher(this.vm.$data, node, viewData, (newValue) => {
                            // TODO 需要是 textExpr 类型的计算
                            this.updaters()["text"](node, newValue);
                        });
                        viewData = this.compute()["textExpr"](viewData);
                        this.updaters()["text"](node, viewData);
                    }
                }
            });
        }
        node2Fragment(node) {
            const fragments = document.createDocumentFragment();
            let firstChild;
            while (firstChild = node.firstChild) {
                fragments.appendChild(firstChild);
            }
            return fragments;
        }
        isElementNode(node) {
            return node.nodeType == 1;
        }
        isDirective(directiveName) {
            return directiveName.startsWith("v-");
        }
        isAtDirective(directiveName) {
            return directiveName.startsWith("@");
        }
        isBindDirective(directiveName) {
            return directiveName.startsWith(":");
        }
        clearDirective(node, attrName) {
            node.removeAttribute(attrName);
        }
    }
    window.MVue = MVue
    

    src/mvue/Observer.js

    class Observer {
        constructor(data) {
            this.data = data;
            this.observe(this.data);
        }
        observe(data) {
            if (data && typeof data == "object") {
                Object.keys(data).forEach(key => {
                    this.defineReactive(data, key, data[key]);
                });
            }
        }
        // 对于 data 来说,如果是非 object 类型的成员,就直接劫持属性
        // 否则递归一直到普通值
        defineReactive(data, key, value) {
            this.observe(value);
            // 定义依赖收集器, 然后每次调用 get 的时候(也就是在 compile 的时候,调用 getValue 的时候会调用 get)
            // 此时,从 compiler 那里获取 node 和 expr。 即观察者对象
            // 对该变量(或者 从 compiler 看来是 expr )的全部依赖 对象
            // 闭包引用 dep
            const dep = new Dep();
            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: false,
                get: () => {
                    if (Dep.target) {
                        dep.addWatcher(Dep.target);
                    }
                    console.log(dep);
                    return value;
                },
                // 这里面必须要使用 箭头函数,从而使得 this 指向 Observer.否则是 data
                set: (newValue) => {
                    // 如果用户赋值了一个 object 变量,就需要对新值进行监听
                    this.observe(newValue);
                    if (newValue != value) {
                        value = newValue;
                    }
                    dep.notify();
                }
            });
        }
    }
    class Watcher {
        constructor(data, node, expr, callback) {
            this.data = data;
            this.node = node;
            this.expr = expr;
            this.callback = callback;
            this.oldValue = this.getOldValue();
        }
        getValue(expr, data) {
            if (expr.indexOf("{{") > -1) {
                return expr.replace(/{{(.+?)}}/g, (...args) => {
                    return this.getValue(args[1], this.data);
                });
            } else {
                return expr.split(".").reduce((value, field) => {
                    return value[field];
                }, data);
            }
        }
        getOldValue() {
            // 将当前的 watcher 绑到 Dep 下面。
            Dep.target = this;
            // 取值,会触发劫持的 get 方法
            let value = this.getValue(this.expr, this.data);
            Dep.target = null;
            return value;
        }
        update() {
            let value = this.getValue(this.expr, this.data);
            this.callback(value);
        }
    }
    class Dep {
        constructor(node, expr) {
            this.watcher = [];
            // this.oldValue = this.getOldValue();
        }
        // getOldValue() {
        //     return 
        // }
        addWatcher(watcher) {
            this.watcher.push(watcher);
        }
        notify() {
            this.watcher.forEach(w => {
                w.update();
            });
        }
    }
    module.exports = {
        Observer,
        Watcher,
        Dep
    };
    
  • 相关阅读:
    Python基础-字符串方法 、文件操作
    Python基础-列表、字典
    Python基础作业-用户登录
    LeetCode 78. Subsets
    LeetCode 77. Combinations
    LeetCode 76. Minimum Window Substring
    LeetCode 74. Search a 2D Matrix
    LeetCode 73. Set Matrix Zeroes
    LightOJ 1043
    LightOJ 1042
  • 原文地址:https://www.cnblogs.com/yumingle/p/14121768.html
Copyright © 2011-2022 走看看