通过手写一个简单版Vue学习其响应式原理

98 阅读4分钟

前情提要


各位好,最近在复习完 Vue 的响应式原理后,做一篇学习笔记。

Vue 一直都是热门的前端框架,那么 Vue 究竟是怎么实现的呢?今天我们通过手写一个简单版 Vue 带大家了解一下。

今天讲解的是 Vue 2.x 也就是通过 Object.defineProperty 实现响应式的相关知识,Vue 3 通过 Proxy 实现的相关知识我们放在下篇文章中讲。

完整项目地址:github.com/zhtzhtx/Ter…

Vue


我们都知道使用 Vue 2.x 是通过 new Vue() 来初始化 Vue 实例的,所以我们先来实现 Vue 这个类,它的功能是:

  • 负责接收初始化的参数(选项)
  • 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
  • 负责调用 observer 监听 data 中所有属性的变化
  • 负责调用 compiler 解析指令/插值表达式

Vue 这个 class 接受一些参数,用来初始化 Vue 的属性

image.png

class Vue {
    constructor (options) {
        // 1. 保存选项的数据
        this.$options = options || {}
        this.$data = options.data || {}
        const el = options.el
        this.$el = typeof options.el === 'string' ? document.querySelector(el) : el
    }
}

接下来,我们需要遍历 data 中的属性并将它们挂载到 Vue 的实例上,因为我们在 Vue 中是通过 this.xx 来读取 data 中的数据的。

image.png

我们先定义一个_proxyData 方法用来遍历 data 的所有属性并通过 Object.defineProperty 方法将它们挂载到 Vue 实例上。

_proxyData (data) {
    // 遍历 data 的所有属性的 key
    Object.keys(data).forEach(key => {
        // 这里的 this 指向 Vue 实例
        Object.defineProperty(this, key, {
            get () {
                return data[key]
            },
            set (newValue) {
                // 如果新值和旧值一样直接返回
                if (data[key] === newValue) return
                data[key] = newValue
            }
        })
    })
}

好了,我们在 Vue 的构造函数中调用_proxyData 方法

class Vue {
    constructor (options) {
        // 1. 保存选项的数据
        this.$options = options || {}
        this.$data = options.data || {}
        const el = options.el
        this.$el = typeof options.el === 'string' ? document.querySelector(el) : el
        // 2. 把data中的成员转换成getter和setter,注入到vue实例中
        this._proxyData(this.$data)
    }
}

这样,Vue 这个类就初步构造完成了。

Observer


接下来,我们来构建 Observer 这个类,它的作用是将 data 中的数据改成响应式数据。在 Vue 中我们通过 this.xx 修改数据时,data 中的数据也会自动修改。

当调用 Observer 这个类时,我们传入 Vue 实例的 data 属性,然后定义一个 walk 方法,用来将 data 属性所对应的对象转化为响应式对象。

class Observer {
    constructor(data) {
        // 我们希望构造类之后,立即将传入的data中的属性转换成getter/setter,所以在构造函数中调用walk()
        this.walk(data)
    }
    walk(data) {
        // 判断传入值是否为对象,对象才能转化为响应式
        if (!data || typeof data !== "object") return
        Object.keys(data).forEach(key => {
            // 对各个属性进行数据拦截
            this.defineReactive(data, key, data[key])
        })
    }
}

然后,我们来写 defineReactive 方法,它和之前写的_proxyData 方法类似

defineReactive(obj, key, value) {
    const _this = this
    // 如果val是对象,把val内部的属性转换成响应式数据
    this.walk(value)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            // 这里不能直接使用data[key],因为会调用get方法,陷入死循环
            return value
        },
        set(newValue) {
            if (newValue === value) return
            // 这里形成一个闭包,一直储存value值
            value = newValue
            // 判断传入的值是不是对象,如果是,转化为响应式对象
            // 这里的this指向obj,而不是Observer实例,所以设置_this
            _this.walk(newValue)
        }
    })
}

好了,这样 Observer 这个类就初步完成了,我们需要在 Vue 的构造函数调用 Observer 创建实例

class Vue {
    constructor (options) {
        // 1. 通过属性保存选项的数据
        this.$options = options || {}
        this.$data = options.data || {}
        this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
        // 2. 把data中的成员转换成getter和setter,注入到vue实例中
        this._proxyData(this.$data)
        // 3. 调用observer对象,监听数据的变化
        new Observer(this.$data)
    }
}

Compiler


下面我们来编写 Compiler 这个类,它的作用是根据 Vue 中的 api, data 中的数据转化成文本节点。比如下面的例子中,msg 和 count 就是 data 中的数据,那么它们如何在初始化 Vue 实例后,转化为文本节点呢?这就体现了 Compiler 的作用。

image.png

好了,我们来看看 Compiler 这个类, 首先它接受 Vue 实例作为参数,因为所有的 data 数据都是挂载在 Vue 实例上的。

class Compiler {
    constructor(vm) {
        // 挂载Vue实例
        this.vm = vm
        // 挂载根节点
        this.el = vm.$el
        // 编译模板
        this.compile(this.el)
    }
}

然后,我们来看 compile 方法,首先获取根节点下所有子节点,其次遍历所有子节点,根据是文本节点还是 DOM 节点来编译数据

// 编译模板
compile(node) {
    // 获取根节点下所有子节点
    const childNodes = node.childNodes
    Array.from(childNodes).forEach(childNode => {
        if (this.isTextNode(childNode)) {
            // 判断是否为文本节点
            this.compileTextNode(childNode)
        } else if (this.isElementNode(childNode)) {
            // 判断是否为元素节点
            this.compileElementNode(childNode)
        }
        // 判断node节点,是否有子节点,如果有子节点,要递归调用compile
        if (childNode.childNodes && childNode.childNodes.length) {
            this.compile(childNode)
        }
    })
}

我们可以根据 node.nodeType 来判断是文本节点还是 DOM 节点

// 判断是否为文本节点
isTextNode(node) {
    return node.nodeType === 3
}
// 判断是否为元素节点
isElementNode(node) {
    return node.nodeType === 1
}

在用于编译文本节点的 compileTextNode 方法中,使用正则表达式获取双花括号中的 data 数据的 key,将其转化为 data 数据

// 编译文本节点
compileTextNode(node) {
    let value = node.textContent
    let reg = /\{\{(.+?)\}\}/
    if (reg.test(value)) {
        // 获取{{}}中的文本
        const key = RegExp.$1.trim()
        // 替换{{}}中的文本
        node.textContent = value.replace(reg, this.vm[key])
    }
}

在用于编译 DOM 节点的 compileElementNode 方法中,先获取 DOM 节点的所有属性,遍历然后判断是否是 Vue 的指令,比如 "v-text" ,根据不同的指令将相应的数据加载到 DOM 节点上。

// 编译元素节点
compileElementNode(node) {
    Array.from(node.attributes).forEach(attr => {
        // 获取属性的名称
        let attrName = attr.name
        // 判断是否为指令
        if (this.isDirective(attrName)) {
            // 如"v-text"截取"text"
            attrName = attrName.substring(2)
            const key = attr.value
            this.update(node, key, attrName)
        }
    })
}

我们可以通过是否以 "v-" 开头判断是否为 Vue 的指令

// 判断是否为指令
isDirective(attrName) {
    return attrName.startsWith("v-")
}

由于是简化版 Vue,这里我们只编译 v-text 和 v-model 两种指令

// 编译指令
update(node, key, attrName) {
    // 这里指 textUpdater 方法和 modelUpdater 方法
    const updateFn = this[attrName + "Updater"]
    // 将this指向Compiler实例
    updateFn && updateFn.call(this, node, this.vm[key], key)
}

先看编译 "v-text" 的 textUpdater 方法,直接将 DOM 节点的文本内容替换。

textUpdater(node, value, key) {
    node.textContent = value
}

再来看编译 "v-model" 的 modelUpdater 方法,将 input 中的值替换同时监听 input 输入的值,将其同步到 Vue 的 data 数据中

// 编译v-model
modelUpdater(node, value, key) {
    node.value = value
    node.addEventListener("input", () => {
        this.vm[key] = node.value
    })
}

好了,这样 Compiler 这个类就完成了,记得在 Vue 中生成它的实例

class Vue {
    constructor(options) {
        // 1. 通过属性保存选项的数据
        this.$options = options || {}
        this.$data = options.data || {}
        this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
        // 2. 把data中的成员转换成getter和setter,注入到vue实例中
        this._proxyData(this.$data)
        // 3. 调用observer对象,监听数据的变化
        new Observer(this.$data)
        // 4. 调用compiler对象,解析指令和差值表达式
        new Compiler(this)
    }
}

Dep


接着,我们来看 Dep 这个类,因为 Vue 强调的是响应式数据,也就说当 data 中数据变化后自动同步到页面中,那么谁来发布更新呢?这就是 Dep 的作用,它是发布/订阅模式中的事件中心。

在 Dep 的构造函数中初始化一个数组,用于存储所有的监听器

class Dep{
    constructor(){
        // 存储所有的监听器
        this.subs=[]
    }
}

定义一个 addSubs 方法用于将监听器存储在数组,如果一个数据包含 update 方法,我们就判定它是监听器

// 添加监听器
addSubs(sub){
    if(sub&&sub.update){
        this.subs.push(sub)
    }
}

定义一个 notify 方法用于当有 data 数据更新时,通知所有的监听器更新

notify(){
    this.subs.forEach(sub=>{
        sub.update()
    })
}

好了,这样 Dep 这个类就完成了,我们需要在 Observer 这个类的 defineReactive 方法中进行依赖收集,当数据更新时通知所有监听器更新

defineReactive(obj, key, value) {
    const _this = this
    // 负责收集依赖,并发送通知
    const dep = new Dep()
    // 如果val是对象,把val内部的属性转换成响应式数据
    this.walk(value)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            return value
        },
        set(newValue) {
            if (newValue === value) return
            // 这里形成一个闭包,一直储存value值
            value = newValue
            // 判断传入的值是不是对象,如果是,转化为响应式对象
            // 这里的this指向obj,而不是Observer实例,所以设置_this
            _this.walk(newValue)
            // 通知依赖更新
            dep.notify()
        }
    })
}

Watcher


最后,我们来看 Watcher 这个类,它就是我们在 Dep 类中提到的监听器。它接受 Vue 实例、监听数据对应的 key还有回调函数作为参数,由于它需要在遍历 data 数据时添加到 Dep 类的数组中,所以我们将它挂载到全局对象上,这里如果挂载在 Window 上也可以,为了方便理解我们将它挂载在 Dep 类上

class Watcher {
    constructor(vm, key, callback) {
        // 挂载Vue实例
        this.vm = vm
        // data中的属性名称
        this.key = key
        // 回调函数负责更新视图
        this.cb = callback
        // 把watcher对象记录到Dep类的静态属性target
        Dep.target = this
        // 触发get方法,在get方法中会调用addSubs
        this.oldValue = vm[key]
        Dep.target = null
    }
}

我们在 Watcher 中定义一个 update 方法,用于当收到 Dep 类通知时调用回调函数来更新视图

// 当数据发生变化的时候更新视图
update() {
    let newValue = this.vm[this.key]
    if(this.oldValue === newValue) return
    // 调用回调函数
    this.cb(newValue)
}

Watcher 类是在 Compiler 类中的compileTextNode 方法、textUpdater 方法和 modelUpdater 方法中初始化实例并获取对应的更新视图的回调函数

// 编译文本节点
compileTextNode(node) {
    let value = node.textContent
    let reg = /\{\{(.+?)\}\}/
    if (reg.test(value)) {
        // 获取{{}}中的文本
        const key = RegExp.$1.trim()
        // 替换{{}}中的文本
        node.textContent = value.replace(reg, this.vm[key])
        new Watcher(this.vm, key, newValue => {
            node.textContent = newValue
        })
    }
}
// 编译v-text
textUpdater(node, value, key) {
    node.textContent = value
    new Watcher(this.vm, key, newValue => {
        node.textContent = newValue
    })
}
// 编译v-model
modelUpdater(node, value, key) {
    node.value = value
    node.addEventListener("input", () => {
        this.vm[key] = node.value
    })
    new Watcher(this.vm, key, newValue => {
        node.value = newValue
    })
}

在 Observer 类的 defineReactive 方法中,需要将其添加到 Dep 实例存储监听器的数组中

defineReactive(obj, key, value) {
    const _this = this
    // 负责收集依赖,并发送通知
    const dep = new Dep()
    // 如果val是对象,把val内部的属性转换成响应式数据
    this.walk(value)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            // 这里不能直接使用data[key],因为会调用get方法,陷入死循环
            const watcher = Dep.target
            watcher && dep.addSubs(watcher)
            return value
        },
        set(newValue) {
            if (newValue === value) return
            // 这里形成一个闭包,一直储存value值
            value = newValue
            // 判断传入的值是不是对象,如果是,转化为响应式对象
            // 这里的this指向obj,而不是Observer实例,所以设置_this
            _this.walk(newValue)
            // 通知依赖更新
            dep.notify()
        }
    })
}

总结


好了,最后让我们总结一下 各个类的功能

  • Vue

    • 记录传入的选项,设置 data/data/el
    • 把 data 的成员注入到 Vue 实例
    • 负责调用 Observer 实现数据响应式处理(数据劫持)
    • 负责调用 Compiler 编译指令/插值表达式等
  • Observer

    • 数据劫持

      • 负责把 data 中的成员转换成 getter/setter
      • 负责把多层属性转换成 getter/setter
      • 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
    • 添加 Dep 和 Watcher 的依赖关系

    • 数据变化发送通知

  • Compiler

    • 负责编译模板,解析指令/插值表达式
    • 负责页面的首次渲染过程
    • 当数据变化后重新渲染
  • Dep

    • 收集依赖,添加订阅者(watcher)
    • 通知所有订阅者
  • Watcher

    • 自身实例化的时候往dep对象中添加自己
    • 当数据变化dep通知所有的 Watcher 实例更新视图