vue2源码分析之响应式原理

目录

一、vue 基础功能

二、Observer浅析

三、Compiler类浅析

1、前置知识Node Types

2、基础功能:

四、dep收集依赖

五、watcher浅析

六、数据驱动浅析(整合完善功能)

1、插值表达式的更新处理。

2、v-text、v-model的更新处理。

七、双向绑定浅析


Vue2 主要通过以下 4 个步骤来实现数据双向绑定的:

实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。

实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。

实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。

实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

在浏览器打印vue,查看其属性方法,

一、vue 基础功能

1、负责接收初始化的参数(option)

2、负责把data中的属性注入到Vue实例,转换成getter/setter

3、负责调用observer监听data中所有属性的变化

4、负责调用compiler解析指定/差值表达式

完成vue基本代码和html应用页面,并打印简化的demo

//需要实现功能
//1、通过属性保存选项的数据
//2、把data中的成员转换成getter、setter,注入到vue实例中
//3、
//4、

class Vue {
    constructor (options){ 
        //1、需要实现功能1通过属性保存选项的数据
        this.$options = options || {}
        this.$data = options && options.data || {}
        this.$el = typeof options.el === 'string'? document.querySelector(options.el): options.el
        //2、把data中的成员转换成getter、setter,注入到vue实例中
        this._proxyData(this.$data)
    }
    _proxyData (data){ //data转化为getter setter
        //遍历data所有属性,把data属性注入到vue实例中
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key , {
                enumerable: true, //可枚举
                configurable: true, //可配置
                get() {
                    return data[key]
                },
                set(newVal) {
                    if(newVal === data[key]){
                        return 
                    }
                    data[key] = newVal
                }
            })
        })
    }
}

 html应用页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <span>{{msg}} </span><span>{{ user.name }}</span> 
        <h3> 第{{count}}次登陆</h3>
        <div v-text="msg"></div>
        <input type="text" v-model="msg">
    </div>
    <!-- <script src="https://cdn.jsdelivr.net/npm/vue@2"></script> -->
    <script src="./vue.js"></script>
    <script src="./observer.js"></script>
    <script src="./compiler.js"></script>
    <script>
        const vm = new Vue({
            el: "#app",
            data: {
                msg: "Hello",
                count: 1,
                user : {
                    name: "gu"
                }
            }
        }) 
        vm.msg = "Hi"
        console.log(vm)
    </script>
</body>
</html>

打印简化的demo

el和$data与注入代码一致

二、Observer浅析

基础功能:

1、负责把data选项中的属性转换成响应式数据

2、data中某个属性也是对象,把该属性转换成响应式数据

3、数据变化发送通知

添加observer基础代码


class Observer {
    constructor(data){
        this.walk(data)
    }
    walk (data) { //遍历所有属性
        if(!data || typeof data !== 'object') { //1、判断data是否是对象
            return
        }
        Object.keys(data).forEach(key=>{
            this.defineReactive(data, key, data[key])
        })
    }
    defineReactive (obj, key, val) { //把属性转换为getter、setter
        this.walk(val) //如果val是对象则会把val内部属性转换为响应式属性
        let _this = this
        Object.defineProperty(obj, key, {
            enumerable: true, //可枚举
            configurable: true, //可配置
            get() {
                return val
            },
            set (newVal) {

            }
        })
    }
}

监听对象属性

 defineReactive (obj, key, val) { //把属性转换为getter、setter
        this.walk(val) //如果val是对象则会把val内部属性转换为响应式属性
        Object.defineProperty(obj, key, {
         //...
        })
}

将属性复制的时候如何重新绑定

set(newVal) {
    if(newVal === val){
        return 
    }
    val = newVal
    this.walk(newVal) //如果新属性绑定的是对象,则将对象把属性转换为getter、setter

}

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。在 get 和 set 方法中,this 指向某个被访问和修改属性的对象。

三、Compiler类浅析

1、前置知识Node Types

文档、元素、属性以及 HTML 或 XML 文档的其他方面拥有不同的节点类型。

前置知识:HTML DOM 节点

存在 12 种不同的节点类型,其中可能会有不同节点类型的子节点:

节点类型描述子节点
1Element代表元素Element, Text, Comment, ProcessingInstruction, CDATASection, EntityReference
2Attr代表属性Text, EntityReference
3Text代表元素或属性中的文本内容。None
4CDATASection代表文档中的 CDATA 部分(不会由解析器解析的文本)。None
5EntityReference代表实体引用。Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference
6Entity代表实体。Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference
7ProcessingInstruction代表处理指令。None
8Comment代表注释。None
9Document代表整个文档(DOM 树的根节点)。Element, ProcessingInstruction, Comment, DocumentType
10DocumentType向为文档定义的实体提供接口None
11DocumentFragment代表轻量级的 Document 对象,能够容纳文档的某个部分Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference
12Notation代表 DTD 中声明的符号。None

2、基础功能:

基础代码

1、负责编译模板,解析指令/差值表达式2、负责页面的首次渲染3、当数据变化后重新渲染视图

//实现功能
//1、编译模板,处理文本节点和元素节点
class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
    }
    //1、编译模板,处理文本节点和元素节点
    compiler (el) {

    }
    //2、编译元素节点,处理指令
    compilerElement (node) {

    }
    //3、编译文本节点,处理差值表达式
    compilerText (node) {

    }
    //4、判断元素属性是否是指令
    isDirective (attrName) {
        return attrName.startWith("v-")
    }
    //5、判断节点是否是文本节点
    isTextNode (node) {
        return node.nodeType === 3
    }
    //6、判断节点是否是元素节点
    isElementNode (node) {
        return node.nodeType === 1
    }
}

编译模板,分发处理文本节点和元素节点

//实现功能
//1、编译模板,处理文本节点和元素节点
class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
        this.compiler(this.el)
    }
    //1、编译模板,处理文本节点和元素节点
    compiler (el) {
        let childNodes = el.childNodes //伪数组
        Array.from(childNodes).forEach( node => {
            if ( this.isTextNode(node) ) { //处理文本节点
                this.compilerText (node) 
            }else if ( this.isElementNode(node) ) { //处理元素节点
                this.compilerElement (node) 
            }
            // 判断node节点,是否有子节点,如果有子节点,要递归调用compile
            if( node.childNodes && node.childNodes.length ){
                this.compiler(node)
            }
        })
    }
   
}

打印文本节点:

处理文本节点,仅能处理遇到的第一个

 //3、编译文本节点,处理差值表达式
    compilerText (node) {
        //使用正则表达式 匹配是否是{{}}
        //替换值
        let reg = /\{\{(.+?)\}\}/
        let val = node.textContent
        console.log("RegExp.$1", RegExp.$1)
        console.log("val", val)
        if( reg.test(val) ){
            let key = RegExp.$1.trim()
            node.textContent = val.replace(reg, this.vm[key])
        }
    }

打印元素节点:

compilerElement (node) {
    console.log(node)
    console.log(node.attributes)
}

编译元素节点,处理指令

 //2、编译元素节点,处理指令
    compilerElement (node) {
        console.log(node)
        console.log(node.attributes)
        //遍历所有的属性节点
        Array.from(node.attributes).forEach((attr)=>{
            //判断是否是指令
            let attrName = attr.name 
            if( this.isDirective(attrName) ){//判断元素属性是否是指令
                //v-text => text
                attrName = attrName.substr(2) //获取属性名
                let key = attr.value //属性值
                this.update(node, key, attrName)
            }
        })  
    }
    //调用指定方法判断是元素属性还是文本节点
    update (node, key, attrName) {
        let upadteFn = this[attrName+"Updater"] 
        upadteFn && upadteFn(node, this.vm[key])
    }

实现响应式机制

四、dep收集依赖

功能:1、收集依赖,添加观察者(watcher)2、通知所有观察者

基础代码

// 实现功能:收集依赖发送通知
// 1、存储所有的观察者
// 2、通知所有观察者
//实现方法为每一个响应式数据创建dep对象,在使用响应式数据的时候收集依赖、创建观察者对象。
// 当数据变化时通知所有观察者对象,调用update方法更新视图
class Dep {
    constructor () {
        //存储所有的观察者
        this.subs = []
    }
    //添加观察者
    addSub () {
        if (sub && sub.update) { //如果update方法,说明是之前定义的compiler
            this.subs.push(sub)
        }
    }
    //发送通知
    notify () {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}

在oberser收集依赖、发送通知

defineReactive (obj, key, val) { //把属性转换为getter、setter
        // watch基础功能
        // 2、!自身实例化的时候往dep对象中添加自己
        let dep = new Dep() //收集依赖
        
        this.walk(val) //如果val是对象则会把val内部属性转换为响应式属性
        let _this = this
        Object.defineProperty(obj, key, {
            enumerable: true, //可枚举
            configurable: true, //可配置
            get() {
                Dep.target && dep.addSub(Dep.target) //收集依赖
                return val
            },
            set (newVal) {
                if(newVal === val){
                    return 
                }
                val = newVal
                _this.walk(newVal) //如果新属性绑定的是对象,则将对象把属性转换为getter、setter
                
            // watch基础功能
            // 1、!当数据变化出发以来,dep通知所有的watcher实例更新视图
                dep.notify()

            }
        })
    }

五、watcher浅析

基础功能

1、当数据变化出发以来,dep通知所有的watcher实例更新视图

2、自身实例化的时候往dep对象中添加自己

基础代码:

// watch基础功能

// 1、当数据变化出发以来,dep通知所有的watcher实例更新视图

// 2、自身实例化的时候往dep对象中添加自己

class Watcher {
    constructor (vm, key, cb) {
        this.vm = vm
        // data中的属性名称
        this.key = key
        // 回调函数负责更新视图
        this.cb = cb
        this.oldValue = vm[key]
    }    
    //当数据发生变化的时候更新视图
    update () {
        let newValue = this.vm(this.key)
        if( this.oldValue === new val ) {
            return
        }
        this.cb(newValue)//更新视图
    }
    
}

dep需要收集watcher,实现方式:

1、把watcher对象记录到dep类的静态属性

2、触发get方法,在get方法中调用addSub

// watch基础功能

// 1、!当数据变化出发以来,dep通知所有的watcher实例更新视图

// 2、!自身实例化的时候往dep对象中添加自己

class Watcher {
    constructor (vm, key, cb) {
        this.vm = vm
        // data中的属性名称
        this.key = key
        // 回调函数负责更新视图
        this.cb = cb


        // dep需要收集watcher,实现方式:
        // 1、把watcher对象记录到dep类的静态属性
        // 2、触发get方法,在get方法中调用addSub

        // !!!1、把watcher对象记录到dep类的静态属性
        Dep.target = this

        this.oldValue = vm[key] //记录旧属性  !!!触发Observer的get方法,在get方法中调用addSub
        Dep.target = null //!!!防止重复添加

    }     
    //当数据发生变化的时候更新视图
    update () {
        let newValue = this.vm(this.key)
        if( this.oldValue === new val ) {
            return
        }
        this.cb(newValue)//更新视图
    }

}

六、数据驱动浅析(整合完善功能)

数据响应式:数据模型仅仅是普通的javaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的dom操作,提高了开发效率

双向绑定:数据改变;视图改变,数据也随之改变。

我们可以使用v-model在表单上创建双向数据绑定

数据驱动Vue最独特的特性之一

开发过程中,仅需要数据本身,不需要关心数据是如何渲染到视图。

1、插值表达式的更新处理。

在compiler处理文本节点时,创建watcher对象,当数据改变更新视图。

//3、编译文本节点,处理差值表达式
    compilerText (node) {
        //使用正则表达式 匹配是否是{{}}
        let reg = /\{\{(.+?)\}\}/g //一个标签仅能识别一个双括号  如:<span>{{msg}} {{name}} </span>,{{name}}将无法识别,需要学习正则加强……
        let val = node.textContent
        console.log("node", node)
        console.log("RegExp", RegExp)
        // console.log("RegExp.$1", RegExp.$1)
        if( reg.test(val) ){
            let key = RegExp.$1.trim()
            node.textContent = val.replace(reg, this.vm[key])


            //!!!创建watcher对象,当数据改变更新视图 此处为插值表达式的处理
            new Watcher(this.vm, key, (newValue)=>{
                node.textContent = newValue
            })
        }   
    }

查看浏览器,插值表达式发生变化

2、v-text、v-model的更新处理。

//调用指定方法判断是元素属性还是文本节点
    update (node, key, attrName) {
        let upadteFn = this[attrName+"Updater"].bind(this)
        upadteFn && upadteFn(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
        })
    }

修改msg

七、双向绑定浅析

在input通过监听事件实现双向绑定,在处理v-mode指令的时候,为节点注册事件

//处理v-model
    modelUpdater (node, key, value) {
        node.value = value
        new Watcher(this.vm, key, (newValue)=>{
            node.value = newValue
        })
        //双向绑定
        node.addEventListener("input", ()=>{
            this.vm[key] = node.value
        })
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值