写在前面
vue3 马上要来了,vue2 学会了吗?
最近看到了不少类似标题的文章,虽然 vue 的双向绑定、虚拟dom、diff算法等等面试常见问题你可能在几年前就学过了,不过让从零开始实现一个 vue,你可以吗。
本着学习的最好方法就是自己实现一次的原则,趁着疫情无法返校,计划实现一个尽量完整的 vue,删掉了 flow 和很多的类型判断,只保留各功能的主流程,旨在为直接阅读 vue 源码提供过渡。
毕竟 vue 源码还是比较难啃的,看网上的文章也很难将各个模块联系起来。而跟着我一个功能一个功能的实现则很轻松,学完之后再去看 vue 源码就可以游刃有余啦。
仅供学习交流使用,觉得看文章太慢的可以直接看源码:github.com/buppt/YourV…
本篇文章是从零实现 vue2 系列第一篇,vue 主流程实现,先不要管双向绑定、虚拟dom 等等,后面会一点一点加上来。文章会最先更新在公众号:BUPPT。
正文
我们按照 vue 的方式,实现功能,一个数字和一个按钮,点击按钮数字加一。
// main.js
import YourVue from './instance'
new YourVue({
el: '#app',
data: {
count: 0,
},
template: `
<div>
<div>{{count}}</div>
<button @click="addCount">addCount</button>
</div>
`,
methods:{
addCount(){
const count = this.count + 1
this.setState({ // 没有双向绑定,先通过setState更新
count
})
}
}
})
实现
首先初始化一个 class,这里需要关注的问题有三个
- 第一个是如何实现 data 和 methods 中的变量通过 this 直接访问
- 第二个如何将 template 模版转换成 dom 元素
- 第三个是如何将事件绑定到 dom 元素上面
先上 YourVue 定义。
export default class YourVue{
constructor(options){
this._init(options)
}
_init(options){
this.$options = options
if (options.data) initData(this)
if (options.methods) initMethod(this)
if (options.el) {
this.$mount()
}
}
$mount(){
this.update()
}
update(){
let el = this.$options.el
el = el && query(el)
if(this.$options.template){
this.el = templateToDom(this.$options.template, this)
el.innerHTML = ''
el.appendChild(this.el)
}
}
setState(data){
Object.keys(data).forEach(key => {
this[key] = data[key]
})
this.update()
}
}
问题一
如何实现 data 和 methods 中的变量通过 this 直接访问?
vue 是通过Object.defineProperty修改了 this 的 get 和 set 函数,这样当访问this.count的时候,其实访问的就是this._data.count。
function initData(vm){
let data = vm.$options && vm.$options.data
vm._data = data
data = vm._data = typeof data === 'function'
? data.call(vm, vm)
: data || {}
Object.keys(data).forEach(key => {
proxy(vm, '_data', key)
})
}
function proxy (target, sourceKey, key) {
const sharedPropertyDefinition = {
enumerable: true,
configurable: true
}
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
而 methods 就是直接在 this 对象上创建了一个 key 指向 this.methods 中的函数。
function initMethod(vm){
const event = vm.$options.methods
Object.keys(event).forEach(key => {
vm[key] = event[key].bind(vm)
})
}
这样就可以通过 this 直接访问到 data 和 methods 中的变量和函数啦,当然这里应该判断 data 和 methods 中的变量是否重复,为了简化代码就省掉了。
问题二
如何将 template 模版转换成 dom 元素?
先解析 template,将 template 解析成语法树,然后再根据 ast 生成 dom树插入到 new 时传入的元素位置。至于是如何从 template 解析成 ast 的,可以看我的上一篇文章,链接:github.com/buppt/Video… ast 就可以了。
{
type: 1
tag: "div"
children: [{…}, {…},
{
type: 1
tag: "button"
attrsMap: {@click: "addCount"}
children: [{type: 3, text: "addCount", parent: button}]
events: {click: ƒ}
parent: div
}
]
}
ast 中的 type 分为三种,type 为1表示 dom节点,type 为3表示纯文本节点,type 为2表示带有变量的文本节点。
然后将 ast 转换为 dom 元素并不是 vue 的思路,这里为了实现功能的闭环先这样实现了,后面实现虚拟 dom 之后会改为通过 render 函数生成 vnode,再通过 vnode 生成 dom 的形式。
export function templateToDom(template, app){
const ast = parse(template, app)
const root = createDom(ast, app)
return root
}
function createDom(ast, app){
if(ast.type === 1){
const root = document.createElement(ast.tag)
ast.children.forEach(child => {
child.parent = root
createDom(child, app)
})
if(ast.parent){
ast.parent.appendChild(root)
}
if(ast.events){
updateListeners(root, ast.events, {}, app)
}
return root
}else if(ast.type === 3 && ast.text.trim()){
ast.parent.textContent = ast.text
}else if(ast.type === 2){
let res = ''
ast.tokens.forEach(item => {
if(typeof item === 'string'){
res += item
}else if(typeof item === 'object'){
res += app[item['@binding']]
}
})
ast.parent.textContent = res
}
}
问题三
第三个是如何将事件绑定到 dom 元素上面?
上面生成 dom 时候这段代码就是给 dom 绑定事件用的。
if (ast.events) {
updateListeners(root, ast.events, {}, app)
}
生成的 ast 中会记录这个元素上的事件和事件对应的函数{click: ƒ},但是并不是直接把这个函数添加到事件上,而是包装了一层invoker函数,这样当绑定的函数发生变化的时候,不用重新解绑再绑定。而是每次执行该函数的时候去寻找要执行的函数。
function updateListeners(elm, on, oldOn, context){
for (let name in on) {
let cur = context[on[name].value]
let old = oldOn[name]
if(isUndef(old)){
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur)
}
elm.addEventListener(name, cur)
}else if(event !== old){
old.fns = cur
on[name] = old
}
}
for (let name in oldOn) {
if (isUndef(on[name])) {
elm.removeEventListener(name, oldOn[name])
}
}
}
function createFnInvoker(fns){
function invoker () {
const fns = invoker.fns
return fns.apply(null, arguments)
}
invoker.fns = fns
return invoker
}
这篇文章就到这里了,你可能会感觉这都很简单啊,有位大佬说得好“会的不难,难的不会”,希望你每次读完文章都有 so easy 的感觉。
本篇代码:github.com/buppt/YourV… 求 star ~