Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue.js手写响应式实现 #26

Open
janeLLLL opened this issue Oct 20, 2020 · 0 comments
Open

Vue.js手写响应式实现 #26

janeLLLL opened this issue Oct 20, 2020 · 0 comments

Comments

@janeLLLL
Copy link
Owner

janeLLLL commented Oct 20, 2020

手写响应式实现

手写响应式实现

数据驱动

  • 数据响应式、双向绑定、数据驱动
  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/$el
    • 把 data 的成员注入到 Vue 实例
    • 负责调用 Observer 实现数据响应式处理(数据劫持)
    • 负责调用 Compiler 编译指令/插值表达式等
  • Observer
    • 数据劫持
      • 负责把 data 中的成员转换成 getter/setter
      • 负责把多层属性转换成 getter/setter
      • 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
    • 添加 Dep 和 Watcher 的依赖关系 数据变化发送通知
  • Compiler
    • 负责编译模板,解析指令/插值表达式
    • 负责页面的首次渲染过程
    • 当数据变化后重新渲染
  • Dep
    • 收集依赖,添加订阅者(watcher) 通知所有订阅者
  • Watcher
    • 自身实例化的时候往dep对象中添加自己
    • 当数据变化dep通知所有的 Watcher 实例更新视图
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant