前言:
Vue 用了好多年了,还只会用!那你就out 了,来思考下Vue 源码的实现。相信使用过computed 的同学都知道。computed 非常好用。因为他有几个显著的优点:
计算属性优点:
1. 解决模板中复杂数据的问题
2. 只有内部依赖数据发生变化时才会再次调用
3. 计算属性会缓存上一次的计算结果
4. 多次复用相同的数值,只会调用一次
既然这么好用。且有这么多优点,那他到底是怎么实现的呢?
开发环境搭建
首先我们用vite 搭建一个简单环境
npm i vite -D
package.json 加入下面代码
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
现在执行 npm run dev
新建一个index.html 这时我们访问 http://localhost:5173/ 就能访问到页面了
新建一个文件夹 source source 下面新建index.js
我们就在这个index.js 里面来写computed的源码实现。
在根目录下新建main.js,index.html 里面引用main.js
准备工作完成,现在就开始写代码吧。
首先回顾一下我们使用Vue 的代码基本格式是这样的。
import Vue from './source/index.js'
var vm = new Vue({
el: '#app',
data () {
return {
a: 1,
b: 2
}
},
template: `
<span>a</span>
<span>*</span>
<span>b</span>
<span>=</span>
<span>{{ result }}</span>
`,
computed: {
result () {
return this.a * this.b
}
}
})
在index.html 里面写一个
<div id="app"></div>
在这里,我们之前是要在main.js中引用Vue官方的Vue.js, 今天我们是写源码,所以我们引用自己的js文件, main.js 引用 source 下面的index.js
import Vue from './source/index.js'
准备工作完毕,现在就来实现实现下computed,
实现前的代码结构思考和架构
思考:
- Vue 是个构造函数,接收一个对象,
- 接收到参数我们给实例属性添加$el 方便后续操作
- 执行 选项中的data函数,获取到返回的结果,赋值给实例方便后续操作
- 往原型上挂载一个总的初始化函数_init, 一般下划线命名的代表给内部使用
- 总的初始方法,需要分别调用处理data响应式 数据的函数,处理computed 处理响应式的函数
所以先写出一个大纲结构大概是这样的
var Vue = (function () {
var Vue = function(options) {
this.$el = document.querySelector(el)
this.$data = options.data()
this._init(this, options.computed, options.template)
}
Vue.prototype._init = function (vm, computed, template) {
dataReactive(vm)
computedReactive(vm, computed)
render(vm, template)
}
function dataReactive(vm) {
}
function computedReactive(vm, computed) {
}
function render(vm, template) {
}
return Vue
})()
data 响应式的实现
紧接着来写具体方法的实现,首先实现下dataReactive. 来看代码
function dataReactive(vm) {
var data = vm.$data
for(let key in data) {
(function (key) {
Object.defineProperty(vm, key, {
get () {
return data[key]
},
set (newVal) {
data[key] = newVal
}
})
})(key)
}
}
主要运用了Object.defineProperty 来实现响应式拦截, 当然set 方法里面是还需要做些事情的,比如说渲染,后面再来看。
接着来看computed 的响应式, 在处理computed 的响应式之前, 先来分析下compouted 数据的特点。
- computed 里面的数据有可能直接是一个方法, 有可能是一个对象, 对象下面才是get 方法。
- computed 里面的数据更新是依赖data 里面的数据的, 所以们要收集依赖
根据这些特点所以我们在实现computed 响应式之前需要处理一下computed 的数据,用一个变量保存起来
声明存储数据的变量:var computedData = {}
来看具体代码实现:
computed 数据初始化处理
function initComputedData (vm, computed) {
for (var key in computed) {
var descriptor = Object.getOwnPropertyDescriptor(computed, key)
var descriptorFn = descriptor.value.get ? descriptor.value.get : descriptor.value // 判断式直接方法还是对象里面的get方法
computedData[key] = {}
computedData[key].value = descriptorFn.call(vm)
computedData[key].get = descriptorFn.bind(vm)
computedData[key].dep = cllectDep(descriptorFn)
}
}
// 收集依赖
function cllectDep(fn) {
var _c = fn.toString().match(/this.(.+?)/g)
if (_c.length > 0) {
for (var i = 0; i < _c.length; i++) {
_c[i] = _c[i].split('.')[1]
}
}
return _c
}
computed 响应式实现
function computedReactive(vm, computed) {
initComputedData(vm ,computed)
for (var key in computedData) {
(function (key) {
Object.defineProperty(vm, key, {
get () {
return computedData[key].value
},
set (newVal) {
computedData[key] = newVal
}
})
})(key)
}
}
这里和data 的响式实现差不多,
渲染函数实现
computed 响应式实现完成之后,接下就是数据渲染了,当然需要编译一下模板,这里没有用虚拟节点,主要通过正则替换实现。通过compileTemplate 函数编译, 通过render 函数渲染
具体代码如下:
function render(vm, template) {
var container = document.createElement('div'),
_el = vm.$el;
container.innerHTML = template
var domTree = compileTemplate(vm, container)
_el.appendChild(domTree)
}
function compileTemplate (vm, container) {
var allNodes = container.getElementsByTagName('*')
var nodeItem = null
for (var i = 0; i < allNodes.length; i++) {
nodeItem = allNodes[i]
console.log(nodeItem, '00')
var matched = nodeItem.textContent.match(reg)
if (matched) {
nodeItem.textContent = nodeItem.textContent.replace(reg, function (node, key) {
dataPool[key.trim()] = nodeItem
console.log(key, 'key', vm[key.trim()], vm)
return vm[key.trim()]
})
}
console.log(vm, 'vm')
}
return container
}
这个时候我们来访问下页面,就能看到具体的内容了
数据更新,模板更新
现在我们只实现了渲染,但是还没有实现更新,这就是之前说的还需要在响应式的set 里面还需要做点工作。
function update (vm, key) {
dataPool[key].textContent = vm[key]
}
dataReactive 的set
set (newVal) {
data[key] = newVal
update(vm, key) //增加了更新函数的调用
}
这时候我们在main.js 手动给vm.b 赋值等于1000 看看效果
可以看到 页面上数据也更新了,
但是你会发现,计算结果没有更新, 因为, 计算属性的更新逻辑还没写。
计算属性更新方法实现:
function updateComputed (vm, key, update) {
var _dep = null
for (let _key in computedData) {
_dep = computedData[_key].dep
for (var i = 0; i < _dep.length; i++) {
if (key === _dep[i]) {
vm[_key] = computedData[_key].get()
update(_key)
}
}
}
}
主要思想:当data 里面的数据变化时, 循环遍历之前处理的computed 的数据, 然后遍历对应key 的 依赖, 如果对应key 有和更新data 的相等, 重新执行get 方法更新模板数据, computed 是在data 变化时更新, 所以也是在dataReactive 的设置里面调用函数。
dataReactive 的设置如下:
data[key] = newVal
update(vm, key)
updateComputed(vm, key, function (key) {
update(vm, key)
})
现在再来看效果, 结果就正常了。
我们现在来改变下a 的值,然后访问五次result ,在computed 的result 函数里面答应reslut 看下函数执行了多少次。
代码如下:
vm.b = 1000
console.log(vm.result)
vm.a = 55
console.log(vm.result)
console.log(vm.result)
console.log(vm.result)
console.log(vm.result)
console.log(vm.result)
result
result () {
console.log('result')
return this.a * this.b
}
运行效果如下:
可以看到函数运行了三次, 一次初始化的时候, 一次是改变b 的值的时候, 再次是改变a的值的时候。改变了a之后,我们连续访问了5次reslult 也没在执行result 函数。 做到了和Vue 的计算属性一样的缓存效果。
感谢收看, 若有更好的想法欢迎一起讨论学习,一起进步