简单实现Vue的双向绑定__前端__Vue.js
发布于 18 天前 作者 banyungong 122 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

前言

前端面试时经常会问到Vue是如何实现双向绑定的,大部分人都会回答是通过数据劫持结合发布-订阅模式实现的,那么具体是怎么实现的,我们来简单的实现下。

双向绑定实现原理

实现双向绑定离不开三个类:Compile、Observer、Watcher、Dep

  • Compile:扫描并解析每个元素节点,根据不同指令模板替换节点内容并绑定对应的更新函数。
  • Observer:依附于每个被监听的对象,一旦依附,监听器转变目标对象的属性为响应式getter/setter方法,便于收集依赖项并分发更新操作。
  • Watcher:连接Compile和Observer,解析表达式并收集依赖项,在表达式值更改时触发回调。可同时用于$watch api和指令集。
  • Dep:依赖收集器,能被Watcher订阅

Vue 实例初始化

        // 初始化
        class Vue {
            constructor(options) {
                this.$options = options || {}
                this.$el = typeof options.el === 'string' // 判断el是否为元素节点
                    ? document.querySelector(options.el) // 若不是,则获取赋值
                    : options.el; // 若是,则直接赋值
                this.$data = options.data;
                if (this.$el) {
                    new Compile(this.$el, this)
                }
            }
        }

Compile 指令解析器

        class Compile {
            constructor(el, vm) {
                this.vm = vm
                const childNodes = [...el.childNodes]; // 获取子节点,进行遍历
                childNodes.forEach(node => {
                    // 根据不同节点类型进行编译
                    if (node.nodeType === 1) {  //元素节点
                        this.compileElement(node)
                    } else if (node.nodeType === 3) { // 文本节点
                        this.compileText(node)
                    }
                })
            }
            compileText(node) {
                // 编译文本节点
            }
            compileElement(node) {
                // 获取该节点的所有属性,对属性进行遍历
                [...node.attributes].forEach(attr => {
                    const name = attr.name;
                    // 看当前name是否是一个指令
                    if (name.startsWith('v-')) { // v-if v-model v-show
                        // 对类似v-model进行操作, 去除v-
                        const attrName = name.substr(2);
                        this.update(node, attr.value, this, attrName) 
                        // 在v-model="text"中 attr.value为"text"

                    } else if (name.startsWith('@')) { // @click
                        // 对事件进行处理 在这里处理的是@click
                        const eventName = name.split('@')[1];
                        this.event(node, attr.value, this, eventName) 
                        // 在@click="clickEvent"中 attr.value为"clickEvent"
                    }

                })
            }
            update(node, key, vm, attrName) {
                const updater = {
                    // 使用的时候 和 Vue 的一样
                    text(node, key, value) {
                        node.textContent = value
                    },
                    // v-model
                    model(node, key, value) {
                        node.value = value
                    }
                    ...
                    ...
                }
                const fn = this.updater[attrName]
                // 从 data 中获取值赋值给node
                fn && fn(node, key, vm.$data[key]) 
            }
            event(node, key, vm, eventName) { // vm为Vue实例
                // 从methods中获取方法,并做一个监听
                const fn = vm.$options.methods && vm.$options.methods[key]; 
                node.addEventListener(eventName, fn.bind(vm), false)
            }
        }

Observer 数据监听器

        class Observer {
            constructor(data) {
                this.observe(data);
            }
            observe(data) {
                // 判断 data 是否为非空对象
                if (!data || typeof data !== 'object') return;
                Object.keys(data).forEach(key => {
                    this.defineReactive(data, key)
                })
            }
            defineReactive(obj, key) {
                const value = obj[key]; // 如果 value 也为对象,递归遍历属性,添加响应式
                this.observe(value);
                Object.defineProperty(obj, key, {
                    // 设置可枚举
                    enumerable: true,
                    // 设置可配置
                    configurable: true,
                    get() {
                        return value;
                    },
                    set(newVal) {
                        if (newVal === value) return;
                        value = newVal // 设置新值
                        // 若新值为对象,递归遍历属性,添加响应式
                        this.observe(value)
                    }
                })
            }
        }

修改Vue初始化方法

        // 初始化
        class Vue {
            constructor(options) {
                ...
                if (this.$data) {
                    // 使用 Observer 把data中的数据转为响应式
                    new Observer(this.$data)
                }
            }
        }

Dep 发布者

        class Dep {
            constructor() {
                // 观察者收集器
                this.subs = []
            }
            addSub(sub) {
                // 判断观察者是否存在且是否拥有update方法
                if (!sub || !sub.update) return;
                this.subs.push(sub)
            }
            notify() {
                // 循环遍历观察者集合并调用他们update方法
                this.subs.forEach(sub => {
                    sub.update();
                })
            }
        }

修改Observer

            ...
            defineReactive(obj, key) {
                // 创建 Dep 对象
                let dep = new Dep()
                Object.defineProperty(obj, key, {
                    ...
                    get() {
                        //订阅数据变化,往Dep中添加观察者
                        Dep.target && dep.addSub(Dep.target);
                        return value;
                    }
                    set(newVal) {
                        ...
                        // 触发通知 更新视图
                        dep.notify()
                        ...
                    }
                    ...
                })
            }
            ...

Watcher 观察者

        class Watcher {
            constructor(vm, expr, callback) {
                this.vm = vm; // Vue实例
                this.expr = expr; // data中的属性名
                this.cb = callback; // 回调
                Dep.target = this; // 把观察者存放在 Dep.target
                // 旧数据 更新视图的时候要进行比较
                this.oldValue = vm[expr]; // vm[expr] 触发了 get 方法
                // 数据赋值完之后Dep.target就可以置空了 不置空会发生错乱
                Dep.target = null
            }
            // 观察者中的必备方法 用来更新视图
            update() {
                // 获取新值
                let newValue = this.vm[this.expr]
                // 比较旧值和新值
                if (newValue === this.oldValue) return
                // 调用具体的更新方法
                this.cb(newValue)
            }
        }

修改Compile

            update(node, key, vm, attrName) {
                const updater = {
                    text(node, key, value) {
                        node.textContent = value
                        // 创建观察者
                        new Watcher(this.vm, key, newValue => {
                            node.textContent = newValue
                        })
                    },
                    model(node, key, value) {
                        node.value = value
                        // 创建观察者
                        new Watcher(this.vm, key, newValue => {
                            node.textContent = newValue
                        })
                    }
                    ...
                }
                ...
            }

代理

我们在使用vue时可以直接通过this.name(vm.name)的方式来获取数据。这是因为vue内部也做了一层代理,把数据获取操作vm上的取值操作代理到vm.$data上

        class Vue {
            constructor(options) {
                this.$options = options || {}
                this.$el = typeof options.el === 'string' // 判断el是否为元素节点
                    ? document.querySelector(options.el) // 若不是,则获取赋值
                    : options.el; // 若是,则直接赋值
                this.$data = options.data;
                if (this.$el) {
                    new Compile(this.$el, this)
                }
                if (this.$data) {
                    // 使用 Obsever 把data中的数据转为响应式
                    new Observer(this.$data)
                }
                this.proxyData(data)
            }
            proxyData(data) {
                for (const name in data) {
                    Object.defineProperty(this, name, {
                        get() {
                            return data[name]
                        },
                        set(newVal) {
                            data[name] = newVal
                        }
                    })
                }
            }
        }

参考资料

Vue的MVVM实现原理:https://juejin.cn/post/6844904183938678798#heading-12

手写一个简易vue响应式带你了解响应式原理:https://juejin.cn/post/6989106100582744072#heading-11

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 不好说君 原文链接:https://juejin.im/post/7012527585666007076

回到顶部