手写实现Vue, 带watch,computed功能

今天用react的同事问到vue的watch功能实现,想搬到react里做一个hook, 在网上搜索了一圈竟然没发现一个实现vue并带有watch,computed功能的。我一时兴起,抽三天时间晚上撸了一个,希望能帮到需要的朋友。

1.html模板,最终实现的MyVue需要实现的功能。

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

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <div id="app">
    <p>名称:{{name}}</p>
    <p v-text="name"></p>
    <p>年龄:{{age}}</p>

    <input type="text" v-model="name">
    <br>
    <label> 综合计算:【{{info}}】 </label>
    <button @click="changeName">按钮事件</button>
    <div v-html="html"></div>
  </div>

  <!-- 存入依赖的地方 -->
  <script src='./dep.js'></script>
  <!-- Watcher是连接Observer和Compile的桥梁,通过它修改dom -->
  <script src='./watcher.js'></script>
  <!-- 把模板编译,生成watcher,注意这个不是代码编译器 -->
  <script src='./compile.js'></script>
  <!-- 手动实现的简单版本vue -->
  <script src='./myVue.js'></script>

  <script>
    const stan = new MyVue({
      el: '#app',
      data: {
        name: "Stanley",
        age: 12,
        html: '<button>注入的html按钮</button>'
      },
      created() {
        console.log('created生命周期')
      },
      computed: { //值有可能是get/set对象,但这里todo
        info() {
          return this.name + ': ' + this.age;
        }
      },
      watch: {
        age(newV, old) {
          console.log('WATCH:', newV, old);
        }
      },
      methods: {
        changeName() {
          this.name = '大神'
          this.age = 18
        }
      }
    })
  </script>
</body>

</html>

2. Dep.js是用来存入依赖watcher的地方


// Dep:管理若干watcher实例,它和key一对一关系
class Dep {
  constructor() {
    this.deps = [];
  }

  addDep(watcher) {
    this.deps.push(watcher);
  }

  notify(v) {
    this.deps.forEach(watcher => watcher.update(v));
  }
}

3.watcher.js 它的实例闭包了vnode,key,dom,各种需要的变量,回调中修改dom或者实现计算属性

// 保存ui中依赖,实现update函数可以更新之
class Watcher {
  constructor(vm, key, cb, name) {
    this.name = name;
    this.vm = vm;
    this.key = key;
    this.cb = cb;

    // 将当前实例指向Dep.target
    Dep.target = this;
    // 读一次key触发getter,这里很重要
    this.vm[this.key]; 
    Dep.target = null;
    console.log('set watcher:' + this.name, this.key);
  }

  update(newV) {
    // 更新,返回新值,watch时才会用到newV
    this.cb.call(this.vm, newV, this.vm[this.key]);
  }
}

4.compile.js html模板处理函数,把{{}}内的值v-model,@click等属性处理,完成从vue实现到dom同步的过程,生成watch,这就是值与dom的依赖关系。

// 遍历模板,将里面的插值表达式处理
// 另外如果发现v-xx, @xx做内部协议事件处理
class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);

    if (this.$el) {
      // 1.$el中的内容搬家到一个fragment,提高操作效率
      this.$fragment = this.node2Fragment(this.$el);

      // 2.编译fragment
      this.compile(this.$fragment);

      // 3.将编译结果追加至宿主中
      this.$el.appendChild(this.$fragment);
    }
  }

  //   遍历el,把里面内容搬到新创建fragment中
  node2Fragment(el) {
    const fragment = document.createDocumentFragment();
    let child;
    while ((child = el.firstChild)) {
      // 由于appenChild是移动操作
      fragment.appendChild(child);
    }
    return fragment;
  }

  //   把动态值替换,把指令和事件做处理
  compile(el) {
    // 遍历el
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
      if (this.isElement(node)) {

        // 如果是元素节点,我们要处理指令v-xx,事件@xx
        this.compileElement(node);
      } else if (this.isInterpolation(node)) {
        this.compileText(node);
      }

      //   递归子元素
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    });
  }

  isElement(node) {
    return node.nodeType === 1;
  }
  //   插值表达式判断
  isInterpolation(node) {
    // 是文本节点,并且 需要满足{{xx}}
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }

  compileElement(node) {
    // 查看node的特性中是否有v-xx,@xx
    const nodeAttrs = node.attributes;
    Array.from(nodeAttrs).forEach(attr => {
      // 获取属性名称和值 v-text="abc"
      const attrName = attr.name; // v-text
      const exp = attr.value; // abc
      // 指令:v-xx
      if (attrName.indexOf("v-") === 0) {
        const dir = attrName.substring(2); // text
        // 执行指令
        this[dir] && this[dir](node, this.$vm, exp);
      } else if(attrName.indexOf('@') === 0) {
          // 事件 @click="handlClick"
          const eventName = attrName.substring(1); // click
          this.eventHandler(node, this.$vm, exp, eventName);
      }
    });
  }
  text(node, vm, exp) {
    this.update(node, vm, exp, "text");
  }

  //   双向数据绑定
  model(node, vm, exp) {
    // update是数据变了改界面
    this.update(node, vm, exp, "model");
    // 界面变了改数值
    node.addEventListener("input", e => {
      vm[exp] = e.target.value;
    });
  }

  modelUpdator(node, value) {
    node.value = value;
  }

  html(node, vm, exp) {
    this.update(node, vm, exp, "html");
  }
  htmlUpdator(node, value) {
    node.innerHTML = value;
  }

  eventHandler(node, vm, exp, eventName){
    // 获取回调函数
    const fn = vm.$options.methods && vm.$options.methods[exp];
    if(eventName && fn) {
      // 函数柯理化,把事件的this绑定到了vm
        node.addEventListener(eventName, fn.bind(vm))
    }
  }

  //   把插值表达式替换为实际内容
  compileText(node) {
    // {{xxx}}
    // RegExp.$1是匹配分组部分
    // console.log(RegExp.$1);

    const exp = RegExp.$1;
    this.update(node, this.$vm, exp, "text");
  }

  // 编写update函数,它可复用
  // exp是表达式, dir是具体操作:text,html,model
  update(node, vm, exp, dir) {
    const fn = this[dir + "Updator"];
    const plainValue = vm[exp];// todo 没做子元素.操作
    const templateText = node.textContent;
    fn && fn(node, plainValue,templateText);
    new Watcher(vm, exp, function(newV,old) {
      // 这里执行的是,例如compile.textUpdator(node, value)
      // 本函匿名数调用时的this是vm
      // node被闭包了;vm也闭包在了watchre实例内,所以下面代码也可以写成fn(node, this.vm[exp])
      fn && fn(node, vm[exp],templateText);
    },'compile:'+ node.tagName);
  }

  textUpdator(node, value,templateText) {
    // 这里出了问题,text里已经没有表达式了,todo
    if(!templateText){
      node.textContent = value;
    }else{
      node.textContent=templateText.replace(/\{\{(.*)\}\}/,value);
    }
  }
}

5 myVue.js 手动实现的简单版本vue,实现了watch,computed,state的初始化。

class MyVue {
  constructor(options) {
    this.$options = options;

    this._state = { data: options.data };
    // 响应化
    this.initState(this._state.data);
    this.initComputed(options.computed);
    this.initWatch(options.watch);

    // 创建编译器
    new Compile(options.el, this);

    if (options.created) {
      options.created.call(this);
    }
  }

  initState(state) {
    // 递归遍历,使传递进来的对象响应化
    this.defineReactive(state);

    Object.keys(state).forEach((key) => {
      // 把state里的变量代理到this
      this.proxy(key, this._state);
    });
  }

  initComputed(computedCfg) {
    if(!computedCfg){
      return;
    }

    Object.keys(computedCfg).forEach((key) => {
      // 把state里的变量代理到this
      this.defineComputed(key, computedCfg[key]);
    });
  }

  initWatch(watchCfg) {
    if(!watchCfg){
      return;
    }

    Object.keys(watchCfg).forEach((key) => {
      // 把state里的变量代理到this
      new Watcher(this,key,watchCfg[key],'user watch');
    });
  }

  // 在vue根上定义属性代理data中的数据
  proxy(key, value) {
    Object.defineProperty(this, key, {
      get() {
        return value.data[key];
      },
      set(newVal) {
        // console.log(key,':',newVal)
        value.data[key] = newVal;
      },
    });
  }

  // computeFunc 值有可能是get/set对象,但这里todo
  // 实际上defineComputed是一个类似于initState,的依赖注入,
  // state 是单个值的注入同步dom元素;computed是一组值依赖,同步一个值
  defineComputed(key, computeFunc) {
    let cacheVal; // 这个就是闭包缓存的值
    const dep = new Dep();// 所有用到这个计算属性的值的地方,都要把watcher存进来
    const onDependencyUpdated = ()=>{
      // 再次计算 计算属性的值
      cacheVal = computeFunc.call(this);
      dep.notify();
    };

    // 将onDependencyUpdated 这个函数传给Dep.target
    Dep.target = new Watcher(this, '', onDependencyUpdated,'computed');
    // 很重要,收集计算属性的依赖值,get把target放到了成员的dep里
    cacheVal = computeFunc.call(this); 
    Dep.target = null;

    Object.defineProperty(this, key, {
      get: function () {
          Dep.target && dep.addDep(Dep.target);
          return cacheVal;
      }, 
      set: function () {
        // 什么也不做,不需要设定计算属性的值,也可以使用用户传入的 todo
      },
    });
  }

  defineReactive = (value) => {
    if (typeof value !== 'object') {
      return;
    }

    Object.keys(value).forEach((key) => {
      let val = value[key];

      // 递归
      this.defineReactive(val);

      // 创建Dep实例:Dep和key一对一对应
      // 它会放多个watcher,每个watcher对应这个key被用在了不同的模板dom
      const dep = new Dep();

      // 给obj定义属性
      Object.defineProperty(value, key, {
        get() {
          Dep.target && console.log(key,'pushed',Dep.target.name)
          // 将Dep.target指向的Watcher实例加入到Dep中
          Dep.target && dep.addDep(Dep.target);
          return val;
        },
        set: (newVal) => {
          if (newVal === val) {
            return;
          }

          this.defineReactive(newVal);
          // 只通知观察了这个变量的watcher(Dep.target)
          // watcher会更新Compile时闭包的dom元素
          dep.notify(newVal);
          val = newVal;
          console.log('RE:', key, ':', newVal, dep);
        },
      });
    });
  };
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值