手写实现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);
        },
      });
    });
  };
}

<think>我们正在处理一个关于使用Vue实现平板手写笔记功能的问题。根据提供的引用,我们可以提取一些关键点: 引用[1]提到了Vue的核心概念,包括数据绑定、指令、计算属性、监视属性等。这为我们提供了Vue的基础知识。 引用[2]提到了手写文档转录工具的核心任务是从手写图像中提取文本信息,并强调了GUI设计的重要性。这提示我们在实现手写笔记功能时,需要考虑用户交互界面。 引用[3]和[4]则提到了Vue2和Vue3中的一些特性,特别是计算属性(computed)和侦听器(watch)的使用,以及Vue3中watchEffect的使用。 用户想要实现功能类似于平板手写笔记,这意味着我们需要在网页上实现一个手写输入的区域,并能够保存手写笔记。 实现思路: 1. 创建画布:使用HTML5的Canvas元素作为手写区域。 2. 事件处理:监听触摸事件(对于移动设备)和鼠标事件(对于桌面设备),以实现手写功能。 3. 数据绑定:使用Vue来管理画布的状态(如画笔颜色、粗细等)和保存的笔记数据。 4. 保存笔记:将Canvas上的绘图转换为图片并保存,或者保存为路径数据以便重新绘制。 步骤: 第一步:创建Vue项目(假设已经搭建好Vue环境) 第二步:在Vue组件中,添加canvas元素,并设置其宽高。 第三步:在Vue组件的生命周期钩子(如mounted)中,获取canvas的上下文,并初始化事件监听。 第四步:实现绘图逻辑,包括开始绘图(mousedown/touchstart)、绘图过程中(mousemove/touchmove)和结束绘图(mouseup/touchend)。 第五步:使用Vue的数据绑定来管理画笔属性(颜色、粗细)和笔记数据(存储绘图路径或图片)。 第六步:提供工具栏,允许用户选择画笔、清除画布、保存笔记等。 考虑到引用[4]中提到了watchcomputed,我们可以使用这些特性来响应画笔属性的变化。 另外,为了保存笔记,我们可以将画布的内容转换为图片(使用canvas.toDataURL()),然后将其保存在Vue的数据中(如一个数组),这样我们可以显示历史笔记。 下面是一个简单的代码框架: 1. 模板部分:包含canvas和工具栏(选择颜色、粗细、清除、保存等按钮)。 2. 数据部分:定义画笔颜色、画笔粗细、绘图状态(是否正在绘制)、存储笔记的数组等。 3. 方法部分:实现绘图相关的方法(开始绘图、绘图、结束绘图)和工具栏的方法(清除画布、保存笔记等)。 4. 生命周期钩子:在mounted中设置画布和事件监听。 注意:在移动设备上,要同时支持触摸事件和鼠标事件。 代码示例(使用Vue3的Composition API): ``` <template> <div> <canvas ref="canvas" @mousedown="startDrawing" @mousemove="draw" @mouseup="stopDrawing" @touchstart="startDrawing" @touchmove="draw" @touchend="stopDrawing"></canvas> <div> <label>画笔颜色:</label> <input type="color" v-model="penColor"> <label>画笔粗细:</label> <input type="range" v-model="penWidth" min="1" max="50"> <button @click="clearCanvas">清除</button> <button @click="saveNote">保存笔记</button> </div> <div v-for="(note, index) in notes" :key="index"> <img :src="note" alt="笔记图片"> </div> </div> </template> <script> import { ref, onMounted } from 'vue'; export default { setup() { const canvas = ref(null); const ctx = ref(null); const isDrawing = ref(false); const penColor = ref('#000000'); const penWidth = ref(5); const notes = ref([]); // 保存笔记图片的数组 // 初始化画布 onMounted(() => { ctx.value = canvas.value.getContext('2d'); // 设置画布宽高为父容器宽高(或者固定值) canvas.value.width = canvas.value.offsetWidth; canvas.value.height = canvas.value.offsetHeight; }); const startDrawing = (e) => { isDrawing.value = true; draw(e); // 为了在触摸开始时画一个点 }; const draw = (e) => { if (!isDrawing.value) return; // 获取坐标(考虑鼠标事件和触摸事件) let clientX, clientY; if (e.type.includes('touch')) { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } // 相对于canvas的位置 const rect = canvas.value.getBoundingClientRect(); const x = clientX - rect.left; const y = clientY - rect.top; ctx.value.lineWidth = penWidth.value; ctx.value.lineCap = 'round'; ctx.value.strokeStyle = penColor.value; ctx.value.lineTo(x, y); ctx.value.stroke(); ctx.value.beginPath(); ctx.value.moveTo(x, y); }; const stopDrawing = () => { isDrawing.value = false; ctx.value.beginPath(); // 重新开始路径,避免下次绘制时连接 }; const clearCanvas = () => { ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height); }; const saveNote = () => { // 将画布内容转换为图片URL const image = canvas.value.toDataURL('image/png'); notes.value.push(image); // 可以在这里将图片保存到服务器或本地 }; return { canvas, startDrawing, draw, stopDrawing, clearCanvas, saveNote, penColor, penWidth, notes }; } }; </script> <style scoped> canvas { border: 1px solid #000; width: 100%; height: 500px; } </style> ``` 注意事项: 1. 上述代码是一个基本实现,实际应用中可能需要优化,例如防抖、保存路径数据以便编辑等。 2. 在移动设备上,可能需要阻止触摸事件的默认行为和冒泡,以避免页面滚动。 3. 保存笔记时,可以根据需要将图片上传到服务器,或者使用本地存储。 引用文献: [^1]提供了Vue的基础知识,包括数据绑定和事件处理,这是我们实现的基础。 [^2]强调了GUI设计的重要性,因此我们设计了工具栏和笔记展示区域。 [^4]中提到的watchcomputed在更复杂的场景中可能会用到,例如当我们需要根据画笔属性的变化来实时更新画布的画笔设置(虽然上述代码是直接在draw方法中使用响应式变量),或者当笔记数据变化时自动保存等。 生成相关问题: 1. 如何使用Vuewatchcomputed来优化手写笔记功能? 2. 如何将手写笔记保存到本地存储? 3. 如何实现手写笔记的撤销和重做功能? 4. Vue2和Vue3在实现手写笔记功能时有何不同? 5. 如何将手写笔记上传到服务器? </think>### 如何使用Vue实现类似平板手写笔记功能 #### 核心实现步骤 1. **创建画布组件** 使用HTML5 Canvas作为手写区域: ```vue <template> <div> <canvas ref="canvas" @mousedown="startDrawing" @mousemove="draw" @mouseup="stopDrawing" @touchstart="startDrawing" @touchmove="draw" @touchend="stopDrawing" ></canvas> </div> </template> ``` 2. **实现绘图逻辑** ```javascript export default { data() { return { isDrawing: false, lastX: 0, lastY: 0, penColor: '#000', penWidth: 3 } }, methods: { startDrawing(e) { this.isDrawing = true; const { offsetX, offsetY } = this.getCoordinates(e); [this.lastX, this.lastY] = [offsetX, offsetY]; }, draw(e) { if (!this.isDrawing) return; const ctx = this.$refs.canvas.getContext('2d'); const { offsetX, offsetY } = this.getCoordinates(e); ctx.beginPath(); ctx.moveTo(this.lastX, this.lastY); ctx.lineTo(offsetX, offsetY); ctx.strokeStyle = this.penColor; ctx.lineWidth = this.penWidth; ctx.lineCap = 'round'; ctx.stroke(); [this.lastX, this.lastY] = [offsetX, offsetY]; }, getCoordinates(e) { if (e.touches) { const rect = e.target.getBoundingClientRect(); return { offsetX: e.touches[0].clientX - rect.left, offsetY: e.touches[0].clientY - rect.top }; } return { offsetX: e.offsetX, offsetY: e.offsetY }; }, stopDrawing() { this.isDrawing = false; } }, mounted() { const canvas = this.$refs.canvas; canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; } } ``` 3. **添加笔记管理功能** 使用Vue的响应式数据管理笔记列表: ```javascript data() { return { notes: [], activeNote: { id: Date.now(), content: null // 存储Canvas图像数据 } } }, methods: { saveNote() { const imageData = this.$refs.canvas.toDataURL('image/png'); this.activeNote.content = imageData; this.notes.push({...this.activeNote}); this.clearCanvas(); }, clearCanvas() { const ctx = this.$refs.canvas.getContext('2d'); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); } } ``` 4. **集成GUI控件**(参考引用[2]的设计原则) - 添加工具栏控制笔触属性 - 实现笔记列表侧边栏 ```vue <div class="toolbar"> <input type="color" v-model="penColor"> <input type="range" v-model="penWidth" min="1" max="20"> <button @click="saveNote">保存</button> <button @click="clearCanvas">清空</button> </div> ``` #### 关键技术点 1. **触摸事件适配** 通过`@touchstart`/`@touchmove`实现移动端支持,使用`getCoordinates()`方法统一处理触摸/鼠标事件坐标[^2] 2. **响应式笔触设置** 利用Vue的`v-model`双向绑定实时同步笔触属性: ```javascript watch: { penWidth(newVal) { this.$refs.canvas.getContext('2d').lineWidth = newVal; }, penColor(newVal) { this.$refs.canvas.getContext('2d').strokeStyle = newVal; } } ``` 3. **数据持久化**(参考引用[4]的响应式处理) ```javascript import { watch } from 'vue'; export default { setup() { const notes = ref(JSON.parse(localStorage.getItem('notes')) || []); watch(notes, (newVal) => { localStorage.setItem('notes', JSON.stringify(newVal)); }, { deep: true }); } } ``` #### 优化建议 1. **性能优化** - 使用Canvas的`requestAnimationFrame`优化绘制性能 - 对触摸事件进行节流处理 2. **高级功能扩展** ```javascript // 实现撤销/重做功能 const history = []; const MAX_HISTORY = 50; function saveState() { history.push(canvas.toDataURL()); if (history.length > MAX_HISTORY) history.shift(); } function undo() { if (history.length < 2) return; history.pop(); // 移除当前状态 loadImage(history[history.length - 1]); } ``` 3. **集成文字识别**(参考引用[2]) - 使用Tesseract.js等OCR库 ```javascript import Tesseract from 'tesseract.js'; function recognizeText() { Tesseract.recognize(canvas.toDataURL()) .then(({ data }) => { console.log('识别结果:', data.text); }); } ``` [^1]: Vue的核心响应式系统和组件架构使得构建复杂交互应用更加高效 [^2]: GUI设计应注重用户交互流畅性,特别是在触摸设备上的体验优化 [^3]: Vue3的Composition API更适合管理复杂的手写板状态逻辑 [^4]: 使用watchEffect可自动追踪笔触属性的变化并实时更新画布 --- ### 相关问题 1. **如何优化Vue手写笔记的绘制性能?** 2. **如何在Vue实现手写笔记的实时协同编辑功能?** 3. **Vue2与Vue3在实现手写功能时有何核心差异?** 4. **如何将手写笔记内容转换为可编辑文本?** 5. **在移动端如何解决Vue手写笔记的触摸延迟问题?**
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值