手撕源码-vue2实现一个简单过程(重要)

142 阅读4分钟

vue实现的简单过程

new Vue() -> init -> $mount -> complie -> render -> vnode -> patch -> dom 

  1. 往vue实例上添加方法。new Vue时直接执行 _init方法,初始化data props methods 等参数
  2. 执行$mount操作: 执行render函数 -> 调用createElement -> createElement中return new vnode实例 -》 返回深层次vnode树
  1. 执行update方法:如果是第一次,递归调用createElm生成真是节点,直接插入整个dom;如果更新,那么要进行diff算法比较,patch方法判断哪些vnode才需要插入

一、初始化参数,定义函数

  1. 首先往实例上添加了render、update等方法
  2. 实例化过程执行_init()方法,往实例上添加属性、初始化data ,将data绑定到实例vm上,创建observer实例,全局监听data,为每个属性创建一个dep实例,Object.defineProperty 添加get 方法, set方法(研究响应式原理会重点分析)
function Vue (options) {
  this._init(options)  // 实例化后直接执行
}

// 往vue实例上添加方法
initMixin(Vue)  // 添加 _init方法
// stateMixin(Vue)  // stateMixin.js 中,添加 $set $watch 方法等
// eventsMixin(Vue) // events.js 中,添加 $on $once $emit 方法等
renderMixin(Vue)  // 添加 _render方法 $nextTick方法
lifecycleMixin(Vue) // 添加 _update $forceUpdate 方法等

// 添加 _init方法
function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        const vm = this
        vm.$options = options
        vm._self = vm

        initRender(vm)  // 往实例上添加属性,如 $createElement
        initState(vm)  // 初始化 props, method, data, watch

        vm.$mount(vm.$option.el)  //挂载元素
    }
}

//添加 _render方法
function renderMixin (Vue) {
    Vue.prototype._render = function () {
        const vm = this
        const { render } = vm.$options

        let vnode = render(vm.$createElement) // new Vue 中的 render 执行
        console.log(32323, vnode)
        return vnode // 返回 vnode
    }
}

// 添加 _update
function lifecycleMixin (Vue) {
    // 主要用来将 _render 返回的 vnode ,运用 diff 算法对比,适当的添加到 dom 中
    Vue.prototype._update = function (vnode) {
        // 这里将 _render 返回的 vnode 插入到 dom中
        // 如果第一次,则直接插入整个 vnode ;如果是更新,那么要进行 diff 算法比较,判断哪些 vnode 才需要插入,这里先不研究
        const vm = this
        const prevVnode = vm._vnode;//之前旧的vnode
        if (!prevVnode) { // 直接插入 vnode
            createElm(vnode, vm.$el)
        } else { // diff 算法比较
            // vm.patch()
        }
    }
}


//  往实例上添加属性,如  $createElement
function initRender (vm) {
    vm.$createElement = createElement
}

//初始化 props,method,data,watch
function initState (vm) {
    let data = vm._data = vm.$options.data
    if (data) {
        // 之后会将data中的属性绑定到实例上
        // observer dep ...
    }
}

二、挂载元素

1. 获取render函数

挂载元素阶段会执行render函数, 生成深层次的vnode树,render函数从何而来的呢?获得render通常有两种方式:

第一种就是,手写render,创建vue实例的时候,不用template的方式写,直接手写render

第二种就是,将template转换成render函数,我们平时写的实际上是一个字符串

// main.js  @vue/cli 4.5.0 
new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app')

// main.js
new Vue({
  el: '#app',
  router,
  store,
  template: '<App/>',
  components: { App }
})

@vue/cli 4.5.0 版本创建的项目中,已经直接写成了render函数的形式

那么,是怎么转换成render函数的呢?这里不做深究,只知道有两种方式可以做到即可:

  1. webpack @vue/cli 创建的项目,template写在 .vue文件中,可以借助vue-loader转换
  2. 用正则去匹配字符串,找出一个个的标识符,不断往后移动,生成 ast 树,像babel等工具都有类似的

vue源码中有这个 compile 的工具函数 compileToFunctions ,之后会去分析这个函数,转换之后的形式是

// 有些地方会把 createElement 写为 _c ,或者 h ,实际上都是同一个函数 createElement
render (createElement) {
    return createElement('div', { attr: { id: 'container' } }, [
        createElement('p', { class: 'nav' }, [
            createElement('span', {key: 'index'}, 'aaa,'),
            createElement('span', 'bbb')
        ]),
        createElement('a', { attr: { href: 'www.baidu.com' } }, '百度')
    ])
}

顺便提一下Vue 开发有两种方式:

Runtime Only 和 Runtime+Compiler

可以理解为,

  • Runtime+Compiler: Compiler 就是带有compileToFunctions 函数的功能,输入template字符串直接能转成 render,所以Runtime+Compiler属于完整版,支持你随便、任何条件下写各种 template
  • Runtime Only : Vue中没有 Compiler 功能,也就是说没有compileToFunctions 函数去将 template 转成 render ,那么只能寄托于:
  1. 直接手写 render ,越过转换的步骤
  2. 利用 webpack loader ,在开发电脑上帮你转换 .vue 文件,打包后生成的一堆 js 文件中,全部变为了 render ,不含有 template 字符串了。

获取到了render函数之后,接下来就是执行render函数了

2. 执行render函数,返回vnode

render函数里面调用了createElement函数,最终返回带层级的vnode树

// 定义 $mount 挂载方法,实际上是在其他文件定义的,因为和跨平台有关,所以可能存在不同的 $mount
Vue.prototype.$mount = function (el) {
    const vm = this
    vm.$el = typeof el === 'string' ? document.querySelector(el) : document.body
    const options = vm.$options;
    if (!options.render) {
        // 如果不存在 render ,那么就需要 compileToFunctions 编译 render了,这里我们刚好跳过了。。。
        // vm.options.render = compileToFunctions(vm.options.template)
    }
    // 继续
    mountComponent(vm) // 在 lifecycle.js 中定义的
}


// lifecycle.js
function mountComponent (vm) {
    // callHook(vm, 'beforeMount') // 生命周期

    let updateComponent = function () {
        vm._update(vm._render())    // 执行 _render()获得 vnode,再执行 _update 渲染到 dom 中
    }
    updateComponent()
}

// 判断 tag ,创建不同的 vnode
function createElement (tag, data, children) {
    if (typeof data !== 'object') {
        children = data
        data = undefined
    }
    let vnode
    if (typeof tag === 'string') { // 认为是标签
        vnode = new VNode(tag, data, children)
    } else { // 认为是子组件
        // vnode = createComponent(Ctor, data, context, children, tag);
    }
    return vnode
}

 // VNode 类
class VNode {
    constructor (tag, data, children) {
        this.tag = tag
        this.data = data
        this.children = children
    }
}

三. 执行_update方法,将vnode渲染到dom中

如果第一次,递归调用 createElm 生成真实节点并插入到 dom 中;如果是更新,那么要进行 diff 算法比较(研究diff算法时再看)

// lifecycle.js
function mountComponent (vm) {
    // callHook(vm, 'beforeMount') // 生命周期

    let updateComponent = function () {
        vm._update(vm._render())    // 执行 _render()获得 vnode,再执行 _update 渲染到 dom 中
    }
    updateComponent()
}

// 添加 _update
function lifecycleMixin (Vue) {
    // 主要用来将 _render 返回的 vnode ,运用 diff 算法对比,适当的添加到 dom 中
    Vue.prototype._update = function (vnode) {
        // 这里将 _render 返回的 vnode 插入到 dom中
        // 如果第一次,则直接插入整个 vnode ;如果是更新,那么要进行 diff 算法比较,判断哪些 vnode 才需要插入,这里先不研究
        const vm = this
        const prevVnode = vm._vnode;//之前旧的vnode
        if (!prevVnode) { // 直接插入 vnode
            createElm(vnode, vm.$el)
        } else { // diff 算法比较
            // vm.patch()
        }
    }
}

  // patch.js
function createElm (vnode, el) {
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    console.log(32, vnode)
    if (tag) { // 因为下面 createChildren 递归时,children 也会作为 vnode 参数传进来,children 可能是字符串时就不存在 .tag (比如span标签内的纯文本,也是span的children)
        vnode.elm = document.createElement(tag) // 创建了真实节点
        createChildren(vnode, children) // 遍历children,递归调用 createElm ,不断插入节点
        insert(el, vnode.elm) // 插入该节点
    }
}

function createChildren (vnode, children) {
  if (Array.isArray(children)) {
    for(let i = 0;i < children.length;i++) {
      createElm(children[i], vnode.elm) // 传入父节点
    }
  } else { // children 是纯文本,比如span标签内的文字
    insert(vnode.elm, document.createTextNode(children))
  }
}

function insert (parentEl, elm) {
    parentEl.appendChild(elm)
}

简化代码

function Vue (options) {
  this._init(options)  // 实例化后直接执行
}

// 往vue实例上添加方法
initMixin(Vue)  // 添加 _init方法
renderMixin(Vue)  // 添加 _render方法 $nextTick方法
lifecycleMixin(Vue) // 添加 _update $forceUpdate 方法等

// 定义 $mount 挂载方法,实际上是在其他文件定义的,因为和跨平台有关,所以可能存在不同的 $mount
Vue.prototype.$mount = function (el) {
    const vm = this
    vm.$el = typeof el === 'string' ? document.querySelector(el) : document.body
    const options = vm.$options;
    if (!options.render) {
        // 如果不存在 render ,那么就需要 compileToFunctions 编译 render了,这里我们刚好跳过了。。。
        // vm.options.render = compileToFunctions(vm.options.template)
    }
    // 继续
    mountComponent(vm) // 在 lifecycle.js 中定义的
}

// 添加 _init方法
function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        const vm = this
        vm.$options = options
        vm._self = vm

        initRender(vm)  // 往实例上添加属性,如 $createElement
        initState(vm)  // 初始化 props, method, data, watch

        vm.$mount(vm.$option.el)  //挂载元素
    }
}

// lifecycle.js
function mountComponent (vm) {
    // callHook(vm, 'beforeMount') // 生命周期

    let updateComponent = function () {
        vm._update(vm._render())    // 执行 _render()获得 vnode,再执行 _update 渲染到 dom 中
    }
    updateComponent()
}

//  往实例上添加属性,如  $createElement
function initRender (vm) {
    vm.$createElement = createElement
}

//初始化 props,method,data,watch
function initState (vm) {
    let data = vm._data = vm.$options.data
    if (data) {
        // 之后会将data中的属性绑定到实例上
        // observer dep ...
    }
}
      
//添加 _render方法
function renderMixin (Vue) {
    Vue.prototype._render = function () {
        const vm = this
        const { render } = vm.$options

        let vnode = render(vm.$createElement) // new Vue 中的 render 执行
        console.log(32323, vnode)
        return vnode // 返回 vnode
    }
}

// 判断 tag ,创建不同的 vnode
function createElement (tag, data, children) {
    if (typeof data !== 'object') {
        children = data
        data = undefined
    }
    let vnode
    if (typeof tag === 'string') { // 认为是标签
        vnode = new VNode(tag, data, children)
    } else { // 认为是子组件
        // vnode = createComponent(Ctor, data, context, children, tag);
    }
    return vnode
}

 // VNode 类
class VNode {
    constructor (tag, data, children) {
        this.tag = tag
        this.data = data
        this.children = children
    }
}

// 添加 _update
function lifecycleMixin (Vue) {
    // 主要用来将 _render 返回的 vnode ,运用 diff 算法对比,适当的添加到 dom 中
    Vue.prototype._update = function (vnode) {
        // 这里将 _render 返回的 vnode 插入到 dom中
        // 如果第一次,则直接插入整个 vnode ;如果是更新,那么要进行 diff 算法比较,判断哪些 vnode 才需要插入,这里先不研究
        const vm = this
        const prevVnode = vm._vnode;//之前旧的vnode
        if (!prevVnode) { // 直接插入 vnode
            createElm(vnode, vm.$el)
        } else { // diff 算法比较
            // vm.patch()
        }
    }
}

  // patch.js
function createElm (vnode, el) {
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    console.log(32, vnode)
    if (tag) { // 因为下面 createChildren 递归时,children 也会作为 vnode 参数传进来,children 可能是字符串时就不存在 .tag (比如span标签内的纯文本,也是span的children)
        vnode.elm = document.createElement(tag) // 创建了真实节点
        createChildren(vnode, children) // 遍历children,递归调用 createElm ,不断插入节点
        insert(el, vnode.elm) // 插入该节点
    }
}

function createChildren (vnode, children) {
  if (Array.isArray(children)) {
    for(let i = 0;i < children.length;i++) {
      createElm(children[i], vnode.elm) // 传入父节点
    }
  } else { // children 是纯文本,比如span标签内的文字
    insert(vnode.elm, document.createTextNode(children))
  }
}

function insert (parentEl, elm) {
    parentEl.appendChild(elm)
}