Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从零写一个 Vue(一)主流程实现 #6

Open
buppt opened this issue Jun 15, 2020 · 0 comments
Open

从零写一个 Vue(一)主流程实现 #6

buppt opened this issue Jun 15, 2020 · 0 comments

Comments

@buppt
Copy link
Owner

buppt commented Jun 15, 2020

写在前面

vue3 马上要来了,vue2 学会了吗?

最近看到了不少类似标题的文章,虽然 vue 的双向绑定、虚拟dom、diff算法等等面试常见问题你可能在几年前就学过了,不过让从零开始实现一个 vue,你可以吗。

本着学习的最好方法就是自己实现一次的原则,趁着疫情无法返校,计划实现一个尽量完整的 vue,删掉了 flow 和很多的类型判断,只保留各功能的主流程,旨在为直接阅读 vue 源码提供过渡。

毕竟 vue 源码还是比较难啃的,看网上的文章也很难将各个模块联系起来。而跟着我一个功能一个功能的实现则很轻松,学完之后再去看 vue 源码就可以游刃有余啦。

仅供学习交流使用,觉得看文章太慢的可以直接看源码:https://github.com/buppt/YourVue

本篇文章是从零实现 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,这里需要关注的问题有三个

  1. 第一个是如何实现 data 和 methods 中的变量通过 this 直接访问
  2. 第二个如何将 template 模版转换成 dom 元素
  3. 第三个是如何将事件绑定到 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 的,可以看我的上一篇文章,链接:#4 。这里知道会生成如下类型的 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 的感觉。

本篇代码:https://github.com/buppt/YourVue/tree/master/oldSrc/1.main_flow 求 star ~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant