Vue1.x of handwritten Vue series

When learning becomes a habit, knowledge becomes common sense. Thank you for your attention , likes , favorites and comments .

New videos and articles will be sent on the WeChat public account as soon as possible, welcome to follow: Li Yongning lyn

The article has been included in the github repository liyongning/blog . Welcome to Watch and Star.

cover

foreword

We used 12 articles to explain the Vue2 framework source code in detail. Next, we will start to write the Vue series, write our own Vue framework, implement the core functions of Vue with the simplest code, and further understand the core principles of Vue.

Why Handwriting Frames

Some people will have questions: I have read the framework source code in detail, even more than two or three times, is this not enough? I think I am already familiar with the source code of the framework, and I don't think it is necessary to write it by hand.

Is it necessary to write the framework by hand ? This is the same as the answer to Is it necessary to read the source code of the framework. See what your starting point is.

Read the source code

If you have a learning attitude, it goes without saying that it is necessary to read the source code of the framework.

Everyone knows that in normal business development, the level of the people around you may be similar to you, so you basically don’t see too many excellent codes and ideas in the business.

And there are many excellent designs and best practices contained in a framework, and there are too many places that make you realize and amaze you when reading. Even if you feel that your current rank is not enough, you may not see so much, but the influence of source code on you is subtle. The more you see excellent code, the more you will unconsciously apply these excellent coding methods you have learned in your usual coding. What's more, most of Vue's code is written by Youda himself, and the quality of the code is beyond doubt.

handwritten frame

As for the handwritten framework is it necessary ? As long as you read the framework source code, you must write one by yourself. The reason is very simple. The purpose of reading the source code of the framework is to learn. You say that you are very familiar with the source code. You say that you have learned it. How to test it? The way of inspection is also very simple, export what you have learned to the outside, in three stages:

  1. Write technical blogs, draw mind maps (master 30%)

  2. Share it with others, such as sharing within a group or recording a video (master 60%)

  3. Handwriting frameworks, building wheels is the best way to test your learning results (master 90%)

Did you find that the first two stages were all talking about other people's things. You said that you learned it. Indeed, you can export it to the outside world. You must have learned it, but how much did you learn? I think it's about 60%, for example:

Someone asks you what is the responsive principle of Vue? After the output of the first two stages, you may say the truth, such as Object.defineProperty, getter, setter, dependency collection, dependency notification watcher update and so on. But can you write about the whole process? If you write it for the first time, it will probably not work. When you implement it, you will find that it is not as simple as you said, and there are far more things to consider than what you said. If you don't believe me, you can try it out and check it out.

You must know that the process of building a wheel is actually the process of your application. Only when you write it out can you really learn it. If you only read but not write, it can basically be regarded as an advanced version of only reading but not practicing .

Therefore, to test whether you really learn and deeply understand the implementation principle of a framework, imitating the wheel is the best way to test.

Handwritten Vue1.x

Before starting, let's do the preparatory work first. In our own working directory, create a new source code directory, such as:

mkdir lyn-vue && cd lyn-vue

I don't want to install and configure the packaging tool additionally here. It's too troublesome. The ESM method natively supported by modern browsers is adopted, so everyone needs to install a serve locally and start a server. Vite is this principle, but its server is implemented by itself, because it needs to do corresponding processing for different resources of import, such as parsing whether the import request is node_modules or the user's own module, or the translation of TS modules, etc. Wait.

npm i serve -g

After installation, execute the command in the lyn-vuedirectory serve, a server will be started locally, and then it will enter the coding stage.

Target

The following sample code is the goal of today, using our own handwritten Vue framework to run this example.

We need to implement the following capabilities:

  • Data responsive interception

    • Original value

    • normal object

    • array

  • Data responsive update

    • Dependency collection, Dep

    • Rely on Notification Watcher Updates

    • compiler, compiler

  • methods + events + data reactive update

  • v-bind directive

  • v-model two-way binding

    • input input box

    • checkbox

    • select

/vue1.0.html

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

<head>
  <meta charset="UTF-8">
  <title>Lyn Vue1.0</title>
</head>

<body>
  <div id="app">
    <h3>数据响应式更新 原理</h3>
    <div>{{ t }}</div>
    <div>{{ t1 }}</div>
    <div>{{ arr }}</div>
    <h3>methods + 事件 + 数据响应式更新 原理</h3>
    <div>
      <p>{{ counter }}</p>
      <button v-on:click="handleAdd"> Add </button>
      <button v-on:click="handleMinus"> Minus </button>
    </div>
    <h3>v-bind</h3>
    <span v-bind:title="title">右键审查元素查看我的 title 属性</span>
    <h3>v-model 原理</h3>
    <div>
      <input type="text" v-model="inputVal" />
      <div>{{ inputVal }}</div>
    </div>
    <div>
      <input type="checkbox" v-model="isChecked">
      <div>{{ isChecked }}</div>
    </div>
    <div>
      <select v-model="selectValue">
        <option value="1">1</option>
        <option value="2">2</option>
        <option value="3">3</option>
      </select>
      <div>{{ selectValue }}</div>
    </div>
  </div>
  <script type="module">
    import Vue from './src/index.js'
    const ins = new Vue({
      el: '#app',
      data() {
        return {
          // 原始值和对象的响应式原理
          t: 't value',
          t1: {
            tt1: 'tt1 value'
          },
          // 数组的响应式原理
          arr: [1, 2, 3],
          // 响应式更新
          counter: 0,
          // v-bind
          title: '看我',
          // v-model
          inputVal: 'test',
          isChecked: true,
          selectValue: 2
        }
      },
      // methods + 事件 + 数据响应式更新 原理
      methods: {
        handleAdd() {
          this.counter++
        },
        handleMinus() {
          this.counter--
        }
      },
    })
    // 数据响应式拦截
    setTimeout(() => {
      console.log('********** 属性值为原始值时的 getter、setter ************')
      console.log(ins.t)
      ins.t = 'change t value'
      console.log(ins.t)
    }, 1000)

    setTimeout(() => {
      console.log('********** 属性的新值为对象的情况 ************')
      ins.t = {
        tt: 'tt value'
      }
      console.log(ins.t.tt)
    }, 2000)

    setTimeout(() => {
      console.log('********** 验证对深层属性的 getter、setter 拦截 ************')
      ins.t1.tt1 = 'change tt1 value'
      console.log(ins.t1.tt1)
    }, 3000)

    setTimeout(() => {
      console.log('********** 将值为对象的属性更新为原始值 ************')
      console.log(ins.t1)
      ins.t1 = 't1 value'
      console.log(ins.t1)
    }, 4000)

    setTimeout(() => {
      console.log('********** 数组操作方法的拦截 ************')
      console.log(ins.arr)
      ins.arr.push(4)
      console.log(ins.arr)
    }, 5000)
  </script>
</body>

</html>

Data responsive interception

Vue constructor

/src/index.js

/**
 * Vue 构造函数
 * @param {*} options new Vue(options) 时传递的配置对象
 */
export default function Vue(options) {
  this._init(options)
}

this._init

/src/index.js

/**
 * 初始化配置对象
 * @param {*} options 
 */
Vue.prototype._init = function (options) {
  // 将 options 配置挂载到 Vue 实例上
  this.$options = options
  // 初始化 options.data
  // 代理 data 对象上的各个属性到 Vue 实例
  // 给 data 对象上的各个属性设置响应式能力
  initData(this)
}

initData

/src/initData.js

/**
 * 1、初始化 options.data
 * 2、代理 data 对象上的各个属性到 Vue 实例
 * 3、给 data 对象上的各个属性设置响应式能力
 * @param {*} vm 
 */
export default function initData(vm) {
  // 获取 data 选项
  let { data } = vm.$options
  // 设置 vm._data 选项,保证它的值肯定是一个对象
  if (!data) {
    vm._data = {}
  } else {
    vm._data = typeof data === 'function' ? data() : data
  }
  // 代理,将 data 对象上的的各个属性代理到 Vue 实例上,支持 通过 this.xx 的方式访问
  for (let key in vm._data) {
    proxy(vm, '_data', key)
  }
  // 设置响应式
  observe(vm._data)
}

proxy

/src/utils.js

/**
 * 将 key 代理到 target 上,
 * 比如 代理 this._data.xx 为 this.xx
 * @param {*} target 目标对象,比如 vm
 * @param {*} sourceKey 原始 key,比如 _data
 * @param {*} key 代理的原始对象上的指定属性,比如 _data.xx
 */
export function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    // target.key 的读取操作实际上返回的是 target.sourceKey.key
    get() {
      return target[sourceKey][key]
    },
    // target.key 的赋值操作实际上是 target.sourceKey.key = newV
    set(newV) {
      target[sourceKey][key] = newV
    }
  })
}

observe

/src/observe.js

/**
 * 通过 Observer 类为对象设置响应式能力
 * @returns Observer 实例
 */
export default function observe(value) {
  // 避免无限递归
  // 当 value 不是对象直接结束递归
  if (typeof value !== 'object') return

  // value.__ob__ 是 Observer 实例
  // 如果 value.__ob__ 属性已经存在,说明 value 对象已经具备响应式能力,直接返回已有的响应式对象
  if (value.__ob__) return value.__ob__

  // 返回 Observer 实例
  return new Observer(value)
}

Observer

/src/observer.js

/**
 * 为普通对象或者数组设置响应式的入口 
 */
export default function Observer(value) {
  // 为对象设置 __ob__ 属性,值为 this,标识当前对象已经是一个响应式对象了
  Object.defineProperty(value, '__ob__', {
    value: this,
    // 设置为 false,禁止被枚举,
    // 1、可以在递归设置数据响应式的时候跳过 __ob__ 
    // 2、将响应式对象字符串化时也不限显示 __ob__ 对象
    enumerable: false,
    writable: true,
    configurable: true
  })

  if (Array.isArray(value)) {
    // 数组响应式
    protoArgument(value)
    this.observeArray(value)
  } else {
    // 对象响应式
    this.walk(value)
  }
}

/**
 * 遍历对象的每个属性,为这些属性设置 getter、setter 拦截
 */
Observer.prototype.walk = function (obj) {
  for (let key in obj) {
    defineReactive(obj, key, obj[key])
  }
}

// 遍历数组的每个元素,为每个元素设置响应式
// 其实这里是为了处理元素为对象的情况,以达到 this.arr[idx].xx 是响应式的目的
Observer.prototype.observeArray = function (arr) {
  for (let item of arr) {
    observe(item)
  }
}

defineReactive

/src/defineReactive.js

/**
 * 通过 Object.defineProperty 为 obj.key 设置 getter、setter 拦截
 */
export default function defineReactive(obj, key, val) {
  // 递归调用 observe,处理 val 仍然为对象的情况
  observe(val)

  Object.defineProperty(obj, key, {
    // 当发现 obj.key 的读取行为时,会被 get 拦截
    get() {
      console.log(`getter: key = ${key}`)
      return val
    },
    // 当发生 obj.key = xx 的赋值行为时,会被 set 拦截
    set(newV) {
      console.log(`setter: ${key} = ${newV}`)
      if (newV === val) return
      val = newV
      // 对新值进行响应式处理,这里针对的是新值为非原始值的情况,比如 val 为对象、数组
      observe(val)
    }
  })
}

protoArgument

/src/protoArgument.js

/**
 * 通过拦截数组的七个方法来实现
 */

// 数组默认原型对象
const arrayProto = Array.prototype
// 以数组默认原型对象为原型创建一个新的对象
const arrayMethods = Object.create(arrayProto)
// 被 patch 的七个方法,通过拦截这七个方法来实现数组响应式
// 为什么是这七个方法?因为只有这七个方法是能更改数组本身的,像 cancat 这些方法都是会返回一个新的数组,不会改动数组本身
const methodsToPatch = ['push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse']

// 遍历 methodsToPatch
methodsToPatch.forEach(method => {
  // 拦截数组的七个方法,先完成本职工作,再额外完成响应式的工作
  Object.defineProperty(arrayMethods, method, {
    value: function(...args) {
      // 完成方法的本职工作,比如 this.arr.push(xx)
      const ret = arrayProto[method].apply(this, args)
      // 将来接着实现响应式相关的能力
      console.log('array reactive')
      return ret
    },
    configurable: true,
    writable: true,
    enumerable: true
  })
})

/**
 * 覆盖数组(arr)的原型对象
 * @param {*} arr 
 */
export default function protoArgument(arr) {
  arr.__proto__ = arrayMethods
}

Effect

If the following effects can be achieved, it means that the data responsive interception function is completed. That is, the code in the "Data responsive interception" section of the sample code in the target (the last bunch of setTimeout).

Video address: https://gitee.com/liyongning/typora-image-bed/raw/master/202203092000920.image

Responsive Principle.gif

Data responsive update

Now that the acquisition and update of data can be intercepted, then some "capabilities" can be added to the intercepted data to complete the function of data responsive update.

These added capabilities are actually things that everyone is familiar with: dependency collection is performed in the getter, and the setter is dependent on notifying the watcher to update.

All attributes (keys) of responsive data objects in Vue1.x and dep have a one-to-one correspondence, one key corresponds to one dep; a watcher will be generated every time the responsive data is referenced in the page, so in Vue1.0 dep and watcher have a one-to-many relationship.

Dependency collection

Dep

/src/dep.js

/**
 * Dep
 * Vue1.0 中 key 和 Dep 是一一对应关系,举例来说:
 * new Vue({
 *   data() {
 *     return {
 *       t1: xx,
 *       t2: {
 *         tt2: xx
 *       },
 *       arr: [1, 2, 3, { t3: xx }]
 *     }
 *   }
 * })
 * data 函数 return 回来的对象是一个 dep
 * 对象中的 key => t1、t2、tt2、arr、t3 都分别对应一个 dep
 */
export default function Dep() {
  // 存储当前 dep 实例收集的所有 watcher
  this.watchers = []
}

// Dep.target 是一个静态属性,值为 null 或者 watcher 实例
// 在实例化 Watcher 时进行赋值,待依赖收集完成后在 Watcher 中又重新赋值为 null
Dep.target = null

/**
 * 收集 watcher
 * 在发生读取操作时(vm.xx) && 并且 Dep.target 不为 null 时进行依赖收集
 */
Dep.prototype.depend = function () {
  // 防止 Watcher 实例被重复收集
  if (this.watchers.includes(Dep.target)) return
  // 收集 Watcher 实例
  this.watchers.push(Dep.target)
}

/**
 * dep 通知自己收集的所有 watcher 执行更新函数
 */
Dep.prototype.notify = function () {
  for (let watcher of this.watchers) {
    watcher.update()
  }
}

Watcher

/src/watcher.js

import Dep from "./dep.js"

/**
 * @param {*} cb 回调函数,负责更新 DOM 的回调函数
 */
export default function Watcher(cb) {
  // 备份 cb 函数
  this._cb = cb
  // 赋值 Dep.target
  Dep.target = this
  // 执行 cb 函数,cb 函数中会发生 vm.xx 的属性读取,进行依赖收集
  cb()
  // 依赖收集完成,Dep.target 重新赋值为 null,防止重复收集
  Dep.target = null
}

/**
 * 响应式数据更新时,dep 通知 watcher 执行 update 方法,
 * 让 update 方法执行 this._cb 函数更新 DOM
 */
Watcher.prototype.update = function () {
  this._cb()
}

Observer

Modify the Observer constructor to set a dep instance on the value.ob object. This dep is the dep of the object itself, which is convenient to use when updating the object itself. For example, it will be used when the array depends on the notification update.

/src/observer.js

/**
 * 为普通对象或者数组设置响应式的入口
 */
export default function Observer(value) {
  // 为对象本身设置一个 dep,方便在更新对象本身时使用,比如 数组通知依赖更新时就会用到
  this.dep = new Dep()  
  // ... 省略已有内容
}

defineReactive

Modify the defineReactive method and add code for dependency collection and dependency notification update

/src/defineReactive.js

/**
 * 通过 Object.defineProperty 为 obj.key 设置 getter、setter 拦截
 * getter 时收集依赖
 * setter 时依赖通过 watcher 更新
 */
export default function defineReactive(obj, key, val) {
  // 递归调用 observe,处理 val 仍然为对象的情况
  const childOb = observe(val)

  const dep = new Dep()

  Object.defineProperty(obj, key, {
    // 当发现 obj.key 的读取行为时,会被 get 拦截
    get() {
      // 读取数据时 && Dep.target 不为 null,则进行依赖收集
      if (Dep.target) {
        dep.depend()
        // 如果存在子 ob,则顺道一块儿完成依赖收集
        if (childOb) {
          childOb.dep.depend()
        }
      }
      console.log(`getter: key = ${key}`)
      return val
    },
    // 当发生 obj.key = xx 的赋值行为时,会被 set 拦截
    set(newV) {
      console.log(`setter: ${key} = ${newV}`)
      if (newV === val) return
      val = newV
      // 对新值进行响应式处理,这里针对的是新值为非原始值的情况,比如 val 为对象、数组
      observe(val)
      // 数据更新,让 dep 通知自己收集的所有 watcher 执行 update 方法
      dep.notify()
    }
  })
}

protoArgument

The patch patch of seven array methods is modified. When an element is added to the array, the new element is responsively processed and updated depending on the notification.

/src/protoArgument.js

/**
 * 通过拦截数组的七个方法来实现
 */

// 数组默认原型对象
const arrayProto = Array.prototype
// 以数组默认原型对象为原型创建一个新的对象
const arrayMethods = Object.create(arrayProto)
// 被 patch 的七个方法,通过拦截这七个方法来实现数组响应式
// 为什么是这七个方法?因为只有这七个方法是能更改数组本身的,像 cancat 这些方法都是会返回一个新的数组,不会改动数组本身
const methodsToPatch = ['push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse']

// 遍历 methodsToPatch
methodsToPatch.forEach(method => {
  // 拦截数组的七个方法,先完成本职工作,再额外完成响应式的工作
  Object.defineProperty(arrayMethods, method, {
    value: function(...args) {
      // 完成方法的本职工作,比如 this.arr.push(xx)
      const ret = arrayProto[method].apply(this, args)
      // 将来接着实现响应式相关的能力
      console.log('array reactive')
      // 新增的元素列表
      let inserted = []
      switch(method) {
        case 'push':
        case 'unshift':
          inserted = args
          break;
        case 'splice':
          // this.arr.splice(idx, num, x, x, x)
          inserted = args.slice(2)
          break;
      }
      // 如果数组有新增的元素,则对新增的元素进行响应式处理
      inserted.length && this.__ob__.observeArray(inserted)
      // 依赖通知更新
      this.__ob__.dep.notify()
      return ret
    },
    configurable: true,
    writable: true,
    enumerable: true
  })
})

/**
 * 覆盖数组(arr)的原型对象
 * @param {*} arr 
 */
export default function protoArgument(arr) {
  arr.__proto__ = arrayMethods
}

At this point, the dependency collection is all complete. But you will find that the page still has no changes, the responsive data is not rendered on the page, and the page does not change when the data is updated. why is that? Is there anything else to do?

In fact, looking back at the code of dependency collection, we will find that there is a place that we have missed. Have you found that the Watcher constructor never seems to be instantiated, which means that the dependency collection is never triggered, because only when the Watcher is instantiated Dep.target will be assigned.

So the question is, when should Watcher be instantiated? You may not have seen the source code of Vue1, but the source code of Vue2 has been read before. Think back carefully, when will you instantiate Watcher.

The answer is mountComponent, which is the mount phase. After initialization, $mount is executed. $mount calls mountComponent. One step in the mountComponent method is to instantiate the Watcher. If you have forgotten this, you can go and look at the source code of this part.

So the next thing we have to implement is the compiler, which is the $mount method.

translater

This part implements a simplified compiler using DOM manipulation. From it, you can see the compilation process of the node tree, and understand the implementation principles of text nodes, v-on:click, v-bind, and v-model instructions.

$mount

/src/index.js

Vue.prototype._init = function (options) {
  ... 省略
  
  // 如果存在 el 配置项,则调用 $mount 方法编译模版
  if (this.$options.el) {
    this.$mount()
  }
}

Vue.prototype.$mount = function () {
  mount(this)
}

mount

/src/compiler/index.js

/**
 * 编译器
 */
export default function mount(vm) {
  // 获取 el 选择器所表示的元素
  let el = document.querySelector(vm.$options.el)

  // 编译节点
  compileNode(Array.from(el.childNodes), vm)
}

compileNode

/src/compiler/compileNode.js

/**
 * 递归编译整棵节点树
 * @param {*} nodes 节点
 * @param {*} vm Vue 实例
 */
export default function compileNode(nodes, vm) {
  // 循环遍历当前节点的所有子节点
  for (let i = 0, len = nodes.length; i < len; i++) {
    const node = nodes[i]
    if (node.nodeType === 1) { // 元素节点
      // 编译元素上的属性节点
      compileAttribute(node, vm)
      // 递归编译子节点
      compileNode(Array.from(node.childNodes), vm)
    } else if (node.nodeType === 3 && node.textContent.match(/{{(.*)}}/)) {
      // 编译文本节点
      compileTextNode(node, vm)
    }
  }
}

compileTextNode

The principle of text node responsive update

/src/compiler/compileTextNode.js

/**
 * 编译文本节点
 * @param {*} node 节点
 * @param {*} vm Vue 实例
 */
export default function compileTextNode(node, vm) {
  // <span>{{ key }}</span>
  const key = RegExp.$1.trim()
  // 当响应式数据 key 更新时,dep 通知 watcher 执行 update 函数,cb 会被调用
  function cb() {
    node.textContent = JSON.stringify(vm[key])
  }
  // 实例化 Watcher,执行 cb,触发 getter,进行依赖收集
  new Watcher(cb)
}

compileAttribute

How the v-on:click, v-bind and v-model directives work

/src/compiler/compileAttribute.js

/**
 * 编译属性节点
 * @param {*} node 节点
 * @param {*} vm Vue 实例
 */
export default function compileAttribute(node, vm) {
  // 将类数组格式的属性节点转换为数组
  const attrs = Array.from(node.attributes)
  // 遍历属性数组
  for (let attr of attrs) {
    // 属性名称、属性值
    const { name, value } = attr
    if (name.match(/v-on:click/)) {
      // 编译 v-on:click 指令
      compileVOnClick(node, value, vm)
    } else if (name.match(/v-bind:(.*)/)) {
      // v-bind
      compileVBind(node, value, vm)
    } else if (name.match(/v-model/)) {
      // v-model
      compileVModel(node, value, vm)
    }
  }
}

compileVOnClick

/src/compiler/compileAttribute.js

/**
 * 编译 v-on:click 指令
 * @param {*} node 节点
 * @param {*} method 方法名
 * @param {*} vm Vue 实例
 */
function compileVOnClick(node, method, vm) {
  // 给节点添加一个 click 事件,回调函数是对应的 method
  node.addEventListener('click', function (...args) {
    // 给 method 绑定 this 上下文
    vm.$options.methods[method].apply(vm, args)
  })
}

compileVBind

/src/compiler/compileAttribute.js

/**
 * 编译 v-bind 指令
 * @param {*} node 节点
 * @param {*} attrValue 属性值
 * @param {*} vm Vue 实例
 */
function compileVBind(node, attrValue, vm) {
  // 属性名称
  const attrName = RegExp.$1
  // 移除模版中的 v-bind 属性
  node.removeAttribute(`v-bind:${attrName}`)
  // 当属性值发生变化时,重新执行回调函数
  function cb() {
    node.setAttribute(attrName, vm[attrValue])
  }
  // 实例化 Watcher,当属性值发生变化时,dep 通知 watcher 执行 update 方法,cb 被执行,重新更新属性
  new Watcher(cb)
}

compileVModel

/src/compiler/compileAttribute.js

/**
 * 编译 v-model 指令
 * @param {*} node 节点 
 * @param {*} key v-model 的属性值
 * @param {*} vm Vue 实例
 */
function compileVModel(node, key, vm) {
  // 节点标签名、类型
  let { tagName, type } = node
  // 标签名转换为小写
  tagName = tagName.toLowerCase()
  if (tagName === 'input' && type === 'text') {
    // <input type="text" v-model="inputVal" />

    // 设置 input 输入框的初始值
    node.value = vm[key]
    // 给节点添加 input 事件,当事件发生时更改响应式数据
    node.addEventListener('input', function () {
      vm[key] = node.value
    })
  } else if (tagName === 'input' && type === 'checkbox') {
    // <input type="checkbox" v-model="isChecked" />

    // 设置选择框的初始状态
    node.checked = vm[key]
    // 给节点添加 change 事件,当事件发生时更改响应式数据
    node.addEventListener('change', function () {
      vm[key] = node.checked
    })
  } else if (tagName === 'select') {
    // <select v-model="selectedValue"></select>

    // 设置下拉框初始选中的选项
    node.value = vm[key]
    // 添加 change 事件,当事件发生时更改响应式数据
    node.addEventListener('change', function () {
      vm[key] = node.value
    })
  }
}

Summarize

At this point, a simplified version of Vue1.x is completed. To recap, we implemented the following functionality:

  • Data responsive interception

    • normal object

    • array

  • Data responsive update

    • Dependency collection

      • Dep

      • Watcher

    • translater

      • text node

      • v-on:click

      • v-bind

      • v-model

The execution result of the example code in the target is as follows:

Animated image address: https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fcceda69f08a4d0a8b4f9c1e96032ad6~tplv-k3u1fbpfcp-watermark.image

May-26-2021 09-12-48.gif

The interviewer asked : How is Vue1.x data responsiveness implemented?

Answer :

The core principle of Vue's data-responsiveness is this Object.defineProperty.

By recursively traversing the entire data object, set a getter and setter for each key in the object. If the key is an array, the array-responsive process is followed.

Array responsiveness is implemented by using Object.defineProperty to intercept the seven methods of the array. First of all, the seven methods are enhanced, and the ability to rely on notification updates is added on the basis of completing the work of the method, and if there is new data, the new data will also be processed responsively.

The ability to update data responsively is achieved through data responsive interception combined with Dep, Watcher, and compiler.

After completing the data initialization (that is, responsive interception), it enters the mounting phase and starts to compile the entire DOM tree. During the compilation process, the responsive data is encountered , and the Watcher is instantiated . At this time, the data read operation will occur and the getter will be triggered. , perform dependency collection, and put the Watcher instance into the dep corresponding to the current reactive property.

When the responsive data is updated in the future, trigger the setter, and then start dep to notify all the Watcher instances collected by itself to execute the update method, trigger the execution of the callback function, and update the DOM.

The implementation of the entire responsive principle of the above Vue1.x.

The interviewer asked : How do you evaluate the design of Vue1.x responsive principles?

Answer :

Vue1.x was actually implemented by Youda in order to solve the pain points in his work. At that time, he felt that various DOM operations were too cumbersome. During initialization, he needed to set data on nodes through DOM operations, and also monitor DOM operations. When updating, update the corresponding data. So he wondered if he could automate this process, which resulted in Vue1.x.

Thinking about it this way, the implementation of Vue1.x is actually very reasonable, and it has indeed achieved the expected goal. Intercept the reading and setting of data through Object.defineProperty. When the page is first rendered, the entire DOM tree is compiled by the compiler, and the initial value is set for the DOM node. When the DOM node is updated, the responsive data or responsive data is automatically updated. When updating, the corresponding DOM node is automatically updated through Watcher.

At this time, Vue has no problem in completing small and medium-sized Web systems. And compared to Vue 2.x, the performance will be better, because when the responsive data is updated, Watcher can directly update the corresponding DOM node, without the VNode overhead and Diff process of 2.x.

But large Web systems can't do it, and for the simple reason that it's because of its design. Because there is a one-to-one correspondence between the Watcher in Vue1.x and the responsive data in the template, that is to say, every time the responsive data is referenced in the page, a Watcher will be generated. In a large-scale system, the amount of data on a page may be very large, which will generate a large number of Watchers, occupy a lot of resources, and cause performance degradation.

So in one sentence, Vue1.x will perform well in small and medium-sized systems, and DOM nodes will be updated in a targeted manner, but large-scale systems have too many Watchers, resulting in excessive resource usage and performance degradation.

So Vue2.x solves this problem by introducing VNode and Diff. The specific implementation principle will be introduced in the next article , Vue2.x of Handwritten Vue Series.

notice

The next article will upgrade Vue1.x implemented in this article to Vue2.x, and introduce Vnode and diff algorithms to solve the performance bottleneck of Vue1.x.

In addition, some other core principles will be implemented additionally, such as computed, asynchronous update queue, child component, slot, etc.

Link

Thank you for your attention , likes , favorites and comments , see you in the next issue.


When learning becomes a habit, knowledge becomes common sense. Thank you for your attention , likes , favorites and comments .

New videos and articles will be sent on the WeChat public account as soon as possible, welcome to follow: Li Yongning lyn

The article has been included in the github repository liyongning/blog . Welcome to Watch and Star.

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324163262&siteId=291194637