本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
更多文章
[vue2]熬夜编写为了让你们通俗易懂的去深入理解vue-router并手写一个
[vue2]熬夜编写为了让你们通俗易懂的去深入理解vuex并手写一个
[vue2]熬夜编写为了让你们通俗易懂的去深入理解nextTick原理
[vue2]熬夜编写为了让你们通俗易懂的去深入理解双向绑定以及解决监听Array数组变化问题
[vue2]熬夜编写为了让你们通俗易懂的去深入理解v-model原理
熬夜不易,点个赞再走吧
起步
我是在根目录新建了一个文件夹,里面分别有一个temp.html和一个reactive.js,这里先利用原本的vue.js调试下能不能正常运行
<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: 0,
}
})
setInterval(() => {
app.counter++
}, 1000)
</script>
这样就是正常的,于是改一下 <script src="node_modules/vue/dist/vue.js"></script>
改为我们新建的reactive.js <script src="./reactive.js"></script>
defineReactive
众所周知,vue2的响应式是利用Object.defineProperty,那么这里先添加一个响应式函数
Object.defineProperty(obj, key, val)
- obj: 要定义的属性对象。这个在手写vue-router中我们定义的是 this,也就是vue实例
- key: 要定义或修改的属性名称。这个在手写vue-router中我们定义的是 current,也就是要变成响应式的目标
- val: 要定义或修改的属性描述符。这个在手写vue-router中我们定义的是默认给了一个 '/'
function defineReactive(obj, key, val) {}
现在对obj的key进行属性拦截
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {})
}
Object.defineProperty能添加或修改对象的属性,存在数据描述符和存取描述符两种形式
而这里会用到存取描述符的get和set两个函数,默认为undefined
set接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log('get', val)
return val
},
set(newVal) {
if (newVal !== val) {
console.log('set', newVal)
observe(val)
val = newVal
}
}
})
}
构建实例
首先我们的起点是new一个vue实例,然后observer劫持所有属性和compile解析指令两个分支进入一个内循环
构建一个实例
并且老规矩,保存选项方便后面使用
class Vue {
constructor(options) {
this.$options = options
this.$data = options.data
}
}
添加observer劫持所有属性和compile解析指令
- observe()我们做了遍历对象的处理,这里传入data去处理下data
- 传入el树和实例对象,遍历options.el模板树,解析其中的动态部分,初始化并更新
class Vue {
constructor(options) {
this.$options = options
this.$data = options.data
observe(this.$data)
new Compile(options.el, this)
}
}
observe
observe observe只处理对象,不处理数组,因为 Vue 的实现中,从性能/体验的性价比考虑,放弃了这个特性,另外做处理,这个在另一篇observe讲过
是对象就遍历给对象中每个属性添加响应式
function observe(obj) {
if (typeof obj !== 'obj' || !!obj) {
return obj
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
proxy
在添加compiler前,还要做个事,就是代理,为什么要代理呢?
因为这个时候模板内是通过app.counter
访问的,这个时候应该是个undefined,如果换成app.data.counter
就能获取
所以现在需要换成app.counter
,就需要添加一个代理
传入实例, 把方法放在observe()之后
proxy(this)
遍历实例中的data对象
function proxy(vm) {
// 访问data中进行遍历,访问key
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
// 通过get传出去
get() {
return vm.$data[key]
},
// 再通过set附上新值
set(val) {
vm.$data[key] = val
console.log(vm.$data[key])
}
})
})
}
这个时候就能通过app.counter
,相当于平时在组件里用this.counter
访问
compile
new Compile(options.el, this)
- 保存选项
- 获取dom
- 编译dom
class Compile {
constructor(el, vm) {
// 保存选项
this.$vm = vm
// 获取dom
const dom = document.querySelector(el)
// 编译dom
this.compile(dom)
}
}
compile
传入的dom,去获取dom的子节点
用最简洁的话总结浏览器运行机制这篇讲了一些子节点的知识
const childNodes = el.childNodes
获得子节点后,判断是文本还是元素
this.isElement(node)
判断是否是元素
this.isInter(node)
判断是否是插值表达式{{}}
childNodes.forEach(node => {
if (this.isElement(node)) {
// 元素
}else if (this.isInter(node)) {
// 插值表达式 {{}}
// 解析{{xxxx}}
// console.log('插值表达式', node)
}
这时候刷新下页面看看效果
正常打印,继续
isInter
正则判断判断是否有 {{}}且nodeType等于3
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
首先先做渲染插值表达式{{}},简单点,新增一个方法compileText并传入node
if (this.isInter(node)) {
this.compileText(node)
}
compileText(node) {
node.textContent = this.$vm[RegExp.$1]
}
RegExp.$1是什么?看这个图
对就是解析{{xxxx}}中的xxxx
isElement
如果nodeType等于1,就代表是元素,给一个true进入if
isElement(node) {
return node.nodeType === 1
}
node里有个属性叫attributes
创建一个变量attrs并赋予它
attrs拿到所有属性
解析动态指令,里面包含指令,属性绑定,事件等,同源码里也有attrs,源码里会有多种判断,这里简写
if (this.isElement(node)) {
const attrs = node.attributes
}
转成数组后进行遍历
if (this.isElement(node)) {
const attrs = node.attributes
Array.from(attrs).forEach(attr=>{})
}
在循环里判断是否是一个动态属性
判断 v-xxx="counter" 之类的指令
v-xxx -> attr.name
counter -> attr.value
Array.from(attrs).forEach(attr=>{
const attrName = attr.name
const exp = attr.value
if(this.isDir(attrName)){
// 如果是v-的开头,就截断它
const dir = attrName.substring(2)
// 如果是合法的指令,就执行该指令对应的函数,传入节点和表达式两个参数
this[dir] && this[dir](node, exp)
}
})
但是不排除子节点下还有子节点,于是递归一下
if (node.childNodes.length > 0) {
this.compile(node)
}
添加指令
this.isDir(attrName)
isDir()中传入v-xxx进行截取,添加一个方法isDir()跟isElement()同级
isDir(attrName){
// 判断前缀是否 v-
return attrName.startsWith('v-')
}
这个时候能截取到指令并执行该指令对应的函数,我们添加几个指令,分别是v-text,v-html,v-if,并进行测试
text(node, exp){
node.textContent = this.$vm[exp]
}
html(node, exp){
node.innerHTML = this.$vm[exp]
}
if(node, exp){
node.style.display = this.$vm[exp]?'block':'none'
}
回到html,标签内添加指令
<div id="app">
<p>{{counter}}</p>
<p v-html="VHtml"></p>
<p v-text="VText"></p>
<p v-if="VIf">show</p>
</div>
<script src="./reactive.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
counter: 0,
VIf:false,
VHtml:'<span style="color:blue">blue html</span>',
VText: 0
}
})
setInterval(() => {
app.counter++
app.VIf = !app.VIf
}, 1000)
</script>
测试
测试v-text,v-html
通过计时器测试v-if
测试正常
目前的reactive.js长这样,贴出来方便你们比对
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
if (newVal !== val) {
observe(val)
val = newVal
}
}
})
}
function observe(obj) {
if (typeof obj !== 'obj' || !!obj) {
return obj
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key]
},
set(val) {
vm.$data[key] = val
new Compile(this.$options.el, this)
}
})
})
}
class Vue {
constructor(options) {
this.$options = options
this.$data = options.data
observe(this.$data)
proxy(this)
new Compile(options.el, this)
}
}
class Compile {
constructor(el, vm) {
this.$vm = vm
const dom = document.querySelector(el)
this.compile(dom)
}
compile(el) {
const childNodes = el.childNodes
childNodes.forEach(node => {
if (this.isElement(node)) {
const attrs = node.attributes
Array.from(attrs).forEach(attr=>{
const attrName = attr.name
const exp = attr.value
if(this.isDir(attrName)){
const dir = attrName.substring(2)
this[dir] && this[dir](node, exp)
}
})
if (node.childNodes.length > 0) {
this.compile(node)
}
} else
if (this.isInter(node)) {
this.compileText(node)
}
})
}
compileText(node) {
node.textContent = this.$vm[RegExp.$1]
}
isElement(node) {
return node.nodeType === 1
}
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
isDir(attrName){
return attrName.startsWith('v-')
}
text(node, exp){
node.textContent = this.$vm[exp]
}
html(node, exp){
node.innerHTML = this.$vm[exp]
}
if(node, exp){
node.style.display = this.$vm[exp]?'block':'none'
}
}
现在我们完成了new MVVM(),defineReactive,observe,proxy,compile,能正常实现响应式和模板编译