带你快速手写一个简易版vue了解vue响应式

起始

在使用vue的时候,数据双向绑定,插值语法...等等,一系列让人眼花缭乱的操做.做为一个使用者搞懂轮子是怎么造,并非为了本身造一个轮子,而是为了更好的使用轮子.javascript

虽然如今vue3已经开始使用,可是不少公司如今使用的应该仍是vue2.X.html

使用

在咱们使用的时候,通常都是在main文件进行初始化,通常都是这样前端

new Vue({
  router,
  store,
  .......
  render: h => h(App)
}).$mount('#app')
复制代码

在这里对于我一个初入前端的切图仔确定不会实现咱们在项目中那么复杂,对于vue咱们还有另外一种用法,在html文件中引入vue.js,譬如这样vue

new Vue({
    el: '#app',
    data: {
      number: 1,
    },
    methods: {
      add() {
        this.number++
      },
      changeInput() {
        console.log('changeInput');
      }
    }
  })
复制代码

就像这样,今天实现的就是以这样使用的,在vue的真正使用中会有三种render,template和el,优先级也是如此排列.java

分析

首先须要分析咱们实现的是怎样的一个类node

  • 首先它会接受一个对象,有el,data,methods.....
  • data中的数据须要响应式处理
  • 解析模板处理,也就是咱们在div中写入的{{number}}之类
  • 响应式更新,这也是最重要的,在改变数据的时候通知视图进行更新
  • 处理事件和指令,好比v-model,@click之类

开始操刀实现

首先建立vue这个类数组

class Vue{
  constructor(options) {
    // 将传入的保存
    this.$options = options;
    // 将传入data,保存在$data中
    this.$data = options.data
  }
}
复制代码

可是在咱们的使用data的值时咱们历来没有使用过$data,咱们都是this.xxx就能够拿到这个值,因而可知咱们首先须要将this.$data中的数据所有代理到这个类上让咱们可使用this.XXX可使用这个也很简单,使用defineProperty就能够实现,下边来实现这个方法.markdown

function proxy(vm) {
  //这里的vm须要咱们拿到vue自己的实例
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get: () => vm.$data[key],
      set: (v) => vm.$data[key] = v
    })
  })
}
复制代码

这个方法很简单,不用作过多赘述,下边在vue类中使用让咱们能够经过实例能够拿到data中的值app

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
+   proxy(this)
  }
复制代码

这样咱们就可使用this.xx拿到,由于咱们尚未实现method因此咱们经过new Vue拿到的实例来实验dom

const app = new Vue({
    el: '#app',
    data: {
      counter: 1,
    },
  })
  console.log(app.number);
复制代码

咱们在控制台能够看到成功输出了numbe的值

image.png 那么对于data的代理就成功了.

模板解析

对于模板解析,须要处理写入的插值表达式(写入的自定义指令和事件,后边会解决)

首先先将模板中的插值解析时页面打开不会满屏的大括号.

class Compile {
  //咱们须要将el和vue自己传入来进行模板解析,el须要用来拿到元素,vue自己则须要其中的data,methds...
  constructor(el, vm) {
    this.$vm = vm
    //拿到咱们解析的元素
    this.$el = document.querySelector(el)

    if(this.$el) {
      // 编写一个函数来解析模板
      this.compile(this.$el)
    }
  }
  compile(el) {
    // 遍历el的子节点 判断他们的类型作相应的处理
    const childNodes = el.childNodes
    if(!childNodes) return;
    childNodes.forEach(node => {
      if(node.nodeType === 1) {
        // 元素 处理指令和事件(后续来处理)
      } else if(this.isInter(node)) {
        // 文本
        this.compileText(node)
      }
      // 在有子元素的状况下须要递归
      if(node.childNodes) {
        this.compile(node)
      }
    })
  }
  
  // 编译文本
  compileText(node) {
      node.textContent = this.$vm[RegExp.$1]
  }

  // 是否插值表达式
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
}
复制代码

而后在vue中使用它.

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
    proxy(this)
+   new Compile(options.el, this)
  }
}
复制代码

这样页面中的插值就能够被替换为data中的数据了,可是咱们在外边经过vue实例修改的时候数据是没有变化的.

实现数据响应式

在实现数据响应式这里,借用源码的思想,使用Observer来作响应式,watcher和dep来通知更新,在vue中一个组件只有一个watcher,而咱们的粒度就没办法相提并论了,毕竟是一个只实现部分的简单版vue. 首先编写Observer

Observer编写

// 遍历obj作响应式
function observer(obj) {
  if(typeof obj !== 'object' || obj === null) {
    return
  }
  // 遍历obj的全部key作响应式
  new Observer(obj);
}
// 遍历obj的全部key作响应式
class Observer {
  constructor(value) {
    this.value = value
    if(Array.isArray(this.value)) {
      // TODO 对于数组的操做不作处理 无非是对于数组的七个方法进行重写覆盖
    } else {
      this.walk(value)
    }
  }
  // 对象响应式
  walk(obj) {
    Object.keys(obj).forEach(key => {
      //这个东西看到不少人会以为很眼熟,固然也只是名字眼熟
      defineReactive(obj, key, obj[key])
    })
  }
}
复制代码

defineReactive这里是用于数据进行响应式处理函数,在进行这个函数的编写以前,还须要先进行watcher的编写

watcher编写

// 监听器:负责依赖更新
const watchers = []; //先不使用dep 先使用一个数组来收集watcher进行更新
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn
    watchers.push(this)
  }

  // 将来被Dep调用
  update() {
    // 执行实际的更新操做
    //由于咱们须要在watcher更新时拿到最新的值,因此须要咱们在这里做为参数传递给咱们在收集到的函数
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}
复制代码

有了watcher以后,须要修改咱们以前写的Compile类来进行watcher的收集,于此同时,修改编译时的修改方法同时为v-model和v-test...作前置基础

Compile进化

class Compile {
  //咱们须要将el和vue自己传入来进行模板解析,el须要用来拿到元素,vue自己则须要其中的data,methds...
  constructor(el, vm) {
    this.$vm = vm
    //拿到咱们解析的元素
    this.$el = document.querySelector(el)

    if(this.$el) {
      // 编写一个函数来解析模板
      this.compile(this.$el)
    }
  }
  compile(el) {
    // 遍历el的子节点 判断他们的类型作相应的处理
    const childNodes = el.childNodes
    if(!childNodes) return;
    childNodes.forEach(node => {
      if(node.nodeType === 1) {
        // 元素 处理指令和事件(后续来处理)
      } else if(this.isInter(node)) {
        // 文本
        this.compileText(node)
      }
      // 在有子元素的状况下须要递归
      if(node.childNodes) {
        this.compile(node)
      }
    })
  }
  
  //新添加函数
  //node 为修改的元素 exp为获取到大括号内的值的key dir为这边自定义的要执行的操做
  update(node, exp, dir) {
    // 初始化
    const fn = this[dir + 'Update']
    fn && fn(node, this.$vm[exp])
    // 更新 在这里建立watcher 并将更新的函数传进去 这里的val就是watcher触发更新函数时传入的最新值
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val)
    })
  }
  //新添加函数
  textUpdate(node, val) {
     node.textContent = val
   }
  // 编译文本
  compileText(node) {
  -   //node.textContent = this.$vm[RegExp.$1]
  +   this.update(node, RegExp.$1, 'text')
  }

  // 是否插值表达式
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
}
复制代码

defineReactive编写

下边来编写defineReactive来达到视图真正的更新

/** * 在key发生变化时能够感知作出操做 * @param {*} obj 对象 * @param {*} key 须要拦截的key * @param {*} val 初始值 */
 function defineReactive(obj, key, val) {
  // 递归
  observer(val);
  Object.defineProperty(obj, key, {
    get() {
      return val
    },
    set(newVal) {
      console.log('set', newVal);
      if (newVal != val) {
        observer(newVal)
        val = newVal
        //这里为粗糙实现 后续会加入dep会更加精致一点
        watchers.forEach(w => w.update())
      }
    }
  })
}
复制代码

在vue类中使用

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
+   observer(this.$data)
    proxy(this)
    new Compile(options.el, this)
  }
}
复制代码

接下来在修改咱们使用的页面

const app = new Vue({
    el: '#app',
    data: {
      number:10,
    }
  })
  console.log(app.number);
  setTimeout(() => {
    app.counter = 100
  }, 1000)
复制代码

就能够在页面中看到在一秒钟以后视图朝着咱们预想的结果发生变化,这样子就达到了我想要的结果,可是这样存在一个问题,就是当咱们在data中定义了不少属性的时候,在页面中使用. 当咱们改变其中一个的时候,咱们全部的watcher都会执行一遍,页面全部用到data的地方都会发生更新,这样的问题我是确定不想要的,这样就须要dep来管理,达到咱们修改其中一个值,那只有修改的地方会发生变化,这样就会更好一点. 接下来先编写dep这个类,这个类的的功能很简单,一个dep管理data中的一条数据,收集和这条数据有关系的watcher,在发生变化时通知更新

Dep编写

class Dep{
  constructor() {
    this.deps = []
  }
  addDep(dep) {
    this.deps.push(dep)
  }
  notify() {
    this.deps.forEach(dep => dep.update())
  }
}
复制代码

在dep为前提时须要修改watcher这个类,同时也再也不须要watchers这个数组来管理.

Watcher进化

// 监听器:负责依赖更新
- //const watchers = []; //先不使用dep 先使用一个数组来收集watcher进行更新
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn
-    watchers.push(this)
    // 触发依赖收集
+   Dep.target = this
    //在这里纯属由于须要触发get来进行收集,下边重写defineReactive是会用到
+   this.vm[this.key]
+   Dep.target = null
  }

  // 将来被Dep调用
  update() {
    // 执行实际的更新操做
    //由于咱们须要在watcher更新时拿到最新的值,因此须要咱们在这里做为参数传递给咱们在收集到的函数
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}
复制代码

编写好Watcher后来从新方法,来使用到dep来管理更新,而不是将全部watcher都进行触发更新

defineReactive进化

/** * 在key发生变化时能够感知作出操做 * @param {*} obj 对象 * @param {*} key 须要拦截的key * @param {*} val 初始值 */
 function defineReactive(obj, key, val) {
  // 递归
  observer(val);
   // 建立Dep实例
  // data中的数据每一项都会进入到此,建立一个Dep
+  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
       // 依赖收集
      // 只有在调用时存在target才会被Dep收集更新(在初始化时设置静态属性target为watcher,被收集)
      //咱们在watcher进入时获取过一次当前使用到的watcher的值这里就会进入get,而且当时wacher将本身设置为了Dep的target,在这里使用到进行收集方便更新,可是在触发以后将dep.target设置为了null,使咱们在平时的读取时不作这个操做
+     Dep.target && dep.addDep(Dep.target)
+     return val
    },
    set(newVal) {
      console.log('set', newVal);
      if (newVal != val) {
        observer(newVal)
        val = newVal
        //这里为粗糙实现 后续会加入dep会更加精致一点
-       watchers.forEach(w => w.update())
        // 被修改时通知全部属于本身的watcher更新
        // 一个watcher对应一处依赖,一个Dep对应一个data中的数据 一个dep的更新能够指向多个watcher
+       dep.notify()
      }
    }
  })
}
复制代码

接下来修改使用的html文件来查看效果

const app = new Vue({
    el: '#app',
    data: {
      counter: 1,
      number:10,
    },
  })
  console.log(app.number);
  setTimeout(() => {
    app.counter = 100
  }, 2000)
复制代码

这样子咱们在没有使用到这个数据的地方就不会产生dom更新.在使用number的地方并无产生变化

image.png 接下来编写指令和事件的梳理 主要修改Compile类在模板解析时处理,处理以前咱们首先修改Vue类,将methods保存

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
+   this.$methods = options.methods
    observer(this.$data)
    proxy(this)
    new Compile(options.el, this)
  }
}
复制代码

Compile最终成型

接下来修改Compile类在模板解析时处理指令和事件

class Compile {
  //咱们须要将el和vue自己传入来进行模板解析,el须要用来拿到元素,vue自己则须要其中的data,methds...
  constructor(el, vm) {
    this.$vm = vm
    //拿到咱们解析的元素
    this.$el = document.querySelector(el)

    if(this.$el) {
      // 编写一个函数来解析模板
      this.compile(this.$el)
    }
  }
  compile(el) {
    // 遍历el的子节点 判断他们的类型作相应的处理
    const childNodes = el.childNodes
    if(!childNodes) return;
    childNodes.forEach(node => {
      if(node.nodeType === 1) {
        // 元素 处理指令和事件
+       const attrs = node.attributes
+       Array.from(attrs).forEach(attr => {
+         // v-xxx="abc"
+         const attrName = attr.name
+         const exp = attr.value
+         //当属性值以v-开头便认为这是一个指令 这里只处理v-text v-model v-html
+         if(attrName.startsWith('v-')) {
+           const dir = attrName.substring(2)
+           this[dir] && this[dir](node, exp)
+         //事件的处理也很是简单
+         } else if(attrName.startsWith('@')) {
+           const dir = attrName.substring(1)
+           this.eventFun(node, exp, dir)
+         }
+       })
      } else if(this.isInter(node)) {
        // 文本
        this.compileText(node)
      }
      // 在有子元素的状况下须要递归
      if(node.childNodes) {
        this.compile(node)
      }
    })
  }
+ //新添加函数 处理事件
  eventFun(node, exp, dir) {
    node.addEventListener(dir, this.$vm.$methods[exp].bind(this.$vm))
  }
  //node 为修改的元素 exp为获取到大括号内的值的key dir为这边自定义的要执行的操做
  update(node, exp, dir) {
    // 初始化
    const fn = this[dir + 'Update']
    fn && fn(node, this.$vm[exp])
    // 更新 在这里建立watcher 并将更新的函数传进去 这里的val就是watcher触发更新函数时传入的最新值
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val)
    })
  }
  textUpdate(node, val) {
     node.textContent = val
   }
+ //新添加函数 处理v-text
  text(node, exp) {
    this.update(node, exp, 'text')
  }
+ //新添加函数 处理v-html
  html(node, exp) {
    this.update(node, exp, 'html')
  }
+ //新添加函数
  htmlUpdate(node, val) {
    node.innerHTML = val
  }
  // 编译文本
  compileText(node) {
  -   //node.textContent = this.$vm[RegExp.$1]
  +   this.update(node, RegExp.$1, 'text')
  }
+ //新添加函数 处理v-model
  model(node, exp) {
    // console.log(node, exp);
    this.update(node, exp, 'model')
    node.addEventListener('input', (e) => {
      // console.log(e.target.value);
      // console.log(this);
      this.$vm[exp] = e.target.value
    })
  }
  // 是否插值表达式
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
}
复制代码

接下来修改html文件查看效果

<body>
  <div id="app"> <p @click="add">{{counter}}</p> <p>{{counter}}</p> <p>{{counter}}</p> <p>{{number}}</p> <p v-text="counter"></p> <p v-html="desc"></p> <p><input type="text" c-model="desc"></p> <p><input type="text" value="changeInput" @input="changeInput"></p> </div> </body> <script> const app = new Vue({ el: '#app', data: { counter: 1, number:10, desc: `<h1 style="color:red">hello CVue</h1>` }, methods: { add() { console.log('add',this); this.counter++ }, changeInput() { console.log('changeInput'); } } }) </script> 复制代码

到这里就编写结束了,页面也达到了预期的效果.

image.png

体验网址: codesandbox.io/s/dazzling-…

做为本身学习的笔记,代码中也有不少疏忽,借鉴别人的代码来实现,不过学到了就是本身的.

最后

点个赞再走吧!

相关文章
相关标签/搜索