双向数据绑定已经是面试中经常被问到的点, 需要对原理和实现都要有一定了解.
下面是实现双向绑定的两种方法:
属性劫持
脏数据检查
一, 属性劫持
主要是通过
Object 对象的 defineProperty 方法, 重写 data 的 set 和 get 函数来实现的.
在属性劫持中, 主要通过 _observe(重定义 get,set 方法, 实现数据变化更新视图),_compile(实现视图初始化, 并对元素绑定事件),_updata(实现具体更新视图方法) 三个方法完成双向绑定.
__observe 方法中,_binding 储存数据相关更新的 watcher 对象列表, set 函数触发回更新所有相关的绑定视图对象:
- Myvue.prototype._observe = function (data) {
- const _this = this
- Object.keys(data).forEach(key => {
- if (data.hasOwnProperty(key)) {
- let value = data[key]
- this._binding[key] = {
- _directives: []
- }
- this._observe(value)
- Object.defineProperty(data, key, {
- enumerable: true,
- configurable: true,
- get() {
- return value
- },
- set(newValue) {
- if (value !== newValue) {
- value = newValue
- _this._binding[key]._directives.forEach(item => {
- item._updata()
- })
- }
- }
- })
- }
- })
- }
_compile 方法中, 会对 DOM 中的绑定命令进行解析, 并绑定相关的处理函数:
- MyVue.prototype._compile = function (root) {
- const _this = this
- const nodes = root.children;
- Object.values(nodes).forEach(nodeChild => {
- if (nodeChild.children.length) {
- this._compile(nodeChild)
- }
- if (nodeChild.hasAttribute('v-click')) {
- nodeChild.addEventListener('click', (function (params) {
- const attrVal = nodeChild.getAttribute('v-click');
- return _this.$methods[attrVal].bind(_this.$data)
- })())
- }
- if (nodeChild.hasAttribute('v-model') && (nodeChild.tagName = 'INPUT' || nodeChild.tagName == 'TEXTAREA')) {
- nodeChild.addEventListener('input', (function (params) {
- var attrVal = nodeChild.getAttribute('v-model');
- _this._binding[attrVal]._directives.push(
- new Watcher({
- el: nodeChild,
- vm: _this,
- exp: attrVal,
- attr: 'value'
- })
- )
- return function () {
- _this.$data[attrVal] = nodeChild.value;
- }
- })())
- }
- if (nodeChild.hasAttribute('v-bind')) {
- const attrVal = nodeChild.getAttribute('v-bind');
- _this._binding[attrVal]._directives.push(
- new Watcher({
- el: nodeChild,
- vm: _this,
- exp: attrVal,
- attr: 'innerhtml'
- })
- )
- }
- })
- }
_updata 函数, 主要在_compile 函数中调用进行视图初始化和 set 函数调用更新绑定数据的相关视图:
- function Watcher({ el, vm, exp, attr }) {
- this.el = el
- this.vm = vm
- this.exp = exp
- this.attr = attr
- this._updata()
- }
- Watcher.prototype._updata = function () {
- this.el[this.attr] = this.vm.$data[this.exp]
- }
网上的一张属性劫持的运行图:
Observer 数据监听器, 能够对数据对象的所有属性进行监听, 如有变动可拿到最新值并通知订阅者, 内部采用 Object.defineProperty 的 getter 和 setter 来实现.
Compile 指令解析器, 它的作用对每个元素节点的指令进行扫描和解析, 根据指令模板替换数据, 以及绑定相应的更新函数.
Watcher 订阅者, 作为连接 Observer 和 Compile 的桥梁, 能够订阅并收到每个属性变动的通知, 执行指令绑定的相应回调函数.
Dep 消息订阅器, 内部维护了一个数组, 用来收集订阅者 (Watcher), 数据变动触发 notify 函数, 再调用订阅者的 update 方法.
完整的代码请参考 Two Way Binding https://github.com/haozhaohang/library/tree/master/双向数据绑定
https://github.com/haozhaohang/library/tree/master/双向数据绑定
二, 脏数据检查
主要通过执行一个检测来遍历所有的数据, 对比你更改了地方, 然后执行变化.
在脏检查中, 作用域 scope 对象中会维护一个 "watcher" 数组, 用来存放所以需要检测的表达式, 以及对应的回调处理函数.
对于所有需要检测的对象, 属性, scope 通过 "watch" 方法添加到 "watcher" 数组中:
- Scope.prototype.watch = function(watchExp, callback) {
- this.watchers.push({
- watchExp: watchExp,
- callback: callback || function() {}
- });
- }
当 Model 对象发生变化的时候, 调用 "digest" 方法进行脏检测, 如果发现脏数据, 就调用对应的回调函数进行界面的更新:
- Scope.prototype.digest = function() {
- var dirty;
- do {
- dirty = false;
- for(var i = 0; i < this.watchers.length; i++) {
- var newVal = this.watchers[i].watchExp(),
- oldVal = this.watchers[i].last;
- if(newVal !== oldVal) {
- this.watchers[i].callback(newVal, oldVal);
- dirty = true;
- this.watchers[i].last = newVal;
- }
- }
- } while(dirty);
- }
完整的代码请参考 Two Way Binding https://github.com/haozhaohang/library/tree/master/双向数据绑定
如果喜欢请关注我的 Github https://github.com/haozhaohang , 给个 Star https://github.com/haozhaohang/library 吧, 我会定期分享一些 JS 中的知识,^_^
来源: https://www.cnblogs.com/hzh-fe/p/8882758.html