• 实现一个简易vue


    1. vue主要的功能实现主要分为3部分:
      • 数据劫持/数据代理:数据改变时通知相关函数进行更新操作
      • 数据依赖收集:建立保存dom节点与数据的关联关系
      • 模板与数据之间的绑定:接受到新数据时对dom节点进行更新(Watcher)
    2. 代码:

        构造方法中拿到传入的参数,再调用一些方法对上面提到的3部分功能进行实现。这些方法都放在了原型链上(prototype),

       这样方便在方法中使用实例的context(this)。

      function VueComponent(vueComponentProps = {}) {
        const {
          template = '',
          data = () => {},
          methods = {},
          watch = {}
        } = vueComponentProps;
        this.$watch = watch;
        this.$watchers = {};
        this.$methods = methods;
        this.$data = data() || {};
        bindVueProperties(this,data() || {});
        this.observe(this.$data);
        this.observe(this);
        bindVueProperties(this,this.$methods);
        this.$template = htmlStringToElement(template);
        this.$vueDomNode = this.compile();
      }

      //因为之后我们需要在执行methods中的方法可能同时要访问data和methods,
      //所以这里利用bindVueProperties将data和methods复制到实例的根层。
      function bindVueProperties(vueInstance, obj) {
        for(let key of Object.keys(obj)) {
          vueInstance[key] = obj[key];
        }  
      }

       · 数据劫持/数据代理

      VueComponent.prototype.observe = function(data, path = "") {
        const self = this;
        if(!data || Object.prototype.toString.call(data) !== "[object Object]") {
          return;
        }

        //递归遍历data的所有key键值对,如果当前的key对应的值是一个引用类型,那么用Proxy进行数据代理的实现
        //如果是原始类型(string, number),例如data = {someKey : 1}时,我们对someKey的值“数字1”无法用Proxy进行拦截
        //这时采用Object.defineProperty中的get和set来实现。当然两种情况都可以用Object.defineProperty进行实现,
        //Proxy比较灵活,能够拦截的种类比较多。在数据改变时我们要通知相应的watcher来更新依赖该数据的dom节点。
        const keys = Object.keys(this.$data);
        for(let key of keys) {
          let value = data[key];
          const currentPath = path + key;
          if(typeof value === 'object') {
            //如果是object,则递归进入下一层结构创建代理
            self.observe(value,currentPath + '. ');
            data[key] = new Proxy(value, {
              set(target, property, value, reseiver) {
                if(!Reflect.set(target, property, value, reseiver)) {
                  return false;
                }
                const keyPath = currentPath + "." + property;
                //属性改变时会通知该key对应的watcher进行更新,并执行watch里的相应key的回调
                updateWatcher(self.$watchers[keyPath],value);
                self.$watch[keyPath] && self.$watch[keyPath](value);
                return true;
              },
              get(target, property, reseiver) {
                return target[property];
              }
            });
          }else {
            //Object.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
            Object.defineProperty(data, key, {  //Object.defineProperty(要定义属性的对象,要定义或修改的属性的名称或symbol,要定义或修改的属性描述符)
              enumerable: true,  //enumerable 当且仅当该属性的enumerable键值为true时,该属性才会出现在对象的枚举属性中
              configurable: false,  //configurable 当且仅当该属性的configurable键值为true时,该属性的描述符才能被改变,同时该属性也能从对应的对象上被删除
              get() {    //当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this不一定是定义该属性的对象)。该函数的返回值会被用作属性的值
                return value;
              },
              set() {    //当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this对象
                value = newVal;
                self.$watch[currentPath] && self.$watch[currentPath](newVal);
                updateWatcher(self.$watchers[currentPath], newVal);
              }  
            })
          }
        }
      }

         我们在编译模板的时候会进行node上数据依赖的收集与事件的监听,首先我们先要把构造函数传进来的模板字符串转换成dom。

    这里用一个htmlStringToElement来实现该功能,思路就是利用html中的template标签来创建该component的dom节点。

      function htmlStringToElement(html = '') {
        const template = document.createElement("template");  //创建一个template标签
        template.innerHTML = html.trim();  //把传进来的html字符串加在template标签中
        return template.content.firstChild;  //返回此时的template内容第一个元素就是传进来的html字符串的dom了
      }

      然后进行模板解析,compile函数中会递归遍历所有的node上的attribute,如果是v-model我们进行双向数据绑定

    以冒号“:”开头的属性我们进行相应数据的监听,如果是“@”开头的则添加该节点上相应的事件监听。

      VueComponent.prototype.compile = function() {
        const self = this;
        const el = self.$template;
        const data = self;
        if(!(el instanceof HTMLElement)) {
          throw new TypeError("template must be an HTMLElement instance");
        }
        const fragment = document.createDocumentFragment();
        let child = el.firstChild;
        while(child) {
          fragment.appendChild(child);
          child = el.firstChild;
        }
        const reg = /{{(.*?)}}/g; //匹配{{xx.xx}}的正则
        
        //对el中的内容进行相应替换
        const replace = (parentNode)=> {
          const children = parentNode.childNodes;
          for(let i = 0; i < children.length; i++) {
            const node = children[i];
            const nodeType = node.nodeType;
            const text = node.textContent;
            //如果是text节点,我们在这里判断内容是否存在模板变量
            if(nodeType === Node.TEXT_NODE && reg.test(text)) {
              node.textContent = text.replace(reg, (matched, placeholder) => {
                let pathArray = [];
                const textContent = placeholder.split('.').reduce((prev, property) => {
                  pathArray.push(property);
                  return prev[property];
                },data);
                const path = pathArray.join(".");      //path就是当前key的层级路径,例如b:{a:1}中a的路径就是a.b
                self.$watchers[path] = self.$watchers[path] || [];
                self.$watchers[path].push(new Watcher(self, node, "textContent", text));
                return textContent;
              })
            }
            
            //如果是element则进行attribute的绑定
            if(nodeType === Node.ELEMENT_NODE) {  //Node.ELEMENT_NODE代表元素节点类型。
              const attrs = node.attributes;  //attributes属性返回该节点的属性节点【集合:不重复,无序】
              for(let i = 0; i < attrs.length; i++) {
                const attr = attrs[i];
                const attrName = attr.name;
                const attrValue = attr.value;
                self.$watchers[attrValue] = self.$watchers[attrValue] || [];
                //如路过属性名称为v-model我们需要添加value属性的绑定和输入事件监听,这里假设都为input事件,可替换为change事件
                if(attrName === 'v-model') {
                  node.addEventListener("input",(e) => {
                    const inputVal = e.target.value;
                    const keys = attrValue.split(".");
                    const lastKey = keys.splice(keys.length - 1)[0];
                    const targetKey = keys.reduce((prev, property) => {
                      return prev[property];
                    }, data);
                    targetKey[lastKey] = inputVal;
                  });
                }
          
                //对value属性的绑定
                if(attrName === ':bind' || attrName === 'v-model') {
                  node.value = attrValue.split(".").reduce((prev,property) => {
                    return prev[property];
                  },data);
                  self.$watchers[attrValue].push(new Watcher(self, node, "value"));
                  node.removeAtrribute(attrName);
                }else if(attrName.startsWith(":")) {    //startsWith用于检测字符串是否以指定的前缀开始
                  //对普通attribute的绑定
                  const attributeName = attrName.splice(1);
                  const attributeValue = attrValue.split(".").reduce((prev,property) => {
                    return prev[property];
                  }, data);
                  node.setAttribute(attributeName, attributeValue);
                  self.$watchers[attrValue].push(new Watcher(self, node, attributeName));
                  node.removeAttribute(attrName);
                }else if (attrName.startsWith("@")) {
                  //事件监听
                  const event = attrName.splice(1);
                  const cb = attrValue;
                  node.addEventListener(event, function() {
                    //data和methods同时放在了实例的根层,所以这里的context设置为self,就可以在方法中同时访问到data和其他methods了
                    self.$methods[cb].call(self);
                  });
                  node.removeAttribute(attrName);
                }
              }
            }
            //如果存在子节点,递归调用replace
            if(node.childNodes && node.childNodes.length) {
              replace(node);
            }
          }
        }
        replace(fragment);
        el.appendChild(fragment);
        return el;
      }

      数据改变时所关联的node需要进行更新这里一个watcher对象来进行node与数值之间的联系,并在数据改变时执行相关的更新视图操作。Watcher类主要负责接收到数据更新通知时,对dom内容的更新,监听到数据改变时会调用updateWatcher这个辅助函数来执行该数据关联的所有dom节点更新函数

      function updateWatcher(watchers = [], value) {
        watchers.forEach(Watcher => {
          Watcher.update(value);
        })
      }

    创建watch实例时我们会对该dom节点node,关联的数据data,还有所对应的attribute进行保存,这里的template是要保存原始的模板变量,例如“{{article.test}}:{{author}}”。之后执行update函数更新TextNode中的内容时会用到,数据更新被拦截后执行updateWatcher,其中又会执行所有依赖的watcher中的update函数。update函数会根据attribute的类型来判断如何更新dom节点。如果是更新TextNode,其中会包含一个或多个模板变量,所以之前要保存原始的template,将原始的模板中各个变量依次替换

      function Watcher(data, node, attribute, template) {
        this.data = data;
        this.node = node;
        this.atrribute = attribute;
        this.template = template;
      }
      
      Watcher.prototype.update = function(value) {
        const attribute = this.attribute;
        const data = this.data;
        const template = this.template;
        const reg = /{{(.*?)}}/g;  //匹配{{xx.xx}}的正则
        if(attribute === "value") {
          this.node[attribute] = value;
        }else if(attribute === "innerText" || attribute === "innerHTML" || atrribute === "textContent") {
          this.node[attribute] = template.replace(reg, (matched, placeholder) => {
            return placeholder.split(".").reduce((prev, property) => {
              return prev[property];
            }, data);
          });
        }else if(attribute === "style") {
          thie.node.style.cssText = value;
        }else {
          this.node.setAttribute(attribute, value);
        }
      }

       测试代码

    <script>
      const IndexComponent = new VueComponent({
        data(): => {
          return {
            article: {
              title: "简易版vue"
            },
            author: "xx"
          }
        },
        watch: {
          author(value) {
            console.log("author:", value);
          }
        },
        methods: {
          onButtonClick() {
            this.author = "cc";
            alert("clicked!");
          }
        },
        template:
            `
              <div>
                <div>template及其他功能测试</div>
                <div>{{article.title}} : {{author}}</div>
                <div><input type="text" v-model="article.title"></div>
                <div><input type="text" v-model="author"></div>
                <div><button @click="onButtonClick">click</button></div>
              </div>
            `
      });
      window.onload = function(){
        document.getElementById("root").appendChild(IndexComponent.$vueDomNode);
      }
    </script>
    <body>
      <div id="root"></div>
    </body>
  • 相关阅读:
    无旋转Treap简介
    bzoj 4318 OSU!
    bzoj 1419 Red is good
    bzoj 4008 亚瑟王
    bzoj 1014 火星人prefix
    更多的莫队
    bzoj 3489 A simple rmq problem
    洛谷 2056 采花
    NOIP 2017 游(划水)记
    UVa 11997 K Smallest Sums
  • 原文地址:https://www.cnblogs.com/wannacc-xx/p/13954567.html
Copyright © 2020-2023  润新知