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

    正文概述 掘金(唯小南)   2020-12-09   357

    目前的MVVM框架都解决了数据与视图直接维护的关系,在Vue中的一个特性就是数据驱动,Vue的学习过程中,经常会看到三个词:数据响应式、双向绑定、数据驱动

    数据响应式

    • 数据(即数据模型)是普通的 JS 对象,当修改数据时,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率

    双向绑定

    • 数据改变,视图改变;视图改变,数据也随之改变
    • 我们可以使用 v-model 在表单元素上创建双向数据绑定

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

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

    Vue2.x版本的数据响应式原理

    采用Object.defindProperty来进行数据劫持

    当把一个普通的JS对象在data中传入给Vue实例时,会遍历这个对象所有的property,并使用Object.defindProperty设置它的getter/setter,在访问对象属性的getter和setter时就可以进行额外操作

    // 多个属性时 就用Object.keys()得到所有自身的key来遍历
    Object.defineProperty(obj, 'count', {
      enumerable: true,
      configurable: true,
      get() {
        // 这里可以做其他处理
        return obj[key]
      },
      set() {
        if (obj[key] === newVal) {
          return
        }
        obj[key] = newVal
        // 简单地在这里进行dom操作
        document.querySelector('#count-div').innerHTML = newVal
      }
    })
    

    Vue3.x版本 使用ES6的Proxy 代理对象

    Proxy创建的代理对象,是可以处理对象的所有属性,不需要每个属性处理

    为原对象创建一个代理对象,在getter、setter中进行额外处理

    代理对象是不会影响到原来的对象,需要使用代理对象才有额外处理的效果

    // obj是没有变化 要使用vm
    let vm = new Proxy(obj, {
      get(target, key) {
        return Reflect.get(target, key)
      },
      set(target, key, newVal) {
        if (newVal === Reflect.get(target, key)) {
          return
        }
        Reflect.set(target, key, newVal)
        // 简单地在这里进行dom操作
        document.querySelector('#app').innerHTML = newVal
      }
    })
    

    发布订阅模式

    • 订阅者:向信号中心订阅一个信息号,当信号被触发后,执行自己的事件
    • 发布者:在某个时机向信息中心发布一个信号,发布信号后会订阅者才执行它自己的事件
    • 信号中心:存储订阅者的订阅,接收发布者的通知

    信号中心隔绝了订阅者和发布者关系,双方不知道对方存在(订阅者不知道谁发布的,发布者不知道谁订阅了)

    例如 Vue 中的eventBus

    // vm是信号中心
    // vm.$on 订阅者
    vm.$on('dataChange' , () => {
      console.log('订阅者工作')
    })
    // vm.$emit 发布者
    vm.$emit('dataChange')
    

    观察者模式

    观察者(订阅者) -- 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('update')
      }
    }
    
    // 测试
    let dep = new Dep()
    let watcher = new Watcher()
    
    dep.addSub(watcher)
    
    dep.notify()
    

    模拟Vue响应式原理

    模拟 Vue 响应式原理简单实现

    模拟Vue响应式原理需要实现这5个内容

    • Vue: 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
    • Observer: 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep
    • Compiler: 解析每个元素中的指令/插值表达式,并替换成相应的数据
    • Dep: 添加观察者(watcher),当数据变化通知所有观察者
    • Watcher: 数据变化更新视图

    Vue

    • constructor (options)构造器:  保存options、data、el

    • options.data的数据挂载到vue实例上

    • 调用proxyData() 把data的数据挂载到vue实例上的

    • 创建Observer对象,把data和data的子成员变成数据响应式

    • 创建Compiler对象编译template

      constructor (options) { // 保存接收的参数 // 保存data // 保存el this.options=optionsthis.options = options || {} this.options=options∣∣this.data = options.data || {} // todo el 没有判空 this.el=typeofoptions.el===string?document.querySelector(options.el):options.el//options.data的数据挂载到vue实例上this.proxyData(this.el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 把options.data的数据挂载到vue实例上 this.proxyData(this.el=typeofoptions.el===′string′?document.querySelector(options.el):options.el//把options.data的数据挂载到vue实例上this.proxyData(this.data) new Observer(this.$data) new Compiler(this) }

      // 遍历data的属性 挂载到this(Vue实例) proxyData (data) { Object.keys(data).forEach(key => { Object.defineProperty(this, key, { configurable: true, enumerable: true, get () { return data[key] }, set (newValue) { if (newValue === data[key]) { return } data[key] = newValue } }) }) }

    Observer

    • constructor() 调用walk

    • walk 会先判断传入的参数是不是对象,不是就返回,是对象就遍历key调用defineReative

    • defineReative 这里进行响应式处理,用Object.defindProperty(),创建这个key的Dep对象,再调用walk(如果这个key的值是对象,就会在这里递归遍历全部子节点)

    • getter里面进行Dep.target判断,加入到subs,返回value

    • setter里面调用walk(传入newVal,如果传入是对象就再进行递归遍历),赋值,dep.notify发布更新

      constructor (data) { this.walk(data) } walk (data) { if (!data || typeof data !== 'object') { return } // 这里是将 this.data的数据进行响应式处理 Object.keys(data).forEach(key => { this.defineReative(data, key, data[key]) }) } // 这里的data 就是 this.data // 这个value是作为get的返回值 避免死递归 defineReative (data, key, value) { let that = this let dep = new Dep() this.walk(value) Object.defineProperty(data, key, { configurable: true, enumerable: true, // 这个get方法是在访问 data.变量时就会触发 // 如果返回 data[key] 就会再次触发 data.key的get函数 // 导致死递归 // 所以用了一个value作为闭包 返回值 get () { // Dep.target只在创建Watcher时存放 // 存放后会立刻触发一次变量get到这里来 if (Dep.target) { dep.addSub(Dep.target) // 添加后要把这个target 置为null // 避免重复添加 Dep.target = null } return value }, set (newValue) { if (value === newValue) { return } value = newValue // 调用walk判断 如果新值是对象就把它的成员变成响应式 that.walk(newValue) // 发布更新信息 让订阅者更新 dep.notify() } }) }

    Compiler

    • constructor() 保存el、实例vm、调用compiler编译模板

    • compiler 获取el的子节点,遍历子节点,区分是文本还是元素节点,对应调用两种处理方法,再遍历时判断当前子节点,是否还有子节点,递归遍历

    • compilerText编译文本节点 判断是不是插值表达式,是的话就在vm里面拿对应的值替到textContent,并且在这里添加一个Watcher,当这里插值表达式对应vm里面的值变化后,再次更新DOM

    • compilerElement 获取节点全部属性,遍历这些属性,找有没有是v-开头的指令,找到指令就用指令名拼接方式,调用对应的处理函数

    • v-text 指令处理类似插值表达式,替换文本,添加Watcher用于更新

    • v-model指令 找到key对应的value,替换value属性(用于表单元素),添加Watcher用于更新

    • v-html指令 拿到key对应的value,用innerHTML方式,插入HTML语法的字符串,添加Watcher用于更新

      const onRE = /^on:/ class Compiler { constructor (vm) { this.el = vm.el this.vm = vm this.compiler(this.el) } // 编译模板 处理文本 元素 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) } if (node.childNodes && node.childNodes.length > 0) { this.compiler(node) } }) } compilerElement (node) { Array.from(node.attributes).forEach(attr => { let attrName = attr.name if (this.isDirective(attrName)) { attrName = attrName.substr(2) let key = attr.value this.update(node, key, attrName) } }) } update (node, key, attrName) { if (onRE.test(attrName)) { // v-on const evnet = attrName.replace(onRE, '') this.onUpdate(node, this.vm[key], evnet) return } let updateFun = this[attrName + 'Update'] updateFun && updateFun.call(this, node, this.vm[key], key) } // 先考虑一个事件 onUpdate (node, value, key) { // value 里面放的是处理事件的函数 // key 里面放的是事件名 node.addEventListener(key, value, false) } htmlUpdate (node, value, key) { node.innerHTML = value new Watcher(this.vm, key, (newValue) => { node.innerHTML = newValue }) } textUpdate (node, value, key) { node.textContent = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } modelUpdate (node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) node.addEventListener('input', () => { this.vm[key] = node.value }) } compilerText (node) { let reg = /\{\{(.+?)\}\}/ let value = node.textContent if (reg.test(value)) { let key = RegExp.1.trim() node.textContent = value.replace(reg, this.vm[key]) new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } isDirective (attrName) { return attrName.startsWith('v-') } isTextNode (node) { return node.nodeType === 3 } isElementNode (node) { return node.nodeType === 1 } }

    Dep

    • constructor() 初始化一个 subs 用来存放Watcher

    • addSub 当触发对应属性(一个属性就new 一个Dep)getter时,如果有Dep.target有值(Watcher),把这个观察者放入subs

    • notify 当触发对应属性setter时,遍历subs里面观察者的update()

      class Dep { constructor () { this.subs = [] } addSub (sub) { if (sub && sub.update) { this.subs.push(sub) } } notify () { this.subs.forEach(sub => { sub.update() }) } }

    Watcher

    • constructor () 存放实例的vm、属性的key、真正更新DOM的cb,给Dep.target赋值自己本身this,给oldValue赋值vm[key](这里触发了一次key的getter,会在那里添加到dep.subs,实现了订阅效果) 

    • update 判断一下newVal是不是变化了,变化就调用cb来更新DOM

      class Watcher { constructor (vm, key, cb) { this.vm = vm this.key = key this.cb = cb Dep.target = this this.oldValue = vm[key] } update () { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) } }


    起源地下载网 » 模拟 Vue 响应式原理简单实现

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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