Vue.js 响应式原理

整体分析

在这里插入图片描述

  • Vue
    • 目标:将 data 数据注入到 Vue 实例,便于方法内操作
  • Observer(发布者)
    • 目标:数据劫持,监听数据变化,并在变化时通知 Dep
  • Dep(消息中心)
    • 目标:存储订阅者以及管理消息的发送
  • Watcher(订阅者)
    • 目标:订阅数据变化,进行试图更新
  • Compiler
    • 目标:解析模板中的指令与插值表达式,并替换成相应的数据

Vue 类

  • 功能
    • 接受配置信息
    • 将 data 的属性转换成 Getter、Setter,并注入到 Vue 实例中
    • *监听 data 中所有属性的变化,设置成响应式数据
    • *调用解析功能(解析模板内的插值表达式、指令等)
class Vue {
    constructor (options) {
        // 1. 存储属性
        this.$options = options || {}
        this.$data = options.data || {}
        // 判断 el 值的类型,并进行相应处理
        const {el} = options
        this.$el = typeof el === 'string' ? document.querySelector(el) : el

        // 2. 将 dta 属性注入到 Vue 实例中
        _proxyData(this, this.$data)

        // *3. 创建 Observer 实例监视 data 的属性变化
        new Observer(this.$data)

        // *4. 调用 Compiler
        new Compiler(this)
    }
}

// 将 data 的属性注入到 Vue 实例
function _proxyData (target, data) {
    Object.keys(data).forEach(key => {
        Object.defineProperty(target, key, {
            enumerable: true,
            configurable: true,
            get () {
                return data[key]
            },
            set (newValue) {
                data[key] = newValue
            }
        })
    })
}

Observer 类

  • 功能
    • 通过数据劫持方式监视 data 中的属性变化,变化时通知消息中心 Dep
    • 需要考虑 data 的属性也可能为对象,也要转换成响应式数据
class Observer {
    // 接受传入的对象,将这个对象的属性转换为 Getter / Setter
    constructor(data) {
        this.data = data
        // 遍历数据
        this.walk(data)
    }
    // 封装用于数据便利的方法
    walk(data) {
        // 将遍历后的属性,都转换为 Getter/Setter
        Object.keys(data).forEach(key => this.convert(key, data[key]))
    }
    // 封装用于将对象转换为响应式数据的方法
    convert(key, value) {
        defineReactive(this.data, key, value)
    }
}

// 用于为对象定义一个响应式的属性
function defineReactive(data, key, value) {
    // 创建消息中心
    const dep = new Dep()

    // 检测是否为对象,如果是,创建一个新的 observer 实例进行管理
    observer(value)

    // 进行数据劫持
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log('获取了属性')

            // * 在触发 Getter 时添加订阅者
            Dep.target && dep.addSub(Dep.target)
            return value
        },
        set(newValue) {
            console.log('设置了属性')
            if (value === newValue) return
            value = newValue
            observer(value)

            // * 数据变化时,通知消息中心
            dep.notify(0)
        }
    })
}

function observer(value) {
    if (typeof value === 'object' && value !== null) {
        return new Observer(value)
    }
}

Dep 类

  • Dep 是 Dependency 的简写,含义 “依赖”,指的是 Dep 用于收集与管理订阅者与发布者之间的依赖关系

  • 功能:

    • *为每个数据收集顶硬的依赖,存储依赖
    • 添加并存储订阅者
    • 数据变化时,通知所有观察者
class Dep {
    constructor () {
        // 存储订阅者
        this.subs = []
    }
    // 添加订阅者
    addSub (sub) {
        if (sub && sub.update) {
            this.subs.push(sub)
        }
    }
    // 通知订阅者的方法
    notify () {
        // 遍历订阅者,并执行更新功能
        this.subs.forEach(sub => {
            sub.update()
        });
    }
}

Watcher 类

  • 功能:
    • 实例化 Watch 时,往 dep 对象中添加自己
    • 当数据变化触发 dep,dep 通知所有对应的 Watcher 实例更新视图
class Watcher {
    constructor (vm, key, cb) {
        // 当前 Vue 实例
        this.vm = vm
        // 订阅的属性名
        this.key = key
        // 数据变化后,要执行的回调
        this.cb= cb

        // 触发 Getter 前,将当前订阅者实例存储给 Dep 类
        Dep.target = this
        // 记录属性更改之前的值,用于进行更新状态检测(导致了属性 Getter 的触发)
        this.oldValue = vm[key]
        // 操作完毕后,清除 target ,用于存储下一个 Watch 实例
        Dep.target = null
    }

    // 封装数据变化时,更新视图的功能
    update () {
        const newValue = this.vm[this.key]
        if (newValue === this.oldValue) return
        // 数据改变,调用更新后的回调
        this.cb(newValue)
    }
}

Compiler 类

  • 功能:
    • 进行编译模板,并解析内部指令与插值表达式
    • 进行页面的首次渲染
    • 数据变化后,重新渲染视图
class Compiler {
    constructor(vm) {
        this.vm = vm
        this.el = vm.$el

        // 初始化模板编译方法
        this.compile(this.el)
    }
    // 基础模板方法
    compile(el) {
        const childNodes = el.childNodes
        Array.from(childNodes).forEach(node => {
            // 检测节点类型(文本节点、元素节点)
            if (isTextNode(node)) {
                // 编译文本节点内容
                this.compileText(node)
            } else if (isElementNode(node)) {
                // 编译元素节点内容
                this.compileElement(node)
                // 检测当前节点是否存在子节点
                if (node.childNodes && node.childNodes.length) {
                    this.compile(node)
                }
            }

        });
    }
    // 封装文本节点编译方法
    compileText(node) {
        const reg = /\{\{(.+?)\}\}/g
        // 去除内容中不必要的空格与换行
        const value = node.textContent.replace(/\s/g, '')
        // 声明数据存储多段文本
        const tokens = []
        // 记录已经操作过的位置的索引
        let lastIndex = 0
        // 记录当前提取内容的初始索引
        let index
        let result
        while (result = reg.exec(value)) {
            // 本次提取内容的初始索引
            index = result.index
            // 处理普通文本
            if (index > lastIndex) {
                // 将中间部分的内容存储到 tokens 中
                tokens.push(value.slice(lastIndex, index))
            }
            // 处理插值表达式内容(去除空格的操作可省略)
            const key = result[1].trim()
            // 根据 key 获取对应属性值,存储到 tokens
            tokens.push(this.vm[key])

            // 更新 lastIndex
            lastIndex = index + result[0].length

            // 创建订阅者,Watcher 实时订阅数据变化
            const pos = tokens.length - 1
            new Watcher(this.vm, key, newValue => {
                // 数据变化,修改 tokens 中的对应数据
                tokens[pos] = newValue
                node.textContent = tokens.join('')
            })
        }
        if (tokens.length) {
            // 初始页面初始渲染
            node.textContent = tokens.join('')
        }
    }
    // 封装元素节点编译方法
    compileElement(node) {
        // 获取属性节点
        Array.from(node.attributes).forEach(attr => {
            // 保存属性名称,并检测属性的功能
            let attrName = attr.name
            if (!isDirective(attrName)) return
            // 获取指令的具体名称
            attrName = attrName.slice(2)
            // 获取指令的值,代表响应式数据的名称
            let key = attr.value
            // 封装 update 方法,用于进行不同指令的功能分配
            this.update(node, key, attrName)
        })
    }
    // 用于进行指令分配的方法
    update(node, key, attrName) {
        // 名称处理
        let updateFn = this[attrName + 'Updater']
        // 检测并调用
        updateFn && updateFn.call(this, node, key, this.vm[key])
    }
    // v-text 处理
    textUpdater(node, key, value) {
        // 给元素设置内容
        node.textContent = value
        // 订阅数据变化
        new Watcher(this.vm, key, newValue => {
            node.textContent = newValue
        })
    }
    // v-model 处理
    modelUpdater(node, key, value) {
        // 给元素设置内容
        node.value = value
        // 订阅数据变化
        new Watcher(this.vm, key, newValue => {
            node.value = newValue
        })
        // 监听 input 事件,实现双向绑定
        node.addEventListener('input', () => {
            this.vm[key] = node.value
        })
    }
}

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

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

// 判断属性名是否为指令
function isDirective(attrName) {
    return attrName.startsWith('v-')
}

功能总结

  • Vue 类
    • 把 data 的属性注入到 Vue 实例
    • 调用Observer 实现数据响应式处理
    • 调用 Compiler 编译模板
  • Observer
    • 将 data 的属性转换成 Getter/Setter
    • 为 Dep 添加订阅者 Watcher
    • 数据变化时发送通知 Dep
  • Dep
    • 收集依赖,添加订阅者(watcher)
    • 通知订阅者
  • Watcher
    • 编译模板时创建订阅者,订阅数据变化
    • 接到 Dep 通知时,调用 Compiler 中的模板功能更新视图
  • Compiler
    • 编译模板,解析指令与插值表达式
    • 负责页面首次渲染与数据变化后重新渲染
      在这里插入图片描述
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值