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
};
原文地址:https://www.cnblogs.com/yumingle/p/14121768.html