「手写实现」mini-vue1

548 阅读3分钟

实现一个简单的 Vue.js。用于理解 Vue响应式原理。这里不做Virtual DOM 、render部分,选择直接操作DOM。

  1. Vue负责把data的数据注入到Vue实例中,并调用ObserverCompiler
  2. Observer负责进行数据的响应化处理,核心就是使用了Object.defineProperty实现的。
  3. Compiler负责解析指令和插值表达式。
  4. Dep负责收集依赖,添加观察者。通知data对应的所有观察者Watcher来更新视图。在Observer类的每一个data转换get和set时,会创建一个Dep实例,用来负责收集依赖并发送通知。在每一个data中在get中收集依赖,在set中通知Watcher实例视图。
  5. Watcher类负责数据更新后,使关联视图重新渲染。

MVVM模型

MVVM图谱.png

Vue的设计思想

vue原理图谱.png

Vue的数据响应式

function definReactive(obj, key, val) {
  // 对象递归处理
  observer(val)
​
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}`)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log(`set ${newVal}`)
        observer(val)
        val = newVal
      }
    },
  })
}
​
function observer(obj) {
  // 判断是否是对象
  if (typeof obj !== 'object' || obj === null) return
​
  Object.keys(obj).forEach((key) => {
    definReactive(obj, key, obj[key])
  })
}
​
const obj = {
  name: 'name',
  a: 'a',
  b: {
    c: 'c',
  },
}
observer(obj)
// Object.defineProperty中不能劫持动态添加和删除的属性
function set(obj, key, val) {
  definReactive(obj, key, val)
}

Vue实现

  1. new Vue的实现

     function definReactive(obj, key, val) {
       // 对象递归处理
       observer(val)
     ​
       const dep = new Dep()
     ​
       Object.defineProperty(obj, key, {
         get() {
           // 有Watcher就收集依赖
           Dep.target && dep.addDep(Dep.target)
           console.log(`get ${key}`)
           return val
         },
         set(newVal) {
           if (newVal !== val) {
             console.log(`set ${newVal}`)
             observer(val)
             val = newVal
     ​
             // 修改执行通知更新
             dep.notify()
           }
         },
       })
     }
     ​
     // 响应式
     function observer(obj) {
       // 判断是否是对象
       if (typeof obj !== 'object' || obj === null) return
     ​
       new Observer(obj)
     }
     ​
     // 代理数据
     function proxy(data, vm) {
       Object.keys(data).forEach((key) => {
         Object.defineProperty(vm, key, {
           get() {
             return vm.$data[key]
           },
           set(newVal) {
             vm.$data[key] = newVal
           },
         })
       })
     }
     ​
     class Vue {
       constructor(options) {
         this.$options = options
         this.$el = options.el
         this.$data = options.data
     ​
         // 1. 将data数据进行响应式数据
         observer(this.$data)
     ​
         // 1.1 数据代理,将响应式数据data代理到Vue实例上可以使外部直接访问
         proxy(this.$data, this)
     ​
         // 2. 编译
         new Compiler(this.$el, this)
       }
     }
    
  2. Compiler的实现

    Vue1.0编译图谱.png
    // 实现编译
    class Compiler {
     constructor(el, vm) {
       this.$vm = vm
       this.$el = document.querySelector(el)
    ​
       if (this.$el) {
         this.compile(this.$el)
       }
     }
    ​
     compile(el) {
       const childNodes = el.childNodes
       childNodes.forEach((node) => {
         if (this.isElement(node)) {
           // 元素节点编译
           if (node.childNodes.length > 0) {
             this.compile(node)
           }
           this.compileElement(node)
         } else if (this.isInertText(node)) {
           // 文本节点编译
           this.compileText(node)
         }
       })
     }
    ​
     // 判断节点是不是元素节点
     isElement(node) {
       return node.nodeType === 1
     }
    ​
     // 判断节点是不是文本节点并且是插值文本{{}}
     isInertText(node) {
       return node.nodeType === 3 && /{{(.*)}}/.test(node.textContent)
     }
    ​
     // 判断是不是h-开头的指令,测试代码使用h-
     isDir(attr) {
       return attr.startsWith('h-')
     }
    ​
     // 判断是不是@开头的方法
     isEvent(attr) {
       return attr.startsWith('@')
     }
    ​
     // 编译文本
     compileText(node) {
       // node.textContent = this.$vm[RegExp.$1]
       this.update(node, RegExp.$1, 'text')
     }
    ​
     // 编译元素
     compileElement(node) {
       const attributes = node.attributes
       Array.from(attributes).forEach((attr) => {
         // h-text="count"
         const attrName = attr.name // h-text
         const exp = attr.value // count
    ​
         if (this.isDir(attrName)) {
           const dir = attrName.substring(2)
           this[dir] && this[dir](node, exp)
         } else if (this.isEvent(attrName)) {
           const dir = attrName.substring(1)
           this.eventHandler(node, exp, dir)
         }
       })
     }
    ​
     // 方法
     eventHandler(node, exp, dir) {
       const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
       node.addEventListener(dir, fn.bind(this.$vm))
     }
    ​
     text(node, exp) {
       // node.textContent = this.$vm[exp]
       this.update(node, exp, 'text')
     }
    ​
     textUpdater(node, val) {
       node.textContent = val
     }
    ​
     html(node, exp) {
       // node.innerHTML = this.$vm[exp]
       this.update(node, exp, 'html')
     }
    ​
     htmlUpdater(node, val) {
       node.innerHTML = val
     }
    ​
     model(node, exp) {
       this.update(node, exp, 'model')
    ​
       node.addEventListener('input', (e) => {
         this.$vm[exp] = e.target.value
       })
     }
    ​
     modelUpdater(node, val) {
       node.value = val
     }
    ​
     // 一个key对应一个Watcher实例
     update(node, exp, dir) {
       const fn = this[dir + 'Updater']
       fn && fn(node, this.$vm[exp])
       new Watcher(this.$vm, exp, function(val) {
         fn && fn(node, val)
       })
     }
    }
    
  3. Observer的实现

     // 实现响应式
     class Observer {
       constructor(value) {
         this.value = value
         this.walk(this.value)
       }
     ​
       walk(obj) {
         Object.keys(obj).forEach((key) => {
           definReactive(obj, key, obj[key])
         })
       }
     }
    
  4. Watcher的实现

    // 实现Watcher监听
    class Watcher {
     constructor(vm, key, updateFn) {
       this.vm = vm
       this.key = key
       this.updateFn = updateFn
    ​
       // 创建watcher时执行getter
       Dep.target = this
       this.vm[this.key]
       Dep.target = null
     }
    ​
     update() {
       this.updateFn.call(this.vm, this.vm[this.key])
     }
    }
    
  5. Dep实现

    // 实现Dep,多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。
    class Dep {
     constructor() {
       this.deps = []
     }
    ​
     addDep(dep) {
       this.deps.push(dep)
     }
    ​
     notify() {
       this.deps.forEach((dep) => dep.update())
     }
    }
    
  6. 兼容数组响应式实现

    // 数组的响应式处理
    // 1. 替换数组中的方法
    const orginalProto = Array.prototype
    // 拷贝一份,进行修改
    const arrayProto = Object.create(orginalProto)
    // 举例常用的4个方法
    ;[('push', 'pop', 'shift', 'unshift')].forEach((method) => {
      arrayProto[method] = function() {
       // 执行原始操作
        orginalProto[method].apply(this, arguments)
        const dep = new Dep()
    ​
        // 执行新的操作更新
        dep.notify()
      }
    })
    ​
    // 实现响应式
    class Observer {
      constructor(value) {
        this.value = value
    ​
        if (typeof value === 'object') {
          this.walk(this.value)
        } else if (Array.isArray(value)) {
          this.arrayWalk(value)
        }
      }
    ​
      // 数组执行方法
      arrayWalk(obj) {
        obj.__proto__ = arrayProto
        const keys = Object.keys()
        for (let i = 0; i < keys.length; i++) {
          observer(obj[i])
        }
      }
    ​
      walk(obj) {
        Object.keys(obj).forEach((key) => {
          definReactive(obj, key, obj[key])
        })
      }
    }
    

最终实现

function definReactive(obj, key, val) {
  // 对象递归处理
  observer(val)
​
  const dep = new Dep()
​
  Object.defineProperty(obj, key, {
    get() {
      // 有Watcher就收集依赖
      Dep.target && dep.addDep(Dep.target)
      console.log(`get ${key}`)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log(`set ${newVal}`)
        observer(val)
        val = newVal
​
        // 修改执行通知更新
        dep.notify()
      }
    },
  })
}
​
// 数组的响应式处理
// 1. 替换数组中的方法
const orginalProto = Array.prototype
// 拷贝一份,进行修改
const arrayProto = Object.create(orginalProto)
// 举例常用的4个方法
;[('push', 'pop', 'shift', 'unshift')].forEach((method) => {
  arrayProto[method] = function() {
    // 执行原始操作
    orginalProto[method].apply(this, arguments)
    const dep = new Dep()
​
    // 执行新的操作更新
    dep.notify()
  }
})
​
function observer(obj) {
  // 判断是否是对象
  if (typeof obj !== 'object' || obj === null) return
​
  new Observer(obj)
}
​
function proxy(data, vm) {
  Object.keys(data).forEach((key) => {
    Object.defineProperty(vm, key, {
      get() {
        return vm.$data[key]
      },
      set(newVal) {
        vm.$data[key] = newVal
      },
    })
  })
}
​
class Vue {
  constructor(options) {
    this.$options = options
    this.$el = options.el
    this.$data = options.data
​
    // 1. 将data数据进行响应式数据
    observer(this.$data)
​
    // 1.1 数据代理,将响应式数据data代理到Vue实例上可以使外部直接访问
    proxy(this.$data, this)
​
    // 2. 编译
    new Compiler(this.$el, this)
  }
}
​
// 实现编译
class Compiler {
  constructor(el, vm) {
    this.$vm = vm
    this.$el = document.querySelector(el)
​
    if (this.$el) {
      this.compile(this.$el)
    }
  }
​
  compile(el) {
    const childNodes = el.childNodes
    childNodes.forEach((node) => {
      if (this.isElement(node)) {
        // 元素节点编译
        if (node.childNodes.length > 0) {
          this.compile(node)
        }
        this.compileElement(node)
      } else if (this.isInertText(node)) {
        // 文本节点编译
        this.compileText(node)
      }
    })
  }
​
  // 判断节点是不是元素节点
  isElement(node) {
    return node.nodeType === 1
  }
​
  // 判断节点是不是文本节点并且是插值文本{{}}
  isInertText(node) {
    return node.nodeType === 3 && /{{(.*)}}/.test(node.textContent)
  }
​
  // 判断是不是h-开头的指令
  isDir(attr) {
    return attr.startsWith('h-')
  }
​
  // 判断是不是@开头的方法
  isEvent(attr) {
    return attr.startsWith('@')
  }
​
  // 编译文本
  compileText(node) {
    // node.textContent = this.$vm[RegExp.$1]
    this.update(node, RegExp.$1, 'text')
  }
​
  // 编译元素
  compileElement(node) {
    const attributes = node.attributes
    Array.from(attributes).forEach((attr) => {
      // h-text="count"
      const attrName = attr.name // h-text
      const exp = attr.value // count
​
      if (this.isDir(attrName)) {
        const dir = attrName.substring(2)
        this[dir] && this[dir](node, exp)
      } else if (this.isEvent(attrName)) {
        const dir = attrName.substring(1)
        this.eventHandler(node, exp, dir)
      }
    })
  }
​
  // 方法
  eventHandler(node, exp, dir) {
    const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
    node.addEventListener(dir, fn.bind(this.$vm))
  }
​
  text(node, exp) {
    // node.textContent = this.$vm[exp]
    this.update(node, exp, 'text')
  }
​
  textUpdater(node, val) {
    node.textContent = val
  }
​
  html(node, exp) {
    // node.innerHTML = this.$vm[exp]
    this.update(node, exp, 'html')
  }
​
  htmlUpdater(node, val) {
    node.innerHTML = val
  }
​
  model(node, exp) {
    this.update(node, exp, 'model')
​
    node.addEventListener('input', (e) => {
      this.$vm[exp] = e.target.value
    })
  }
​
  modelUpdater(node, val) {
    node.value = val
  }
​
  // 一个key对应一个Watcher实例
  update(node, exp, dir) {
    const fn = this[dir + 'Updater']
    fn && fn(node, this.$vm[exp])
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val)
    })
  }
}
​
// 实现Dep,多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。
class Dep {
  constructor() {
    this.deps = []
  }
​
  addDep(dep) {
    this.deps.push(dep)
  }
​
  notify() {
    this.deps.forEach((dep) => dep.update())
  }
}
​
// 实现Watcher监听
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm
    this.key = key
    this.updateFn = updateFn
​
    // 创建watcher时执行getter
    Dep.target = this
    this.vm[this.key]
    Dep.target = null
  }
​
  update() {
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}
​
// 实现响应式
class Observer {
  constructor(value) {
    this.value = value
​
    if (typeof value === 'object') {
      this.walk(this.value)
    } else if (Array.isArray(value)) {
      this.arrayWalk(value)
    }
  }
​
  // 数组执行方法
  arrayWalk(obj) {
    obj.__proto__ = arrayProto
    const keys = Object.keys()
    for (let i = 0; i < keys.length; i++) {
      observer(obj[i])
    }
  }
​
  walk(obj) {
    Object.keys(obj).forEach((key) => {
      definReactive(obj, key, obj[key])
    })
  }
}

结语

  1. 响应式其实是使用了Object.defineProperty进行数据的劫持,使用Observer进行响应式处理。
  2. 数组遍历每一个值,都让其变化响应式。
  3. 获取节点,判断是元素还是插值文本,文本直接渲染,元素遍历属性做属性处理
  4. 在数据变化的时候进行订阅并执行对应的更新函数重新渲染。一个key就是一个watcher,
  5. 由于一个key是可以多次使用,建立Dep,一个key只有一个Dep但是可以有多个watcher, Dep中管理多个watcher,在订阅的时候添加,并统一执行更新,做到精确更新。