手写响应式实现

416 阅读9分钟

手写响应式实现

数据驱动

  • 数据响应式、双向绑定、数据驱动
  1. 数据响应式

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

  2. 双向绑定

    • 数据改变,视图改变;视图改变,数据也随之改变
    • 使用v-model在表单元素上创建双向数据绑定
  3. 数据驱动是Vue独特的特性之一

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

数据响应

Vue2.x:defineProperty数据劫持,当访问或者设置vm中的成员的时候,做一些干预操作;数据更改,更新DOM值

Vue2.x通过Object.defineProperty()来劫持对象中属性,给属性添加setter、getter,每一个属性创建一个dep对象,dep负责收集依赖,在数据变动时dep对象通知watcher对象,watcher内部负责更新视图

//数据劫持:当访问或者设置vm中的成员的时候,做一些干预操作
let vm = {}
Object.defineProperty(vm, 'msg', {
    //可枚举(可遍历)
    enumerable:true,
    //可配置
    configurable:true,
    /**
    * 1.当获取值的时候执行
    */
    get(){
        console.log("获取值")
        return data.msg
    }
    /**
    * 2.当设置值的时候执行
    */
    set(newValue){
    	console.log("设置值")
    	if(newValue === data.msg){
            return
        }
    	data.msg = newValue
    	//数据更改,更新DOM值
    	document.querySelector('#app').textContent = data.msg
    }
})
//test
vm.msg = '1'
console.log(vm.msg)
//控制台出现设置值 / 获取值
  • 如果有多个属性需要转换getter/setter如何处理?

    在外层添加一个循环forEach

proxyData(data)

function proxyData(data) {
    Object.keys(data).forEach(key => {
        Object.defineProperty(vm, 'msg', {
            ...同上
        })
    })
}

Vue 3.x

  • Proxy代理对象
  • 直接监听对象,而非属性
  • ES6中新增,IE不支持,性能由浏览器优化
let data = {
    msg: '1',
    count: 0
}

let vm = new Proxy(data, {
    /**
    * 1.当访问vm的成员会执行
    * target对象, key属性 不需要传递,由系统完成
    */
    get(target, key不需要传递,由系统完成){
        return target[key]
    }
    /**
    * 2.当设置vm的成员会执行
    */
    set(target,key,newValue){
        if(target[key] === newValue){
            return
        }
        target[key] = newValue
        document.querySelector('#app').textContent = target[key]
	}
})

//test
vm.msg = '1'
console.log(vm.msg)

发布/订阅模式

  • 订阅者
  • 发布者
  • 信号中心

发布/订阅模式:存在“信号中心”,某任务执行完成,向信号中心“发布”一个信号,其它任务可以向信号中心“订阅”这个信号,从而知道什么时候自己可以开始执行。

  • vue的自定义事件
1.创建vue实例
2.$on注册事件,同一个事件可以注册多个事件处理函数
3.到了某时机使用$emit触发这个事件
  • 兄弟组件的通信过程
1.创建eventBus.js
2.创建vue实例/事件中心
3.定义两个组件,组件互相不知道存在
4.A组件定义$emit触发B组件内容/发布消息;B组件注册$on事件/订阅消息
  • 模拟自定义事件的实现
1.定义变量,去存储事件名称
//{ 'click' : [fn1, fn2], 'change': [fn]}
2.$emit:在事件对象中寻找对应的方法,再去执行
class EventEmitter {
    constructor() {
        //{ 'click' : [fn1, fn2], 'change': [fn]}
        this.subs = Object.create(null)
    }
    //注册事件
    //eventType:事件名称,handler:方法
    $on(eventType, handler) {
        this.subs[eventType] = this.subs[eventType] || []
        this.subs[eventType].push(handler)
    }
    
    //触发事件
    //eventType:事件名称
    $emit(eventType) {
        if(this.subs[eventType]){
            this.subs[eventType].forEach(handler => {
                handler()
            })
        }
    }
}

//test
let em = new EventEmitter()
em.$on('click', ()=>{
    console.log('1')
})
em.$on('click', ()=>{
    console.log('2')
})

em.$emit('click')
  • 同时,模拟发布/订阅者模式可以通过兄弟传值体会

观察者模式

  • 观察者(订阅者)Watcher
    • update():当事件发生时,具体要做的事情
  • 目标(发布者)Dep
    • subs数组:存储所有的观察者
    • addSub():添加观察者
    • notify():当事件发生时,调用所有观察者的update()方法
  • 和发布/订阅模式的区别:没有事件中心;并且发布者需要知道订阅者的存在
//发布者-目标
class Dep{
    constructor(){
        //记录所有的订阅者
        this.subs = []
    }
    
    //添加观察者
    addSub(sub) {
        if(sub && sub.update){
            this.subs.push(sub)
        }
    }
    
    //发布通知
    notify(){
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}
//订阅者-观察者
class Watcher() {
    update(){
        console.log('1')
    }
}

//test
let dep = new Dep()
let watcher = new Watcher()

dep.addSub(watcher)//添加观察者
dep.notify()//通知观察者,并且调用方法

//不需要创建Vue实例

发布/订阅和观察者模式总结

  • 观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所以观察者的订阅者与发布者之间是存在依赖的
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方存在

代码模拟vue响应式原理

  • vue基本结构
  • 打印vue实例观察
  • 整体结构

Vue

  • 功能:

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

    $options
    $el
    $data
    //属性:记录构造函数传过来的参数
    _proxyData()
    //私有方法:把data中属性转换注入实例
    
  • 代码

//vue.js
class Vue {
    constructor(options) {
        //1. 通过属性保存选项的数据
        this.$options = options || {}
        this.$data = options.data || {}
        this.$options = typeof options === 'string' ? document.query.querySelector(options.el) : options.el//如果是DOM对象直接返回
		//2. 把data中的成员转换 为getter和setter,注入到实例中
        this._proxyData(this.$data)
		//3. 调用observer对象,监听数据的变化
        new Observer(this.$data)
		//4. 调用compiler对象,解析指令和插值表达式
        new Compiler(this)
    }
    _proxyData(data){
        //遍历data中所有的属性
        Object.keys(data).forEach(key => {
            //把data属性注入到vue实例中
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return data[key]
                },
                set(newValue){
                    if(newValue === data[key]){
                        return
                    }
                    data[key] = newValue
                }
            })
        })
        
    }
}

使用:

<!--index.html-->
<script src="./js/vue.js"></script>
<script>
	let vm = new Vue({
        el:'#app',//选择器
        data: {
            msg: 'hello',
            count: 100
        }
    })
</script>

Observer

  • 功能:

    • 负责把data选项中的属性转换成响应式数据
    • data中的某个属性也是对象,把该属性转换成响应式数据
    • 数据变化发送通知:集合观察者去实现
  • 结构

    walk(data)
    //遍历所有属性
    defineReactive(data,key,value)
    //把属性转换成get和set
    
  • 代码

新建observer.js

class observer {
    constructor(data) {
        this.walk(data)//从vue接收data
    }
    walk(data) {
        //1.判断data是否是对象
        if(!data || typeof data !== 'object'){
            return
        }
        //2.遍历data对象的所有属性
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
            //使用到了this,箭头函数不会改变this的指向
        })
    }
	defineReactive(boj, key, val){
        let that = this
        //为每一个属性创建对应的dep对象:负责收集依赖,并发送通知
        let dep = new Dep()
        
        //如果val是对象,把val内部的属性
        this.walk(val)
        Object.defineProperty(obj, key, {
            enumrable: true,
            configurable: true,
            get() {
                Dep.target && Dep.addSub(Dep.target)//收集依赖:Dep.target里存储的就是watcher对象;在dep类中并没有定义它,是在watcher类中定义的
                return val

            },
            set(newValue) {
            	if(newValue === val){
                    return
                }
                val = newValue
                this.walk(newValue)
                //发送通知
                dep.notify()//发送通知
        	}
        })
    }
}

使用:

<!--index.html-->
<script src="./js/observer.js"></script>

结果:把$data转换为get和set

defineReactive

  • 需要修改$data内数据为响应式

Compiler类

  • 功能

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

    el//DOM对象
    vm//vue实例
    compile(el)//遍历DOM对象的所有节点
    
    //解析差值表达式
    compileElement(node)//解析元素中指令
    compileText(node)//解析差值表达式
    
    isDirective(attrName)//判断当前属性是否是指令
    //判断是文本节点还是元素节点
    isTextNode(node)
    isElementNode(node)
    
  • 代码

//compiler.js
class compiler {
    constructor(vm) {
        this.el = vm.$el
		this.vm = vm
        this.compile(this.el)
    }
    //编译模板,处理文本节点和元素节点
    compile(el) {
        let childNodes = el.childNodes
        //循环遍历节点:第一层子节点
        Array.from(childNodes).forEach(node => {
            //处理文本节点
            if(this.isTextNode(node)) {
                this.complieText(node)
            }
            //处理元素节点
            else if(this.isElementNode){
                this.compileElement(node)
            }
            //判断node节点,是否有子节点,如果有子节点,要递归调用compile
            if(node.childNodes && node.childNodes.length) {
                this.compile(node)
            }
        })
    }
    //编译元素节点,处理指令
    compileElement(node) {
        //console.log(node.attributes)
        /**
        * 属性名称和属性值name/value
        */
        //遍历所有的属性节点
        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
                
            }
        })
    }
    
    update (node, key, attrName) {
        let updateFn = this.[attrName + 'Updater']
        updateFn && updateFn.call(this, node, this.vm[key], key)
        //使用call改变内部方法的指向,此处的this就是compile对象???????????????????
    }
    /**
    * 都需要创建watcher对象
    */
    //处理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
        new Watcher(this.vm, key, (newValue) => {
            node.value = newValue
        })
        //双向绑定
        node.addEventListener('input', () => {
            this.vm[key] = node.value
        })
    }
    
    //编译文本节点,处理差值表达式
    compileText(node) {
        //{{ msg }}
        let reg = /\{\{(.+?)\}\}/
        let value = node.textContent
        if(reg.test(value)) {
            let key = RegExp.$1.trim()
            node.textContent = value.replace(reg, this.vm[key])
            //创建watcher对象。当数据改变更新视图
            new Watcher(this.vm, key, (newValue) => {
                node.textContent = newValue
            })
        }
    }
    /**
    * 创建watcher对象end
    */
    
    //判断元素是否是指令:判断是否是以'v-'开头
    isDirective(attrName) {
        return attrName.startsWith('v-')
    }
    //判断节点是否是文本节点:看nodeType的值
    isTextNode(node) {
        return node.nodeType === 3
    }
    //判断节点是否是元素节点
    isElementNode(node) {
        return node.nodeType === 1
    }
}

使用:

<!--index.html-->
<script src="./js/compiler.js"></script>

Dep

  • 功能

    • 收集依赖,添加观察者watcher
    • 通知所有观察者
  • 结构

    subs//数组,存储dep中所有的watcher
    addSub(sub)
    notify()//发布通知,通知所有的观察者
    
  • 代码

//dep.js
class Dep {
    constructor() {
        //存储所有的观察者
        this.subs = []
    }
    //添加观察者
    addSub(sub) {
        if(sub && sub.update) {
            this.subs.push(sub)
        }
    }
    //发送通知
    notify() {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}

Watcher

image-20201014140737494

  • 功能

    • 当数据变化触发依赖,dep通知所有的Wathcher实例更新视图
    • 自身实例化的时候往dep对象中添加自己
  • 结构

    vm//vue实例
    key//data中的属性名称
    cb//回调函数:更新视图
    oldValue//记录数据变化之前的值
    update()/比较新旧值是否发生变化,不更新视图
    
  • 代码

//watcher.js
class Watcher {
    constructor(vm, key, cb){
        this.vm = vm
        this.key = key
        this.cb = cb
        
        //把watcher对象记录到Dep类的静态属性target中
        Dep.target = this
        //触发get方法,在get方法中会调用addSub
        
        this.oldValue = vm[key]
        Dep.target = null
    }
    //更新视图
    update() {
        let newValue = this.vm[this.key]
        if(this.oldValue === newValue){
            return
        }
        this.cb(newValue)//如果值不等要更新视图
    }
}

使用:注意顺序

<!--index.html-->
  <script src="./js/dep.js"></script>
  <script src="./js/watcher.js"></script>
  <script src="./js/compiler.js"></script>
  <script src="./js/observer.js"></script>
  <script src="./js/vue.js"></script>

创建watcher类对象

  • 指令和差值表达式都是依赖数据的,所有依赖数据的位置都需要创建一个watcher对象,当数据改变时,会通知所有watcher对象改变视图
  • compiler.js中,textUpdatermodelUpdatecompileText需要创建watcher对象

双向绑定

  • 视图变化 <--> 数据变化
  • v-model设置的:modelUpdate()
//compiler.js
modelUpdate(node, value, key){
    ...
    //双向绑定
    node.addEventListener('input', () => {
        this.vm[key] = node.value
    })
}

调试

首次渲染/数据改变

总结

流程回顾:

image-20201014160453450

  1. 属性重新赋值成对象,是否是响应式的?

    vm:{msg : 1}
    vm.msg = {w:'1'}
    

  2. Vue实例新增一个成员是否是响应式的?

    不是。在Vue的构造函数中new Observer(this.$data)会把所有data转换为响应式数据,这件事在new Vue中执行。如果仅仅是vm.test='1'只是给vm增加了一个js属性。

    如何把新增数据转换为响应式数据

    使用Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property

    Vue.set(vm.someObject, 'b', 2)
    this.$set(this.someObject,'b',2)
    
  • 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 实例更新视图

本文首发于我的GitHub博客,其它博客同步更新。