Advanced-Frontend/Daily-Interview-Question

第 74 题: 使用 JavaScript Proxy 实现简单的数据绑定

zeroone001 opened this issue · 16 comments

第 74 题: 使用 JavaScript Proxy 实现简单的数据绑定
<body>
  hello,world
  <input type="text" id="model">
  <p id="word"></p>
</body>
<script>
  const model = document.getElementById("model")
  const word = document.getElementById("word")
  var obj= {};

  const newObj = new Proxy(obj, {
      get: function(target, key, receiver) {
        console.log(`getting ${key}!`);
        return Reflect.get(target, key, receiver);
      },
      set: function(target, key, value, receiver) {
        console.log('setting',target, key, value, receiver);
        if (key === "text") {
          model.value = value;
          word.innerHTML = value;
        }
        return Reflect.set(target, key, value, receiver);
      }
    });

  model.addEventListener("keyup",function(e){
    newObj.text = e.target.value
  })
</script>

Proxy实现一个简单的双向绑定的 todo list

<div id="app">
    <input type="text" id="input">
    <div>
      TODO:
      <span id="text"></span>
    </div>
    <div id="btn">Add To Todo List</div>
    <ul id="list"></ul>
  </div>
const input = document.getElementById('input')
    const text = document.getElementById('text')
    const list = document.getElementById('list')
    const btn = document.getElementById('btn')

    let render

    const inputObj = new Proxy({}, {
      get (target, key, receiver) {
        return Reflect.get(target, key, receiver)
      },
      set (target, key, value, receiver) {
        if (key === 'text') {
          input.value = value
          text.innerHTML = value
        }
        return Reflect.set(target, key, value, receiver)
      }
    })

    class Render {
      constructor (arr) {
        this.arr = arr
      }
      init () {
        const fragment = document.createDocumentFragment()
        for (let i = 0; i < this.arr.length; i++) {
          const li = document.createElement('li')
          li.textContent = this.arr[i]
          fragment.appendChild(li)
        }
        list.appendChild(fragment)
      }
      addList (val) {
        const li = document.createElement('li')
        li.textContent = val
        list.appendChild(li)
      }
    }

    const todoList = new Proxy([], {
      get (target, key, receiver) {
        return Reflect.get(target, key, receiver)
      },
      set (target, key, value, receiver) {
        if (key !== 'length') {
          render.addList(value)
        }
        return Reflect.set(target, key, value, receiver)
      }
    })

    window.onload = () => {
      render = new Render([])
      render.init()
    }

    input.addEventListener('keyup', e => {
      inputObj.text = e.target.value
    })

    btn.addEventListener('click', () => {
      todoList.push(inputObj.text)
      inputObj.text = ''
    })
<b id="count"></b>
<button onclick="increase()">+</button>
<button onclick="decrease()">-</button>
const data = { count: 0 };
const proxy = new Proxy(data, {
  get(target, property) {
    return target[property];
  },
  set(target, property, value) {
    target[property] = value;
    render(value);
  }
});

render(proxy.count);

function render(value) {
  document.getElementById('count').innerHTML = value;
}

function increase() {
  proxy.count += 1;
}

function decrease() {
  proxy.count -= 1; 
}
        let person = {
            name:'jesse',
            age:25
        }
        let proxy = new Proxy(person,{
            get(target,prop){
                console.log('get')
                return target[prop]
            },
            set(obj,prop,value){
                if(value>=30){
                    throw new Error('invalid')
                }
                obj[prop] = value
            }
        })
        console.log(proxy.name) //get jesse
        proxy.age = 30   //Uncaught Error: invalid

利用Proxy实现一个简化版的MVVM
参照vue的响应式设计模式,将数据劫持部分的Obejct.defineProperty替换为Proxy即可,其他部分,如compile(编译器没有实现,用写好的html模拟已完成编译),watcher,dep,事件监听等基本保持不变,简单实现代码如下:

<!-- html部分 -->
<div id="foo"></div>
<input type="text" name="" id="bar"/>
// js部分
class Watcher{
	constructor(cb){
		this.cb = cb;
	}
	update(){
		this.cb()
	}
}
class Dep{
	constructor(){
		this.subs = [];
	}
	publish(){
		this.subs.forEach((item)=>{
			item.update && item.update();
		})
	}
}
class MVVM{
	constructor(data){
		let that = this;
		this.dep = new Dep();
		this.data = new Proxy(data,{
			get(obj, key, prox){
				that.dep.target && that.dep.subs.push(that.dep.target);
				return obj[key]
			},
			set(obj, key, value, prox){
				obj[key] = value;
				that.dep.publish();
				return true;
			}
		})
		this.compile();
	}
	compile(){
		
		let divWatcher = new Watcher(()=>{
			this.compileUtils().div();
		})
		this.dep.target = divWatcher;
		this.compileUtils().div();
		this.dep.target = null;
		
		let inputWatcher = new Watcher(()=>{
			this.compileUtils().input();
		})
		this.dep.target = inputWatcher;
		this.compileUtils().input();
		this.compileUtils().addListener();
		this.dep.target = null;
	}
	compileUtils(){
		let that = this;
		return {
			div(){
				document.getElementById('foo').innerHTML = that.data.foo;
			},
			input(){
				document.getElementById('bar').value = that.data.bar;
			},
			addListener(){
				document.getElementById('bar').addEventListener('input', function(){
					that.data.bar = this.value;
				})
			}
		}
	}
}
let mvvm = new MVVM({foo: 'foo233', bar: 'bar233'})

通过mvvm.data.foo或者mvvm.data.bar可以操作数据,可以观察到view做出了改变;在输入框改变输入值,也可以通过mvvm.data观察到数据被触发改变

zwmmm commented
<b id="count"></b>
<button onclick="increase()">+</button>
<button onclick="decrease()">-</button>
const data = { count: 0 };
const proxy = new Proxy(data, {
  get(target, property) {
    return target[property];
  },
  set(target, property, value) {
    target[property] = value;
    render(value);
  }
});

render(proxy.count);

function render(value) {
  document.getElementById('count').innerHTML = value;
}

function increase() {
  proxy.count += 1;
}

function decrease() {
  proxy.count -= 1; 
}

set 方法必须返回 true 或者 false 你这样写是有问题的 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set

<input type="text" id="input" >
<p id="p"></p>
<script>
  const input = document.getElementById('input');
  const p = document.getElementById('p');
  const obj = {};
  const newObj = new Proxy(obj, {
    get: function(target, key, receiver) {
      console.log(`getting ${key}!`);
      return Reflect.get(target, key, receiver);
    },
    set: function(target, key, value, receiver) {
      console.log(target, key, value, receiver);
      if (key === 'text') {
        input.value = value;
        p.innerHTML = value;
      }
      return Reflect.set(target, key, value, receiver);
    },
  });

  input.addEventListener('keyup', function(e) {
    newObj.text = e.target.value;
  });
</script>
<body>
  His Name:<span id="username"></span>
</body>
<script>
//定义需要监控的_data对象
  var _data = { name: "zhangsan", age: 18 };

//监控_data
  var data = new Proxy(_data, {
    set(obj, key, value) {
      obj[key] = value;
     //当有值的时候刷新显示区域
      render();
    },
    get(obj, key) {
      return obj[key];
    }
  })

  function render() {
    document.getElementById("username").innerHTML = data.name;
  }
  render();
</script>

可打开f12修改 data.name 查看改变情况

监听dom的value变化 去更新 obj
obj的数据发生变化 去更新 dom


普通简易版本:

    名字:<input type="text" id="name"><br/>
    你的名字: <p id="pName"></p>


    <script type="text/javascript">
      var obj = {
        name: ''
      }
      Object.defineProperty(obj, 'name', {
        set: function(value) {
          document.getElementById('name').value = value
          document.getElementById('pName').innerHTML = value
        }
      })
      document.getElementById('name').addEventListener('input', function(e){
        obj.name = e.target.value
      })

    </script>

proxy版本 好像没啥特殊的:

    名字:<input type="text" id="name"><br/>
    你的名字: <p id="pName"></p>


    
    <script type="text/javascript">
      // Proxy
      var obj = {
        name: ''
      }
      var proxyObj = new Proxy(obj, {
        get: function(target, key, receiver) {
          return Reflect.get(target, key, receiver)
        },
        set: function(target, key, value, receiver) {
          if (key === 'name') {
            document.getElementById('name').value = value
            document.getElementById('pName').innerHTML = value
          }
          return Reflect.set(target, key, value, receiver)
        }
      })

      document.getElementById('name').addEventListener('input', function(e){
        proxyObj.name = e.target.value
      })
    </script>
<body>
  <div id="root">
    <input type="text" v-model="title">
    <input type="text" v-model="title">
    <div v-bind="title"> </div>
  </div>

  <script>
    'user strict'
    function View() {
      // 设置代理拦截
      let proxy = new Proxy({}, {
        get(obj, property) { },
        set(obj, property, value) {
          document.querySelectorAll(`[v-model='${property}'],[v-bind='${property}']`)
            .forEach(el => el.innerHTML = el.value = value)
        }
      })

      // 初始化 绑定元素
      this.run = function () {
        const elems = document.querySelectorAll("[v-model]");
        elems.forEach(el => {
          el.addEventListener('keyup', event => {
            proxy[event.target.getAttribute('v-model')] = event.target.value;
          })
        })
      }

    }

    let view = new View();
    view.run();
  </script>

</body>
  <input id="inp" type="text" oninput="handleChange()">
  <div id="app"></div>
  <script>

    let inp = document.getElementById('inp')
    let app = document.getElementById('app')
    
    let obj = {
      defaultValue: 'hello world'
    }
    let proxy = new Proxy(obj, {
      get: function(obj, key) {
        console.log('get')
        return obj[key]
      },
      set(obj, key, value) {
        obj.defaultValue = value
        notify()
      }
    })

    app.innerHTML = proxy.defaultValue
    inp.value = proxy.defaultValue
    
    function notify() { 
      app.innerHTML = proxy.defaultValue
    }

    function handleChange() {
      proxy.defaultValue = inp.value
    }
  </script>
<body>
  hello,world
  <input type="text" id="model">
  <p id="word"></p>
</body>
<script>
  const model = document.getElementById("model")
  const word = document.getElementById("word")
  var obj= {};

  const newObj = new Proxy(obj, {
      get: function(target, key, receiver) {
        console.log(`getting ${key}!`);
        return Reflect.get(target, key, receiver);
      },
      set: function(target, key, value, receiver) {
        console.log('setting',target, key, value, receiver);
        if (key === "text") {
          model.value = value;
          word.innerHTML = value;
        }
        return Reflect.set(target, key, value, receiver);
      }
    });

  model.addEventListener("keyup",function(e){
    newObj.text = e.target.value
  })
</script>

model.value = value; 这一行代码是不是不需要写

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <span></span>
    <br>
    <input type="text" oninput="input(event)">
    <script>
        let spanEl = document.querySelector('span')
        let inputEl = document.querySelector('input')
        let data = {
            val: null
        }
        let proxy = new Proxy(data, {
            get(target, p, receiver) {
                return Reflect.get(target, p, receiver)
            },
            set(target, p, val, receiver) {
                spanEl.innerText = val
                inputEl.value = val
                return Reflect.set(target, p, val, receiver)
            }
        })

        function input(e) {
            proxy.val = e.target.value
        }

    </script>
</body>

</html>
    <input type="text" id="input">
    <p id="text">hello world</p>

    <script>
        let input = document.getElementById('input');
        let text = document.getElementById('text');

        let obj = {};

        let proxy = new Proxy(obj, { 
            get(target, property){
                return Reflect.get(...arguments);
            },
            
            set(target, property, value){ 
                target[property] = value; 
                text.innerText = target[property]; 
                return Reflect.set(...arguments); 
            }
            
        })

        proxy.val = text.innerText;
        input.value = proxy.val;

        input.addEventListener('input', function(){
            proxy.val = this.value;
        })
    </script>

let activeEffect = null;
let wm = new WeakMap();

function watchEffect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = null;
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key);
      return Reflect.get(target, key, receiver)
    },
    set(target, key, val, receiver) {
      Reflect.set(target, key, val, receiver);
      trigger(target, key);
    }
  })
}

function track(target, key) {
  if (activeEffect) {
    let map = wm.get(target);
    if (!map) {
      map = new Map();
      wm.set(target, map);
    }
    let bucket = map.get(key);
    if (!bucket) {
      bucket = [];
      map.set(key, bucket);
    }
    bucket.push(activeEffect);
  }
}

function trigger(target, key) {
  let map = wm.get(target);
  if (map) {
    let bucket = map.get(key);
    if (bucket) {
      bucket.forEach(item => item());
    }
  }
}

const t1 = reactive({a: 1, b: 2});
watchEffect(() => {
  console.log(t1.a);
})
setTimeout(() => {
  t1.a = 222;
}, 3000);