[front-end] 双向绑定

本文深入探讨了MVVM模式下的数据双向绑定原理,通过劫持数据对象的方式实现数据的监听和DOM元素的关联,详细解释了如何利用Object.defineProperty来劫持属性的变化,并介绍了观察者模式在其中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一直没仔细研究过这个,感觉自己还是太不够敏感了,原来只是看到有双向绑定,没细想过原理。
my git

hijack方式

hijack主要分为两部分:对数据的劫持、与DOM的关联。两个原理听起来都很简单写起来都是各种小坑

数据劫持

git
原理很简单,利用Object.defineProperty对数据进行劫持
坑1. hijack obj必须在 hijack propert之前,不然会触发多次劫持后的getter方法,当然实际上getter里只有一个绑定,所以也没啥问题

const hijackObj = function hijackObj (obj) {
  if (isObj(obj)) {
    Object.keys(obj).forEach((key) => {
      // if hijack property first, would trigger getter several times!
      if (isObj(obj[key])) {
        hijackObj(obj[key])
      }
      hijackProperty(obj, key, obj[key])
    })
  }
}

const hijackProperty = function hijackProperty (obj, key, val) {
  Object.defineProperty(obj, key, {
    configurable: false,
    get () {
      // do sth here
      return val
    },
    set (nVal) {
      // do sth here
      val = nVal; // eslint-disable-line
    }
  })
}

写法和vue的写法不太一样,主要是函数定义和分层上修改了下,我觉得这样更明确,如果要看vue的话可以看参考文档里的
除此之外,为了暴露对象(在vue中是vm),还定义了一个全局变量,同样采用劫持的方式对其进行修改。由于这个对象并不是原本对象,且原本对象深层的修改也会触发表层的改变,因此只需要劫持最顶上一层就可以了

Object.keys(obj).forEach((key) => {
    Object.defineProperty(window.hm, key, {
      configurable: false,
      get () {
        return obj[key]
      },
      set (nVal) {
        obj[key] = nVal;
      }
    })
  })

此时对window.hm的修改就是对原本obj的修改,原本obj的修改也会在window.hm上显现出来

数据触发

双向绑定的核心点之一是数据的更新,现在,数据修改时已经可以在setter中获取到并处理,那就可以通过观察者模型在setter中触发一个更新事件。

const hijackProperty = function hijackProperty (obj, key, val) {
  // 每一个property都有一个model
  const model = new Model()

  Object.defineProperty(obj, key, {
    configurable: false,
    get () {
      // do sth here
      return val
    },
    set (nVal) {
      model.trigger('val-change', nVal)
      val = nVal
    }
  })
}

Model是自定义的一个事件类,每个属性都有一个model,从而成为一个监听者,当元素发生改变时触发闭包内model的’val-change’事件
显然,观察者应该在关联DOM时传入,这点上Vue的传入方式非常巧妙……

DOM关联

git
DOM关联是hijack的另一个重要组成部分,负责找到所有需要变化的DOM并绑定成为观察者

const compileNode = function compileNode (ele) {
  Array.from(ele.childNodes).forEach((node) => {
    // text node
    if (node.nodeType === 3) {
      compileTextNode(node)
      return
    } else {
      compileEleNode(node)
    }

    if (node.childNodes && node.childNodes.length) {
      compileNode(node)
    }
  })
}

以text节点的绑定为例
这段代码只实现了简单的单一property绑定,且没有绑定到深层

const compileTextNode = function compileTextNode (node) {
  const pattern = /\{\{(.*)\}\}/
  const match = pattern.exec(node.nodeValue)
  if (!match) {
    return
  }

  const rawVal = node.nodeValue
  const prop = match[1]

  // bind to watcher and replace here
  window.hm.target = (val) => {
    node.nodeValue = rawVal.replace(pattern, val)
  }
  node.nodeValue = rawVal.replace(pattern, obj[prop])
  window.hm.target = null
}

rawVal.replace(pattern, obj[prop])会触发obj[prop]的getter事件,在这个时候就可以将观察者注入到其model对象中

const hijackProperty = function hijackProperty (obj, key, val) {
  // 每一个property都有一个model
  const model = new Model()

  Object.defineProperty(obj, key, {
    configurable: false,
    get () {
      // 利用全局变量传递cb进来
      if (window.hm.target) {
        model.on('val-change', window.hm.target)
      }
      return val
    },
    set (nVal) {
      model.trigger('val-change', nVal)
      val = nVal
    }
  })
}

采用全局对象的方式,可以在getter中判断这次获取是否需要添加观察者,从而实现观察者的注入
Vue采用的是Dep构造函数的属性判断,可以看参考文档的写法,实质上也是一个全局对象

首次遍历的时候会有多次DOM操作与替换,为了减少reflow的性能损耗,在外层采用fragment

const compile = function compile (ele) {
  if (!ele) {
    return
  }

  let fragment = transEleToFragment(ele)
  compileNode(fragment)
  ele.appendChild(fragment)
}

在启动的时候调用一次bind(obj)实现数据劫持、使用compile(document.body)实现DOM关联,这样就可以实现数据的双向绑定了,而且可以通过window.hm获取到实时的数据变化

其他

Vue采用的还有一层Watcher,作为数据更新时的中间发布层,相当于一个发布订阅模型
我觉得没有必要……所以没写

脏检查

// TODO

参考

  1. 数据双向绑定的分析和简单实现
  2. 剖析Vue原理&实现双向绑定MVVM
  3. MVVM基础之双向绑定原理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值