前端那些事儿

03Vue源码实现

2020-07-05  本文已影响0人  LM林慕

Vue 源码实现

理解 Vue 的设计思想

image.png

MVVM 框架的三要素:数据响应式模板引擎渲染

数据响应式:监听数据变化并在视图中更新

模板引擎:提供描述视图的模板语法

渲染:如何将模板转换为 html

数据响应式原理

数据变更能够响应在视图中,就是数据响应式,Vue2 中利用 Object.defineProperty() 实现变更检测。

image.png

简单实现

const obj = {}

function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    get () {
      console.log(`get ${key}:${val}`)
      return val
    },
    set (newVal) {
      if (newVal !== val) {
        console.log(`set ${key}:${newVal}`)
        val = newVal
      }
    }
  })
}

defineReactive(obj, 'foo', 'foo')

obj.foo = 'test foo'

结合视图

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

<head></head>

<body>
  <div id="app"></div>
  <script>
    const obj = {}
    function defineReactive(obj, key, val) {
      Object.defineProperty(obj, key, {
        get() {
          console.log(`get ${key}:${val}`);
          return val
        },
        set(newVal) {
          if (newVal !== val) {
            val = newVal
            update()
          }
        }
      })
    }
    defineReactive(obj, 'foo', '')
    obj.foo = new Date().toLocaleTimeString()
    function update() {
      app.innerText = obj.foo
    }
    setInterval(() => {
      obj.foo = new Date().toLocaleTimeString()
    }, 1000);
  </script>
</body>

</html>

遍历需要响应化的对象

// 对象响应化:遍历每个key,定义getter、setter
function observe (obj) {
  if (typeof obj !== 'object' || obj == null) {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

const obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } }
observe(obj)
obj.foo = 'foooooooooooo'
obj.bar = 'barrrrrrrrrrr'
obj.baz.a = 10 // 嵌套对象no ok

当有嵌套的对象怎么办呢?

解决方法:

function defineReactive (obj, key, val) {
  observe(val)    // 递归 observer 方法处理嵌套对象
  Object.defineProperty(obj, key, {
    //...
  })
}

解决赋的值是对象的情况

obj.baz = {a:1}
obj.baz.a = 10 // no ok
set(newVal) {
    if (newVal !== val) {
        observe(newVal) // 新值是对象的情况
        notifyUpdate()

如果添加、删除了新属性无法检测

obj.dong='林慕'
obj.dong // 并没有 get 信息

测试

set(obj,'dong','林慕’)
obj.dong

写到现在,大家应该也发现了,Object.defineProperty() 不支持检测对象,如果修改对象的话需要 Vue.set 方法,即上面的 set 方法,当然,源码中的set 会有一些边界判断条件,当确定是对象时,执行 defineReactive 方法,将对象进行响应式绑定。

思考:Vue 数组的响应化是如何处理的呢?

Vue 在处理数组时将可以改变数组的7个方法进行了重写,分别是 push、pop、shift、unshift、splice、sort 和 reverse。

重写数组的实现方式如下:

const arrayProto = Array.prototype
// 先克隆一份数组原型
export const arrayMethods = Object.create(arrayProto)
// 七个改变数组的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

// 拦截变化方法并发出事件
methodsToPatch.forEach(function (method) {
  // 缓存原方法
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    // 执行原始方法
    const result = original.apply(this, args)
    // 额外通知变更,只有这7个方法有这个待遇
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 对新加入对象进行响应化处理
    if (inserted) ob.observerArray(inserted)
    // 通知改变
    ob.dep.notify()
    return result
  })
})

【注】:最后面的总结部分贴出的源码,未包含数组的响应式处理,如需添加,可查看数组的响应式处理有何特殊之处

Vue 中的数据响应化

目标代码

<!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>{{counter}}</p>
  </div>
  <script src="node_modules/vue/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        counter: 1
      },
    })
    setInterval(() => {
      app.counter++
    }, 1000);
  </script>
</body>

</html>

Vue 原理分析

初始化流程

image.png

响应式流程

image.png
  1. new Vue() 首先先执行初始化,对 data 执行响应化处理,这个过程发生在 Observer 中

  2. 同时对模板执行编译,找到其中动态绑定的数据,从 data 中获取并初始化视图,这个过程发生在 Compile 中

  3. 同时定义⼀个更新函数和 Watcher,将来对应数据变化时 Watcher 会调用更新函数

  4. 由于 data 的某个 key 在⼀个视图中可能出现多次,所以每个 key 都需要⼀个管家 Dep 来管理多个 Watcher

  5. 将来 data 中数据⼀旦发生变化,会首先找到对应的 Dep,通知所有 Watcher 执行更新函数

涉及类型介绍

KVue

框架构造函数:执行初始化

function observe (obj) {
  if (typeof obj !== 'object' || obj === null) {
    return
  }

  new Observer(obj)
}

function defineReactive (obj, key, val) { }

class KVue {
  constructor(options) {
    this.$options = options
    this.$data = options.data

    observe(this.$data)
  }
}

class Observer {
  constructor(value) {
    this.value = value
    this.walk(value)
  }
  walk (obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}
class KVue {
  constructor(options) {
    // ...
    proxy(this, '$data')
  }
}

function proxy (vm) {
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get () {
        return vm.$data[key]
      },
      set (newVal) {
        vm.$data[key] = newVal
      }
    })
  })
}

编译 —— Compile

image.png

初始化视图

根据节点类型编译,compile.js

class Compile {
  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
    Array.from(childNodes).forEach(node => {
      // 判断其类型
      if (this.isElement(node)) {
        console.log('编译元素:', node.nodeName)
      } else if (this.isInterpolation(node)) {
        console.log('编译插值文本:', node.textContent)
      }
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

  // 元素
  isElement (node) {
    return node.nodeType === 1
  }

  // 判断是否是插值表达式{{xxx}}
  isInterpolation (node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
}  

编译插值,compile.js

compile(el) {
  // ...
    } else if (this.isInerpolation(node)) {
      // console.log("编译插值⽂本" + node.textContent);
      this.compileText(node);
    }
  });
}
compileText(node) {
  console.log(RegExp.$1);
  node.textContent = this.$vm[RegExp.$1];
}

编译元素

compile(el) {
  //...
  if (this.isElement(node)) {
    // console.log("编译元素" + node.nodeName);
    this.compileElement(node)
  }
}
compileElement(node) {
  let nodeAttrs = node.attributes;
  Array.from(nodeAttrs).forEach(attr => {
    let attrName = attr.name;
    let exp = attr.value;
    if (this.isDirective(attrName)) {
      let dir = attrName.substring(2);
      this[dir] && this[dir](node, exp);
    }
  });
}
isDirective(attr) {
  return attr.indexOf("k-") == 0;
}
text(node, exp) {
  node.textContent = this.$vm[exp];
}

k-html

html(node, exp) {
    node.innerHTML = this.$vm[exp]
}

依赖收集

视图中会用到 data 中某 key,这称为依赖。同⼀个 key 可能出现多次,每次都需要收集出来用⼀个 Watcher 来维护它们,此过程称为依赖收集。多个 Watcher 需要⼀个 Dep 来管理,需要更新时由 Dep 统⼀通知。

看下面案例,理出思路:

new Vue({
  template:
    `<div>
      <p>{{name1}}</p>
      <p>{{name2}}</p>
      <p>{{name1}}</p>
    <div>`,
  data: {
    name1: 'name1',
    name2: 'name2'
  }
});
image.png

实现思路

  1. defineReactive 时为每⼀个 key 创建⼀个 Dep 实例

  2. 初始化视图时读取某个 key,例如 name1,创建⼀个 watcher1

  3. 由于触发 name1 的 getter 方法,便将 watcher1添加到 name1 对应的 Dep 中

  4. 当 name1 更新,setter 触发时,便可通过对应 Dep 通知其管理所有 Watcher 更新

image.png

创建 Watcher,kvue.js

const watchers = []  // 临时用于保存 watcher 测试用
// 监听器:负责更新视图
class Watcher {
  constructor(vm, key, updateFn) {
    // kvue 实例
    this.vm = vm;
    // 依赖 key
    this.key = key;
    // 更新函数
    this.updateFn = updateFn;
    // 临时放入 watchers 数组
    watchers.push(this)
  }
  // 更新
  update () {
    this.updateFn.call(this.vm, this.vm[this.key]);
  }
}

编写更新函数、创建 watcher

// 调 update 函数执插值文本赋值
compileText(node) {
  // console.log(RegExp.$1);
  // node.textContent = this.$vm[RegExp.$1];
  this.update(node, RegExp.$1, 'text')
}
text(node, exp) {
  this.update(node, exp, 'text')
}
html(node, exp) {
  this.update(node, exp, 'html')
}
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)
  })
}
textUpdater(node, val) {
  node.textContent = val;
}
htmlUpdater(node, val) {
  node.innerHTML = val
}

声明 Dep

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

创建 watcher 时触发 getter

class Watcher {
  constructor(vm, key, updateFn) {
    Dep.target = this;
    this.vm[this.key];
    Dep.target = null;
  }
}

依赖收集,创建 Dep 实例

defineReactive(obj, key, val) {
  this.observe(val);
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get () {
      Dep.target && dep.addDep(Dep.target);
      return val
    },
    set (newVal) {
      if (newVal === val) return
      dep.notify()
    }
  })
}

总结

以上是一个简单的 Vue 实现,此时 Watcher 监听的粒度太过于精细,导致 Watcher 过多,不需要 vdom。

后面的文章会写类似于 Vue2.0 的监听粒度问题,Vue2.0 的监听粒度会折中,每个组件一个 Watcher,当组件内部的值发生变化时,响应式系统已经知道是哪个组件发生了变化,然后在组件内部进性 diff 算法的操作,最后更新为最新的节点信息。

整体代码

html 部分:

<!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>example</title>
  <script src="./lvue.js" charset="utf-8"></script>
</head>

<body>
  <div id="app">
    <p>{{name}}</p>
    <p k-text="name"></p>
    <p>{{age}}</p>
    <p>
      {{doubleAge}}
    </p>
    <input type="text" k-model="name">
    <button @click="changeName">呵呵</button>
    <div k-html="html"></div>
  </div>
  <script src='./compile.js'></script>
  <script src='./lvue.js'></script>

  <script>
    let k = new KVue({
      el: '#app',
      data: {
        name: 'i am test',
        age: 12,
        html: '<button>这是一个按钮</button>'
      },
      created() {
        console.log('开始啦')
        setTimeout(() => {
          this.name = '我是蜗牛'
        }, 1600)
      },
      methods: {
        changeName() {
          this.name = 'changed name'
          this.age = 1
          this.id = 'xxx'
          console.log(1, this)
        }
      }
    })
  </script>
</body>

</html>

Watcher、Dep 部分

class KVue {
  constructor(options) {
    this.$options = options // 挂载实例
    this.$data = options.data  // 数据响应化
    // 监听拦截数据
    this.observe(this.$data)

    // // 模拟一个 watcher 创建
    // new Watcher()
    // this.$data.a
    // new Watcher()
    // this.$data.c.d
    // // 模拟结束
    new Compile(options.el, this)
    // created 执行
    if (options.created) {
      options.created.call(this)
    }
  }
  observe (value) {
    if (!value || typeof value !== 'object') {
      return
    }
    // 遍历该对象
    Object.keys(value).forEach(key => {
      this.defineReactive(value, key, value[key])
      // 代理 data 中的属性到 vue 实例上
      this.proxyData(key)
    })
  }
  // 数据响应化
  defineReactive (obj, key, val) {

    this.observe(val) // 递归解决数据的嵌套

    const dep = new Dep() // 每执行一次 defineReactive,就创建一个 Dep 实例

    Object.defineProperty(obj, key, { // 数据劫持
      configurable: true,
      enumerable: true,
      get () {
        Dep.target && dep.addDep(Dep.target)
        return val
      },
      set (newVal) {
        if (newVal === val) {
          return
        }
        val = newVal
        console.log(`${key}属性更新了:${val}`)
        dep.notify()
      }
    })
  }
  proxyData (key) {
    Object.defineProperty(this, key, {
      configurable: true,
      enumerable: true,
      get () {
        return this.$data[key]
      },
      set (newVal) {
        this.$data[key] = newVal
      }
    })
  }
}
// Dep:用来管理 Watcher
class Dep {
  constructor() {
    this.deps = [] // 这里存放若干依赖(watcher),一个依赖对应一个属性,依赖就是视图上的引用
  }
  addDep (dep) {
    this.deps.push(dep)
  }
  notify () {
    this.deps.forEach(dep => dep.update())
  }
}
// Watcher:小秘书,界面中的一个依赖对应一个小秘书
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb
    // 将当前 Watcher 实例指定到 Dep 静态属性 target
    Dep.target = this
    this.vm[this.key]  // 触发 getter、添加依赖
    Dep.target = null
  }
  update () {
    console.log('Watcher监听的属性更新了')
    this.cb.call(this.vm, this.vm[this.key])
  }
}

Compile 部分

// 用法 new Compile(el,vm)
class Compile {
  constructor(el, vm) {
    // 要遍历的宿主节点
    this.$el = document.querySelector(el)
    this.$vm = vm
    // 开始编译
    if (this.$el) {
      // 转换内部内容为片段 Fragment
      this.$fragment = this.node2Fragment(this.$el)
      // 执行编译
      this.compile(this.$fragment)
      // 将编译完的 html 结果追加至 $el
      this.$el.appendChild(this.$fragment)
    }
  }
  // 将宿主元素中代码片段拿出来遍历,这样做比较高效
  node2Fragment (el) {
    const frag = document.createDocumentFragment()
    // 将 el 中所有子元素搬家至 frag 中
    let child
    while (child = el.firstChild) {
      frag.appendChild(child)
    }
    return frag
  }
  // 编译过程
  compile (el) {
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      // 类型判断
      if (this.isElement(node)) {
        // 元素
        console.log('编译元素' + node.nodeName)
        // 查找k-/@/:
        const nodeAttrs = node.attributes
        Array.from(nodeAttrs).forEach((attr) => {
          const attrName = attr.name // 属性名
          const exp = attr.value  // 属性值
          if (this.isDirective(attrName)) {
            // k-text
            const dir = attrName.substring(2)
            // 执行指令
            this[dir] && this[dir](node, this.$vm, exp)
          }
          if (this.isEvent(attrName)) {
            const dir = attrName.substring(1) // @click
            this.eventHandler(node, this.$vm, exp, dir)
          }
        })
      } else if (this.isInterpolation(node)) {
        // 插值文本
        console.log('编译文本' + node.textContent)
        this.compileText(node)
      }
      // 递归子节点
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }
  compileText (node) {
    console.log(RegExp.$1)
    // node.textContent = this.$vm.$data[RegExp.$1]
    this.update(node, this.$vm, RegExp.$1, 'text')
  }
  // 更新函数
  update (node, vm, exp, dir) {
    const updaterFn = this[dir + 'Updater']
    // 初始化
    updaterFn && updaterFn(node, vm[exp])
    // 依赖收集
    new Watcher(vm, exp, function (value) {
      updaterFn && updaterFn(node, value)
    })
  }
  html (node, vm, exp) {
    this.update(node, vm, exp, 'html')
  }
  htmlUpdater (node, value) {
    node.innerHTML = value
  }
  text (node, vm, exp) {
    this.update(node, vm, exp, 'text')
  }
  // 双绑
  model (node, vm, exp) {
    // 指定 input 的 value 属性
    this.update(node, vm, exp, 'model')
    // 视图对模型响应
    node.addEventListener('input', e => {
      vm[exp] = e.target.value
    })
  }
  modelUpdater (node, value) {
    node.value = value
  }
  textUpdater (node, value) {
    node.textContent = value
  }
  // 事件处理器
  eventHandler (node, vm, exp, dir) {
    let fn = vm.$options.methods && vm.$options.methods[exp]
    if (dir && fn) {
      node.addEventListener(dir, fn.bind(vm))
    }
  }
  isDirective (attr) {
    return attr.indexOf('k-') === 0
  }
  isEvent (attr) {
    return attr.indexOf('@') === 0
  }
  isElement (node) {
    return node.nodeType === 1
  }
  // 插值文本
  isInterpolation (node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
}
上一篇下一篇

猜你喜欢

热点阅读