首页 > 其他分享 >Vue双向绑定原理梳理

Vue双向绑定原理梳理

时间:2022-11-18 14:44:12浏览次数:43  
标签:node Vue target 绑定 vm value key data 梳理

简介

vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。
数据劫持通过 Object.defineProperty()方法,Vue对数据的劫持主要分为两类,一类是对象,一类是数组。

对对象的劫持

  1. 构造一个监听器 Observer ,用来劫持并监听所有属性,添加到收集器 Dep 中,当数据发生变化的时候发出一个 notice(预告)。
  2. 添加一个收集器 Dep , 用来收集订阅者 Watcher

视图中会用到 data 中的 key ,这称为依赖。同⼀个 key 可能出现多次,每次都需要收集出来用⼀个 Watcher 来维护它们,此过程称为依赖收集多个 Watcher 需要⼀个 Dep 来管理,需要更新时由 Dep 统⼀通知。

  1. 构造一个订阅者 Watcher ,一方面接收监听器 Observer 通过Dep传递过来的数据变化,一方面通知解析器 Compile 进行界面更新。
  2. 实现一个解析器 Compile ,实现指令解析,初始化视图,并订阅数据变化,绑定好更新函数。值得注意的是,解析器在创建DOM的时候,先用的 DocumentFragment,等完全解析后,才赋值给了真实DOM去渲染界面(DOM.appendChild(DocumentFragment))。DocumentFragment 是一个文档片段接口,它的变化不会触发DOM树的重新渲染。
    image

Dep 如何收集 Watcher

Watcher 在取值的时候让 Dep.target 指向 this 即当前的 watcher,取值结束之后,让 Dep.targetnull ,这样就可以通过属性的 get 方法里面将当前的 watcher 添加到属性里面的实例 dep 中。

// 通过获取操作, 触发属性里面的get方法
key.split('.').reduce((total, current) => total[current], vm._data)
// get 方法
get: function getter () {                
  if (Dep.target) { // 在这里添加一个订阅者
    dep.addSub(Dep.target);                
  }                
  return val;            
}

对数组的劫持

由于不能使用 Object.defineProperty() 方法,所以它是这样操作的:
在Vue中创建了一个数组方法拦截器,在拦截器内重写了操作数组的一些方法(主要有7个,push,pop,shift,unshift,splice,sort,reverse),当数组实例使用操作数组方法时,其实使用的是拦截器中重写的方法,而不再使用 Array.prototype 上的原生方法。

以上说明可结合vue源码或下面模拟代码理解

vue 响应式原理简单模拟

class Vue {
  constructor(options) {
    this.$options = options
    const vm = this
    if (this.$options.data) {
      this.initData(vm)
    }
    if (this.$options.el) {
      compile(this.$options.el, vm)
    }
  }
  initData(vm) {
    let data = vm.$options.data
    data = typeof data === 'function' ? data.call(vm) : data
    vm._data = data
    observe(data)
    // 这个是为了实现 vm.name可以直接访问的代理方法
    for (let key in data) {
      proxy(vm, key, data[key])
    }
  }
}

// 代理
function proxy(target, key, value) {
  Object.defineProperty(target, key, {
    get() {
      return target['_data'][key]
    },
    set(newValue) {
      target['_data'][key] = newValue
    }
  })
}

function observe(data) {
  if (data === null || typeof data !== 'object') {
    return
  }
  return new Observer(data)
}

// 这里值考虑了对象和对象嵌套, 没有考虑数组的情况
class Observer {
  constructor(data) {
    this.walk(data)
  }
  walk(data) {
    // 遍历对象的每一项, 添加响应式方法
    Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
  }
}

// 给属性收集依赖
function defineReactive(target, key, value) {
  // 如果值是对象的话, 也需要重复添加
  observe(value)
  // 给属性添加dep实例, 因为是闭包,这个空间不会被销毁
  let dep = new Dep()
  Object.defineProperty(target, key, {
    get() {
      Dep.target && dep.addSub(Dep.target)
      return value
    },
    set(newValue) {
      value = newValue
      observe(newValue)
      // debugger
      dep.notify()
    }
  })
}


/** 解析模板
​ 使用documentFragment创建模板, 注意fragment.append 时会一处页面上的元素, while循环结束后, 页面就没了,
​ 然后对模板里面的每一项进行解析, 先实例 node.nodeType === 3 的元素, 表示文本节点, 看文本节点里面有没有匹配到{{name}}模板表达式的, 如果有, 如vm._data里面去除对应的值, 替换文本的值, 最后vm.$el.appendChild(fragment)就可以将替换后的结果显示在页面上
​ 对nodeType === 1 的元素, 即标签解析, 这里我们处理的是input, 获取节点的所有属性, 一个伪数组, 变成真数组, 里面有个nodeName = v-model 和 nodeValue = name 的, 同样获取vm._data里面name的值, 然后让节点的 node.value = 这个值, 就能显示在输入框里面了, 这就是数据改变视图.接下来是视图改变数据, 添加input方法, 为node 添加 addEventListener方法, input, 然后让vm._data里面对应属性的值等于e.target.value, 这样就实现了视图改变数据
​ 重点: 上面的两种情况, nodeType == 3 的时候更新方法是 node.nodeValue = newValue, nodeType == 1 的时候更新方法是 node.value = value, 需要将这两个方法封装到 watcher中, 在更新之后 new 一个 Watcher, 并将对应的参数传入, 后面在获取值的时候就会自动收集依赖, set值的时候就会触发更新。
 * @param {Object} el
 * @param {Object} vm
 */
function compile(el, vm) {
  vm.$el = el = document.querySelector(el)
  
  const fragment = document.createDocumentFragment()
  let child
  while (child = el.firstChild) {
    fragment.append(child)
  }

  fragment_compile(fragment)

  function fragment_compile(node) {
    const parttern = /\{\{\s(\S+)\s\}\}/
    // 文本节点
    if (node.nodeType === 3) {
      // 匹配{{}}, 第一项为匹配的内容, 第二项为匹配的变量名称
      const match = parttern.exec(node.nodeValue)
      if (match) {
        const needChangeValue = node.nodeValue
        // 获取到匹配的内容, 可能是 msg,   也可能是 mmm.msg,
        // 注意通过 vm[mmm.msg]是拿不到数据的, 要 vm[mmm][msg]
        // 获取真实的值, 替换掉模板里面的 {{name}}, 真实的值从vm.$options.data里面取
        let arr = match[1].split('.')
        let value = arr.reduce(
          (total, current) => total[current], vm._data
        )
        // 将真实的值替换掉模板字符串, 这个就是更新模板的方法, 将这个方法封装到watcher里面
        node.nodeValue = needChangeValue.replace(parttern, value)
        const updateFn = value => {
          node.nodeValue = needChangeValue.replace(parttern, value)
        }
        // 有个问题, node.nodeValue在执行过一次之后, 值就变了, 不是 {{name}}, 而是 12345, 要救{{name}}里面的name暂存起来
        new Watcher(vm, match[1], updateFn)
      }
      return

    }
    // 元素节点
    if (node.nodeType === 1 && node.nodeName === 'INPUT') {
      // 伪数组
      const attrs = node.attributes
      let attr = Array.prototype.slice.call(attrs)
      // 里面有个nodeName -< v-model,  有个nodeValue 对应 name
      attr.forEach(item => {
        if (item.nodeName === 'v-model') {
          let value = getVmValue(item.nodeValue, vm)
          // input标签是修改node.value 
          node.value = value
          // 也需要添加watcher
          new Watcher(vm, item.nodeValue, newValue => node.value = newValue)
          // 添加input事件
          node.addEventListener('input', e => {
            const name = item.nodeValue
            // 给vm上的属性赋值
            // 不能直接 vm._data[name] = e.target.value , 因为那么可能是 a.b
            // 也不能直接获取b的值, 然后赋新值, 因为这个值是一个值类型, 需要先获取前面的引用类型
            // 如: let tem = vm._data.a   然后 tem[b] = 新值,  这样就可以
            const arr1 = name.split('.')
            const arr2 = arr1.slice(0, arr1.length - 1)
            const head = arr2.reduce((total, current) => total[current], vm._data)
            head[arr1[arr1.length - 1]] = e.target.value
          })
        }
      })
    }
    node.childNodes.forEach(child => fragment_compile(child))
  }

  vm.$el.appendChild(fragment)
}

// 依赖收集
class Dep {
  constructor() {
    // 里面装的是属性收集的watcher
    this.subs = []
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

// 订阅者
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm
    this.key = key
    this.callback = callback
    // 让Dep.target属性指向当前watcher
    Dep.target = this
    // 通过获取操作, 触发属性里面的get方法
    key.split('.').reduce((total, current) => total[current], vm._data)
    Dep.target = null
  }
  update() {
    const value = this.key.split('.').reduce((total, current) => total[current], this.vm._data)
    this.callback(value)
  }
}

function getVmValue(key, vm) {
  return key.split('.').reduce((total, current) => total[current], vm._data)
}

function setVmValue(key, vm) {
  let tem = key.split('.')
  let fin = tem.reduce((total, current) => total[current], vm._data)
  return fin
}


window.Vue = Vue;

标签:node,Vue,target,绑定,vm,value,key,data,梳理
From: https://www.cnblogs.com/weizwz/p/16903178.html

相关文章

  • 搭建直播平台,vue+audio 有新消息时加提示音
    搭建直播平台,vue+audio有新消息时加提示音 <audiocontrols="controls"hiddensrc="./static/tip.mp3"ref="audio"></audio>​有新消息时,用以下代码即可播放指定的......
  • 为什么vue3要选用proxy,好处是什么?
    提问Object.defineProperty()和proxy的区别?为什么vue3要选用proxy,好处是什么?proxyProxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋......
  • Vue3, setup语法糖、Composition API全方位解读
    起初Vue3.0暴露变量必须return出来,template中才能使用;Vue3.2中只需要在script标签上加上setup属性,组件在编译的过程中代码运行的上下文是在setup()函数中,无......
  • 解读Vue3模板编译优化
    今天的文章打算学习下Vue3下的模板编译与Vue2下的差异,以及VDOM下Diff算法的优化。编译入口了解过Vue3的同学肯定知道Vue3引入了新的组合Api,在组件mount阶......
  • Vue.nextTick核心原理
    相信大家在写vue项目的时候,一定会发现一个神奇的api,Vue.nextTick。为什么说它神奇呢,那是因为在你做某些操作不生效时,将操作写在Vue.nextTick内,就神奇的生效了。那这是什么......
  • Vue3源码解读之patch
    例子代码本篇将要讲解domdiff,那么咱们结合下面的例子来进行讲解,这个例子是在上一篇文章的基础上,加了一个数据变更,也就是list的值发生了改变。html中增加了一个按钮change......
  • 如何正确学习vue3.0源码
    为什么要学源码技术是第一生产力学习API的设计目的、思路、取舍学习优秀的代码风格学习组织代码的方式学习实现方法的技巧学习ES67新API、TS高级用法不给自......
  • 上帝视角看Vue源码整体架构+相关源码问答
    前言这段时间利用课余时间夹杂了很多很多事把Vue2源码学习了一遍,但很多都是跟着视频大概过了一遍,也都画了自己的思维导图。但还是对详情的感念模糊不清,故这段时间对源码......
  • vue3语法汇总
    组合式API基础 setup 组件选项在创建组件之前执行,一旦 props 被解析,并充当合成API的入口点。setup的两个注意点:1、setup执行时机,在beforeCreate之前执行一次,thi......
  • Vue2基础知识点
    <!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"conten......