最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手写响应式实现

    正文概述 掘金(螺旋静)   2020-11-24   809

    手写响应式实现

    数据驱动

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

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

    2. 双向绑定

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

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

    数据响应

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

    //数据劫持:当访问或者设置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

    手写响应式实现

    • 功能

      • 当数据变化触发依赖,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
        })
    }
    

    调试

    首次渲染/数据改变

    总结

    流程回顾:

    手写响应式实现

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

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

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

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

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

    • Vue
      • 记录传入的选项,设置 data/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博客,其它博客同步更新。


    起源地下载网 » 手写响应式实现

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元