Skip to content

2017.09 Vue2 파트별 구현

파트별 구현

옵져버

js
const obj = {};
Observer(obj, 'counter', (newValue, oldValue) => {
  console.log('LOG:', newValue, oldValue)
});

obj.counter = 0;
obj.counter = 1;

// LOG: 0 undefined
// LOG: 1 0
js
const Observer = (obj, property, callback) => {
  let value = obj[property];

  Object.defineProperty(
    obj,
    property,
    {
      configurable: true,
      enumerable: true,
      set: (newValue) => {
        callback(newValue, value);
        value = newValue;
      },
      get: () => value
    }
  );
};

탬플릿 바인딩

html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>

<div id="one-way-bind">
  {{language}}
</div>

<script src="./template.js"></script>
<script>
  const template = useTemplate('#one-way-bind');
  template.bindData({
    language: '한국어'
  });
</script>
</body>
</html>
js
const useTemplate = (selector) => {
  const TEMPLATE_PROPERTY = '$template';
  const elem = document.querySelector(selector);
  const delimiters = ['{{', '}}'];
  const cacheData = {};

  const changeTextNode = elem => {
    let startPosition = 0;
    let endPosition = 0;
    let val = '';
    let copiedTemplate = '';

    //템플릿 캐싱
    if (!elem.hasOwnProperty(TEMPLATE_PROPERTY)) {
      elem[TEMPLATE_PROPERTY] = elem.textContent;
    }

    copiedTemplate = elem[TEMPLATE_PROPERTY];

    while (startPosition > -1) {
      val = '';
      //시작 위치를 찾는 다
      startPosition = copiedTemplate.indexOf(
        delimiters[0],
        startPosition
      );
      //데이터가 모두 변경되어 탬플릿 키워드가 없을 때
      if (startPosition === -1) break;

      //종료 위치를 찾는 다
      endPosition = copiedTemplate.indexOf(
        delimiters[1],
        startPosition + delimiters[0].length - 1
      );
      //변수 key값을 찾는 다.
      const variableName = copiedTemplate.substring(
        startPosition + delimiters[0].length,
        endPosition
      );

      //데이터에 변수값이 있을 경우 반영한다.
      if (typeof cacheData[variableName] !== 'undefined') {
        val = cacheData[variableName];
      }

      copiedTemplate = copiedTemplate.replace(
        delimiters[0] + variableName + delimiters[1],
        val,
      );
    }

    if (elem.textContent !== copiedTemplate) {
      elem.textContent = copiedTemplate;
    }
  };

  const cacheObj = data => {
    for (const key in data) {
      cacheData[key] = data[key];
    }
  };

  const traversal = elem => {
    elem.childNodes.forEach(node => {
      if (node.childNodes.length > 0) {
        traversal(node);
      } else {
        changeTextNode(node);
      }
    });
  };

  const bindData = data => {
    cacheObj(data);
    traversal(elem);
  };

  return {bindData}
};

디렉티브

html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>

<ul>
  <li helloworld></li>
  <li helloworld></li>
  <li helloworld></li>
</ul>
<button
  type="button"
  on-click="changeButtonName(event)">
  Button
</button>

<script src="./directive.js"></script>
<script>
  Directive.define({
    name: 'on-click',
    bind: (elem, [callbackName, hasEvent]) => {
      elem.onclick = (event) => {
        window[callbackName].call(elem, hasEvent ? event : undefined);
      };
    }
  });

  Directive.define({
    name: 'helloworld',
    bind: (elem) => {
      elem.innerHTML = 'Hello World';
    }
  });


  Directive.render('body');

  function changeButtonName(event) {
    this.innerHTML = 'Changed Button';
  }
</script>
</body>
</html>
js
const Directive = (() => {
  const store = new Set();

  const define = ({name, bind}) => {
    store.add({name, bind});
  };

  const parseAttr = (_attr) => {
    let attr = _attr;
    if (attr.indexOf('(') > -1) {
      attr = attr
        .replace('(', ',')
        .replace(')', '');
    }

    return attr.split(',');
  };

  const render = (parentSelector) => {
    const parent = document.querySelector(parentSelector);

    store.forEach((directive) => {
      const finedElems = parent.querySelectorAll(`[${directive.name}]`);

      finedElems.forEach((elem) => {
        const directiveValue = parseAttr(elem.getAttribute(directive.name));
        directive.bind(elem, directiveValue);
      });
    });
  };

  return {define, parseAttr, render}
})();

컴포넌트

html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>

<helloworld></helloworld>
<ul>
  <list></list>
  <list></list>
  <list></list>
  <list></list>
  <list></list>
</ul>

<script src="./component.js"></script>
<script>
  Component.define({
    name: 'helloworld',
    template: '<div>Hello World</div>'
  });

  Component.define({
    name: 'list',
    template: '<li>Item</li>'
  });

  Component.render('body');
</script>
</body>
</html>
js
const Component = (() => {
  const store = new Set();

  const parseHTML = (template) => {
    const shallowElement = document.createElement('div');
    shallowElement.innerHTML = template;
    return shallowElement.childNodes
  };

  const define = ({name, template}) => {
    store.add({
      name,
      template: parseHTML(template)[0]
    });
  };

  const render = (parentSelector) => {
    const parent = document.querySelector(parentSelector);

    store.forEach((component) => {
      const finedElems = parent.querySelectorAll(component.name);

      finedElems.forEach((elem) => {
        //Element 참조 방지
        const clonedTemplate = component.template.cloneNode(true);
        elem.parentNode.replaceChild(
          clonedTemplate,
          elem
        );
      });
    });
  };

  return {define, render}
})();

파트 통합

html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>

<div id="app">
  <h3>Data Binding</h3>
  <p>{{message}}</p>
  <input type="text" v-model="message">
  <h3>Default Directive(v-onclick)</h3>
  <button type="button" v-onclick="reverseMessage">
    {{btnName}}
  </button>
  <h3>Components</h3>
  <hello-component></hello-component>
  <ul>
    <list></list>
    <list></list>
    <list></list>
    <list></list>
  </ul>
  <h3>Directives</h3>
  <div helloworld></div>
</div>

<script src="./component.js"></script>
<script src="./directive.js"></script>
<script src="./observer.js"></script>
<script src="./template.js"></script>
<script src="./vanillajs-vue.js"></script>
<script>
  vanillaJsVue({
    el: '#app',
    data: {
      message: 'Hello World',
      btnName: 'Reverse Message'
    },
    watch: {
      message: val => {
        console.log(val);
      }
    },
    methods: {
      reverseMessage() {
        this.message = this.message.split('').reverse().join('');
      }
    },
    directives: {
      'helloworld': {
        bind: el => {
          el.innerHTML = 'Hello World';
        }
      }
    },
    components: {
      'hello-component': {
        template: '<p>Hello Component!!</p>'
      },
      'list': {
        template: '<li>list</li>'
      }
    }
  });
</script>
</body>
</html>
js
function vanillaJsVue(options){
  let template = null;
  const watcher = {};

  const vModel = {
    name: 'v-model',
    bind: (elem, variableName) => {
      elem.value = options.data[variableName];
      elem.oninput = ((variableName => () => {
        options.data[variableName] = elem.value;
      })(variableName));
    }
  };

  const vOnClick = {
    name: 'v-onclick',
    bind: (elem, callbackName) => {
      elem.onclick = ((callbackName => event => {
        options.methods[callbackName].call(options.data, event);
      })(callbackName));
    }
  };

  const platformDirectives = [
    vOnClick,
    vModel
  ];

  const initTemplate = () => {
    template = useTemplate(options.el);
    template.bindData(options.data);
  };

  const runWatcher = (keyName, newVal) => {
    let callbacks = [];
    if(watcher.hasOwnProperty(keyName)){
      callbacks = watcher[keyName];
      callbacks.forEach(callback => {
        callback.call(options.data, newVal);
      });
    }
  };

  const initWatch = () => {
    let keyName;

    for(keyName in options.watch){
      watcher[keyName] = watcher[keyName] || [];
      watcher[keyName].push(options.watch[keyName]);
    }

    for(keyName in options.data){
      (((dataObj, keyName, template) => {
        Observer(
          dataObj,
          keyName,
          (newValue) => {
            template.bindData({
              [keyName]: newValue
            });
            runWatcher(keyName, newValue);
          }
        );
      })(options.data, keyName, template));
    }
  };

  const initDirective = () => {
    let directiveName = '';

    platformDirectives.forEach(directive => {
      Directive.define(directive);
    });

    for(directiveName in options.directives){
      Directive.define({
        name: directiveName,
        bind: ((bindCallback => (elem, args) => {
          bindCallback(elem, ...args);
        })(options.directives[directiveName].bind))
      });
    }

    Directive.render(options.el);
  };

  const initComponents = () => {
    let componentName = '';
    for(componentName in options.components){
      Component.define({
        name: componentName,
        template: options.components[componentName].template
      });
    }

    Component.render(options.el);
  };

  const init = () => {
    initTemplate();
    initWatch();
    initDirective();
    initComponents();
  };

  init(options);
}