2020年11月20日

响应式原理(3)- vue数据响应式处理(极简版)

作者 rourou
  • 将模板生成带参数的虚拟dom树(vue生成的是AST)
  • 将其dom树数据转化为响应式数据【示例中原数据为数组类型直接赋值对象类型数据,原数组不是一个响应式数据,所以赋值时出现问题。vue是用的是Watcher监听处理】
  • 将虚拟dom树生成真实的dom(html)渲染到页面上【本示例是数据改变则全局刷新,vue其实是才用diff进行虚拟dom比较找出差异进行局部渲染】
<!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>
  <div id="root" title="aaa">
    <p>{{name}}:{{message.info}}</p>
    <p>{{name}}</p>
    <p>{{message.info}}</p>
  </div>
  <script>
    function JGVue(option){
      this._data = option.data;
      this._el = document.querySelector(option.el);
      this._parent = this._el.parentNode;
      reactify(this._data, this);

      // 挂载
      this.mount();
    }

    JGVue.prototype.mount = function(){
      this.render = this.createRenderFn();
      this.mountComponent();
    }
    JGVue.prototype.mountComponent = function () {
      // 执行 mountComponent() 函数 
      // 挂载组件(需使用发布订阅模式,渲染和计算的行为交给watcher完成)
      let mount = () => {
        this.update( this.render() )
      }
      mount.call( this ); // 本质应该交给 watcher 来调用, 但是还没有讲到这里
    }
    JGVue.prototype.createRenderFn = function(){
      
      let ast = getVnode(this._el);
      return function render(){
        // 带数据的 ast
        let _tmp = combine( ast, this._data );
        return _tmp;
      }
    }

    JGVue.prototype.update = function (vnode) {
      // 简化, 直接生成 HTML DOM replaceChild 到页面中
      // 父元素.replaceChild(新,旧)
      let newDom = parseDom (vnode);

      this._parent.replaceChild(newDom,document.querySelector('#root'));//会将页面的DOM全部替换,vue是使用diff算法进行判断增删改。因为每次全部替换即每次需重新去获取跟元素

    }
    class VNode{
      constructor(tag, attr, value, type){
        this.tag = tag && tag.toLowerCase();
        this.attr = attr;
        this.value = value;
        this.type = type;
        this.children = [];
      }
      getData(data){
        this.children.push(data);
      }
    }
    function getVnode(el){
      var type = el.nodeType;
      var tag,value;
      var attr = {};
      var _vNode = null;
      if(type == 1){
        // 元素节点
        tag = el.nodeName;
        var attrs = el.attributes;
        for(var i = 0;i<attrs.length;i++){
          attr[ attrs[i].nodeName ] = attrs[i].nodeValue;
        }
        _vNode = new VNode(tag, attr, value, type);

        var childNode = el.childNodes;
        for(var i=0;i<childNode.length;i++){
          _vNode.getData( getVnode(childNode[i]) );
        }
        
      }else if(type == 3){
        // 文本节点
        value = el.nodeValue;
        _vNode = new VNode(tag, attr, value, type);
      }
      return _vNode;
    }
    var rz = /\{\{(.+?)\}\}/g;
    function combine(vnode, data){
      var tag = vnode.tag;
      var attr = vnode.attr;
      var value = vnode.value;
      var type = vnode.type;
      var children = vnode.children;
      var _vNode = null;
      if(type == 1){
        // 元素节点
        _vNode =new VNode(tag, attr, value, type);

        for(var i=0;i<children.length;i++){
          _vNode.getData( combine(children[i], data) )
        }

      }else if(type == 3){
        // 文本节点
        var res = value.trim().replace(rz, function(_,g){
          return getValueByPath(data, g);
        })
        _vNode =new VNode(tag, attr, res, type);

      }
      return _vNode;
    }
    function getValueByPath(obj, path){
      var arr = path.split('.');
      let res = obj;
      let prop;
      // arr.shift()取出数组的第一个值
      while( prop = arr.shift() ){
        res = res[ prop ];
      }
      return res;
    }
    function parseDom (res){
      let tag = res.tag;
      let type = res.type;
      let value = res.value;
      let attr = res.attr;
      let children = res.children;
      if(type == 1){
        if(tag != undefined){
          var ele = document.createElement(tag);
        }
        if(attr != undefined){
          for(var i in attr){
            ele.setAttribute(i,attr[i]);
          }
        }
        if(children.length != 0){
          for(var i=0;i<children.length;i++){
            val = ele.appendChild(parseDom(children[i]));
          }
        }
      }else if(type == 3){
        var ele = document.createTextNode(value);
      }
      return ele;
    }

    // 数组方法拦截开始
    let ARRAY_METHOD = ['push'];
    let array_methods = Object.create(Array.prototype);
    // 3 循环需扩展的数组方法名,并创建一个方法
     ARRAY_METHOD.forEach(method => {
      array_methods[ method ] = function (){
        console.log('调用拦截');
        // 变为响应式数据
        for(var i=0;i<arguments.length;i++){
          reactify(arguments[i] )//有缺陷,wacher可解决
        }
        // 调用原有的方式
        let res = Array.prototype[ method ].apply(this , arguments);
        return res;
      }
    })
// 数组方法拦截结束

    // 简化版本
    function defineReactive(target, key, value, enumerable){
      // value 为函数内部局部作用域(闭包,解决数据访问安全问题)
      var that = this;
      if(typeof value === 'object' && value!= null && !Array.isArray(value)){
        reactify(value,that);
      }
      
      Object.defineProperty(target, key, {
        configurable: true,//设置属性是否可以被删除,属性是否可更改
        // Writable:设置属性是否是可写的。Writable和set、get同时使用
        enumerable: !!enumerable,//是否可枚举(使用for...in遍历是是否能访问到键)
        set(newValue){
          if(typeof newValue === 'object' && newValue!= null && !Array.isArray(newValue)){
            reactify(newValue);
          }else if(Array.isArray(newValue)){
            for(var i=0;i<newValue.length;i++){
              reactify(newValue[i]);
            }
          }
          value = newValue;
          // 实例化对象获取。vue中是watcher实现不存在获取不到实例化对象;现目前使用其他办法解决(调用时将实例化对象当参数传入)
          console.log(that)
          that.mountComponent()
        },
        get(){
          return value;
        }
      })
    }
//不完整
    function reactify(data,vm){
      let keys = Object.keys(data);

      for(var i=0;i<keys.length;i++){
        let key = keys[i];
        let val = data[key];

        if(Array.isArray(val)){
          // 数组类型
          val.__proto__ = array_methods;//数组响应式方法调用

          for(var j=0;j<val.length;j++){
            reactify(val[j],vm);
          }
        }else{
          // 引用类型
          defineReactive.call(vm, data, key, val, true);
        }
      }
    }


    let app = new JGVue({
      el: '#root',
      data: {
        name: '肉肉',
        message: {
          info: '还是嘎嘎'
        }
      }
    })
  </script>
</body>
</html>